Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeb3dd0220 | ||
|
|
9647a33209 | ||
|
|
2d966863e3 | ||
|
|
a66632f767 | ||
|
|
1c2ee631f4 | ||
|
|
259ff3436f | ||
|
|
a750ecaafd | ||
|
|
8b3601248d | ||
|
|
6420020309 | ||
|
|
65d86a42d7 | ||
|
|
4eaffdc6a4 | ||
|
|
454e8a3962 | ||
|
|
11e8a9d5c4 | ||
|
|
8137f2692f | ||
|
|
6933424fca | ||
|
|
ff8a10c231 | ||
|
|
6a94500170 | ||
|
|
41eb47bc15 | ||
|
|
211f772447 | ||
|
|
6d6e3fd127 | ||
|
|
14ef0ab6d1 | ||
|
|
8434069539 | ||
|
|
c7621f251f | ||
|
|
a5122cc076 | ||
|
|
5993d6862b | ||
|
|
348b612f98 | ||
|
|
f714b59c88 | ||
|
|
b627499acd | ||
|
|
43beccc2eb | ||
|
|
b8275b3404 | ||
|
|
6e4da08164 | ||
|
|
f2ac52bcdf | ||
|
|
46e976513b | ||
|
|
816ccf6509 | ||
|
|
73e265d8fd | ||
|
|
6117097a93 | ||
|
|
fee732dba1 | ||
|
|
1138e8062f | ||
|
|
123cf0d7b8 | ||
|
|
bd14ee3b1f | ||
|
|
e63eee0c68 | ||
|
|
070feb4b66 | ||
|
|
308417852c | ||
|
|
9c3a976250 | ||
|
|
688973bae5 | ||
|
|
d93b0be0d4 | ||
|
|
773abbae13 | ||
|
|
0f3579fd7b | ||
|
|
db7b7158fa | ||
|
|
b5a61f2dd8 | ||
|
|
8347f15de2 | ||
|
|
26309fad10 | ||
|
|
ba4d8b0b35 | ||
|
|
dc11e0c79c | ||
|
|
a13d931cfd | ||
|
|
fea2ad3d7d | ||
|
|
b8c10def8e | ||
|
|
80afc28719 | ||
|
|
561cd77e68 | ||
|
|
d8554ec67e | ||
|
|
a43237d4f9 | ||
|
|
6af7d782d4 | ||
|
|
6d2b778ac5 | ||
|
|
eada773048 | ||
|
|
9bff4e061e | ||
|
|
753ce8e119 | ||
|
|
4be6719db4 | ||
|
|
a6e1dc4269 | ||
|
|
ad241b9f84 | ||
|
|
53e76288cb | ||
|
|
0f15b6d198 | ||
|
|
143e93f483 | ||
|
|
93a5bb2baa | ||
|
|
ebfc8e5a1b | ||
|
|
972b9b6e91 | ||
|
|
56cccc6781 | ||
|
|
b680bb5372 | ||
|
|
217b3ae488 | ||
|
|
959392f815 | ||
|
|
d1e13e48fe | ||
|
|
1c04502963 | ||
|
|
34da23783e | ||
|
|
2b603cfbb5 | ||
|
|
a1fe22c08c | ||
|
|
bc04ae24a7 | ||
|
|
48216e1653 | ||
|
|
8aca82a593 | ||
|
|
68d8cbe68a | ||
|
|
4bc3acb14d | ||
|
|
a974ec9dee | ||
|
|
e91c43466d | ||
|
|
dfc4ab6dd8 | ||
|
|
9f5ffde2dd | ||
|
|
678bef2360 | ||
|
|
3b12cdc179 | ||
|
|
261754f18b | ||
|
|
980db9cb0c | ||
|
|
a80cd455de | ||
|
|
289e3d6286 | ||
|
|
4b181620d8 | ||
|
|
088ec7fd94 | ||
|
|
289f1fe134 | ||
|
|
6b9ac52af6 | ||
|
|
0e422eb70d | ||
|
|
df32545b07 | ||
|
|
bab340a849 | ||
|
|
494d44f8bb | ||
|
|
ad1a81e8a0 | ||
|
|
b0d507c2dd | ||
|
|
619e5e944b | ||
|
|
e2d0faa539 | ||
|
|
cbb6c82113 | ||
|
|
00376b43f8 | ||
|
|
fb163a2493 | ||
|
|
14c493fc63 | ||
|
|
625682a4da | ||
|
|
eec8f45de5 | ||
|
|
0855e3e32f | ||
|
|
fab3ce46b8 | ||
|
|
08c8ebc100 | ||
|
|
0039fe09be | ||
|
|
97beed6fd1 | ||
|
|
994314cfa3 | ||
|
|
db6dbe34c3 | ||
|
|
fa7ef361e2 | ||
|
|
296349a83b | ||
|
|
563458e0b5 | ||
|
|
169301fa18 | ||
|
|
0363e6f122 | ||
|
|
c259324185 | ||
|
|
57767d9906 | ||
|
|
1a26f9fb82 | ||
|
|
19ab7db239 | ||
|
|
ded430c155 | ||
|
|
506acda70b | ||
|
|
d173844f98 | ||
|
|
7919ae3628 | ||
|
|
188d42dcd6 | ||
|
|
7735394373 | ||
|
|
30bf8a1724 | ||
|
|
0310560938 | ||
|
|
2969161afb | ||
|
|
a6213d4a6f | ||
|
|
12268bd065 | ||
|
|
35a66c3550 | ||
|
|
402a974a70 | ||
|
|
079cd95e0e | ||
|
|
e9f5aaa397 | ||
|
|
ca0236fdfd | ||
|
|
da3edb35cc | ||
|
|
ef6054b859 | ||
|
|
1046976c08 | ||
|
|
f8cbbfc2cd | ||
|
|
f083fc0d7b | ||
|
|
72cb820f1f | ||
|
|
d5d33247fb | ||
|
|
9f95a10591 | ||
|
|
5ca083105c | ||
|
|
6bc8d8dd56 | ||
|
|
dce89bcfa8 | ||
|
|
e5370ce859 | ||
|
|
6f35d73fe8 | ||
|
|
af2fb5f399 | ||
|
|
e976565550 | ||
|
|
7a6b4adcc1 | ||
|
|
ec30665bc4 | ||
|
|
9651d62b96 | ||
|
|
a6e3c8984d | ||
|
|
777d9ac353 | ||
|
|
d01fccd949 | ||
|
|
4cd0d1c397 | ||
|
|
bf3ec060f7 | ||
|
|
40a3267615 | ||
|
|
ff19d6744e | ||
|
|
c26af16688 | ||
|
|
216b3cfe01 | ||
|
|
fd70556c82 | ||
|
|
0fee20cc12 | ||
|
|
32f6de1eec | ||
|
|
8671f08a0e | ||
|
|
299484f7b2 | ||
|
|
d16086fcdb | ||
|
|
1968b7da31 | ||
|
|
95a40a45e5 | ||
|
|
0ad7f080b7 | ||
|
|
aca3048036 | ||
|
|
1158cd42cc | ||
|
|
0e2b7458d7 | ||
|
|
cf7d740004 | ||
|
|
dc4c4d9ade | ||
|
|
a347ae3676 | ||
|
|
7da2a8a15e | ||
|
|
0002559d6e | ||
|
|
afda0ed4da | ||
|
|
473f389bbe | ||
|
|
b7c000b78b | ||
|
|
4ac8247db0 | ||
|
|
0471d1ac97 | ||
|
|
c0221dafde | ||
|
|
8f337f17e4 | ||
|
|
de5a63f7b2 | ||
|
|
52a744b443 | ||
|
|
4be5fa598e | ||
|
|
d03a7e7a98 | ||
|
|
ebc4d6fdbd | ||
|
|
35ffd02814 | ||
|
|
4c72479d12 | ||
|
|
907c761f71 | ||
|
|
97206aa8f6 | ||
|
|
2ec1664408 | ||
|
|
97822f35fd | ||
|
|
c5e72d1a2b | ||
|
|
117c9e069e | ||
|
|
951a9f81d6 | ||
|
|
6f30c33f11 | ||
|
|
8050117adf | ||
|
|
98031aacce | ||
|
|
2781ce6fa0 | ||
|
|
08db39d76a | ||
|
|
da267e1488 | ||
|
|
ff825d6123 | ||
|
|
6d6bb8fc57 | ||
|
|
7836ab83d0 | ||
|
|
8568728152 | ||
|
|
8783983945 | ||
|
|
4055f451fd | ||
|
|
31777ce804 | ||
|
|
97a9e92969 | ||
|
|
c5ca414e8f | ||
|
|
2c5628b5fb | ||
|
|
a338e937e5 | ||
|
|
1089572d66 | ||
|
|
ddb2460187 | ||
|
|
745f39ed20 | ||
|
|
18cccd514f | ||
|
|
9c0eba4f00 | ||
|
|
f19625d835 | ||
|
|
c4754d9f85 | ||
|
|
acdc5dc1f4 | ||
|
|
4927609af0 | ||
|
|
e845fe3e71 | ||
|
|
d807efd4f8 | ||
|
|
5f76695298 | ||
|
|
aadb1f3109 | ||
|
|
cb986912c4 | ||
|
|
7e451d9033 | ||
|
|
a024b6d692 | ||
|
|
a45ccf1db7 | ||
|
|
30bf430a91 | ||
|
|
df00776e6c | ||
|
|
1dc0d0c12e | ||
|
|
002fd411ea | ||
|
|
6311cf0c5a | ||
|
|
d71543beb6 | ||
|
|
5a9837f676 | ||
|
|
77754cf32f | ||
|
|
43a74fb99b | ||
|
|
1bef3a3f8f | ||
|
|
b6b6be34f6 | ||
|
|
f1cde3e873 | ||
|
|
dcd6ae2e01 | ||
|
|
8984172ff6 | ||
|
|
87a5e7f030 | ||
|
|
115d07da5a | ||
|
|
e67ed023cd | ||
|
|
1f1d753791 | ||
|
|
34fcce60af | ||
|
|
e7a90d9a7c | ||
|
|
9d6d5e1a14 | ||
|
|
763cf81c97 | ||
|
|
0b64a46bdc | ||
|
|
d417bf058d | ||
|
|
46088bfbb5 | ||
|
|
a6ea622f78 | ||
|
|
847ed8570b | ||
|
|
3a9ab817a9 | ||
|
|
2a5c2ed192 | ||
|
|
8867bec8e6 | ||
|
|
262da85399 | ||
|
|
51a70f2164 | ||
|
|
0854a658d8 | ||
|
|
d99056ced5 | ||
|
|
4a5473b4e7 | ||
|
|
d3f368efd7 | ||
|
|
ca052566de | ||
|
|
c101e262d6 | ||
|
|
1f942301ad | ||
|
|
b6db1e12b7 | ||
|
|
3bfc6ea8d7 | ||
|
|
ec30dd4788 | ||
|
|
990ebbdbcb | ||
|
|
99a856be97 | ||
|
|
260d0641df | ||
|
|
16963859e1 | ||
|
|
010a201d0c | ||
|
|
95014e6348 | ||
|
|
724912ec48 | ||
|
|
46d8a11c51 | ||
|
|
065cd05c96 | ||
|
|
a4564bed3a | ||
|
|
4ef3e587cb | ||
|
|
d6259836e0 | ||
|
|
ef246cfef3 | ||
|
|
90dd0ddb94 | ||
|
|
bd2c54398b | ||
|
|
e65776f9f0 | ||
|
|
5cafdd9878 | ||
|
|
49882a05d1 | ||
|
|
8e36a2bc83 | ||
|
|
24e47195e5 | ||
|
|
4b37958140 | ||
|
|
3edff32bde | ||
|
|
78c5e0f7ef | ||
|
|
49229e1723 | ||
|
|
14e2bfd1bf | ||
|
|
3c49e2d0ed | ||
|
|
d5bc66c9a4 | ||
|
|
4f6977668b | ||
|
|
17826c28e5 | ||
|
|
a3d71dd694 | ||
|
|
a5c39342ba | ||
|
|
ad75c541cf | ||
|
|
cd46cbeb8f | ||
|
|
888e1ad5d3 | ||
|
|
abb7108321 | ||
|
|
da21a2a410 | ||
|
|
904b5bf0f5 | ||
|
|
d21aa97219 | ||
|
|
6ee75fbdba | ||
|
|
c3b2a0c955 | ||
|
|
0ac42c145e | ||
|
|
4a77bc930f | ||
|
|
29c3080eb6 | ||
|
|
57516177a0 | ||
|
|
34d8235551 | ||
|
|
8d51245161 | ||
|
|
55ad98ecc4 | ||
|
|
ed04181bb9 | ||
|
|
868fba55cd | ||
|
|
05351c0ac1 | ||
|
|
a707129146 | ||
|
|
2f3138207c | ||
|
|
dc7286a0ef | ||
|
|
5e66119bf0 | ||
|
|
b751fd11e4 | ||
|
|
5e238bfbfe | ||
|
|
7e20a3cac6 | ||
|
|
7c1d6e4d2e | ||
|
|
13c2157e36 | ||
|
|
7033315cd3 | ||
|
|
775476f97c | ||
|
|
eb62c9ce6a | ||
|
|
60be4df76e | ||
|
|
f888cfd565 | ||
|
|
044126188d | ||
|
|
0fec711545 | ||
|
|
7a6afb8aea | ||
|
|
1e72177991 | ||
|
|
cdf070189e | ||
|
|
64bf455b29 | ||
|
|
0a53ee87b9 | ||
|
|
ef85b71ee4 | ||
|
|
ac0133e8fa | ||
|
|
25ba6df007 | ||
|
|
6d750897a8 | ||
|
|
a7747c537b | ||
|
|
65b6a21938 | ||
|
|
4e6e78ccd2 | ||
|
|
fd5d6546b2 | ||
|
|
402d484182 | ||
|
|
4bf0914f1e | ||
|
|
96519296e1 | ||
|
|
03d06685b0 | ||
|
|
7d57c56b17 | ||
|
|
4ce766853c | ||
|
|
dc7bbd848c | ||
|
|
81744e6523 | ||
|
|
b66f471e53 | ||
|
|
202b8a92af | ||
|
|
1e98f6b781 | ||
|
|
f53e8fbd21 | ||
|
|
8e95871534 | ||
|
|
595a2d2baa | ||
|
|
227772bc33 | ||
|
|
2ca98e477d | ||
|
|
477d71e0b9 | ||
|
|
677bfc3f6b | ||
|
|
c7721b048d | ||
|
|
1417f091fe | ||
|
|
d22964bcfd | ||
|
|
59314ab17d | ||
|
|
5c083d99e2 | ||
|
|
d3abb0756e | ||
|
|
4de9848e34 | ||
|
|
bdbbb12f96 | ||
|
|
c1ea3f49ca | ||
|
|
905c45f225 | ||
|
|
352e5c6cb0 | ||
|
|
2d291c8f5e | ||
|
|
86630fa1e0 | ||
|
|
8e02170dd1 | ||
|
|
9edf95ec93 | ||
|
|
2add875a38 | ||
|
|
b20309f7aa | ||
|
|
8996df5c09 | ||
|
|
7b5f02b4c8 | ||
|
|
fc0287a829 | ||
|
|
ed8bca5394 | ||
|
|
9458f4011d | ||
|
|
f1bd36a3b8 | ||
|
|
f060e02fc0 | ||
|
|
0fa2e5d3a0 | ||
|
|
6d0055ecb5 | ||
|
|
2d3ce9c290 | ||
|
|
796b276d02 | ||
|
|
ed98b5c807 | ||
|
|
f1da7dcb0c | ||
|
|
35a539eee2 | ||
|
|
79ffb7d502 | ||
|
|
a2e6f621a2 | ||
|
|
96e97143f2 | ||
|
|
4126a1a808 | ||
|
|
3ce902123f | ||
|
|
7125357a44 | ||
|
|
ec453b89b8 | ||
|
|
303404acc6 | ||
|
|
f11d42e72d | ||
|
|
329c8d64a8 | ||
|
|
698ce8f0e6 | ||
|
|
b06e11ba09 | ||
|
|
d689a7408b | ||
|
|
c899218fb9 | ||
|
|
c42f2a05d2 | ||
|
|
70ed552e1a | ||
|
|
9a07649b8a | ||
|
|
9a6ac544dd | ||
|
|
06a1e26655 | ||
|
|
626e7c2c61 | ||
|
|
a7aeb491ba | ||
|
|
7ba8f72d7b | ||
|
|
d0f74d7a99 | ||
|
|
7f33fa5368 | ||
|
|
0ab97266f6 | ||
|
|
32f896f19d | ||
|
|
de9965213a | ||
|
|
c2118f4830 | ||
|
|
6f0e0748e1 | ||
|
|
8e5fc13ba4 | ||
|
|
7a35099fb9 | ||
|
|
d08bee6817 | ||
|
|
4fc96904d6 | ||
|
|
66bc1f28dd | ||
|
|
30d082e709 | ||
|
|
1b11123797 | ||
|
|
654c2add0d | ||
|
|
6d179d6d3d | ||
|
|
361faa6646 | ||
|
|
7fc22ba0b7 | ||
|
|
9c82e6ab68 | ||
|
|
78c18d4ffe | ||
|
|
978dafc62b | ||
|
|
8919d8fb7d | ||
|
|
cdb7dd9175 | ||
|
|
d6b7c17402 | ||
|
|
7e247c14b2 | ||
|
|
e6af48fc4a | ||
|
|
9703843602 | ||
|
|
d53a7ce5f1 | ||
|
|
6e4a6515e0 | ||
|
|
bf304ffde5 | ||
|
|
7cefe9d367 | ||
|
|
12cccc25cc | ||
|
|
34f62cb3b3 | ||
|
|
820d327093 | ||
|
|
04279342a4 | ||
|
|
1cfe084cc2 | ||
|
|
4629cc1b19 | ||
|
|
b4e87466ba | ||
|
|
ae82f43078 | ||
|
|
7cc185ac57 | ||
|
|
4d37e1d640 | ||
|
|
74cf9e2573 | ||
|
|
3c2de2a38d | ||
|
|
9dd8216dfb | ||
|
|
905ddafe41 | ||
|
|
62a0e57f38 | ||
|
|
be03a191c4 | ||
|
|
cf7d3b5a7e | ||
|
|
939d8a7078 | ||
|
|
db62a51d5a | ||
|
|
9ebf5751fd | ||
|
|
f7c6b7fce2 | ||
|
|
850b34ce33 | ||
|
|
1a64a88d3d | ||
|
|
be8f00ab8a | ||
|
|
ddedb8a48b | ||
|
|
ba392362ee | ||
|
|
8fbd47e199 | ||
|
|
6aaf0c812a | ||
|
|
33977b2282 | ||
|
|
37f70bdf0f | ||
|
|
5d6203f5f9 | ||
|
|
082aabc735 | ||
|
|
ba15526516 | ||
|
|
4a2576fbc2 | ||
|
|
97d95b4bcb | ||
|
|
d0a9d8c581 | ||
|
|
78c779faaf | ||
|
|
8d81aca439 | ||
|
|
6c3dbc3c6f | ||
|
|
b5411841cf | ||
|
|
5c6a725712 | ||
|
|
03efeaeb1a | ||
|
|
c075187088 | ||
|
|
416b17062e | ||
|
|
dd88eb4cc1 | ||
|
|
5fa0821e07 | ||
|
|
6e9bad5772 | ||
|
|
5769d438a0 | ||
|
|
8a48ff96ef | ||
|
|
0782c97d4f | ||
|
|
542cc75dc4 | ||
|
|
1b445ad789 | ||
|
|
9baed4256a | ||
|
|
2b4df7e8c7 | ||
|
|
6e28dec712 | ||
|
|
ba9f0ca672 | ||
|
|
7261ccdc90 | ||
|
|
6fbadbe5bc | ||
|
|
26d7ba72aa | ||
|
|
44e4df2492 | ||
|
|
d8482b533a | ||
|
|
2082480ddd | ||
|
|
e8c4e394fd | ||
|
|
0c5052ebae | ||
|
|
d3b2ae1975 | ||
|
|
f4f0f489eb | ||
|
|
3368ebf067 | ||
|
|
b0ac10d94a | ||
|
|
c87a8b86ae | ||
|
|
d5d71d7097 | ||
|
|
c960a8b909 | ||
|
|
3257f78d24 | ||
|
|
a9133bf933 | ||
|
|
8e8147b5ac | ||
|
|
711f4de3d7 | ||
|
|
68998e062e | ||
|
|
f40d1f6bd5 | ||
|
|
048c3cd14d | ||
|
|
08ca353205 | ||
|
|
c83cdc9335 | ||
|
|
e91e3c076d | ||
|
|
8be7eec2ca | ||
|
|
acc4493517 | ||
|
|
ff467402c0 | ||
|
|
49d3eb5a3f | ||
|
|
889aa6d652 | ||
|
|
78e0457843 | ||
|
|
7f568faef6 | ||
|
|
be045f3156 | ||
|
|
3b5e81d514 | ||
|
|
346bdf2ca8 | ||
|
|
501289786c | ||
|
|
24ffe223be | ||
|
|
32c561d78e | ||
|
|
ff65b37336 | ||
|
|
3f698ca012 | ||
|
|
785bc2556e | ||
|
|
e198282b4d | ||
|
|
24e59c378b | ||
|
|
c1b4aae573 | ||
|
|
f5339ef97f | ||
|
|
2b53fae16f | ||
|
|
1d3466c7a0 | ||
|
|
4d8b2029d2 | ||
|
|
8ad0da2109 | ||
|
|
12827ef0a6 | ||
|
|
4c399a41f7 | ||
|
|
25968a35cc | ||
|
|
548d095223 | ||
|
|
1683bcb3e5 | ||
|
|
991843acb0 | ||
|
|
8b85437b6b | ||
|
|
6259f0de70 | ||
|
|
d86bc1d84d | ||
|
|
88bdb0815d | ||
|
|
97f64fe5e6 | ||
|
|
196fef3a1b | ||
|
|
a2d756074f | ||
|
|
b2f23d619d | ||
|
|
9d1d66b0f5 | ||
|
|
c61e44d55b | ||
|
|
76c5c283b9 | ||
|
|
2d88afb77e | ||
|
|
7fd09761cb | ||
|
|
cb53c53c2b | ||
|
|
77ff6349f6 | ||
|
|
879ad4001d | ||
|
|
e1436ab964 | ||
|
|
007bea0a4b | ||
|
|
67b76558fc | ||
|
|
f60429efdf | ||
|
|
f140e20c6f | ||
|
|
c502ab9500 | ||
|
|
425ae55422 | ||
|
|
e9006a234b | ||
|
|
ef6aa4e415 | ||
|
|
1901c3c798 | ||
|
|
6cb046f29e | ||
|
|
e7e67e300b | ||
|
|
3f948ba49b | ||
|
|
96f33e4052 | ||
|
|
68d60f4f64 | ||
|
|
f7153da83d | ||
|
|
16d53897c7 | ||
|
|
32d405bca4 | ||
|
|
2f747fc09d | ||
|
|
32d5a1d3f7 | ||
|
|
db44b8ea54 | ||
|
|
1f512c8ba1 | ||
|
|
417dc9dc42 | ||
|
|
bd66ea8acc | ||
|
|
dba9d13b00 | ||
|
|
1e034a794f | ||
|
|
a261c66f34 | ||
|
|
b94bd73d33 | ||
|
|
a9be6b17b9 | ||
|
|
ac6f8e8551 | ||
|
|
23fb2bfc7f | ||
|
|
ff1c3722a8 | ||
|
|
1712368f6a | ||
|
|
d7529bf6b5 | ||
|
|
6b145bb3cc | ||
|
|
72e905675d | ||
|
|
2fbcf7e9b6 | ||
|
|
0ade4dbf00 | ||
|
|
d70ad9d733 | ||
|
|
51a19878dd | ||
|
|
ddca6a1ad8 | ||
|
|
cac74122f2 | ||
|
|
991b2b9721 | ||
|
|
b6663acf91 | ||
|
|
7f74e72cea | ||
|
|
bd1df642fa | ||
|
|
575b0241e4 | ||
|
|
f1ed6304a4 | ||
|
|
a42d9871bd | ||
|
|
7a2ea7cace | ||
|
|
fb39811d95 | ||
|
|
78e88f4c42 | ||
|
|
db63af5f7a | ||
|
|
c3e04da796 | ||
|
|
2a59043ef1 | ||
|
|
3d76a35a68 | ||
|
|
c1733fc89c | ||
|
|
388c1bec45 | ||
|
|
a2869e9d2a | ||
|
|
3e5ff42474 | ||
|
|
9366eaf374 | ||
|
|
cb4c93a632 | ||
|
|
adaffd7189 | ||
|
|
821dc22e80 | ||
|
|
b0a71e96cc | ||
|
|
40e156992a | ||
|
|
ab031d9da1 | ||
|
|
209a76c5ce | ||
|
|
4bd74b5817 | ||
|
|
dbb1a86ca3 | ||
|
|
c066883dbd | ||
|
|
0b6b6dc7fd | ||
|
|
c546464d9f | ||
|
|
7b3e2c58e2 | ||
|
|
d6127082e9 | ||
|
|
67a5fdaf96 | ||
|
|
af9156b18a | ||
|
|
7af20fd2d9 | ||
|
|
2a5a428bc0 | ||
|
|
4b7d47465a | ||
|
|
5f4e2d8e06 | ||
|
|
f8ef8507b5 | ||
|
|
2ae143e6da | ||
|
|
38e8f52293 | ||
|
|
b143fc9297 | ||
|
|
dd10f78401 | ||
|
|
90208b774a | ||
|
|
bc35d9fe1a | ||
|
|
472d637c9f | ||
|
|
20c86613a7 | ||
|
|
5ac69ed7de | ||
|
|
0620fe5e11 | ||
|
|
44a819226f | ||
|
|
cf2cf7898a | ||
|
|
ee212b69a9 | ||
|
|
87d91560f8 | ||
|
|
bccca00aaf | ||
|
|
d6e36ed50a | ||
|
|
83f7fa768d | ||
|
|
2d49006aee | ||
|
|
c2e58dcb62 | ||
|
|
f154c5e521 | ||
|
|
eadbebb4fa | ||
|
|
40cf1c7549 | ||
|
|
9c4e305a84 | ||
|
|
167197fcda | ||
|
|
e3a40aca6f | ||
|
|
0611e0c4bd | ||
|
|
d6857693bb | ||
|
|
b5ccf7a58f | ||
|
|
bfca1822a0 | ||
|
|
1bad630641 | ||
|
|
8a9a82fc80 |
No files matched your search
+1
-1
@@ -6,7 +6,7 @@
|
||||
"browser": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"ecmaVersion": 2017,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
## Issue
|
||||
|
||||
Add these issues to elsewhere:
|
||||
- LDAP: https://github.com/wekan/wekan-ldap/issues
|
||||
- Snap: https://github.com/wekan/wekan-snap/issues
|
||||
|
||||
Other Wekan issues can be added here.
|
||||
|
||||
+13
-1
@@ -8,7 +8,6 @@ npm-debug.log
|
||||
.vscode/
|
||||
.idea/
|
||||
.build/*
|
||||
packages/
|
||||
package-lock.json
|
||||
**/parts/
|
||||
**/stage
|
||||
@@ -17,3 +16,16 @@ package-lock.json
|
||||
snap/.snapcraft/
|
||||
.idea
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
.build*
|
||||
*.browserify.js.cached
|
||||
*.browserify.js.map
|
||||
.build*
|
||||
versions.json
|
||||
.versions
|
||||
.npm
|
||||
.build*
|
||||
._*
|
||||
.Trashes
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
+9
-7
@@ -31,7 +31,9 @@ kenton:accounts-sandstorm
|
||||
service-configuration@1.0.11
|
||||
useraccounts:unstyled
|
||||
useraccounts:flow-routing
|
||||
salleman:accounts-oidc
|
||||
wekan-ldap
|
||||
wekan-accounts-cas
|
||||
wekan-accounts-oidc
|
||||
|
||||
# Utilities
|
||||
check@1.2.5
|
||||
@@ -63,9 +65,7 @@ mousetrap:mousetrap
|
||||
mquandalle:jquery-textcomplete
|
||||
mquandalle:jquery-ui-drag-drop-sort
|
||||
mquandalle:mousetrap-bindglobal
|
||||
mquandalle:perfect-scrollbar
|
||||
peerlibrary:blaze-components@=0.15.1
|
||||
perak:markdown
|
||||
templates:tabs
|
||||
verron:autosize
|
||||
simple:json-routes
|
||||
@@ -81,12 +81,14 @@ staringatlights:fast-render
|
||||
mixmax:smart-disconnect
|
||||
accounts-password@1.5.0
|
||||
cfs:gridfs
|
||||
eluck:accounts-lockout
|
||||
rzymek:fullcalendar
|
||||
momentjs:moment@2.22.2
|
||||
browser-policy-framing
|
||||
mquandalle:moment
|
||||
msavin:usercache
|
||||
wekan:wekan-ldap
|
||||
wekan:accounts-cas
|
||||
msavin:sjobs
|
||||
wekan-scrollbar
|
||||
mquandalle:perfect-scrollbar
|
||||
mdg:meteor-apm-agent
|
||||
meteorhacks:unblock
|
||||
lucasantoniassi:accounts-lockout
|
||||
wekan-markdown
|
||||
+9
-7
@@ -60,7 +60,6 @@ ecmascript-runtime@0.5.0
|
||||
ecmascript-runtime-client@0.5.0
|
||||
ecmascript-runtime-server@0.5.0
|
||||
ejson@1.1.0
|
||||
eluck:accounts-lockout@0.9.0
|
||||
email@1.2.3
|
||||
es5-shim@4.6.15
|
||||
fastclick@1.0.13
|
||||
@@ -82,8 +81,10 @@ launch-screen@1.1.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.1.19
|
||||
lucasantoniassi:accounts-lockout@1.0.0
|
||||
matb33:collection-hooks@0.8.4
|
||||
matteodem:easy-search@1.6.4
|
||||
mdg:meteor-apm-agent@3.1.2
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.8.2
|
||||
meteor-base@1.2.0
|
||||
@@ -93,6 +94,7 @@ meteorhacks:collection-utils@1.2.0
|
||||
meteorhacks:meteorx@1.4.1
|
||||
meteorhacks:picker@1.0.3
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
meteorhacks:unblock@1.1.0
|
||||
meteorspark:util@0.2.0
|
||||
minifier-css@1.2.16
|
||||
minifier-js@2.2.2
|
||||
@@ -117,7 +119,6 @@ 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:sjobs@3.0.6
|
||||
msavin:usercache@1.0.0
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@2.2.33
|
||||
@@ -131,7 +132,6 @@ peerlibrary:base-component@0.16.0
|
||||
peerlibrary:blaze-components@0.15.1
|
||||
peerlibrary:computed-field@0.7.0
|
||||
peerlibrary:reactive-field@0.3.0
|
||||
perak:markdown@1.0.5
|
||||
promise@0.10.0
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
@@ -144,8 +144,6 @@ reload@1.1.11
|
||||
retry@1.0.9
|
||||
routepolicy@1.0.12
|
||||
rzymek:fullcalendar@3.8.0
|
||||
salleman:accounts-oidc@1.0.9
|
||||
salleman:oidc@1.0.9
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
sha@1.0.9
|
||||
@@ -180,7 +178,11 @@ useraccounts:unstyled@1.14.2
|
||||
verron:autosize@3.0.8
|
||||
webapp@1.4.0
|
||||
webapp-hashing@1.0.9
|
||||
wekan:accounts-cas@0.1.0
|
||||
wekan:wekan-ldap@0.0.2
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-markdown@1.0.7
|
||||
wekan-oidc@1.0.12
|
||||
wekan-scrollbar@3.1.3
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-ldap@0.0.2
|
||||
yasaricli:slugify@0.0.7
|
||||
zimme:active-route@2.3.2
|
||||
+1062
-10
File diff suppressed because it is too large.
Load diff
+60
-82
@@ -1,81 +1,12 @@
|
||||
FROM debian:buster-slim
|
||||
FROM ubuntu:disco
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Declare Arguments
|
||||
ARG NODE_VERSION
|
||||
ARG METEOR_RELEASE
|
||||
ARG METEOR_EDGE
|
||||
ARG USE_EDGE
|
||||
ARG NPM_VERSION
|
||||
ARG FIBERS_VERSION
|
||||
ARG ARCHITECTURE
|
||||
ARG SRC_PATH
|
||||
ARG WITH_API
|
||||
ARG MATOMO_ADDRESS
|
||||
ARG MATOMO_SITE_ID
|
||||
ARG MATOMO_DO_NOT_TRACK
|
||||
ARG MATOMO_WITH_USERNAME
|
||||
ARG BROWSER_POLICY_ENABLED
|
||||
ARG TRUSTED_URL
|
||||
ARG WEBHOOKS_ATTRIBUTES
|
||||
ARG OAUTH2_ENABLED
|
||||
ARG OAUTH2_CLIENT_ID
|
||||
ARG OAUTH2_SECRET
|
||||
ARG OAUTH2_SERVER_URL
|
||||
ARG OAUTH2_AUTH_ENDPOINT
|
||||
ARG OAUTH2_USERINFO_ENDPOINT
|
||||
ARG OAUTH2_TOKEN_ENDPOINT
|
||||
ARG LDAP_ENABLE
|
||||
ARG LDAP_PORT
|
||||
ARG LDAP_HOST
|
||||
ARG LDAP_BASEDN
|
||||
ARG LDAP_LOGIN_FALLBACK
|
||||
ARG LDAP_RECONNECT
|
||||
ARG LDAP_TIMEOUT
|
||||
ARG LDAP_IDLE_TIMEOUT
|
||||
ARG LDAP_CONNECT_TIMEOUT
|
||||
ARG LDAP_AUTHENTIFICATION
|
||||
ARG LDAP_AUTHENTIFICATION_USERDN
|
||||
ARG LDAP_AUTHENTIFICATION_PASSWORD
|
||||
ARG LDAP_LOG_ENABLED
|
||||
ARG LDAP_BACKGROUND_SYNC
|
||||
ARG LDAP_BACKGROUND_SYNC_INTERVAL
|
||||
ARG LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED
|
||||
ARG LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS
|
||||
ARG LDAP_ENCRYPTION
|
||||
ARG LDAP_CA_CERT
|
||||
ARG LDAP_REJECT_UNAUTHORIZED
|
||||
ARG LDAP_USER_SEARCH_FILTER
|
||||
ARG LDAP_USER_SEARCH_SCOPE
|
||||
ARG LDAP_USER_SEARCH_FIELD
|
||||
ARG LDAP_SEARCH_PAGE_SIZE
|
||||
ARG LDAP_SEARCH_SIZE_LIMIT
|
||||
ARG LDAP_GROUP_FILTER_ENABLE
|
||||
ARG LDAP_GROUP_FILTER_OBJECTCLASS
|
||||
ARG LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE
|
||||
ARG LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE
|
||||
ARG LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT
|
||||
ARG LDAP_GROUP_FILTER_GROUP_NAME
|
||||
ARG LDAP_UNIQUE_IDENTIFIER_FIELD
|
||||
ARG LDAP_UTF8_NAMES_SLUGIFY
|
||||
ARG LDAP_USERNAME_FIELD
|
||||
ARG LDAP_FULLNAME_FIELD
|
||||
ARG LDAP_MERGE_EXISTING_USERS
|
||||
ARG LDAP_SYNC_USER_DATA
|
||||
ARG LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
ARG LDAP_SYNC_GROUP_ROLES
|
||||
ARG LDAP_DEFAULT_DOMAIN
|
||||
ARG LOGOUT_WITH_TIMER
|
||||
ARG LOGOUT_IN
|
||||
ARG LOGOUT_ON_HOURS
|
||||
ARG LOGOUT_ON_MINUTES
|
||||
ARG CORS
|
||||
|
||||
# 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"
|
||||
ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python git ca-certificates gcc-7" \
|
||||
NODE_VERSION=v8.14.0 \
|
||||
ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python3 python3-pip git ca-certificates gcc-8" \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v8.16.0 \
|
||||
METEOR_RELEASE=1.6.0.1 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
@@ -84,6 +15,13 @@ ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential pyth
|
||||
ARCHITECTURE=linux-x64 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
|
||||
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
|
||||
EMAIL_NOTIFICATION_TIMEOUT=30000 \
|
||||
MATOMO_ADDRESS="" \
|
||||
MATOMO_SITE_ID="" \
|
||||
MATOMO_DO_NOT_TRACK=true \
|
||||
@@ -92,12 +30,17 @@ ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential pyth
|
||||
TRUSTED_URL="" \
|
||||
WEBHOOKS_ATTRIBUTES="" \
|
||||
OAUTH2_ENABLED=false \
|
||||
OAUTH2_LOGIN_STYLE=redirect \
|
||||
OAUTH2_CLIENT_ID="" \
|
||||
OAUTH2_SECRET="" \
|
||||
OAUTH2_SERVER_URL="" \
|
||||
OAUTH2_AUTH_ENDPOINT="" \
|
||||
OAUTH2_USERINFO_ENDPOINT="" \
|
||||
OAUTH2_TOKEN_ENDPOINT="" \
|
||||
OAUTH2_ID_MAP="" \
|
||||
OAUTH2_USERNAME_MAP="" \
|
||||
OAUTH2_FULLNAME_MAP="" \
|
||||
OAUTH2_EMAIL_MAP="" \
|
||||
LDAP_ENABLE=false \
|
||||
LDAP_PORT=389 \
|
||||
LDAP_HOST="" \
|
||||
@@ -134,15 +77,26 @@ ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential pyth
|
||||
LDAP_USERNAME_FIELD="" \
|
||||
LDAP_FULLNAME_FIELD="" \
|
||||
LDAP_MERGE_EXISTING_USERS=false \
|
||||
LDAP_EMAIL_FIELD="" \
|
||||
LDAP_EMAIL_MATCH_ENABLE=false \
|
||||
LDAP_EMAIL_MATCH_REQUIRE=false \
|
||||
LDAP_EMAIL_MATCH_VERIFIED=false \
|
||||
LDAP_SYNC_USER_DATA=false \
|
||||
LDAP_SYNC_USER_DATA_FIELDMAP="" \
|
||||
LDAP_SYNC_GROUP_ROLES="" \
|
||||
LDAP_DEFAULT_DOMAIN=""
|
||||
LOGOUT_WITH_TIMER="false" \
|
||||
LDAP_DEFAULT_DOMAIN="" \
|
||||
LDAP_SYNC_ADMIN_STATUS="" \
|
||||
LDAP_SYNC_ADMIN_GROUPS="" \
|
||||
HEADER_LOGIN_ID="" \
|
||||
HEADER_LOGIN_FIRSTNAME="" \
|
||||
HEADER_LOGIN_LASTNAME="" \
|
||||
HEADER_LOGIN_EMAIL="" \
|
||||
LOGOUT_WITH_TIMER=false \
|
||||
LOGOUT_IN="" \
|
||||
LOGOUT_ON_HOURS="" \
|
||||
LOGOUT_ON_MINUTES="" \
|
||||
CORS=""
|
||||
CORS="" \
|
||||
DEFAULT_AUTHENTICATION_METHOD=""
|
||||
|
||||
# Copy the app to the image
|
||||
COPY ${SRC_PATH} /home/wekan/app
|
||||
@@ -154,6 +108,7 @@ RUN \
|
||||
\
|
||||
# OS dependencies
|
||||
apt-get update -y && apt-get install -y --no-install-recommends ${BUILD_DEPS} && \
|
||||
pip3 install -U pip setuptools wheel && \
|
||||
\
|
||||
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
|
||||
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
|
||||
@@ -237,23 +192,43 @@ RUN \
|
||||
fi; \
|
||||
\
|
||||
# Get additional packages
|
||||
mkdir -p /home/wekan/app/packages && \
|
||||
#mkdir -p /home/wekan/app/packages && \
|
||||
chown wekan:wekan --recursive /home/wekan && \
|
||||
cd /home/wekan/app/packages && \
|
||||
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/flow-router.git kadira-flow-router && \
|
||||
gosu wekan:wekan git clone --depth 1 -b master git://github.com/meteor-useraccounts/core.git meteor-useraccounts-core && \
|
||||
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/meteor-accounts-cas.git && \
|
||||
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/wekan-ldap.git && \
|
||||
# REPOS BELOW ARE INCLUDED TO WEKAN REPO
|
||||
#cd /home/wekan/app/packages && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git && \
|
||||
#gosu wekan:wekan git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git && \
|
||||
#gosu wekan:wekan mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc && \
|
||||
#gosu wekan:wekan mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc && \
|
||||
#gosu wekan:wekan rm -rf meteor-accounts-oidc && \
|
||||
sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js && \
|
||||
cd /home/wekan/.meteor && \
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor -- help; \
|
||||
\
|
||||
# extract the OpenAPI specification
|
||||
npm install -g api2html@0.3.0 && \
|
||||
mkdir -p /home/wekan/python && \
|
||||
chown wekan:wekan --recursive /home/wekan/python && \
|
||||
cd /home/wekan/python && \
|
||||
gosu wekan:wekan git clone --depth 1 -b master https://github.com/Kronuz/esprima-python && \
|
||||
cd /home/wekan/python/esprima-python && \
|
||||
python3 setup.py install --record files.txt && \
|
||||
cd /home/wekan/app &&\
|
||||
mkdir -p ./public/api && \
|
||||
python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml && \
|
||||
/opt/nodejs/bin/api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml; \
|
||||
# Build app
|
||||
cd /home/wekan/app && \
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor npm install && \
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
|
||||
cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
|
||||
rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
|
||||
chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
|
||||
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
|
||||
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
|
||||
@@ -272,10 +247,13 @@ RUN \
|
||||
# Cleanup
|
||||
apt-get remove --purge -y ${BUILD_DEPS} && \
|
||||
apt-get autoremove -y && \
|
||||
npm uninstall -g api2html &&\
|
||||
rm -R /var/lib/apt/lists/* && \
|
||||
rm -R /home/wekan/.meteor && \
|
||||
rm -R /home/wekan/app && \
|
||||
rm -R /home/wekan/app_build && \
|
||||
cat /home/wekan/python/esprima-python/files.txt | xargs rm -R && \
|
||||
rm -R /home/wekan/python && \
|
||||
rm /home/wekan/install_meteor.sh
|
||||
|
||||
ENV PORT=8080
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2018 The Wekan Team
|
||||
Copyright (c) 2014-2019 The Wekan Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[](https://codeclimate.com/github/wekan/wekan)
|
||||
[](https://david-dm.org/wekan/wekan)
|
||||
[](https://www.openhub.net/p/wekan)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
|
||||
|
||||
## [Translate Wekan at Transifex](https://transifex.com/wekan/wekan)
|
||||
|
||||
@@ -22,9 +23,8 @@ It's better than at chat where details get lost when chat scrolls up.
|
||||
|
||||
## Chat
|
||||
|
||||
[![Wekan Vanila Chat][vanila_badge]][vanila_chat] - Most Wekan community and developers are here at #wekan chat channel.
|
||||
Use webbrowser to register, and after that you can also alternatively use mobile app Rocket.Chat by Rocket.Chat with
|
||||
address https://chat.vanila.io and same username and password.
|
||||
[![Wekan Chat][vanila_badge]][wekan_chat] - Most Wekan community and developers are here. Works on webbrowser
|
||||
and PWA app that can be added as icon on Android and bookmark on iOS, used like native app.
|
||||
|
||||
[Wekan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
|
||||
|
||||
@@ -39,33 +39,55 @@ address https://chat.vanila.io and same username and password.
|
||||
Wekan is an completely [Open Source][open_source] and [Free software][free_software]
|
||||
collaborative kanban board application with MIT license.
|
||||
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
|
||||
Whether you’re maintaining a personal todo list, planning your holidays with some friends,
|
||||
or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool
|
||||
to keep your things organized. They give you a visual overview of the current state of your project,
|
||||
and make you productive by allowing you to focus on the few items that matter the most.
|
||||
|
||||
Since Wekan is a free software, you don’t have to trust us with your data and can
|
||||
install Wekan on your own computer or server. In fact we encourage you to do
|
||||
that by providing one-click installation on various platforms.
|
||||
|
||||
- [Features][features]: Wekan has real-time user interface. Not all features are implemented, yet.
|
||||
- [Platforms][platforms]: Wekan supports many platforms and plan is to add more. This will be the first place to look if you want to **install** it, test out and learn more in depth.
|
||||
- Wekan is used in [most countries of the world](https://snapcraft.io/wekan).
|
||||
- Wekan largest user has 13k users using Wekan in their company.
|
||||
- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 50 languages.
|
||||
- [Features][features]: Wekan has real-time user interface.
|
||||
- [Platforms][platforms]: Wekan supports many platforms.
|
||||
Wekan is critical part of new platforms Wekan is currently being integrated to.
|
||||
- [Integrations][integrations]: Current possible integrations and future plans.
|
||||
- [Team](https://github.com/wekan/wekan/wiki/Team): The people who spends their time and make Wekan into what it is right now.
|
||||
|
||||
## Requirements
|
||||
|
||||
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
|
||||
[Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
|
||||
[More Platforms](https://github.com/wekan/wekan/wiki/Platforms). [ARM progress](https://github.com/wekan/wekan/issues/1053#issuecomment-410919264).
|
||||
- 1 GB RAM minimum free for Wekan. Production server should have miminum total 4 GB RAM.
|
||||
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/devel/docker-compose.yml): 3 frontend servers,
|
||||
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
|
||||
- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
|
||||
- SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
|
||||
Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
|
||||
Wekan on Sandstorm is not usually affected by any Standalone Wekan (Snap/Docker/Source) security issues.
|
||||
- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
|
||||
New features and fixes are added to Wekan [many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
|
||||
- [Backups](https://github.com/wekan/wekan/wiki/Backup) of Wekan database once a day miminum.
|
||||
Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
|
||||
Some bug can cause Wekan board to not load at all, requiring manual fixing of database content.
|
||||
|
||||
## Roadmap
|
||||
|
||||
[Roadmap](https://github.com/wekan/wekan/wiki/Roadmap)
|
||||
[Roadmap Milestones](https://github.com/wekan/wekan/milestones)
|
||||
|
||||
Upcoming Wekan App Development Platform will make possible many use cases. If you don't find your feature or integration in
|
||||
GitHub issues and [Features][features] or [Integrations][integrations] page at wiki, please add them.
|
||||
[Developer Documentation][dev_docs]
|
||||
|
||||
We are very welcoming to new developers and teams to submit new pull requests to devel branch to make this Wekan App Development Platform possible faster. Please see [Developer Documentation][dev_docs] to get started.
|
||||
- There is many companies and individuals contributing code to Wekan, to add features and bugfixes
|
||||
[many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
|
||||
- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
|
||||
- [Commercial Support](https://wekan.team).
|
||||
- [Bounties](https://wekan.team/bounties/index.html).
|
||||
|
||||
We also welcome sponsors for features and bugfixes. By working directly with Wekan you get the benefit of active maintenance and new features added by growing Wekan developer community.
|
||||
|
||||
Actual work happens at [Wekan GitHub issues][wekan_issues].
|
||||
|
||||
See [Development links on Wekan wiki](https://github.com/wekan/wekan/wiki#Development) bottom of the page for more info.
|
||||
|
||||
If you want to know what is going on exactly this moment, you can check out the [project page](https://github.com/wekan/wekan/projects/2).
|
||||
We also welcome sponsors for features and bugfixes.
|
||||
By working directly with Wekan you get the benefit of active maintenance and new features added by growing Wekan developer community.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -111,4 +133,7 @@ with [Meteor](https://www.meteor.com).
|
||||
[open_source]: https://en.wikipedia.org/wiki/Open-source_software
|
||||
[free_software]: https://en.wikipedia.org/wiki/Free_software
|
||||
[vanila_badge]: https://vanila.io/img/join-chat-button2.png
|
||||
[vanila_chat]: https://chat.vanila.io/channel/wekan
|
||||
[wekan_chat]: https://community.vanila.io/wekan
|
||||
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)
|
||||
@@ -0,0 +1,9 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v2.65.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
userScripts:
|
||||
build: stacksmith/user-scripts/build.sh
|
||||
boot: stacksmith/user-scripts/boot.sh
|
||||
run: stacksmith/user-scripts/run.sh
|
||||
@@ -99,6 +99,9 @@ template(name="boardActivities")
|
||||
else
|
||||
| {{{_ 'activity-added' memberLink cardLink}}}.
|
||||
|
||||
if($eq activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
|
||||
|
||||
if($eq activityType 'moveCard')
|
||||
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
|
||||
|
||||
@@ -114,6 +117,12 @@ template(name="boardActivities")
|
||||
if($eq activityType 'removedLabel')
|
||||
| {{{_ 'activity-removed-label' lastLabel cardLink}}}.
|
||||
|
||||
if($eq activityType 'setCustomField')
|
||||
| {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
|
||||
|
||||
if($eq activityType 'unsetCustomField')
|
||||
| {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
|
||||
|
||||
if($eq activityType 'unjoinMember')
|
||||
if($eq user._id member._id)
|
||||
| {{{_ 'activity-unjoined' cardLink}}}.
|
||||
@@ -129,7 +138,7 @@ template(name="cardActivities")
|
||||
p.activity-desc
|
||||
+memberName(user=user)
|
||||
if($eq activityType 'createCard')
|
||||
| {{_ 'activity-added' cardLabel list.title}}.
|
||||
| {{_ 'activity-added' cardLabel listName}}.
|
||||
if($eq activityType 'importCard')
|
||||
| {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
|
||||
if($eq activityType 'joinMember')
|
||||
@@ -170,6 +179,10 @@ template(name="cardActivities")
|
||||
| {{_ 'activity-sent' cardLabel boardLabel}}.
|
||||
if($eq activityType 'moveCard')
|
||||
| {{_ 'activity-moved' cardLabel oldList.title list.title}}.
|
||||
|
||||
if($eq activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
|
||||
|
||||
if($eq activityType 'addAttachment')
|
||||
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
|
||||
if attachment.isImage
|
||||
|
||||
@@ -74,6 +74,8 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastLabel(){
|
||||
const lastLabelId = this.currentData().labelId;
|
||||
if (!lastLabelId)
|
||||
return null;
|
||||
const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(lastLabelId);
|
||||
if(lastLabel.name === undefined || lastLabel.name === ''){
|
||||
return lastLabel.color;
|
||||
@@ -82,6 +84,28 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
},
|
||||
|
||||
lastCustomField(){
|
||||
const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
|
||||
if (!lastCustomField)
|
||||
return null;
|
||||
return lastCustomField.name;
|
||||
},
|
||||
|
||||
lastCustomFieldValue(){
|
||||
const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
|
||||
if (!lastCustomField)
|
||||
return null;
|
||||
const value = this.currentData().value;
|
||||
if (lastCustomField.settings.dropdownItems && lastCustomField.settings.dropdownItems.length > 0) {
|
||||
const dropDownValue = _.find(lastCustomField.settings.dropdownItems, (item) => {
|
||||
return item._id === value;
|
||||
});
|
||||
if (dropDownValue)
|
||||
return dropDownValue.name;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
return this.currentData().list().title;
|
||||
},
|
||||
@@ -117,6 +141,8 @@ BlazeComponent.extendComponent({
|
||||
|
||||
customField() {
|
||||
const customField = this.currentData().customField();
|
||||
if (!customField)
|
||||
return null;
|
||||
return customField.name;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('archivedBoards');
|
||||
|
||||
@@ -20,16 +20,20 @@ template(name="boardBody")
|
||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
|
||||
if showOverlay.get
|
||||
.board-overlay
|
||||
if isViewSwimlanes
|
||||
if currentBoard.isTemplatesBoard
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
if isViewLists
|
||||
+listsGroup
|
||||
if isViewCalendar
|
||||
else if isViewSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else if isViewLists
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
|
||||
template(name="calendarView")
|
||||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
if isViewCalendar
|
||||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
@@ -1,5 +1,6 @@
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
const swimlaneWhileSortingHeight = 150;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
@@ -35,6 +36,37 @@ BlazeComponent.extendComponent({
|
||||
this._isDragging = false;
|
||||
// Used to set the overlay
|
||||
this.mouseHasEnterCardDetails = false;
|
||||
|
||||
// fix swimlanes sort field if there are null values
|
||||
const currentBoardData = Boards.findOne(Session.get('currentBoard'));
|
||||
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
|
||||
if (nullSortSwimlanes.count() > 0) {
|
||||
const swimlanes = currentBoardData.swimlanes();
|
||||
let count = 0;
|
||||
swimlanes.forEach((s) => {
|
||||
Swimlanes.update(s._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
|
||||
// fix lists sort field if there are null values
|
||||
const nullSortLists = currentBoardData.nullSortLists();
|
||||
if (nullSortLists.count() > 0) {
|
||||
const lists = currentBoardData.lists();
|
||||
let count = 0;
|
||||
lists.forEach((l) => {
|
||||
Lists.update(l._id, {
|
||||
$set: {
|
||||
sort: count,
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
onRendered() {
|
||||
const boardComponent = this;
|
||||
@@ -43,21 +75,64 @@ BlazeComponent.extendComponent({
|
||||
$swimlanesDom.sortable({
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-canvas',
|
||||
helper: 'clone',
|
||||
helper(evt, item) {
|
||||
const helper = $(`<div class="swimlane"
|
||||
style="flex-direction: column;
|
||||
height: ${swimlaneWhileSortingHeight}px;
|
||||
width: $(boardComponent.width)px;
|
||||
overflow: hidden;"/>`);
|
||||
helper.append(item.clone());
|
||||
// Also grab the list of lists of cards
|
||||
const list = item.next();
|
||||
helper.append(list.clone());
|
||||
return helper;
|
||||
},
|
||||
handle: '.js-swimlane-header',
|
||||
items: '.js-swimlane:not(.placeholder)',
|
||||
items: '.swimlane:not(.placeholder)',
|
||||
placeholder: 'swimlane placeholder',
|
||||
distance: 7,
|
||||
start(evt, ui) {
|
||||
const listDom = ui.placeholder.next('.js-swimlane');
|
||||
const parentOffset = ui.item.parent().offset();
|
||||
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
listDom.addClass('moving-swimlane');
|
||||
boardComponent.setIsDragging(true);
|
||||
|
||||
ui.placeholder.insertAfter(ui.placeholder.next());
|
||||
boardComponent.origPlaceholderIndex = ui.placeholder.index();
|
||||
|
||||
// resize all swimlanes + headers to be a total of 150 px per row
|
||||
// this could be achieved by setIsDragging(true) but we want immediate
|
||||
// result
|
||||
ui.item.siblings('.js-swimlane').css('height', `${swimlaneWhileSortingHeight - 26}px`);
|
||||
|
||||
// set the new scroll height after the resize and insertion of
|
||||
// the placeholder. We want the element under the cursor to stay
|
||||
// at the same place on the screen
|
||||
ui.item.parent().get(0).scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
|
||||
},
|
||||
beforeStop(evt, ui) {
|
||||
const parentOffset = ui.item.parent().offset();
|
||||
const siblings = ui.item.siblings('.js-swimlane');
|
||||
siblings.css('height', '');
|
||||
|
||||
// compute the new scroll height after the resize and removal of
|
||||
// the placeholder
|
||||
const scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
|
||||
|
||||
// then reset the original view of the swimlane
|
||||
siblings.removeClass('moving-swimlane');
|
||||
|
||||
// and apply the computed scrollheight
|
||||
ui.item.parent().get(0).scrollTop = scrollTop;
|
||||
},
|
||||
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 prevSwimlaneDom = ui.item.prev('.js-swimlane').get(0);
|
||||
const nextSwimlaneDom = ui.item.next('.js-swimlane').get(0);
|
||||
const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
|
||||
const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
|
||||
const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
|
||||
|
||||
$swimlanesDom.sortable('cancel');
|
||||
@@ -72,6 +147,30 @@ BlazeComponent.extendComponent({
|
||||
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(evt, ui) {
|
||||
// get the mouse position in the sortable
|
||||
const parentOffset = ui.item.parent().offset();
|
||||
const cursorY = evt.pageY - parentOffset.top + ui.item.parent().scrollTop();
|
||||
|
||||
// compute the intended index of the placeholder (we need to skip the
|
||||
// slots between the headers and the list of cards)
|
||||
const newplaceholderIndex = Math.floor(cursorY / swimlaneWhileSortingHeight);
|
||||
let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;
|
||||
|
||||
// if we are scrolling far away from the bottom of the list
|
||||
if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
|
||||
destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
|
||||
}
|
||||
|
||||
// update the placeholder position in the DOM tree
|
||||
if (destPlaceholderIndex !== ui.placeholder.index()) {
|
||||
if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
|
||||
ui.placeholder.insertBefore(ui.placeholder.siblings().slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1));
|
||||
} else {
|
||||
ui.placeholder.insertAfter(ui.placeholder.siblings().slice(destPlaceholderIndex - 1, destPlaceholderIndex));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ugly touch event hotfix
|
||||
@@ -103,7 +202,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
isViewCalendar() {
|
||||
const currentUser = Meteor.user();
|
||||
if (!currentUser) return true;
|
||||
if (!currentUser) return false;
|
||||
return (currentUser.profile.boardView === 'board-view-cal');
|
||||
},
|
||||
|
||||
@@ -233,4 +332,9 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
};
|
||||
},
|
||||
isViewCalendar() {
|
||||
const currentUser = Meteor.user();
|
||||
if (!currentUser) return false;
|
||||
return (currentUser.profile.boardView === 'board-view-cal');
|
||||
},
|
||||
}).register('calendarView');
|
||||
@@ -50,6 +50,6 @@ position()
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin: 0
|
||||
padding: 0 40px 0px 0
|
||||
padding: 0 0px 0px 0
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
@@ -7,71 +7,69 @@ template(name="boardHeaderBar")
|
||||
|
||||
.board-header-btns.left
|
||||
unless isMiniScreen
|
||||
unless isSandstorm
|
||||
if currentBoard
|
||||
if currentUser
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
if currentBoard
|
||||
if currentUser
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
.board-header-btns.right
|
||||
if currentBoard
|
||||
if isMiniScreen
|
||||
unless isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
if currentUser
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
span {{_ watchLevel}}
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
@@ -96,10 +94,11 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-search
|
||||
span {{_ 'search'}}
|
||||
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
title="{{_ 'board-view'}}")
|
||||
i.fa.fa-th-large
|
||||
span {{_ currentUser.profile.boardView}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
title="{{_ 'board-view'}}")
|
||||
i.fa.fa-th-large
|
||||
span {{_ currentUser.profile.boardView}}
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
@@ -112,40 +111,8 @@ template(name="boardHeaderBar")
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
|
||||
i.board-header-btn-icon.fa.fa-navicon
|
||||
|
||||
template(name="boardMenuPopup")
|
||||
ul.pop-over-list
|
||||
li: a.js-custom-fields {{_ 'custom-fields'}}
|
||||
li: a.js-open-archives {{_ 'archived-items'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li: a.js-change-board-color {{_ 'board-change-color'}}
|
||||
//-
|
||||
XXX Language should be handled by sandstorm, but for now display a
|
||||
language selection link in the board menu. This link is normally present
|
||||
in the header bar that is not displayed on sandstorm.
|
||||
if isSandstorm
|
||||
li: a.js-change-language {{_ 'language'}}
|
||||
unless isSandstorm
|
||||
if currentUser.isBoardAdmin
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
|
||||
li: a.js-archive-board {{_ 'archive-board'}}
|
||||
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-subtask-settings {{_ 'subtask-settings'}}
|
||||
|
||||
if isSandstorm
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
|
||||
li: a.js-import-board {{_ 'import-board-c'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-subtask-settings {{_ 'subtask-settings'}}
|
||||
a.board-header-btn.js-toggle-sidebar
|
||||
i.fa.fa-navicon
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
@@ -196,64 +163,6 @@ template(name="boardChangeWatchPopup")
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'muted-info'}}
|
||||
|
||||
template(name="boardChangeColorPopup")
|
||||
.board-backgrounds-list.clearfix
|
||||
each backgroundColors
|
||||
.board-background-select.js-select-background
|
||||
span.background-box(class="board-color-{{this}}")
|
||||
if isSelected
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="boardSubtaskSettingsPopup")
|
||||
form.board-subtask-settings
|
||||
h3 {{_ 'show-parent-in-minicard'}}
|
||||
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-full-path'}}
|
||||
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-parent'}}
|
||||
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-full-path'}}
|
||||
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-parent'}}
|
||||
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'no-parent'}}
|
||||
div
|
||||
hr
|
||||
|
||||
div.check-div
|
||||
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
span {{_ 'show-subtasks-field'}}
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-board'}}
|
||||
select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}")
|
||||
each boards
|
||||
if isBoardSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
if isNullBoardSelected
|
||||
option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value='null') {{_ 'custom-field-dropdown-none'}}
|
||||
div
|
||||
hr
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-list'}}
|
||||
select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}")
|
||||
each lists
|
||||
if isListSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
label
|
||||
@@ -275,14 +184,10 @@ template(name="createBoard")
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import-board'}}
|
||||
|
||||
template(name="chooseBoardSource")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
|
||||
li
|
||||
a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
@@ -297,28 +202,3 @@ template(name="boardChangeTitlePopup")
|
||||
template(name="boardCreateRulePopup")
|
||||
p {{_ 'close-board-pop'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
|
||||
|
||||
|
||||
template(name="archiveBoardPopup")
|
||||
p {{_ 'close-board-pop'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
|
||||
|
||||
template(name="outgoingWebhooksPopup")
|
||||
each integrations
|
||||
form.integration-form
|
||||
if title
|
||||
h4 {{title}}
|
||||
else
|
||||
h4 {{_ 'no-name'}}
|
||||
label
|
||||
| URL
|
||||
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
|
||||
input(type="hidden" value=_id name="id")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
form.integration-form
|
||||
h4
|
||||
| {{_ 'new-outgoing-webhook'}}
|
||||
label
|
||||
| URL
|
||||
input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
@@ -97,6 +97,9 @@ BlazeComponent.extendComponent({
|
||||
currentUser.setBoardView('board-view-lists');
|
||||
}
|
||||
},
|
||||
'click .js-toggle-sidebar'() {
|
||||
Sidebar.toggle();
|
||||
},
|
||||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
},
|
||||
@@ -135,124 +138,6 @@ Template.boardHeaderBar.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
backgroundColors() {
|
||||
return Boards.simpleSchema()._schema.color.allowedValues;
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
return currentBoard.color === this.currentData().toString();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-select-background'(evt) {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
const newColor = this.currentData().toString();
|
||||
currentBoard.setColor(newColor);
|
||||
evt.preventDefault();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('boardChangeColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
allowsSubtasks() {
|
||||
return this.currentBoard.allowsSubtasks;
|
||||
},
|
||||
|
||||
isBoardSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
isNullBoardSelected() {
|
||||
return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined);
|
||||
},
|
||||
|
||||
boards() {
|
||||
return Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
lists() {
|
||||
return Lists.find({
|
||||
boardId: this.currentBoard._id,
|
||||
archived: false,
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
hasLists() {
|
||||
return this.lists().count() > 0;
|
||||
},
|
||||
|
||||
isListSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if ((result === null) || (result === undefined)) {
|
||||
result = 'no-parent';
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-field-has-subtasks'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
|
||||
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks);
|
||||
},
|
||||
'change .js-field-deposit-board'(evt) {
|
||||
let value = evt.target.value;
|
||||
if (value === 'null') {
|
||||
value = null;
|
||||
}
|
||||
this.currentBoard.setSubtasksDefaultBoardId(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'change .js-field-deposit-list'(evt) {
|
||||
this.currentBoard.setSubtasksDefaultListId(evt.target.value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click .js-field-show-parent-in-minicard'(evt) {
|
||||
const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id;
|
||||
const options = [
|
||||
'prefix-with-full-path',
|
||||
'prefix-with-parent',
|
||||
'subtext-with-full-path',
|
||||
'subtext-with-parent',
|
||||
'no-parent'];
|
||||
options.forEach(function(element) {
|
||||
if (element !== value) {
|
||||
$(`#${element} .materialCheckBox`).toggleClass('is-checked', false);
|
||||
$(`#${element}`).toggleClass('is-checked', false);
|
||||
}
|
||||
});
|
||||
$(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
|
||||
$(`#${value}`).toggleClass('is-checked', true);
|
||||
this.currentBoard.setPresentParentTask(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('boardSubtaskSettingsPopup');
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'createBoard';
|
||||
@@ -304,16 +189,11 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
submit: this.onSubmit,
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-board-template': Popup.open('searchElement'),
|
||||
}];
|
||||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'chooseBoardSource';
|
||||
},
|
||||
}).register('chooseBoardSourcePopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
onSubmit(evt) {
|
||||
super.onSubmit(evt);
|
||||
@@ -363,50 +243,3 @@ BlazeComponent.extendComponent({
|
||||
}];
|
||||
},
|
||||
}).register('boardChangeWatchPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
integrations() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return Integrations.find({ boardId: `${boardId}` }).fetch();
|
||||
},
|
||||
|
||||
integration(id) {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return Integrations.findOne({ _id: id, boardId: `${boardId}` });
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'submit'(evt) {
|
||||
evt.preventDefault();
|
||||
const url = evt.target.url.value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
let id = null;
|
||||
let integration = null;
|
||||
if (evt.target.id) {
|
||||
id = evt.target.id.value;
|
||||
integration = this.integration(id);
|
||||
if (url) {
|
||||
Integrations.update(integration._id, {
|
||||
$set: {
|
||||
url: `${url}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Integrations.remove(integration._id);
|
||||
}
|
||||
} else if (url) {
|
||||
Integrations.insert({
|
||||
userId: Meteor.userId(),
|
||||
enabled: true,
|
||||
type: 'outgoing-webhooks',
|
||||
url: `${url}`,
|
||||
boardId: `${boardId}`,
|
||||
activities: ['all'],
|
||||
});
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('outgoingWebhooksPopup');
|
||||
@@ -22,13 +22,16 @@ template(name="boardList")
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
|
||||
p.board-list-item-desc= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
|
||||
|
||||
p.board-list-item-desc= description
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ 'my-boards'}}
|
||||
@@ -36,3 +39,6 @@ template(name="boardListHeaderBar")
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archives'}}
|
||||
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
i.fa.fa-clone
|
||||
span {{_ 'templates'}}
|
||||
@@ -1,5 +1,20 @@
|
||||
const subManager = new SubsManager();
|
||||
|
||||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
templatesBoardId() {
|
||||
return Meteor.user().getTemplatesBoardId();
|
||||
},
|
||||
templatesBoardSlug() {
|
||||
return Meteor.user().getTemplatesBoardSlug();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
Meteor.subscribe('setting');
|
||||
@@ -9,11 +24,9 @@ BlazeComponent.extendComponent({
|
||||
return Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
type: 'board',
|
||||
}, { sort: ['title'] });
|
||||
},
|
||||
|
||||
isStarred() {
|
||||
const user = Meteor.user();
|
||||
return user && user.hasStarred(this.currentData()._id);
|
||||
@@ -42,6 +55,21 @@ BlazeComponent.extendComponent({
|
||||
Meteor.user().toggleBoardStar(boardId);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click .js-clone-board'(evt) {
|
||||
Meteor.call('cloneBoard',
|
||||
this.currentData()._id,
|
||||
Session.get('fromBoard'),
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
this.setError(err.error);
|
||||
} else {
|
||||
Session.set('fromBoard', null);
|
||||
Utils.goBoardId(res);
|
||||
}
|
||||
}
|
||||
);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click .js-accept-invite'() {
|
||||
const boardId = this.currentData()._id;
|
||||
Meteor.user().removeInvite(boardId);
|
||||
|
||||
@@ -94,13 +94,27 @@ $spaceBetweenTiles = 16px
|
||||
.is-star-active
|
||||
color: white
|
||||
|
||||
.fa-clone
|
||||
position: absolute;
|
||||
bottom: 0
|
||||
font-size: 14px
|
||||
height: 18px
|
||||
line-height: 18px
|
||||
opacity: 0
|
||||
right: 0
|
||||
padding: 9px 9px
|
||||
transition-duration: .15s
|
||||
transition-property: color, font-size, background
|
||||
|
||||
li:hover a
|
||||
&:hover
|
||||
.fa-star,
|
||||
.fa-clone,
|
||||
.fa-star-o
|
||||
color: white
|
||||
|
||||
.fa-star,
|
||||
.fa-clone,
|
||||
.fa-star-o
|
||||
color: white
|
||||
opacity: .75
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
template(name="miniboard")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -1,6 +1,6 @@
|
||||
template(name="cardDetails")
|
||||
section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
|
||||
.card-details-header
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
@@ -19,16 +19,14 @@ template(name="cardDetails")
|
||||
a.js-parent-card(href=linkForCard) {{title}}
|
||||
// else
|
||||
{{_ 'top-level-card'}}
|
||||
unless isSandstorm
|
||||
if isLinkedCard
|
||||
h3.linked-card-location
|
||||
+viewer
|
||||
| {{getBoardTitle}} > {{getTitle}}
|
||||
if isLinkedCard
|
||||
h3.linked-card-location
|
||||
+viewer
|
||||
| {{getBoardTitle}} > {{getTitle}}
|
||||
|
||||
if getArchived
|
||||
if isLinkedBoard
|
||||
unless isSandstorm
|
||||
p.warning {{_ 'board-archived'}}
|
||||
p.warning {{_ 'board-archived'}}
|
||||
else
|
||||
p.warning {{_ 'card-archived'}}
|
||||
|
||||
@@ -192,11 +190,9 @@ template(name="cardDetails")
|
||||
unless currentUser.isNoComments
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
unless isSandstorm
|
||||
+activities(card=this mode="linkedcard")
|
||||
+activities(card=this mode="linkedcard")
|
||||
else if isLinkedBoard
|
||||
unless isSandstorm
|
||||
+activities(card=this mode="linkedboard")
|
||||
+activities(card=this mode="linkedboard")
|
||||
else
|
||||
+activities(card=this mode="card")
|
||||
|
||||
@@ -225,15 +221,16 @@ template(name="cardDetailsActionsPopup")
|
||||
if canModifyCard
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-members {{_ 'card-edit-members'}}
|
||||
li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
//li: a.js-members {{_ 'card-edit-members'}}
|
||||
//li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
|
||||
li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
|
||||
li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
|
||||
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
|
||||
li: a.js-set-card-color {{_ 'setCardColorPopup-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
|
||||
@@ -264,7 +261,7 @@ template(name="copyChecklistToManyCardsPopup")
|
||||
|
||||
template(name="boardsAndLists")
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
@@ -302,40 +299,48 @@ template(name="cardMorePopup")
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}")
|
||||
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'}}
|
||||
span.clearfix
|
||||
br
|
||||
h2 {{_ 'change-card-parent'}}
|
||||
label {{_ 'source-board'}}:
|
||||
select.js-field-parent-board
|
||||
if isTopLevel
|
||||
option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
each boards
|
||||
if isParentBoard
|
||||
option(value="{{_id}}" selected) {{title}}
|
||||
else
|
||||
option(value="{{_id}}") {{title}}
|
||||
if isTopLevel
|
||||
option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
|
||||
label {{_ 'parent-card'}}:
|
||||
select.js-field-parent-card
|
||||
if isTopLevel
|
||||
option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
if isParentCard
|
||||
option(value="{{_id}}" selected) {{title}}
|
||||
else
|
||||
option(value="{{_id}}") {{title}}
|
||||
option(value="none") {{_ 'custom-field-dropdown-none'}}
|
||||
br
|
||||
| {{_ 'added'}}
|
||||
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
|
||||
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
|
||||
|
||||
|
||||
template(name="setCardColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, enableClickOnTouch } = Utils;
|
||||
|
||||
let cardColors;
|
||||
Meteor.startup(() => {
|
||||
cardColors = Cards.simpleSchema()._schema.color.allowedValues;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
mixins() {
|
||||
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
|
||||
@@ -73,14 +78,13 @@ BlazeComponent.extendComponent({
|
||||
//Scroll top
|
||||
const cardViewStartTop = $cardView.offset().top;
|
||||
const cardContainerScrollTop = $cardContainer.scrollTop();
|
||||
|
||||
let topOffset = false;
|
||||
if(cardViewStartTop < 0){
|
||||
topOffset = 0;
|
||||
} else if(cardViewStartTop - cardContainerScrollTop > 100) {
|
||||
topOffset = cardViewStartTop - cardContainerScrollTop - 100;
|
||||
if(cardViewStartTop !== 100){
|
||||
topOffset = cardViewStartTop - 100;
|
||||
}
|
||||
if(topOffset !== false) {
|
||||
bodyBoardComponent.scrollTop(topOffset);
|
||||
bodyBoardComponent.scrollTop(cardContainerScrollTop + topOffset);
|
||||
}
|
||||
|
||||
},
|
||||
@@ -112,6 +116,7 @@ BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
if (!Utils.isMiniScreen()) {
|
||||
Meteor.setTimeout(() => {
|
||||
$('.card-details').mCustomScrollbar({theme:'minimal-dark', setWidth: false, setLeft: 0, scrollbarPosition: 'outside', mouseWheel: true });
|
||||
this.scrollParentContainer();
|
||||
}, 500);
|
||||
}
|
||||
@@ -336,6 +341,7 @@ Template.cardDetailsActionsPopup.events({
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'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-move-card-to-top' (evt) {
|
||||
evt.preventDefault();
|
||||
const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort));
|
||||
@@ -406,11 +412,13 @@ Template.moveCardPopup.events({
|
||||
// XXX We should *not* get the currentCard from the global state, but
|
||||
// instead from a “component” state.
|
||||
const card = Cards.findOne(Session.get('currentCard'));
|
||||
const bSelect = $('.js-select-boards')[0];
|
||||
const boardId = bSelect.options[bSelect.selectedIndex].value;
|
||||
const lSelect = $('.js-select-lists')[0];
|
||||
const newListId = lSelect.options[lSelect.selectedIndex].value;
|
||||
const listId = lSelect.options[lSelect.selectedIndex].value;
|
||||
const slSelect = $('.js-select-swimlanes')[0];
|
||||
card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
|
||||
card.move(card.swimlaneId, newListId, 0);
|
||||
const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
|
||||
card.move(boardId, swimlaneId, listId, 0);
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
@@ -424,6 +432,7 @@ BlazeComponent.extendComponent({
|
||||
const boards = Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$ne: Meteor.user().getTemplatesBoardId()},
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
@@ -450,32 +459,15 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('boardsAndLists');
|
||||
|
||||
|
||||
function cloneCheckList(_id, checklist) {
|
||||
'use strict';
|
||||
const checklistId = checklist._id;
|
||||
checklist.cardId = _id;
|
||||
checklist._id = null;
|
||||
const newChecklistId = Checklists.insert(checklist);
|
||||
ChecklistItems.find({checklistId}).forEach(function(item) {
|
||||
item._id = null;
|
||||
item.checklistId = newChecklistId;
|
||||
item.cardId = _id;
|
||||
ChecklistItems.insert(item);
|
||||
});
|
||||
}
|
||||
|
||||
Template.copyCardPopup.events({
|
||||
'click .js-done'() {
|
||||
const card = Cards.findOne(Session.get('currentCard'));
|
||||
const oldId = card._id;
|
||||
card._id = null;
|
||||
const lSelect = $('.js-select-lists')[0];
|
||||
card.listId = lSelect.options[lSelect.selectedIndex].value;
|
||||
listId = lSelect.options[lSelect.selectedIndex].value;
|
||||
const slSelect = $('.js-select-swimlanes')[0];
|
||||
card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
|
||||
const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
|
||||
const bSelect = $('.js-select-boards')[0];
|
||||
card.boardId = bSelect.options[bSelect.selectedIndex].value;
|
||||
const boardId = bSelect.options[bSelect.selectedIndex].value;
|
||||
const textarea = $('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
// insert new card to the bottom of new list
|
||||
@@ -484,38 +476,13 @@ Template.copyCardPopup.events({
|
||||
if (title) {
|
||||
card.title = title;
|
||||
card.coverId = '';
|
||||
const _id = Cards.insert(card);
|
||||
const _id = card.copy(boardId, swimlaneId, listId);
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
// card will disappear instantly.
|
||||
// See https://github.com/wekan/wekan/issues/80
|
||||
Filter.addException(_id);
|
||||
|
||||
// copy checklists
|
||||
let cursor = Checklists.find({cardId: oldId});
|
||||
cursor.forEach(function() {
|
||||
cloneCheckList(_id, arguments[0]);
|
||||
});
|
||||
|
||||
// copy subtasks
|
||||
cursor = Cards.find({parentId: oldId});
|
||||
cursor.forEach(function() {
|
||||
'use strict';
|
||||
const subtask = arguments[0];
|
||||
subtask.parentId = _id;
|
||||
subtask._id = null;
|
||||
/* const newSubtaskId = */ Cards.insert(subtask);
|
||||
});
|
||||
|
||||
// copy card comments
|
||||
cursor = CardComments.find({cardId: oldId});
|
||||
cursor.forEach(function () {
|
||||
'use strict';
|
||||
const comment = arguments[0];
|
||||
comment.cardId = _id;
|
||||
comment._id = null;
|
||||
CardComments.insert(comment);
|
||||
});
|
||||
Popup.close();
|
||||
}
|
||||
},
|
||||
@@ -552,9 +519,8 @@ Template.copyChecklistToManyCardsPopup.events({
|
||||
Filter.addException(_id);
|
||||
|
||||
// copy checklists
|
||||
let cursor = Checklists.find({cardId: oldId});
|
||||
cursor.forEach(function() {
|
||||
cloneCheckList(_id, arguments[0]);
|
||||
Checklists.find({cardId: oldId}).forEach((ch) => {
|
||||
ch.copy(_id);
|
||||
});
|
||||
|
||||
// copy subtasks
|
||||
@@ -568,13 +534,8 @@ Template.copyChecklistToManyCardsPopup.events({
|
||||
});
|
||||
|
||||
// copy card comments
|
||||
cursor = CardComments.find({cardId: oldId});
|
||||
cursor.forEach(function () {
|
||||
'use strict';
|
||||
const comment = arguments[0];
|
||||
comment.cardId = _id;
|
||||
comment._id = null;
|
||||
CardComments.insert(comment);
|
||||
CardComments.find({cardId: oldId}).forEach((cmt) => {
|
||||
cmt.copy(_id);
|
||||
});
|
||||
}
|
||||
Popup.close();
|
||||
@@ -585,11 +546,48 @@ Template.copyChecklistToManyCardsPopup.events({
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
this.currentColor = new ReactiveVar(this.currentCard.color);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return cardColors.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
if (this.currentColor.get() === null) {
|
||||
return color === 'white';
|
||||
}
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
'click .js-submit' () {
|
||||
this.currentCard.setColor(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-remove-color'() {
|
||||
this.currentCard.setColor(null);
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('setCardColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
this.parentBoard = new ReactiveVar(null);
|
||||
this.parentCard = this.currentCard.parentCard();
|
||||
if (this.parentCard) {
|
||||
this.parentBoard = this.parentCard.board();
|
||||
const list = $('.js-field-parent-card');
|
||||
list.val(this.parentCard._id);
|
||||
this.parentBoard.set(this.parentCard.board()._id);
|
||||
} else {
|
||||
this.parentBoard = null;
|
||||
this.parentBoard.set(null);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -597,6 +595,9 @@ BlazeComponent.extendComponent({
|
||||
const boards = Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {
|
||||
$ne: Meteor.user().getTemplatesBoardId(),
|
||||
},
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
@@ -604,8 +605,12 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
cards() {
|
||||
if (this.parentBoard) {
|
||||
return this.parentBoard.cards();
|
||||
const currentId = Session.get('currentCard');
|
||||
if (this.parentBoard.get()) {
|
||||
return Cards.find({
|
||||
boardId: this.parentBoard.get(),
|
||||
_id: {$ne: currentId},
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -613,8 +618,8 @@ BlazeComponent.extendComponent({
|
||||
|
||||
isParentBoard() {
|
||||
const board = this.currentData();
|
||||
if (this.parentBoard) {
|
||||
return board._id === this.parentBoard;
|
||||
if (this.parentBoard.get()) {
|
||||
return board._id === this.parentBoard.get();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -628,11 +633,10 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
setParentCardId(cardId) {
|
||||
if (cardId === 'null') {
|
||||
cardId = null;
|
||||
this.parentCard = null;
|
||||
} else {
|
||||
if (cardId) {
|
||||
this.parentCard = Cards.findOne(cardId);
|
||||
} else {
|
||||
this.parentCard = null;
|
||||
}
|
||||
this.currentCard.setParentId(cardId);
|
||||
},
|
||||
@@ -669,23 +673,14 @@ BlazeComponent.extendComponent({
|
||||
'change .js-field-parent-board'(evt) {
|
||||
const selection = $(evt.currentTarget).val();
|
||||
const list = $('.js-field-parent-card');
|
||||
list.empty();
|
||||
if (selection === 'none') {
|
||||
this.parentBoard = null;
|
||||
list.prop('disabled', true);
|
||||
this.parentBoard.set(null);
|
||||
} else {
|
||||
this.parentBoard = Boards.findOne(selection);
|
||||
this.parentBoard.cards().forEach(function(card) {
|
||||
list.append(
|
||||
$('<option></option>').val(card._id).html(card.title)
|
||||
);
|
||||
});
|
||||
subManager.subscribe('board', $(evt.currentTarget).val());
|
||||
this.parentBoard.set(selection);
|
||||
list.prop('disabled', false);
|
||||
}
|
||||
list.append(
|
||||
`<option value='none' selected='selected'>${TAPi18n.__('custom-field-dropdown-none')}</option>`
|
||||
);
|
||||
this.setParentCardId('null');
|
||||
this.setParentCardId(null);
|
||||
},
|
||||
'change .js-field-parent-card'(evt) {
|
||||
const selection = $(evt.currentTarget).val();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@import 'nib'
|
||||
|
||||
.card-details
|
||||
padding: 0 20px
|
||||
padding: 0
|
||||
flex-shrink: 0
|
||||
flex-basis: 470px
|
||||
flex-basis: 510px
|
||||
will-change: flex-basis
|
||||
overflow-y: scroll
|
||||
overflow-x: hidden
|
||||
@@ -14,8 +14,16 @@
|
||||
box-shadow: 0 0 7px 0 darken(white, 30%)
|
||||
transition: flex-basis 0.1s
|
||||
|
||||
.mCustomScrollBox
|
||||
padding-left: 0
|
||||
|
||||
.ps-scrollbar-y-rail
|
||||
pointer-event: all
|
||||
position: absolute;
|
||||
|
||||
.card-details-canvas
|
||||
width: 470px
|
||||
padding-left: 20px;
|
||||
|
||||
.card-details-header
|
||||
margin: 0 -20px 5px
|
||||
@@ -125,10 +133,92 @@ input[type="submit"].attachment-add-link-submit
|
||||
|
||||
.card-details-canvas
|
||||
width: 100%
|
||||
|
||||
padding-left: 0px;
|
||||
|
||||
.card-details-header
|
||||
.close-card-details
|
||||
margin-right: 0px
|
||||
|
||||
.card-details-menu
|
||||
margin-right: 10px
|
||||
|
||||
card-details-color(background, color...)
|
||||
background: background !important
|
||||
if color
|
||||
color: color !important //overwrite text for better visibility
|
||||
|
||||
.card-details-white
|
||||
card-details-color(unset, #000) //Black text for better visibility
|
||||
border: 1px solid #eee
|
||||
|
||||
.card-details-green
|
||||
card-details-color(#3cb500, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-yellow
|
||||
card-details-color(#fad900, #000) //Black text for better visibility
|
||||
|
||||
.card-details-orange
|
||||
card-details-color(#ff9f19, #000) //Black text for better visibility
|
||||
|
||||
.card-details-red
|
||||
card-details-color(#eb4646, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-purple
|
||||
card-details-color(#a632db, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-blue
|
||||
card-details-color(#0079bf, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-pink
|
||||
card-details-color(#ff78cb, #000) //Black text for better visibility
|
||||
|
||||
.card-details-sky
|
||||
card-details-color(#00c2e0, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-black
|
||||
card-details-color(#4d4d4d, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-lime
|
||||
card-details-color(#51e898, #000) //Black text for better visibility
|
||||
|
||||
.card-details-silver
|
||||
card-details-color(#c0c0c0, #000) //Black text for better visibility
|
||||
|
||||
.card-details-peachpuff
|
||||
card-details-color(#ffdab9, #000) //Black text for better visibility
|
||||
|
||||
.card-details-crimson
|
||||
card-details-color(#dc143c, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-plum
|
||||
card-details-color(#dda0dd, #000) //Black text for better visibility
|
||||
|
||||
.card-details-darkgreen
|
||||
card-details-color(#006400, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-slateblue
|
||||
card-details-color(#6a5acd, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-magenta
|
||||
card-details-color(#ff00ff, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-gold
|
||||
card-details-color(#ffd700, #000) //Black text for better visibility
|
||||
|
||||
.card-details-navy
|
||||
card-details-color(#000080, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-gray
|
||||
card-details-color(#808080, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-saddlebrown
|
||||
card-details-color(#8b4513, #ffffff) //White text for better visibility
|
||||
|
||||
.card-details-paleturquoise
|
||||
card-details-color(#afeeee, #000) //Black text for better visibility
|
||||
|
||||
.card-details-mistyrose
|
||||
card-details-color(#ffe4e1, #000) //Black text for better visibility
|
||||
|
||||
.card-details-indigo
|
||||
card-details-color(#4b0082, #ffffff) //White text for better visibility
|
||||
@@ -51,7 +51,6 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
|
||||
padding-right: 3%
|
||||
z-index: 17
|
||||
border-radius: 3px
|
||||
top: 50%
|
||||
|
||||
p
|
||||
position: relative
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
template(name="minicard")
|
||||
.minicard(
|
||||
class="{{#if isLinkedCard}}linked-card{{/if}}"
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}")
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="minicard-{{colorClass}}")
|
||||
if cover
|
||||
.minicard-cover(style="background-image: url('{{cover.url}}');")
|
||||
if labels
|
||||
@@ -9,6 +10,8 @@ template(name="minicard")
|
||||
each labels
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
|
||||
.parent-prefix
|
||||
| {{ parentString ' > ' }}
|
||||
|
||||
@@ -94,6 +94,19 @@
|
||||
.minicard-custom-field-item
|
||||
max-width:50%;
|
||||
flex-grow:1;
|
||||
.handle
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
display:none;
|
||||
@media only screen and (max-width: 1199px) {
|
||||
display:block;
|
||||
}
|
||||
.fa-arrows
|
||||
font-size:20px;
|
||||
color: #ccc;
|
||||
.minicard-title
|
||||
p:last-child
|
||||
margin-bottom: 0
|
||||
@@ -189,3 +202,86 @@
|
||||
border-top-right-radius: 0
|
||||
z-index: 15
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.15)
|
||||
|
||||
minicard-color(background, color...)
|
||||
background-color: background
|
||||
if color
|
||||
color: color //overwrite text for better visibility
|
||||
&:hover:not(.minicard-composer),
|
||||
.is-selected &,
|
||||
.draggable-hover-card &
|
||||
background: darken(background, 3%)
|
||||
.draggable-hover-card &
|
||||
background: darken(background, 7%)
|
||||
|
||||
.minicard-green
|
||||
minicard-color(#3cb500, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-yellow
|
||||
minicard-color(#fad900)
|
||||
|
||||
.minicard-orange
|
||||
minicard-color(#ff9f19)
|
||||
|
||||
.minicard-red
|
||||
minicard-color(#eb4646, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-purple
|
||||
minicard-color(#a632db, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-blue
|
||||
minicard-color(#0079bf, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-pink
|
||||
minicard-color(#ff78cb)
|
||||
|
||||
.minicard-sky
|
||||
minicard-color(#00c2e0, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-black
|
||||
minicard-color(#4d4d4d, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-lime
|
||||
minicard-color(#51e898)
|
||||
|
||||
.minicard-silver
|
||||
minicard-color(#c0c0c0)
|
||||
|
||||
.minicard-peachpuff
|
||||
minicard-color(#ffdab9)
|
||||
|
||||
.minicard-crimson
|
||||
minicard-color(#dc143c, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-plum
|
||||
minicard-color(#dda0dd)
|
||||
|
||||
.minicard-darkgreen
|
||||
minicard-color(#006400, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-slateblue
|
||||
minicard-color(#6a5acd, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-magenta
|
||||
minicard-color(#ff00ff, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-gold
|
||||
minicard-color(#ffd700)
|
||||
|
||||
.minicard-navy
|
||||
minicard-color(#000080, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-gray
|
||||
minicard-color(#808080, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-saddlebrown
|
||||
minicard-color(#8b4513, #ffffff) //White text for better visibility
|
||||
|
||||
.minicard-paleturquoise
|
||||
minicard-color(#afeeee)
|
||||
|
||||
.minicard-mistyrose
|
||||
minicard-color(#ffe4e1)
|
||||
|
||||
.minicard-indigo
|
||||
minicard-color(#4b0082, #ffffff) //White text for better visibility
|
||||
@@ -26,6 +26,13 @@ BlazeComponent.extendComponent({
|
||||
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
|
||||
if(window.matchMedia('(max-width: 1199px)').matches) {
|
||||
$( '.js-minicards' ).sortable({
|
||||
handle: '.handle',
|
||||
});
|
||||
}
|
||||
|
||||
$cards.sortable({
|
||||
connectWith: '.js-minicards:not(.js-list-full)',
|
||||
tolerance: 'pointer',
|
||||
@@ -60,7 +67,13 @@ BlazeComponent.extendComponent({
|
||||
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||
const swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
let swimlaneId = '';
|
||||
const boardView = Meteor.user().profile.boardView;
|
||||
if (boardView === 'board-view-swimlanes' || currentBoard.isTemplatesBoard())
|
||||
swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
|
||||
else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal'))
|
||||
swimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
@@ -73,12 +86,12 @@ BlazeComponent.extendComponent({
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
|
||||
card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
|
||||
card.move(currentBoard._id, swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
|
||||
});
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
card.move(swimlaneId, listId, sortIndex.base);
|
||||
card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
// transparent, because that won't work during a list drag.
|
||||
background: darken(white, 13%)
|
||||
border-left: 1px solid darken(white, 20%)
|
||||
border-bottom: 1px solid #CCC
|
||||
padding: 0
|
||||
float: left
|
||||
|
||||
@@ -44,12 +43,24 @@
|
||||
background: white
|
||||
margin: -3px 0 8px
|
||||
|
||||
.list-header
|
||||
.list-header-card-count
|
||||
height: 35px
|
||||
|
||||
.list-header-add
|
||||
flex: 0 0 auto
|
||||
margin: 20px 12px 4px
|
||||
padding: 20px 12px 4px
|
||||
position: relative
|
||||
min-height: 20px
|
||||
|
||||
.list-header
|
||||
flex: 0 0 auto
|
||||
padding: 20px 12px 4px
|
||||
position: relative
|
||||
min-height: 20px
|
||||
background-color: #e4e4e4;
|
||||
border-bottom: 6px solid #e4e4e4;
|
||||
|
||||
|
||||
&.ui-sortable-handle
|
||||
cursor: grab
|
||||
|
||||
@@ -68,16 +79,18 @@
|
||||
text-overflow: ellipsis
|
||||
word-wrap: break-word
|
||||
|
||||
|
||||
.list-header-watch-icon
|
||||
padding-left: 10px
|
||||
color: #a6a6a6
|
||||
|
||||
|
||||
.list-header-menu
|
||||
position: absolute
|
||||
padding: 7px
|
||||
padding: 27px 19px
|
||||
margin-top: 1px
|
||||
top: -@padding
|
||||
right: -@padding
|
||||
top: -7px
|
||||
right: -7px
|
||||
|
||||
.list-header-plus-icon
|
||||
color: #a6a6a6
|
||||
@@ -143,9 +156,12 @@
|
||||
float: left
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
.list-header-menu
|
||||
margin-right: 30px
|
||||
|
||||
.mini-list
|
||||
flex: 0 0 60px
|
||||
height: 60px
|
||||
height: auto
|
||||
width: 100%
|
||||
border-left: 0px
|
||||
border-bottom: 1px solid darken(white, 20%)
|
||||
@@ -154,6 +170,8 @@
|
||||
display: block
|
||||
width: 100%
|
||||
border-left: 0px
|
||||
&:first-child
|
||||
margin-left: 0px
|
||||
|
||||
&.ui-sortable-helper
|
||||
flex: 0 0 60px
|
||||
@@ -172,8 +190,16 @@
|
||||
border-left: 0px
|
||||
border-bottom: 1px solid darken(white, 20%)
|
||||
|
||||
.list-header
|
||||
.list-body
|
||||
padding: 15px 19px;
|
||||
|
||||
.list-header
|
||||
padding: 0 12px 0px
|
||||
border-bottom: 0px solid #e4e4e4
|
||||
height: 60px
|
||||
margin-top: 10px
|
||||
display: flex
|
||||
align-items: center
|
||||
.list-header-left-icon
|
||||
display: inline
|
||||
padding: 7px
|
||||
@@ -185,8 +211,10 @@
|
||||
.list-header-menu-icon
|
||||
position: absolute
|
||||
padding: 7px
|
||||
top: -@padding
|
||||
top: 50%
|
||||
transform: translateY(-50%)
|
||||
right: 17px
|
||||
font-size: 20px
|
||||
|
||||
.link-board-wrapper
|
||||
display: flex
|
||||
@@ -198,3 +226,85 @@
|
||||
.search-card-results
|
||||
max-height: 250px
|
||||
overflow: hidden
|
||||
|
||||
.sk-spinner-list
|
||||
margin-top: unset !important
|
||||
|
||||
list-header-color(background, color...)
|
||||
border-bottom: 6px solid background
|
||||
|
||||
.list-header-white
|
||||
list-header-color(#ffffff, #4d4d4d) //Black text for better visibility
|
||||
border: 1px solid #eee
|
||||
|
||||
.list-header-green
|
||||
list-header-color(#3cb500, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-yellow
|
||||
list-header-color(#fad900, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-orange
|
||||
list-header-color(#ff9f19, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-red
|
||||
list-header-color(#eb4646, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-purple
|
||||
list-header-color(#a632db, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-blue
|
||||
list-header-color(#0079bf, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-pink
|
||||
list-header-color(#ff78cb, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-sky
|
||||
list-header-color(#00c2e0, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-black
|
||||
list-header-color(#4d4d4d, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-lime
|
||||
list-header-color(#51e898, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-silver
|
||||
list-header-color(unset, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-peachpuff
|
||||
list-header-color(#ffdab9, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-crimson
|
||||
list-header-color(#dc143c, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-plum
|
||||
list-header-color(#dda0dd, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-darkgreen
|
||||
list-header-color(#006400, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-slateblue
|
||||
list-header-color(#6a5acd, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-magenta
|
||||
list-header-color(#ff00ff, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-gold
|
||||
list-header-color(#ffd700, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-navy
|
||||
list-header-color(#000080, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-gray
|
||||
list-header-color(#808080, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-saddlebrown
|
||||
list-header-color(#8b4513, #ffffff) //White text for better visibility
|
||||
|
||||
.list-header-paleturquoise
|
||||
list-header-color(#afeeee, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-mistyrose
|
||||
list-header-color(#ffe4e1, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.list-header-indigo
|
||||
list-header-color(#4b0082, #ffffff) //White text for better visibility
|
||||
@@ -4,7 +4,7 @@ template(name="listBody")
|
||||
if cards.count
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
each (cards (idOrNull ../../_id))
|
||||
each (cardsWithLimit (idOrNull ../../_id))
|
||||
a.minicard-wrapper.js-minicard(href=absoluteUrl
|
||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
@@ -12,6 +12,9 @@ template(name="listBody")
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
|
||||
if canSeeAddCard
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
@@ -20,6 +23,16 @@ template(name="listBody")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-card'}}
|
||||
|
||||
template(name="spinnerList")
|
||||
.sk-spinner.sk-spinner-wave.sk-spinner-list(
|
||||
class=currentBoard.colorClass
|
||||
id="showMoreResults")
|
||||
.sk-rect1
|
||||
.sk-rect2
|
||||
.sk-rect3
|
||||
.sk-rect4
|
||||
.sk-rect5
|
||||
|
||||
template(name="addCardForm")
|
||||
.minicard.minicard-composer.js-composer
|
||||
if getLabels
|
||||
@@ -34,14 +47,19 @@ template(name="addCardForm")
|
||||
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
unless isSandstorm
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-link {{_ 'link'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-search {{_ 'search'}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-link {{_ 'link'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-search {{_ 'search'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-card-template {{_ 'template'}}
|
||||
|
||||
template(name="autocompleteLabelLine")
|
||||
.minicard-label(class="card-label-{{colorName}}" title=labelName)
|
||||
@@ -51,11 +69,9 @@ template(name="linkCardPopup")
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
else
|
||||
option(value="{{_id}}") {{title}}
|
||||
option(value="{{_id}}") {{title}}
|
||||
input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
@@ -74,22 +90,41 @@ template(name="linkCardPopup")
|
||||
option(value="{{getId}}") {{getTitle}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
unless isSandstorm
|
||||
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
|
||||
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
|
||||
|
||||
template(name="searchCardPopup")
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
select.js-select-boards
|
||||
each boards
|
||||
if $eq _id currentBoard._id
|
||||
option(value="{{_id}}" selected) {{_ 'current'}}
|
||||
else
|
||||
template(name="searchElementPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required)
|
||||
unless isTemplateSearch
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each boards
|
||||
option(value="{{_id}}") {{title}}
|
||||
form.js-search-term-form
|
||||
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus)
|
||||
.list-body.js-perfect-scrollbar.search-card-results
|
||||
.minicards.clearfix.js-minicards
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
if isBoardTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniboard(this)
|
||||
if isListTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minilist(this)
|
||||
if isSwimlaneTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniswimlane(this)
|
||||
if isCardTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
unless isTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
@@ -1,6 +1,12 @@
|
||||
const subManager = new SubsManager();
|
||||
const InfiniteScrollIter = 10;
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// for infinite scrolling
|
||||
this.cardlimit = new ReactiveVar(InfiniteScrollIter);
|
||||
},
|
||||
|
||||
mixins() {
|
||||
return [Mixins.PerfectScrollbar];
|
||||
},
|
||||
@@ -39,27 +45,56 @@ BlazeComponent.extendComponent({
|
||||
const labelIds = formComponent.labels.get();
|
||||
const customFields = formComponent.customFields.get();
|
||||
|
||||
const boardId = this.data().board();
|
||||
const board = this.data().board();
|
||||
let linkedId = '';
|
||||
let swimlaneId = '';
|
||||
const boardView = Meteor.user().profile.boardView;
|
||||
if (boardView === 'board-view-swimlanes')
|
||||
swimlaneId = this.parentComponent().parentComponent().data()._id;
|
||||
else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal'))
|
||||
swimlaneId = boardId.getDefaultSwimline()._id;
|
||||
|
||||
let cardType = 'cardType-card';
|
||||
if (title) {
|
||||
if (board.isTemplatesBoard()) {
|
||||
swimlaneId = this.parentComponent().parentComponent().data()._id; // Always swimlanes view
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
// If this is the card templates swimlane, insert a card template
|
||||
if (swimlane.isCardTemplatesSwimlane())
|
||||
cardType = 'template-card';
|
||||
// If this is the board templates swimlane, insert a board template and a linked card
|
||||
else if (swimlane.isBoardTemplatesSwimlane()) {
|
||||
linkedId = Boards.insert({
|
||||
title,
|
||||
permission: 'private',
|
||||
type: 'template-board',
|
||||
});
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
boardId: linkedId,
|
||||
});
|
||||
cardType = 'cardType-linkedBoard';
|
||||
}
|
||||
} else if (boardView === 'board-view-swimlanes')
|
||||
swimlaneId = this.parentComponent().parentComponent().data()._id;
|
||||
else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal'))
|
||||
swimlaneId = board.getDefaultSwimline()._id;
|
||||
|
||||
const _id = Cards.insert({
|
||||
title,
|
||||
members,
|
||||
labelIds,
|
||||
customFields,
|
||||
listId: this.data()._id,
|
||||
boardId: boardId._id,
|
||||
boardId: board._id,
|
||||
sort: sortIndex,
|
||||
swimlaneId,
|
||||
type: 'cardType-card',
|
||||
type: cardType,
|
||||
linkedId,
|
||||
});
|
||||
|
||||
// if the displayed card count is less than the total cards in the list,
|
||||
// we need to increment the displayed card count to prevent the spinner
|
||||
// to appear
|
||||
const cardCount = this.data().cards(this.idOrNull(swimlaneId)).count();
|
||||
if (this.cardlimit.get() < cardCount) {
|
||||
this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
|
||||
}
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
@@ -92,9 +127,9 @@ BlazeComponent.extendComponent({
|
||||
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
|
||||
MultiSelection[methodName](this.currentData()._id);
|
||||
|
||||
// If the card is already selected, we want to de-select it.
|
||||
// XXX We should probably modify the minicard href attribute instead of
|
||||
// overwriting the event in case the card is already selected.
|
||||
// If the card is already selected, we want to de-select it.
|
||||
// XXX We should probably modify the minicard href attribute instead of
|
||||
// overwriting the event in case the card is already selected.
|
||||
} else if (Session.equals('currentCard', this.currentData()._id)) {
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
@@ -114,11 +149,31 @@ BlazeComponent.extendComponent({
|
||||
|
||||
idOrNull(swimlaneId) {
|
||||
const currentUser = Meteor.user();
|
||||
if (currentUser.profile.boardView === 'board-view-swimlanes')
|
||||
if (currentUser.profile.boardView === 'board-view-swimlanes' ||
|
||||
this.data().board().isTemplatesBoard())
|
||||
return swimlaneId;
|
||||
return undefined;
|
||||
},
|
||||
|
||||
cardsWithLimit(swimlaneId) {
|
||||
const limit = this.cardlimit.get();
|
||||
const selector = {
|
||||
listId: this.currentData()._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId)
|
||||
selector.swimlaneId = swimlaneId;
|
||||
return Cards.find(Filter.mongoSelector(selector), {
|
||||
sort: ['sort'],
|
||||
limit,
|
||||
});
|
||||
},
|
||||
|
||||
showSpinner(swimlaneId) {
|
||||
const list = Template.currentData();
|
||||
return list.cards(swimlaneId).count() > this.cardlimit.get();
|
||||
},
|
||||
|
||||
canSeeAddCard() {
|
||||
return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
},
|
||||
@@ -179,7 +234,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
pressKey(evt) {
|
||||
// Pressing Enter should submit the card
|
||||
if (evt.keyCode === 13) {
|
||||
if (evt.keyCode === 13 && !evt.shiftKey) {
|
||||
evt.preventDefault();
|
||||
const $form = $(evt.currentTarget).closest('form');
|
||||
// XXX For some reason $form.submit() does not work (it's probably a bug
|
||||
@@ -188,8 +243,8 @@ BlazeComponent.extendComponent({
|
||||
// work.
|
||||
$form.find('button[type=submit]').click();
|
||||
|
||||
// Pressing Tab should open the form of the next column, and Maj+Tab go
|
||||
// in the reverse order
|
||||
// Pressing Tab should open the form of the next column, and Maj+Tab go
|
||||
// in the reverse order
|
||||
} else if (evt.keyCode === 9) {
|
||||
evt.preventDefault();
|
||||
const isReverse = evt.shiftKey;
|
||||
@@ -211,7 +266,8 @@ BlazeComponent.extendComponent({
|
||||
return [{
|
||||
keydown: this.pressKey,
|
||||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-search': Popup.open('searchCard'),
|
||||
'click .js-search': Popup.open('searchElement'),
|
||||
'click .js-card-template': Popup.open('searchElement'),
|
||||
}];
|
||||
},
|
||||
|
||||
@@ -249,7 +305,7 @@ BlazeComponent.extendComponent({
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
callback($.map(currentBoard.labels, (label) => {
|
||||
if (label.name.indexOf(term) > -1 ||
|
||||
label.color.indexOf(term) > -1) {
|
||||
label.color.indexOf(term) > -1) {
|
||||
return label;
|
||||
}
|
||||
return null;
|
||||
@@ -286,17 +342,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// Prefetch first non-current board id
|
||||
const boardId = Boards.findOne({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$ne: Session.get('currentBoard')},
|
||||
}, {
|
||||
sort: ['title'],
|
||||
})._id;
|
||||
// Subscribe to this board
|
||||
subManager.subscribe('board', boardId);
|
||||
this.selectedBoardId = new ReactiveVar(boardId);
|
||||
this.selectedBoardId = new ReactiveVar('');
|
||||
this.selectedSwimlaneId = new ReactiveVar('');
|
||||
this.selectedListId = new ReactiveVar('');
|
||||
|
||||
@@ -322,6 +368,7 @@ BlazeComponent.extendComponent({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$ne: Session.get('currentBoard')},
|
||||
type: 'board',
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
@@ -329,7 +376,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
swimlanes() {
|
||||
if (!this.selectedBoardId) {
|
||||
if (!this.selectedBoardId.get()) {
|
||||
return [];
|
||||
}
|
||||
const swimlanes = Swimlanes.find({boardId: this.selectedBoardId.get()});
|
||||
@@ -339,7 +386,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
lists() {
|
||||
if (!this.selectedBoardId) {
|
||||
if (!this.selectedBoardId.get()) {
|
||||
return [];
|
||||
}
|
||||
const lists = Lists.find({boardId: this.selectedBoardId.get()});
|
||||
@@ -360,6 +407,7 @@ BlazeComponent.extendComponent({
|
||||
archived: false,
|
||||
linkedId: {$nin: ownCardsIds},
|
||||
_id: {$nin: ownCardsIds},
|
||||
type: {$nin: ['template-card']},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -427,12 +475,25 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
// Prefetch first non-current board id
|
||||
let board = Boards.findOne({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$ne: Session.get('currentBoard')},
|
||||
});
|
||||
this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-card-template');
|
||||
this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-list-template');
|
||||
this.isSwimlaneTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-open-add-swimlane-menu');
|
||||
this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-add-board');
|
||||
this.isTemplateSearch = this.isCardTemplateSearch ||
|
||||
this.isListTemplateSearch ||
|
||||
this.isSwimlaneTemplateSearch ||
|
||||
this.isBoardTemplateSearch;
|
||||
let board = {};
|
||||
if (this.isTemplateSearch) {
|
||||
board = Boards.findOne(Meteor.user().profile.templatesBoardId);
|
||||
} else {
|
||||
// Prefetch first non-current board id
|
||||
board = Boards.findOne({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$nin: [Session.get('currentBoard'), Meteor.user().profile.templatesBoardId]},
|
||||
});
|
||||
}
|
||||
if (!board) {
|
||||
Popup.close();
|
||||
return;
|
||||
@@ -442,20 +503,21 @@ BlazeComponent.extendComponent({
|
||||
subManager.subscribe('board', boardId);
|
||||
this.selectedBoardId = new ReactiveVar(boardId);
|
||||
|
||||
this.boardId = Session.get('currentBoard');
|
||||
// In order to get current board info
|
||||
subManager.subscribe('board', this.boardId);
|
||||
board = Boards.findOne(this.boardId);
|
||||
// List where to insert card
|
||||
const list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
this.listId = Blaze.getData(list[0])._id;
|
||||
// Swimlane where to insert card
|
||||
const swimlane = $(Popup._getTopStack().openerElement).closest('.js-swimlane');
|
||||
this.swimlaneId = '';
|
||||
if (board.view === 'board-view-swimlanes')
|
||||
this.swimlaneId = Blaze.getData(swimlane[0])._id;
|
||||
else
|
||||
this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
|
||||
if (!this.isBoardTemplateSearch) {
|
||||
this.boardId = Session.get('currentBoard');
|
||||
// In order to get current board info
|
||||
subManager.subscribe('board', this.boardId);
|
||||
this.swimlaneId = '';
|
||||
// Swimlane where to insert card
|
||||
const swimlane = $(Popup._getTopStack().openerElement).parents('.js-swimlane');
|
||||
if (Meteor.user().profile.boardView === 'board-view-swimlanes')
|
||||
this.swimlaneId = Blaze.getData(swimlane[0])._id;
|
||||
else
|
||||
this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
|
||||
// List where to insert card
|
||||
const list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
this.listId = Blaze.getData(list[0])._id;
|
||||
}
|
||||
this.term = new ReactiveVar('');
|
||||
},
|
||||
|
||||
@@ -464,6 +526,7 @@ BlazeComponent.extendComponent({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
_id: {$ne: Session.get('currentBoard')},
|
||||
type: 'board',
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
@@ -475,7 +538,21 @@ BlazeComponent.extendComponent({
|
||||
return [];
|
||||
}
|
||||
const board = Boards.findOne(this.selectedBoardId.get());
|
||||
return board.searchCards(this.term.get(), false);
|
||||
if (!this.isTemplateSearch || this.isCardTemplateSearch) {
|
||||
return board.searchCards(this.term.get(), false);
|
||||
} else if (this.isListTemplateSearch) {
|
||||
return board.searchLists(this.term.get());
|
||||
} else if (this.isSwimlaneTemplateSearch) {
|
||||
return board.searchSwimlanes(this.term.get());
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
const boards = board.searchBoards(this.term.get());
|
||||
boards.forEach((board) => {
|
||||
subManager.subscribe('board', board.linkedId);
|
||||
});
|
||||
return boards;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
@@ -489,20 +566,99 @@ BlazeComponent.extendComponent({
|
||||
this.term.set(evt.target.searchTerm.value);
|
||||
},
|
||||
'click .js-minicard'(evt) {
|
||||
// LINK CARD
|
||||
const card = Blaze.getData(evt.currentTarget);
|
||||
const _id = Cards.insert({
|
||||
title: card.title, //dummy
|
||||
listId: this.listId,
|
||||
swimlaneId: this.swimlaneId,
|
||||
boardId: this.boardId,
|
||||
sort: Lists.findOne(this.listId).cards().count(),
|
||||
type: 'cardType-linkedCard',
|
||||
linkedId: card.linkedId || card._id,
|
||||
});
|
||||
Filter.addException(_id);
|
||||
// 0. Common
|
||||
const title = $('.js-element-title').val().trim();
|
||||
if (!title)
|
||||
return;
|
||||
const element = Blaze.getData(evt.currentTarget);
|
||||
element.title = title;
|
||||
let _id = '';
|
||||
if (!this.isTemplateSearch || this.isCardTemplateSearch) {
|
||||
// Card insertion
|
||||
// 1. Common
|
||||
element.sort = Lists.findOne(this.listId).cards().count();
|
||||
// 1.A From template
|
||||
if (this.isTemplateSearch) {
|
||||
element.type = 'cardType-card';
|
||||
element.linkedId = '';
|
||||
_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);
|
||||
}
|
||||
Filter.addException(_id);
|
||||
// List insertion
|
||||
} else if (this.isListTemplateSearch) {
|
||||
element.sort = Swimlanes.findOne(this.swimlaneId).lists().count();
|
||||
element.type = 'list';
|
||||
_id = element.copy(this.boardId, this.swimlaneId);
|
||||
} else if (this.isSwimlaneTemplateSearch) {
|
||||
element.sort = Boards.findOne(this.boardId).swimlanes().count();
|
||||
element.type = 'swimlalne';
|
||||
_id = element.copy(this.boardId);
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
board = Boards.findOne(element.linkedId);
|
||||
board.sort = Boards.find({archived: false}).count();
|
||||
board.type = 'board';
|
||||
board.title = element.title;
|
||||
delete board.slug;
|
||||
_id = board.copy();
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('searchCardPopup');
|
||||
}).register('searchElementPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.cardlimit = this.parentComponent().cardlimit;
|
||||
|
||||
this.listId = this.parentComponent().data()._id;
|
||||
this.swimlaneId = '';
|
||||
|
||||
const boardView = Meteor.user().profile.boardView;
|
||||
if (boardView === 'board-view-swimlanes')
|
||||
this.swimlaneId = this.parentComponent().parentComponent().parentComponent().data()._id;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
this.spinner = this.find('.sk-spinner-list');
|
||||
this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0];
|
||||
|
||||
$(this.container).on(`scroll.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
|
||||
$(window).on(`resize.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
|
||||
|
||||
this.updateList();
|
||||
},
|
||||
|
||||
onDestroyed() {
|
||||
$(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`);
|
||||
$(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`);
|
||||
},
|
||||
|
||||
updateList() {
|
||||
if (this.spinnerInView()) {
|
||||
this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
|
||||
window.requestIdleCallback(() => this.updateList());
|
||||
}
|
||||
},
|
||||
|
||||
spinnerInView() {
|
||||
const parentViewHeight = this.container.clientHeight;
|
||||
const bottomViewPosition = this.container.scrollTop + parentViewHeight;
|
||||
|
||||
const threshold = this.spinner.offsetTop;
|
||||
|
||||
// spinner deleted
|
||||
if (!this.spinner.offsetTop) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return bottomViewPosition > threshold;
|
||||
},
|
||||
|
||||
}).register('spinnerList');
|
||||
@@ -1,5 +1,7 @@
|
||||
template(name="listHeader")
|
||||
.list-header.js-list-header
|
||||
.list-header.js-list-header(
|
||||
class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
|
||||
class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}")
|
||||
+inlinedForm
|
||||
+editListTitleForm
|
||||
else
|
||||
@@ -49,6 +51,9 @@ template(name="listActionPopup")
|
||||
li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
|
||||
unless currentUser.isCommentOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-set-color-list {{_ 'set-color-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if cards.count
|
||||
li: a.js-select-cards {{_ 'list-select-cards'}}
|
||||
@@ -111,3 +116,13 @@ template(name="wipLimitErrorPopup")
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
// note: we use the swimlane palette to have more than just the border
|
||||
span.card-label.palette-color.js-palette-color(class="swimlane-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
@@ -1,3 +1,8 @@
|
||||
let listsColors;
|
||||
Meteor.startup(() => {
|
||||
listsColors = Lists.simpleSchema()._schema.color.allowedValues;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
canSeeAddCard() {
|
||||
const list = Template.currentData();
|
||||
@@ -72,6 +77,7 @@ Template.listActionPopup.helpers({
|
||||
|
||||
Template.listActionPopup.events({
|
||||
'click .js-list-subscribe' () {},
|
||||
'click .js-set-color-list': Popup.open('setListColor'),
|
||||
'click .js-select-cards' () {
|
||||
const cardIds = this.allCards().map((card) => card._id);
|
||||
MultiSelection.add(cardIds);
|
||||
@@ -154,3 +160,34 @@ Template.listMorePopup.events({
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentList = this.currentData();
|
||||
this.currentColor = new ReactiveVar(this.currentList.color);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return listsColors.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
'click .js-submit' () {
|
||||
this.currentList.setColor(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-remove-color'() {
|
||||
this.currentList.setColor(null);
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('setListColorPopup');
|
||||
@@ -0,0 +1,8 @@
|
||||
template(name="minilist")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -36,7 +36,10 @@ import sanitizeXss from 'xss';
|
||||
const at = HTML.CharRef({html: '@', str: '@'});
|
||||
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
|
||||
const view = this;
|
||||
let content = Blaze.toHTML(view.templateContentBlock);
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
if (!currentBoard)
|
||||
return HTML.Raw(sanitizeXss(content));
|
||||
const knowedUsers = currentBoard.members.map((member) => {
|
||||
const u = Users.findOne(member.userId);
|
||||
if(u){
|
||||
@@ -45,7 +48,6 @@ Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
|
||||
return member;
|
||||
});
|
||||
const mentionRegex = /\B@([\w.]*)/gi;
|
||||
let content = Blaze.toHTML(view.templateContentBlock);
|
||||
|
||||
let currentMention;
|
||||
while ((currentMention = mentionRegex.exec(content)) !== null) {
|
||||
|
||||
@@ -4,39 +4,38 @@ template(name="header")
|
||||
list all starred boards with a link to go there. This is inspired by the
|
||||
Reddit "subreddit" bar.
|
||||
The first link goes to the boards page.
|
||||
unless isSandstorm
|
||||
if currentUser
|
||||
#header-quick-access(class=currentBoard.colorClass)
|
||||
if isMiniScreen
|
||||
ul
|
||||
li
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
if currentUser
|
||||
#header-quick-access(class=currentBoard.colorClass)
|
||||
if isMiniScreen
|
||||
ul
|
||||
li
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
|
||||
if currentList
|
||||
each currentBoard.lists
|
||||
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
|
||||
a.js-select-list
|
||||
= title
|
||||
#header-new-board-icon
|
||||
else
|
||||
ul
|
||||
li
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
each currentUser.starredBoards
|
||||
li.separator -
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
if currentList
|
||||
each currentBoard.lists
|
||||
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
|
||||
a.js-select-list
|
||||
= title
|
||||
else
|
||||
li.current {{_ 'quick-access-description'}}
|
||||
#header-new-board-icon
|
||||
else
|
||||
ul
|
||||
li
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
each currentUser.starredBoards
|
||||
li.separator -
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
= title
|
||||
else
|
||||
li.current {{_ 'quick-access-description'}}
|
||||
|
||||
a#header-new-board-icon.js-create-board
|
||||
i.fa.fa-plus(title="Create a new board")
|
||||
a#header-new-board-icon.js-create-board
|
||||
i.fa.fa-plus(title="Create a new board")
|
||||
|
||||
+headerUserBar
|
||||
+headerUserBar
|
||||
|
||||
#header(class=currentBoard.colorClass)
|
||||
//-
|
||||
@@ -46,19 +45,16 @@ template(name="header")
|
||||
#header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
|
||||
+Template.dynamic(template=headerBar)
|
||||
|
||||
unless hideLogo
|
||||
//unless hideLogo
|
||||
|
||||
//-
|
||||
On sandstorm, the logo shouldn't be clickable, because we only have one
|
||||
page/document on it, and we don't want to see the home page containing
|
||||
the list of all boards.
|
||||
if isSandstorm
|
||||
.wekan-logo
|
||||
img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
|
||||
else
|
||||
unless currentSetting.hideLogo
|
||||
a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
|
||||
img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
|
||||
|
||||
// unless currentSetting.hideLogo
|
||||
// a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
|
||||
// img(src="{{pathFor '/logo-header.png'}}" alt="")
|
||||
|
||||
if appIsOffline
|
||||
+offlineWarning
|
||||
|
||||
@@ -188,8 +188,6 @@
|
||||
width: 100%
|
||||
padding: 10px 0px
|
||||
z-index: 30
|
||||
position: absolute
|
||||
bottom: 0px
|
||||
|
||||
ul
|
||||
width: calc(100% - 60px)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
head
|
||||
title Wekan
|
||||
meta(name="viewport"
|
||||
content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
|
||||
title
|
||||
meta(name="viewport" content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
|
||||
meta(http-equiv="X-UA-Compatible" content="IE=edge")
|
||||
//- XXX We should use pathFor in the following `href` to support the case
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
@@ -9,35 +8,32 @@ head
|
||||
packages.
|
||||
link(rel="shortcut icon" href="/wekan-favicon.png")
|
||||
link(rel="apple-touch-icon" href="/wekan-favicon.png")
|
||||
link(rel="mask-icon" href="/wekan-150.svg")
|
||||
link(rel="mask-icon" href="/wekan-logo-150.svg")
|
||||
link(rel="manifest" href="/wekan-manifest.json")
|
||||
|
||||
template(name="userFormsLayout")
|
||||
section.auth-layout
|
||||
unless currentSetting.hideLogo
|
||||
h1.at-form-landing-logo
|
||||
img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
|
||||
if currentSetting.hideLogo
|
||||
h1
|
||||
br
|
||||
br
|
||||
section.auth-dialog
|
||||
+Template.dynamic(template=content)
|
||||
if isCas
|
||||
.at-form
|
||||
button#cas(class='at-btn submit' type='submit') {{casSignInLabel}}
|
||||
div.at-form-lang
|
||||
select.select-lang.js-userform-set-language
|
||||
each languages
|
||||
if isCurrentLanguage
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
if isLoading
|
||||
+loader
|
||||
else
|
||||
+Template.dynamic(template=content)
|
||||
if currentSetting.displayAuthenticationMethod
|
||||
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
|
||||
div.at-form-lang
|
||||
select.select-lang.js-userform-set-language
|
||||
each languages
|
||||
if isCurrentLanguage
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
|
||||
template(name="defaultLayout")
|
||||
+header
|
||||
#content
|
||||
| {{{afterBodyStart}}}
|
||||
+Template.dynamic(template=content)
|
||||
| {{{beforeBodyEnd}}}
|
||||
if (Modal.isOpen)
|
||||
#modal
|
||||
.overlay
|
||||
@@ -63,3 +59,14 @@ template(name="message")
|
||||
unless currentUser
|
||||
with(pathFor route='atSignIn')
|
||||
p {{{_ 'page-maybe-private' this}}}
|
||||
|
||||
template(name="loader")
|
||||
h1.loadingText {{_ 'loading'}}
|
||||
.lds-roller
|
||||
div
|
||||
div
|
||||
div
|
||||
div
|
||||
div
|
||||
div
|
||||
div
|
||||
@@ -6,14 +6,36 @@ const i18nTagToT9n = (i18nTag) => {
|
||||
return i18nTag;
|
||||
};
|
||||
|
||||
const validator = {
|
||||
set(obj, prop, value) {
|
||||
if (prop === 'state' && value !== 'signIn') {
|
||||
$('.at-form-authentication').hide();
|
||||
} else if (prop === 'state' && value === 'signIn') {
|
||||
$('.at-form-authentication').show();
|
||||
}
|
||||
// The default behavior to store the value
|
||||
obj[prop] = value;
|
||||
// Indicate success
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
Template.userFormsLayout.onCreated(function() {
|
||||
Meteor.call('getDefaultAuthenticationMethod', (error, result) => {
|
||||
this.data.defaultAuthenticationMethod = new ReactiveVar(error ? undefined : result);
|
||||
const instance = this;
|
||||
instance.currentSetting = new ReactiveVar();
|
||||
instance.isLoading = new ReactiveVar(false);
|
||||
|
||||
Meteor.subscribe('setting', {
|
||||
onReady() {
|
||||
instance.currentSetting.set(Settings.findOne());
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
Meteor.subscribe('setting');
|
||||
});
|
||||
|
||||
Template.userFormsLayout.onRendered(() => {
|
||||
AccountsTemplates.state.form.keys = new Proxy(AccountsTemplates.state.form.keys, validator);
|
||||
|
||||
const i18nTag = navigator.language;
|
||||
if (i18nTag) {
|
||||
T9n.setLanguage(i18nTagToT9n(i18nTag));
|
||||
@@ -23,7 +45,19 @@ Template.userFormsLayout.onRendered(() => {
|
||||
|
||||
Template.userFormsLayout.helpers({
|
||||
currentSetting() {
|
||||
return Settings.findOne();
|
||||
return Template.instance().currentSetting.get();
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().isLoading.get();
|
||||
},
|
||||
|
||||
afterBodyStart() {
|
||||
return currentSetting.customHTMLafterBodyStart;
|
||||
},
|
||||
|
||||
beforeBodyEnd() {
|
||||
return currentSetting.customHTMLbeforeBodyEnd;
|
||||
},
|
||||
|
||||
languages() {
|
||||
@@ -50,17 +84,6 @@ Template.userFormsLayout.helpers({
|
||||
const curLang = T9n.getLanguage() || 'en';
|
||||
return t9nTag === curLang;
|
||||
},
|
||||
/*
|
||||
isCas() {
|
||||
return Meteor.settings.public &&
|
||||
Meteor.settings.public.cas &&
|
||||
Meteor.settings.public.cas.loginUrl;
|
||||
},
|
||||
|
||||
casSignInLabel() {
|
||||
return TAPi18n.__('casSignIn', {}, T9n.getLanguage() || 'en');
|
||||
},
|
||||
*/
|
||||
});
|
||||
|
||||
Template.userFormsLayout.events({
|
||||
@@ -69,33 +92,14 @@ Template.userFormsLayout.events({
|
||||
T9n.setLanguage(i18nTagToT9n(i18nTag));
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click button#cas'() {
|
||||
Meteor.loginWithCas(function() {
|
||||
if (FlowRouter.getRouteName() === 'atSignIn') {
|
||||
FlowRouter.go('/');
|
||||
}
|
||||
});
|
||||
},
|
||||
'click #at-btn'(event, instance) {
|
||||
/* All authentication method can be managed/called here.
|
||||
!! DON'T FORGET to correctly fill the fields of the user during its creation if necessary authenticationMethod : String !!
|
||||
*/
|
||||
const email = $('#at-field-username_and_email').val();
|
||||
const password = $('#at-field-password').val();
|
||||
|
||||
if (FlowRouter.getRouteName() !== 'atSignIn' || password === '' || email === '') {
|
||||
return;
|
||||
if (FlowRouter.getRouteName() === 'atSignIn') {
|
||||
instance.isLoading.set(true);
|
||||
authentication(event, instance)
|
||||
.then(() => {
|
||||
instance.isLoading.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Stop submit #at-pwd-form
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
Meteor.subscribe('user-authenticationMethod', email, {
|
||||
onReady() {
|
||||
return authentication.call(this, instance, email, password);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -105,37 +109,63 @@ Template.defaultLayout.events({
|
||||
},
|
||||
});
|
||||
|
||||
function authentication(instance, email, password) {
|
||||
let user = Users.findOne();
|
||||
// Authentication with password
|
||||
if (user && user.authenticationMethod === 'password') {
|
||||
$('#at-pwd-form').submit();
|
||||
// Meteor.call('logoutWithTimer', user._id, () => {});
|
||||
return this.stop();
|
||||
}
|
||||
async function authentication(event, instance) {
|
||||
const match = $('#at-field-username_and_email').val();
|
||||
const password = $('#at-field-password').val();
|
||||
|
||||
// If user doesn't exist, uses the default authentication method if it defined
|
||||
if (user === undefined) {
|
||||
user = {
|
||||
'authenticationMethod': instance.data.defaultAuthenticationMethod.get(),
|
||||
};
|
||||
}
|
||||
if (!match || !password) return undefined;
|
||||
|
||||
// Authentication with LDAP
|
||||
if (user.authenticationMethod === 'ldap') {
|
||||
// Use the ldap connection package
|
||||
Meteor.loginWithLDAP(email, password, function(error) {
|
||||
if (!error) {
|
||||
// Meteor.call('logoutWithTimer', Users.findOne()._id, () => {});
|
||||
return FlowRouter.go('/');
|
||||
}
|
||||
return error;
|
||||
const result = await getAuthenticationMethod(instance.currentSetting.get(), match);
|
||||
|
||||
if (result === 'password') return undefined;
|
||||
|
||||
// Stop submit #at-pwd-form
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
switch (result) {
|
||||
case 'ldap':
|
||||
return new Promise((resolve) => {
|
||||
Meteor.loginWithLDAP(match, password, function() {
|
||||
resolve(FlowRouter.go('/'));
|
||||
});
|
||||
});
|
||||
|
||||
case 'cas':
|
||||
return new Promise((resolve) => {
|
||||
Meteor.loginWithCas(match, password, function() {
|
||||
resolve(FlowRouter.go('/'));
|
||||
});
|
||||
});
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/* else {
|
||||
process.env.DEFAULT_AUTHENTICATION_METHOD is not defined
|
||||
} */
|
||||
|
||||
return this.stop();
|
||||
}
|
||||
|
||||
function getAuthenticationMethod({displayAuthenticationMethod, defaultAuthenticationMethod}, match) {
|
||||
if (displayAuthenticationMethod) {
|
||||
return $('.select-authentication').val();
|
||||
}
|
||||
return getUserAuthenticationMethod(defaultAuthenticationMethod, match);
|
||||
}
|
||||
|
||||
function getUserAuthenticationMethod(defaultAuthenticationMethod, match) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
Meteor.subscribe('user-authenticationMethod', match, {
|
||||
onReady() {
|
||||
const user = Users.findOne();
|
||||
|
||||
const authenticationMethod = user
|
||||
? user.authenticationMethod
|
||||
: defaultAuthenticationMethod;
|
||||
|
||||
resolve(authenticationMethod);
|
||||
},
|
||||
});
|
||||
} catch(error) {
|
||||
resolve(defaultAuthenticationMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -61,7 +61,7 @@ body
|
||||
display: block
|
||||
float: right
|
||||
font-size: 24px
|
||||
|
||||
|
||||
.modal-content-wide
|
||||
width: 800px
|
||||
min-height: 0px
|
||||
@@ -290,7 +290,7 @@ kbd
|
||||
// Implement a thiner close icon as suggested in
|
||||
// https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
|
||||
.fa.fa-times-thin:before
|
||||
content: '\00d7';
|
||||
content: '\00d7'
|
||||
|
||||
.fa.fa-globe.colorful, .fa.fa-bell.colorful
|
||||
color: #4caf50
|
||||
@@ -385,8 +385,8 @@ a
|
||||
|
||||
@media screen and (max-width: 800px)
|
||||
#content
|
||||
margin: 1px 0px 49px 0px
|
||||
height: calc(100% - 96px)
|
||||
margin: 1px 0px 0px 0px
|
||||
height: calc(100% - 0px)
|
||||
|
||||
> .wrapper
|
||||
margin-top: 0px
|
||||
@@ -399,3 +399,103 @@ a
|
||||
height: 37px
|
||||
margin: 8px 10px 0 0
|
||||
width: 50px
|
||||
|
||||
.select-authentication
|
||||
width: 100%
|
||||
|
||||
.auth-layout
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
height: 100%
|
||||
|
||||
.auth-dialog
|
||||
margin: 0 !important
|
||||
|
||||
.loadingText
|
||||
text-align: center
|
||||
|
||||
.lds-roller
|
||||
display: block
|
||||
margin: auto
|
||||
position: relative
|
||||
width: 64px
|
||||
height: 64px
|
||||
|
||||
div
|
||||
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite
|
||||
transform-origin: 32px 32px
|
||||
|
||||
div:after
|
||||
content: " "
|
||||
display: block
|
||||
position: absolute
|
||||
width: 6px
|
||||
height: 6px
|
||||
border-radius: 50%
|
||||
background: #dedede
|
||||
margin: -3px 0 0 -3px
|
||||
|
||||
div:nth-child(1)
|
||||
animation-delay: -0.036s
|
||||
|
||||
div:nth-child(1):after
|
||||
top: 50px
|
||||
left: 50px
|
||||
|
||||
div:nth-child(2)
|
||||
animation-delay: -0.072s
|
||||
|
||||
div:nth-child(2):after
|
||||
top: 54px
|
||||
left: 45px
|
||||
|
||||
div:nth-child(3)
|
||||
animation-delay: -0.108s
|
||||
|
||||
div:nth-child(3):after
|
||||
top: 57px
|
||||
left: 39px
|
||||
|
||||
div:nth-child(4)
|
||||
animation-delay: -0.144s
|
||||
|
||||
div:nth-child(4):after
|
||||
top: 58px
|
||||
left: 32px
|
||||
|
||||
div:nth-child(5)
|
||||
animation-delay: -0.18s
|
||||
|
||||
div:nth-child(5):after
|
||||
top: 57px
|
||||
left: 25px
|
||||
|
||||
div:nth-child(6)
|
||||
animation-delay: -0.216s
|
||||
|
||||
div:nth-child(6):after
|
||||
top: 54px
|
||||
left: 19px
|
||||
|
||||
div:nth-child(7)
|
||||
animation-delay: -0.252s
|
||||
|
||||
div:nth-child(7):after
|
||||
top: 50px
|
||||
left: 14px
|
||||
|
||||
div:nth-child(8)
|
||||
animation-delay: -0.288s
|
||||
|
||||
div:nth-child(8):after
|
||||
top: 45px
|
||||
left: 10px
|
||||
|
||||
@keyframes lds-roller
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
@@ -36,7 +36,33 @@ template(name="boardActions")
|
||||
div.trigger-text
|
||||
| {{_'r-card'}}
|
||||
div.trigger-button.js-add-arch-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-add-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-swimlane-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-create-card'}}
|
||||
div.trigger-dropdown
|
||||
input(id="card-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-in-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="list-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-create-card-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,52 @@ BlazeComponent.extendComponent({
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-create-card-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
const cardName = this.find('#card-name').value;
|
||||
const listName = this.find('#list-name').value;
|
||||
const swimlaneName = this.find('#swimlane-name2').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'createCard',
|
||||
swimlaneName,
|
||||
cardName,
|
||||
listName,
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
|
||||
},
|
||||
'click .js-add-swimlane-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
const swimlaneName = this.find('#swimlane-name').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'addSwimlane',
|
||||
swimlaneName,
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
|
||||
},
|
||||
'click .js-add-spec-move-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
|
||||
@@ -35,9 +35,21 @@ template(name="cardActions")
|
||||
div.trigger-button.js-add-removeall-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{{_'r-set-color'}}}
|
||||
button.trigger-button.trigger-button-color.js-show-color-palette(
|
||||
id="color-action"
|
||||
class="card-details-{{cardColorButton}}")
|
||||
| {{{_ cardColorButtonText }}}
|
||||
div.trigger-button.js-set-color-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
template(name="setCardActionsColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
@@ -1,6 +1,20 @@
|
||||
let cardColors;
|
||||
Meteor.startup(() => {
|
||||
cardColors = Cards.simpleSchema()._schema.color.allowedValues;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.subscribe('allRules');
|
||||
this.cardColorButtonValue = new ReactiveVar('green');
|
||||
},
|
||||
|
||||
cardColorButton() {
|
||||
return this.cardColorButtonValue.get();
|
||||
},
|
||||
|
||||
cardColorButtonText() {
|
||||
return `color-${ this.cardColorButtonValue.get() }`;
|
||||
},
|
||||
|
||||
labels() {
|
||||
@@ -112,7 +126,63 @@ BlazeComponent.extendComponent({
|
||||
boardId,
|
||||
});
|
||||
},
|
||||
'click .js-show-color-palette'(event){
|
||||
const funct = Popup.open('setCardActionsColor');
|
||||
const colorButton = this.find('#color-action');
|
||||
if (colorButton.value === '') {
|
||||
colorButton.value = 'green';
|
||||
}
|
||||
funct.call(this, event);
|
||||
},
|
||||
'click .js-set-color-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
const selectedColor = this.cardColorButtonValue.get();
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'setColor',
|
||||
selectedColor,
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
},
|
||||
}];
|
||||
},
|
||||
|
||||
}).register('cardActions');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentAction = this.currentData();
|
||||
this.colorButtonValue = Popup.getOpenerComponent().cardColorButtonValue;
|
||||
this.currentColor = new ReactiveVar(this.colorButtonValue.get());
|
||||
},
|
||||
|
||||
colors() {
|
||||
return cardColors.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
'click .js-submit' () {
|
||||
this.colorButtonValue.set(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('setCardActionsColorPopup');
|
||||
@@ -43,6 +43,25 @@ template(name="checklistActions")
|
||||
div.trigger-button.js-add-check-item-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{{_'r-add-checklist'}}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name-3",type=text,placeholder="{{{_'r-name'}}}")
|
||||
div.trigger-text
|
||||
| {{{_'r-with-items'}}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-items",type=text,placeholder="{{{_'r-items-list'}}}")
|
||||
div.trigger-button.js-add-checklist-items-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{{_'r-checklist-note'}}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,29 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
events() {
|
||||
return [{
|
||||
'click .js-add-checklist-items-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
const checklistName = this.find('#checklist-name-3').value;
|
||||
const checklistItems = this.find('#checklist-items').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const triggerId = Triggers.insert(trigger);
|
||||
const actionId = Actions.insert({
|
||||
actionType: 'addChecklistWithItems',
|
||||
checklistName,
|
||||
checklistItems,
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
Rules.insert({
|
||||
title: ruleName,
|
||||
triggerId,
|
||||
actionId,
|
||||
boardId,
|
||||
});
|
||||
|
||||
},
|
||||
'click .js-add-checklist-action' (event) {
|
||||
const ruleName = this.data().ruleName.get();
|
||||
const trigger = this.data().triggerVar.get();
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
display: inline-block
|
||||
float: left
|
||||
margin: revert
|
||||
|
||||
.hide-element
|
||||
display:none !important
|
||||
.user-details
|
||||
display:inline-block
|
||||
.rules-btns-group
|
||||
position: absolute
|
||||
right: 0
|
||||
@@ -120,6 +123,15 @@
|
||||
.trigger-text
|
||||
font-size: 16px
|
||||
display:inline-block
|
||||
.trigger-inline-button
|
||||
font-size: 16px
|
||||
display: inline;
|
||||
padding: 6px;
|
||||
border: 1px solid #eee
|
||||
border-radius: 4px
|
||||
box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
|
||||
&:hover, &.is-active
|
||||
box-shadow: 0 0 0 2px darken(white, 60%) inset
|
||||
.trigger-text.trigger-text-email
|
||||
margin-left: 5px;
|
||||
margin-top: 10px;
|
||||
@@ -160,6 +172,17 @@
|
||||
box-shadow: 0 0 0 2px darken(white, 60%) inset
|
||||
.trigger-button.trigger-button-email
|
||||
top:30px
|
||||
.trigger-button.trigger-button-person
|
||||
right:-40px
|
||||
.trigger-button.trigger-button-color
|
||||
top: unset
|
||||
position: unset
|
||||
transform: unset
|
||||
font-size: 16px
|
||||
width:auto
|
||||
padding-left: 10px
|
||||
padding-right: 10px
|
||||
height:40px
|
||||
.trigger-item.trigger-item-mail
|
||||
height:300px
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ BlazeComponent.extendComponent({
|
||||
setTrigger() {
|
||||
this.rulesCurrentTab.set('trigger');
|
||||
},
|
||||
|
||||
sanitizeObject(obj){
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if(obj[key] === '' || obj[key] === undefined){
|
||||
obj[key] = '*';
|
||||
}}
|
||||
);
|
||||
},
|
||||
setRulesList() {
|
||||
this.rulesCurrentTab.set('rulesList');
|
||||
},
|
||||
@@ -42,8 +48,27 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
'click .js-goto-action' (event) {
|
||||
event.preventDefault();
|
||||
// Add user to the trigger
|
||||
const username = $(event.currentTarget.offsetParent).find('.user-name').val();
|
||||
let trigger = this.triggerVar.get();
|
||||
trigger.userId = '*';
|
||||
if(username !== undefined ){
|
||||
const userFound = Users.findOne({username});
|
||||
if(userFound !== undefined){
|
||||
trigger.userId = userFound._id;
|
||||
this.triggerVar.set(trigger);
|
||||
}
|
||||
}
|
||||
// Sanitize trigger
|
||||
trigger = this.triggerVar.get();
|
||||
this.sanitizeObject(trigger);
|
||||
this.triggerVar.set(trigger);
|
||||
this.setAction();
|
||||
},
|
||||
'click .js-show-user-field' (event) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget.offsetParent).find('.user-details').removeClass('hide-element');
|
||||
},
|
||||
'click .js-goto-rules' (event) {
|
||||
event.preventDefault();
|
||||
this.setRulesList();
|
||||
@@ -68,3 +93,5 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
}).register('rulesMain');
|
||||
|
||||
|
||||
@@ -1,43 +1,58 @@
|
||||
template(name="boardTriggers")
|
||||
div.trigger-item
|
||||
div.trigger-item#trigger-two
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="gen-action")
|
||||
option(value="created") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-filter
|
||||
div.trigger-text
|
||||
| {{_'r-the-board'}}
|
||||
div.trigger-button.js-add-gen-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
| {{_'r-is'}}
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="create-action")
|
||||
option(value="created") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
| {{_'r-added-to'}}
|
||||
div.trigger-text
|
||||
| {{_'r-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-create-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-item#trigger-three
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card-is-moved'}}
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-filter
|
||||
div.trigger-text
|
||||
| {{_'r-is-moved'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-moved-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-item#trigger-four
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card-is'}}
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-filter
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="move-action")
|
||||
option(value="moved-to") {{_'r-moved-to'}}
|
||||
@@ -45,21 +60,54 @@ template(name="boardTriggers")
|
||||
div.trigger-text
|
||||
| {{_'r-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-moved-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-item#trigger-five
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card-is'}}
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-filter
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="arch-action")
|
||||
option(value="archived") {{_'r-archived'}}
|
||||
option(value="unarchived") {{_'r-unarchived'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-arch-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
| {{{_'r-board-note'}}}
|
||||
|
||||
template(name="boardCardTitlePopup")
|
||||
form
|
||||
label
|
||||
| Card Title Filter
|
||||
input.js-card-filter-name(type="text" value=title autofocus)
|
||||
input.js-card-filter-button.primary.wide(type="submit" value="{{_ 'set-filter'}}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,67 +1,57 @@
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
|
||||
this.provaVar = new ReactiveVar('');
|
||||
this.currentPopupTriggerId = 'def';
|
||||
this.cardTitleFilters = {};
|
||||
},
|
||||
setNameFilter(name){
|
||||
this.cardTitleFilters[this.currentPopupTriggerId] = name;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-add-gen-trigger' (event) {
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const datas = this.data();
|
||||
const actionSelected = this.find('#gen-action').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (actionSelected === 'created') {
|
||||
datas.triggerVar.set({
|
||||
activityType: 'createCard',
|
||||
boardId,
|
||||
'listName': '*',
|
||||
desc,
|
||||
});
|
||||
}
|
||||
if (actionSelected === 'removed') {
|
||||
datas.triggerVar.set({
|
||||
activityType: 'removeCard',
|
||||
boardId,
|
||||
desc,
|
||||
});
|
||||
}
|
||||
|
||||
'click .js-open-card-title-popup'(event){
|
||||
const funct = Popup.open('boardCardTitle');
|
||||
const divId = $(event.currentTarget.parentNode.parentNode).attr('id');
|
||||
//console.log('current popup');
|
||||
//console.log(this.currentPopupTriggerId);
|
||||
this.currentPopupTriggerId = divId;
|
||||
funct.call(this, event);
|
||||
},
|
||||
'click .js-add-create-trigger' (event) {
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
const datas = this.data();
|
||||
const actionSelected = this.find('#create-action').value;
|
||||
const listName = this.find('#create-list-name').value;
|
||||
const swimlaneName = this.find('#create-swimlane-name').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (actionSelected === 'created') {
|
||||
datas.triggerVar.set({
|
||||
activityType: 'createCard',
|
||||
boardId,
|
||||
listName,
|
||||
desc,
|
||||
});
|
||||
}
|
||||
if (actionSelected === 'removed') {
|
||||
datas.triggerVar.set({
|
||||
activityType: 'removeCard',
|
||||
boardId,
|
||||
listName,
|
||||
desc,
|
||||
});
|
||||
}
|
||||
const divId = $(event.currentTarget.parentNode).attr('id');
|
||||
const cardTitle = this.cardTitleFilters[divId];
|
||||
// move to generic funciont
|
||||
datas.triggerVar.set({
|
||||
activityType: 'createCard',
|
||||
boardId,
|
||||
cardTitle,
|
||||
swimlaneName,
|
||||
listName,
|
||||
desc,
|
||||
});
|
||||
},
|
||||
'click .js-add-moved-trigger' (event) {
|
||||
const datas = this.data();
|
||||
const desc = Utils.getTriggerActionDesc(event, this);
|
||||
|
||||
const swimlaneName = this.find('#create-swimlane-name-2').value;
|
||||
const actionSelected = this.find('#move-action').value;
|
||||
const listName = this.find('#move-list-name').value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const divId = $(event.currentTarget.parentNode).attr('id');
|
||||
const cardTitle = this.cardTitleFilters[divId];
|
||||
if (actionSelected === 'moved-to') {
|
||||
datas.triggerVar.set({
|
||||
activityType: 'moveCard',
|
||||
boardId,
|
||||
listName,
|
||||
cardTitle,
|
||||
swimlaneName,
|
||||
'oldListName': '*',
|
||||
desc,
|
||||
});
|
||||
@@ -70,6 +60,8 @@ BlazeComponent.extendComponent({
|
||||
datas.triggerVar.set({
|
||||
activityType: 'moveCard',
|
||||
boardId,
|
||||
cardTitle,
|
||||
swimlaneName,
|
||||
'listName': '*',
|
||||
'oldListName': listName,
|
||||
desc,
|
||||
@@ -82,8 +74,9 @@ BlazeComponent.extendComponent({
|
||||
const boardId = Session.get('currentBoard');
|
||||
|
||||
datas.triggerVar.set({
|
||||
activityType: 'moveCard',
|
||||
'activityType': 'moveCard',
|
||||
boardId,
|
||||
'swimlaneName': '*',
|
||||
'listName':'*',
|
||||
'oldListName': '*',
|
||||
desc,
|
||||
@@ -114,3 +107,13 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
}).register('boardTriggers');
|
||||
|
||||
|
||||
Template.boardCardTitlePopup.events({
|
||||
submit(evt, tpl) {
|
||||
const title = tpl.$('.js-card-filter-name').val().trim();
|
||||
Popup.getOpenerComponent().setNameFilter(title);
|
||||
evt.preventDefault();
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
@@ -1,79 +1,114 @@
|
||||
template(name="cardTriggers")
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-label-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="label-action")
|
||||
option(value="added") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-label-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-the-label-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="spec-label")
|
||||
each labels
|
||||
option(value="#{_id}")
|
||||
= name
|
||||
div.trigger-text
|
||||
option(value="#{_id}" style="background-color: #{name}")
|
||||
= translatedname
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="spec-label-action")
|
||||
option(value="added") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-label-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-member'}}
|
||||
div.trigger-dropdown
|
||||
select(id="gen-member-action")
|
||||
option(value="added") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-member-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-the-member'}}
|
||||
div.trigger-dropdown
|
||||
input(id="spec-member",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
input(id="spec-member",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="spec-member-action")
|
||||
option(value="added") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-member-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-attach'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="attach-action")
|
||||
option(value="added") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-attachment-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
@@ -6,7 +6,10 @@ BlazeComponent.extendComponent({
|
||||
const labels = Boards.findOne(Session.get('currentBoard')).labels;
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
if (labels[i].name === '' || labels[i].name === undefined) {
|
||||
labels[i].name = labels[i].color.toUpperCase();
|
||||
labels[i].name = labels[i].color;
|
||||
labels[i].translatedname = `${TAPi18n.__(`color-${ labels[i].color}`)}`;
|
||||
} else {
|
||||
labels[i].translatedname = labels[i].name;
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
|
||||
@@ -9,6 +9,13 @@ template(name="checklistTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-check-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -27,6 +34,13 @@ template(name="checklistTriggers")
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-check-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -38,6 +52,13 @@ template(name="checklistTriggers")
|
||||
select(id="gen-comp-check-action")
|
||||
option(value="completed") {{_'r-completed'}}
|
||||
option(value="uncompleted") {{_'r-made-incomplete'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-comp-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -53,6 +74,13 @@ template(name="checklistTriggers")
|
||||
select(id="spec-comp-check-action")
|
||||
option(value="completed") {{_'r-completed'}}
|
||||
option(value="uncompleted") {{_'r-made-incomplete'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-comp-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -64,6 +92,13 @@ template(name="checklistTriggers")
|
||||
select(id="check-item-gen-action")
|
||||
option(value="checked") {{_'r-checked'}}
|
||||
option(value="unchecked") {{_'r-unchecked'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -79,5 +114,12 @@ template(name="checklistTriggers")
|
||||
select(id="check-item-spec-action")
|
||||
option(value="checked") {{_'r-checked'}}
|
||||
option(value="unchecked") {{_'r-unchecked'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
div.trigger-text
|
||||
| {{_'r-by'}}
|
||||
div.trigger-dropdown
|
||||
input(class="user-name",type=text,placeholder="{{_'username'}}")
|
||||
div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
|
||||
i.fa.fa-plus
|
||||
@@ -2,5 +2,8 @@ template(name='connectionMethod')
|
||||
div.at-form-authentication
|
||||
label {{_ 'authentication-method'}}
|
||||
select.select-authentication
|
||||
each authentications
|
||||
option(value="{{value}}") {{_ value}}
|
||||
each authentications
|
||||
if isSelected value
|
||||
option(value="{{value}}" selected) {{_ value}}
|
||||
else
|
||||
option(value="{{value}}") {{_ value}}
|
||||
@@ -31,4 +31,7 @@ Template.connectionMethod.helpers({
|
||||
authentications() {
|
||||
return Template.instance().authenticationMethods.get();
|
||||
},
|
||||
isSelected(match) {
|
||||
return Template.instance().data.authenticationMethod === match;
|
||||
},
|
||||
});
|
||||
@@ -17,7 +17,7 @@ template(name='statistics')
|
||||
table
|
||||
tbody
|
||||
tr
|
||||
th {{_ 'Wekan_version'}}
|
||||
th Wekan {{_ 'info'}}
|
||||
td {{statistics.version}}
|
||||
tr
|
||||
th {{_ 'Node_version'}}
|
||||
|
||||
@@ -3,8 +3,13 @@ template(name="people")
|
||||
unless currentUser.isAdmin
|
||||
| {{_ 'error-notAuthorized'}}
|
||||
else
|
||||
.content-title
|
||||
span {{_ 'people'}}
|
||||
.content-title.ext-box
|
||||
.ext-box-left
|
||||
span {{_ 'people'}}
|
||||
input#searchInput(placeholder="{{_ 'search'}}")
|
||||
button#searchButton {{_ 'search'}}
|
||||
.ext-box-right
|
||||
span {{_ 'people-number'}} #{peopleNumber}
|
||||
.content-body
|
||||
.side-menu
|
||||
ul
|
||||
@@ -103,4 +108,4 @@ template(name="editUserPopup")
|
||||
| {{_ 'password'}}
|
||||
input.js-profile-password(type="password")
|
||||
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
@@ -8,6 +8,8 @@ BlazeComponent.extendComponent({
|
||||
this.error = new ReactiveVar('');
|
||||
this.loading = new ReactiveVar(false);
|
||||
this.people = new ReactiveVar(true);
|
||||
this.findUsersOptions = new ReactiveVar({});
|
||||
this.number = new ReactiveVar(0);
|
||||
|
||||
this.page = new ReactiveVar(1);
|
||||
this.loadNextPageLocked = false;
|
||||
@@ -26,6 +28,33 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
});
|
||||
},
|
||||
events() {
|
||||
return [{
|
||||
'click #searchButton'() {
|
||||
this.filterPeople();
|
||||
},
|
||||
'keydown #searchInput'(event) {
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
this.filterPeople();
|
||||
}
|
||||
},
|
||||
}];
|
||||
},
|
||||
filterPeople() {
|
||||
const value = $('#searchInput').first().val();
|
||||
if (value === '') {
|
||||
this.findUsersOptions.set({});
|
||||
} else {
|
||||
const regex = new RegExp(value, 'i');
|
||||
this.findUsersOptions.set({
|
||||
$or: [
|
||||
{ username: regex },
|
||||
{ 'profile.fullname': regex },
|
||||
{ 'emails.address': regex },
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
loadNextPage() {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
@@ -49,9 +78,14 @@ BlazeComponent.extendComponent({
|
||||
this.loading.set(w);
|
||||
},
|
||||
peopleList() {
|
||||
return Users.find({}, {
|
||||
const users = Users.find(this.findUsersOptions.get(), {
|
||||
fields: {_id: true},
|
||||
});
|
||||
this.number.set(users.count());
|
||||
return users;
|
||||
},
|
||||
peopleNumber() {
|
||||
return this.number.get();
|
||||
},
|
||||
}).register('people');
|
||||
|
||||
|
||||
@@ -13,3 +13,24 @@ table
|
||||
|
||||
tr:nth-child(even)
|
||||
background-color: #dddddd;
|
||||
|
||||
.ext-box
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 34px;
|
||||
|
||||
.ext-box-left
|
||||
display: flex;
|
||||
width: 40%
|
||||
|
||||
span
|
||||
vertical-align: center;
|
||||
line-height: 34px;
|
||||
margin-right: 10px;
|
||||
|
||||
input, button
|
||||
margin: 0 10px 0 0;
|
||||
padding: 0;
|
||||
|
||||
button
|
||||
min-width: 60px;
|
||||
@@ -134,16 +134,41 @@ template(name='announcementSettings')
|
||||
|
||||
template(name='layoutSettings')
|
||||
ul#layout-setting.setting-detail
|
||||
li.layout-form
|
||||
//li.layout-form
|
||||
.title {{_ 'hide-logo'}}
|
||||
.form-group.flex
|
||||
input.form-control#hide-logo(type="radio" name="hideLogo" value="true" checked="{{#if currentSetting.hideLogo}}checked{{/if}}")
|
||||
span {{_ 'yes'}}
|
||||
input.form-control#hide-logo(type="radio" name="hideLogo" value="false" checked="{{#unless currentSetting.hideLogo}}checked{{/unless}}")
|
||||
span {{_ 'no'}}
|
||||
li.layout-form
|
||||
.title {{_ 'display-authentication-method'}}
|
||||
.form-group.flex
|
||||
input.form-control#display-authentication-method(type="radio" name="displayAuthenticationMethod" value="true" checked="{{#if currentSetting.displayAuthenticationMethod}}checked{{/if}}")
|
||||
span {{_ 'yes'}}
|
||||
input.form-control#display-authentication-method(type="radio" name="displayAuthenticationMethod" value="false" checked="{{#unless currentSetting.displayAuthenticationMethod}}checked{{/unless}}")
|
||||
span {{_ 'no'}}
|
||||
li.layout-form
|
||||
.title {{_ 'default-authentication-method'}}
|
||||
+selectAuthenticationMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
|
||||
li.layout-form
|
||||
.title {{_ 'custom-product-name'}}
|
||||
.form-group
|
||||
input.form-control#product-name(type="text", placeholder="Wekan" value="{{currentSetting.productName}}")
|
||||
input.form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}")
|
||||
li.layout-form
|
||||
.title {{_ 'add-custom-html-after-body-start'}}
|
||||
textarea#customHTMLafterBodyStart.form-control= currentSetting.customHTMLafterBodyStart
|
||||
li.layout-form
|
||||
.title {{_ 'add-custom-html-before-body-end'}}
|
||||
textarea#customHTMLbeforeBodyEnd.form-control= currentSetting.customHTMLbeforeBodyEnd
|
||||
li
|
||||
button.js-save-layout.primary {{_ 'save'}}
|
||||
|
||||
|
||||
template(name='selectAuthenticationMethod')
|
||||
select#defaultAuthenticationMethod
|
||||
each authentications
|
||||
if isSelected value
|
||||
option(value="{{value}}" selected) {{_ value}}
|
||||
else
|
||||
option(value="{{value}}") {{_ value}}
|
||||
@@ -62,6 +62,9 @@ BlazeComponent.extendComponent({
|
||||
toggleHideLogo() {
|
||||
$('#hide-logo').toggleClass('is-checked');
|
||||
},
|
||||
toggleDisplayAuthenticationMethod() {
|
||||
$('#display-authentication-method').toggleClass('is-checked');
|
||||
},
|
||||
switchMenu(event) {
|
||||
const target = $(event.target);
|
||||
if (!target.hasClass('active')) {
|
||||
@@ -87,7 +90,7 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
inviteThroughEmail() {
|
||||
const emails = $('#email-to-invite').val().trim().split('\n').join(',').split(',');
|
||||
const emails = $('#email-to-invite').val().toLowerCase().trim().split('\n').join(',').split(',');
|
||||
const boardsToInvite = [];
|
||||
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () {
|
||||
boardsToInvite.push($(this).data('id'));
|
||||
@@ -140,13 +143,20 @@ BlazeComponent.extendComponent({
|
||||
|
||||
const productName = $('#product-name').val().trim();
|
||||
const hideLogoChange = ($('input[name=hideLogo]:checked').val() === 'true');
|
||||
const displayAuthenticationMethod = ($('input[name=displayAuthenticationMethod]:checked').val() === 'true');
|
||||
const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
|
||||
const customHTMLafterBodyStart = $('#customHTMLafterBodyStart').val().trim();
|
||||
const customHTMLbeforeBodyEnd = $('#customHTMLbeforeBodyEnd').val().trim();
|
||||
|
||||
try {
|
||||
|
||||
Settings.update(Settings.findOne()._id, {
|
||||
$set: {
|
||||
productName,
|
||||
hideLogo: hideLogoChange,
|
||||
customHTMLafterBodyStart,
|
||||
customHTMLbeforeBodyEnd,
|
||||
displayAuthenticationMethod,
|
||||
defaultAuthenticationMethod,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -155,23 +165,20 @@ BlazeComponent.extendComponent({
|
||||
this.setLoading(false);
|
||||
}
|
||||
|
||||
saveMailServerInfo();
|
||||
DocHead.setTitle(productName);
|
||||
|
||||
},
|
||||
|
||||
sendSMTPTestEmail() {
|
||||
Meteor.call('sendSMTPTestEmail', (err, ret) => {
|
||||
if (!err && ret) { /* eslint-disable no-console */
|
||||
if (!err && ret) {
|
||||
const message = `${TAPi18n.__(ret.message)}: ${ret.email}`;
|
||||
console.log(message);
|
||||
alert(message);
|
||||
} else {
|
||||
const reason = err.reason || '';
|
||||
const message = `${TAPi18n.__(err.error)}\n${reason}`;
|
||||
console.log(message, err);
|
||||
alert(message);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
});
|
||||
},
|
||||
|
||||
@@ -186,6 +193,7 @@ BlazeComponent.extendComponent({
|
||||
'click button.js-send-smtp-test-email': this.sendSMTPTestEmail,
|
||||
'click a.js-toggle-hide-logo': this.toggleHideLogo,
|
||||
'click button.js-save-layout': this.saveLayout,
|
||||
'click a.js-toggle-display-authentication-method': this.toggleDisplayAuthenticationMethod,
|
||||
}];
|
||||
},
|
||||
}).register('setting');
|
||||
@@ -258,3 +266,29 @@ BlazeComponent.extendComponent({
|
||||
}];
|
||||
},
|
||||
}).register('announcementSettings');
|
||||
|
||||
|
||||
Template.selectAuthenticationMethod.onCreated(function() {
|
||||
this.authenticationMethods = new ReactiveVar([]);
|
||||
|
||||
Meteor.call('getAuthenticationsEnabled', (_, result) => {
|
||||
if (result) {
|
||||
// TODO : add a management of different languages
|
||||
// (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
|
||||
this.authenticationMethods.set([
|
||||
{value: 'password'},
|
||||
// Gets only the authentication methods availables
|
||||
...Object.entries(result).filter((e) => e[1]).map((e) => ({value: e[0]})),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.selectAuthenticationMethod.helpers({
|
||||
authentications() {
|
||||
return Template.instance().authenticationMethods.get();
|
||||
},
|
||||
isSelected(match) {
|
||||
return Template.instance().data.authenticationMethod === match;
|
||||
},
|
||||
});
|
||||
@@ -52,6 +52,10 @@
|
||||
|
||||
.main-body
|
||||
padding: 0.1em 1em
|
||||
-webkit-user-select: auto // Safari 3.1+
|
||||
-moz-user-select: auto // Firefox 2+
|
||||
-ms-user-select: auto // IE 10+
|
||||
user-select: auto // Standard syntax
|
||||
|
||||
ul
|
||||
li
|
||||
|
||||
@@ -4,22 +4,21 @@ template(name="settingHeaderBar")
|
||||
|
||||
.setting-header-btns.left
|
||||
unless isMiniScreen
|
||||
unless isSandstorm
|
||||
if currentUser
|
||||
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
|
||||
i.fa(class="fa-cog")
|
||||
span {{_ 'settings'}}
|
||||
if currentUser
|
||||
a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
|
||||
i.fa(class="fa-cog")
|
||||
span {{_ 'settings'}}
|
||||
|
||||
a.setting-header-btn.people(href="{{pathFor 'people'}}")
|
||||
i.fa(class="fa-users")
|
||||
span {{_ 'people'}}
|
||||
a.setting-header-btn.people(href="{{pathFor 'people'}}")
|
||||
i.fa(class="fa-users")
|
||||
span {{_ 'people'}}
|
||||
|
||||
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
|
||||
i.fa(class="fa-info-circle")
|
||||
span {{_ 'info'}}
|
||||
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
|
||||
i.fa(class="fa-info-circle")
|
||||
span {{_ 'info'}}
|
||||
|
||||
else
|
||||
a.setting-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
else
|
||||
a.setting-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
span {{_ 'log-in'}}
|
||||
@@ -1,9 +1,9 @@
|
||||
template(name="sidebar")
|
||||
.board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}")
|
||||
a.sidebar-tongue.js-toggle-sidebar(
|
||||
class="{{#if isTongueHidden}}is-hidden{{/if}}",
|
||||
title="{{showTongueTitle}}")
|
||||
i.fa.fa-angle-left
|
||||
//a.sidebar-tongue.js-toggle-sidebar(
|
||||
// class="{{#if isTongueHidden}}is-hidden{{/if}}",
|
||||
// title="{{showTongueTitle}}")
|
||||
// i.fa.fa-navicon
|
||||
.sidebar-shadow
|
||||
.sidebar-content.sidebar-shortcuts
|
||||
a.board-header-btn.js-shortcuts
|
||||
@@ -11,7 +11,7 @@ template(name="sidebar")
|
||||
span {{_ 'keyboard-shortcuts' }}
|
||||
.sidebar-content.js-board-sidebar-content.js-perfect-scrollbar
|
||||
a.hide-btn.js-hide-sidebar
|
||||
i.fa.fa-angle-right
|
||||
i.fa.fa-navicon
|
||||
unless isDefaultView
|
||||
h2
|
||||
a.fa.fa-chevron-left.js-back-home
|
||||
@@ -34,6 +34,9 @@ template(name="membersWidget")
|
||||
h3
|
||||
i.fa.fa-user
|
||||
| {{_ 'members'}}
|
||||
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
|
||||
i.board-header-btn-icon.fa.fa-cog
|
||||
|
||||
.board-widget-content
|
||||
each currentBoard.activeMembers
|
||||
+userAvatar(userId=this.userId showStatus=true)
|
||||
@@ -53,6 +56,130 @@ template(name="membersWidget")
|
||||
button.js-member-invite-accept.primary {{_ 'accept'}}
|
||||
button.js-member-invite-decline {{_ 'decline'}}
|
||||
|
||||
template(name="boardChangeColorPopup")
|
||||
.board-backgrounds-list.clearfix
|
||||
each backgroundColors
|
||||
.board-background-select.js-select-background
|
||||
span.background-box(class="board-color-{{this}}")
|
||||
if isSelected
|
||||
i.fa.fa-check
|
||||
|
||||
template(name="boardSubtaskSettingsPopup")
|
||||
form.board-subtask-settings
|
||||
h3 {{_ 'show-parent-in-minicard'}}
|
||||
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-full-path'}}
|
||||
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'prefix-with-parent'}}
|
||||
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-full-path'}}
|
||||
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'subtext-with-parent'}}
|
||||
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
|
||||
span {{_ 'no-parent'}}
|
||||
div
|
||||
hr
|
||||
|
||||
div.check-div
|
||||
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
|
||||
span {{_ 'show-subtasks-field'}}
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-board'}}
|
||||
select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}")
|
||||
each boards
|
||||
if isBoardSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
if isNullBoardSelected
|
||||
option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}}
|
||||
else
|
||||
option(value='null') {{_ 'custom-field-dropdown-none'}}
|
||||
div
|
||||
hr
|
||||
|
||||
label
|
||||
| {{_ 'deposit-subtasks-list'}}
|
||||
select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}")
|
||||
each lists
|
||||
if isListSelected
|
||||
option(value=_id selected="selected") {{title}}
|
||||
else
|
||||
option(value=_id) {{title}}
|
||||
|
||||
template(name="chooseBoardSource")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
|
||||
li
|
||||
a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
|
||||
|
||||
template(name="archiveBoardPopup")
|
||||
p {{_ 'close-board-pop'}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
|
||||
|
||||
template(name="outgoingWebhooksPopup")
|
||||
each integrations
|
||||
form.integration-form
|
||||
if title
|
||||
h4 {{title}}
|
||||
else
|
||||
h4 {{_ 'no-name'}}
|
||||
label
|
||||
| URL
|
||||
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
|
||||
input(type="hidden" value=_id name="id")
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
form.integration-form
|
||||
h4
|
||||
| {{_ 'new-outgoing-webhook'}}
|
||||
label
|
||||
| URL
|
||||
input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
|
||||
input.primary.wide(type="submit" value="{{_ 'save'}}")
|
||||
|
||||
template(name="boardMenuPopup")
|
||||
ul.pop-over-list
|
||||
li: a.js-custom-fields {{_ 'custom-fields'}}
|
||||
li: a.js-open-archives {{_ 'archived-items'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li: a.js-change-board-color {{_ 'board-change-color'}}
|
||||
//-
|
||||
XXX Language should be handled by sandstorm, but for now display a
|
||||
language selection link in the board menu. This link is normally present
|
||||
in the header bar that is not displayed on sandstorm.
|
||||
if isSandstorm
|
||||
li: a.js-change-language {{_ 'language'}}
|
||||
unless isSandstorm
|
||||
if currentUser.isBoardAdmin
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
li: a.js-archive-board {{_ 'archive-board'}}
|
||||
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-subtask-settings {{_ 'subtask-settings'}}
|
||||
|
||||
if isSandstorm
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
|
||||
li: a.js-import-board {{_ 'import-board-c'}}
|
||||
li: a.js-archive-board {{_ 'archive-board'}}
|
||||
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-subtask-settings {{_ 'subtask-settings'}}
|
||||
|
||||
template(name="labelsWidget")
|
||||
.board-widget.board-widget-labels
|
||||
h3
|
||||
@@ -83,17 +210,16 @@ template(name="memberPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-filter-member {{_ 'filter-cards'}}
|
||||
unless isSandstorm
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-change-role
|
||||
| {{_ 'change-permissions'}}
|
||||
span.quiet (#{memberType})
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
if $eq currentUser._id userId
|
||||
a.js-leave-member {{_ 'leave-board'}}
|
||||
else if currentUser.isBoardAdmin
|
||||
a.js-remove-member {{_ 'remove-from-board'}}
|
||||
a.js-change-role
|
||||
| {{_ 'change-permissions'}}
|
||||
span.quiet (#{memberType})
|
||||
li
|
||||
if $eq currentUser._id userId
|
||||
a.js-leave-member {{_ 'leave-board'}}
|
||||
else if currentUser.isBoardAdmin
|
||||
a.js-remove-member {{_ 'remove-from-board'}}
|
||||
|
||||
|
||||
template(name="removeMemberPopup")
|
||||
|
||||
@@ -142,6 +142,52 @@ Template.memberPopup.helpers({
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.events({
|
||||
'click .js-rename-board': Popup.open('boardChangeTitle'),
|
||||
'click .js-custom-fields'() {
|
||||
Sidebar.setView('customFields');
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-open-archives'() {
|
||||
Sidebar.setView('archives');
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
currentBoard.archive();
|
||||
// XXX We should have some kind of notification on top of the page to
|
||||
// confirm that the board was successfully archived.
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
Popup.close();
|
||||
Boards.remove(currentBoard._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
|
||||
});
|
||||
|
||||
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.memberPopup.events({
|
||||
'click .js-filter-member'() {
|
||||
Filter.members.toggle(this.userId);
|
||||
@@ -190,7 +236,14 @@ Template.membersWidget.helpers({
|
||||
|
||||
Template.membersWidget.events({
|
||||
'click .js-member': Popup.open('member'),
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-manage-board-members': Popup.open('addMember'),
|
||||
'click .js-import': Popup.open('boardImportBoard'),
|
||||
submit: this.onSubmit,
|
||||
'click .js-import-board': Popup.open('chooseBoardSource'),
|
||||
'click .js-open-archived-board'() {
|
||||
Modal.open('archivedBoards');
|
||||
},
|
||||
'click .sandstorm-powerbox-request-identity'() {
|
||||
window.sandstormRequestIdentity();
|
||||
},
|
||||
@@ -209,6 +262,59 @@ Template.membersWidget.events({
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
integrations() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return Integrations.find({ boardId: `${boardId}` }).fetch();
|
||||
},
|
||||
|
||||
integration(id) {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return Integrations.findOne({ _id: id, boardId: `${boardId}` });
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'submit'(evt) {
|
||||
evt.preventDefault();
|
||||
const url = evt.target.url.value;
|
||||
const boardId = Session.get('currentBoard');
|
||||
let id = null;
|
||||
let integration = null;
|
||||
if (evt.target.id) {
|
||||
id = evt.target.id.value;
|
||||
integration = this.integration(id);
|
||||
if (url) {
|
||||
Integrations.update(integration._id, {
|
||||
$set: {
|
||||
url: `${url}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Integrations.remove(integration._id);
|
||||
}
|
||||
} else if (url) {
|
||||
Integrations.insert({
|
||||
userId: Meteor.userId(),
|
||||
enabled: true,
|
||||
type: 'outgoing-webhooks',
|
||||
url: `${url}`,
|
||||
boardId: `${boardId}`,
|
||||
activities: ['all'],
|
||||
});
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('outgoingWebhooksPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'chooseBoardSource';
|
||||
},
|
||||
}).register('chooseBoardSourcePopup');
|
||||
|
||||
Template.labelsWidget.events({
|
||||
'click .js-label': Popup.open('editLabel'),
|
||||
'click .js-add-label': Popup.open('createLabel'),
|
||||
@@ -258,6 +364,124 @@ function draggableMembersLabelsWidgets() {
|
||||
Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
|
||||
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
backgroundColors() {
|
||||
return Boards.simpleSchema()._schema.color.allowedValues;
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
return currentBoard.color === this.currentData().toString();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-select-background'(evt) {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
const newColor = this.currentData().toString();
|
||||
currentBoard.setColor(newColor);
|
||||
evt.preventDefault();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('boardChangeColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
},
|
||||
|
||||
allowsSubtasks() {
|
||||
return this.currentBoard.allowsSubtasks;
|
||||
},
|
||||
|
||||
isBoardSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
isNullBoardSelected() {
|
||||
return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined);
|
||||
},
|
||||
|
||||
boards() {
|
||||
return Boards.find({
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
lists() {
|
||||
return Lists.find({
|
||||
boardId: this.currentBoard._id,
|
||||
archived: false,
|
||||
}, {
|
||||
sort: ['title'],
|
||||
});
|
||||
},
|
||||
|
||||
hasLists() {
|
||||
return this.lists().count() > 0;
|
||||
},
|
||||
|
||||
isListSelected() {
|
||||
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if ((result === null) || (result === undefined)) {
|
||||
result = 'no-parent';
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-field-has-subtasks'(evt) {
|
||||
evt.preventDefault();
|
||||
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
|
||||
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
|
||||
$('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks);
|
||||
},
|
||||
'change .js-field-deposit-board'(evt) {
|
||||
let value = evt.target.value;
|
||||
if (value === 'null') {
|
||||
value = null;
|
||||
}
|
||||
this.currentBoard.setSubtasksDefaultBoardId(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'change .js-field-deposit-list'(evt) {
|
||||
this.currentBoard.setSubtasksDefaultListId(evt.target.value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
'click .js-field-show-parent-in-minicard'(evt) {
|
||||
const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id;
|
||||
const options = [
|
||||
'prefix-with-full-path',
|
||||
'prefix-with-parent',
|
||||
'subtext-with-full-path',
|
||||
'subtext-with-parent',
|
||||
'no-parent'];
|
||||
options.forEach(function(element) {
|
||||
if (element !== value) {
|
||||
$(`#${element} .materialCheckBox`).toggleClass('is-checked', false);
|
||||
$(`#${element}`).toggleClass('is-checked', false);
|
||||
}
|
||||
});
|
||||
$(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
|
||||
$(`#${value}`).toggleClass('is-checked', true);
|
||||
this.currentBoard.setPresentParentTask(value);
|
||||
evt.preventDefault();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('boardSubtaskSettingsPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
|
||||
@@ -2,6 +2,10 @@ template(name="archivesSidebar")
|
||||
+basicTabs(tabs=tabs)
|
||||
|
||||
+tabContent(slug="cards")
|
||||
p.quiet
|
||||
a.js-restore-all-cards {{_ 'restore-all'}}
|
||||
| -
|
||||
a.js-delete-all-cards {{_ 'delete-all'}}
|
||||
each archivedCards
|
||||
.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
@@ -16,23 +20,35 @@ template(name="archivesSidebar")
|
||||
p.no-items-message {{_ 'no-archived-cards'}}
|
||||
|
||||
+tabContent(slug="lists")
|
||||
p.quiet
|
||||
a.js-restore-all-lists {{_ 'restore-all'}}
|
||||
| -
|
||||
a.js-delete-all-lists {{_ 'delete-all'}}
|
||||
ul.archived-lists
|
||||
each archivedLists
|
||||
li.archived-lists-item
|
||||
if currentUser.isBoardMember
|
||||
button.js-restore-list
|
||||
i.fa.fa-undo
|
||||
= title
|
||||
if currentUser.isBoardMember
|
||||
p.quiet
|
||||
a.js-restore-list {{_ 'restore'}}
|
||||
| -
|
||||
a.js-delete-list {{_ 'delete'}}
|
||||
else
|
||||
li.no-items-message {{_ 'no-archived-lists'}}
|
||||
|
||||
+tabContent(slug="swimlanes")
|
||||
p.quiet
|
||||
a.js-restore-all-swimlanes {{_ 'restore-all'}}
|
||||
| -
|
||||
a.js-delete-all-swimlanes {{_ 'delete-all'}}
|
||||
ul.archived-lists
|
||||
each archivedSwimlanes
|
||||
li.archived-lists-item
|
||||
if currentUser.isBoardMember
|
||||
button.js-restore-swimlane
|
||||
i.fa.fa-undo
|
||||
= title
|
||||
if currentUser.isBoardMember
|
||||
p.quiet
|
||||
a.js-restore-swimlane {{_ 'restore'}}
|
||||
| -
|
||||
a.js-delete-swimlane {{_ 'delete'}}
|
||||
else
|
||||
li.no-items-message {{_ 'no-archived-swimlanes'}}
|
||||
@@ -44,19 +44,67 @@ BlazeComponent.extendComponent({
|
||||
card.restore();
|
||||
}
|
||||
},
|
||||
'click .js-restore-all-cards'() {
|
||||
this.archivedCards().forEach((card) => {
|
||||
if(card.canBeRestored()){
|
||||
card.restore();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
|
||||
const cardId = this._id;
|
||||
Cards.remove(cardId);
|
||||
Popup.close();
|
||||
}),
|
||||
'click .js-delete-all-cards': Popup.afterConfirm('cardDelete', () => {
|
||||
this.archivedCards().forEach((card) => {
|
||||
Cards.remove(card._id);
|
||||
});
|
||||
Popup.close();
|
||||
}),
|
||||
|
||||
'click .js-restore-list'() {
|
||||
const list = this.currentData();
|
||||
list.restore();
|
||||
},
|
||||
'click .js-restore-all-lists'() {
|
||||
this.archivedLists().forEach((list) => {
|
||||
list.restore();
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-delete-list': Popup.afterConfirm('listDelete', function() {
|
||||
this.remove();
|
||||
Popup.close();
|
||||
}),
|
||||
'click .js-delete-all-lists': Popup.afterConfirm('listDelete', () => {
|
||||
this.archivedLists().forEach((list) => {
|
||||
list.remove();
|
||||
});
|
||||
Popup.close();
|
||||
}),
|
||||
|
||||
'click .js-restore-swimlane'() {
|
||||
const swimlane = this.currentData();
|
||||
swimlane.restore();
|
||||
},
|
||||
'click .js-restore-all-swimlanes'() {
|
||||
this.archivedSwimlanes().forEach((swimlane) => {
|
||||
swimlane.restore();
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-delete-swimlane': Popup.afterConfirm('swimlaneDelete', function() {
|
||||
this.remove();
|
||||
Popup.close();
|
||||
}),
|
||||
'click .js-delete-all-swimlanes': Popup.afterConfirm('swimlaneDelete', () => {
|
||||
this.archivedSwimlanes().forEach((swimlane) => {
|
||||
swimlane.remove();
|
||||
});
|
||||
Popup.close();
|
||||
}),
|
||||
}];
|
||||
},
|
||||
}).register('archivesSidebar');
|
||||
@@ -2,7 +2,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
customFields() {
|
||||
return CustomFields.find({
|
||||
boardId: Session.get('currentBoard'),
|
||||
boardIds: {$in: [Session.get('currentBoard')]},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -103,7 +103,6 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
evt.preventDefault();
|
||||
|
||||
const data = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
name: this.find('.js-field-name').value.trim(),
|
||||
type: this.type.get(),
|
||||
settings: this.getSettings(),
|
||||
@@ -114,6 +113,7 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
|
||||
// insert or update
|
||||
if (!this.data()._id) {
|
||||
data.boardIds = [Session.get('currentBoard')];
|
||||
CustomFields.insert(data);
|
||||
} else {
|
||||
CustomFields.update(this.data()._id, {$set: data});
|
||||
@@ -122,8 +122,16 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-delete-custom-field': Popup.afterConfirm('deleteCustomField', function() {
|
||||
const customFieldId = this._id;
|
||||
CustomFields.remove(customFieldId);
|
||||
const customField = CustomFields.findOne(this._id);
|
||||
if (customField.boardIds.length > 1) {
|
||||
CustomFields.update(customField._id, {
|
||||
$pull: {
|
||||
boardIds: Session.get('currentBoard'),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
CustomFields.remove(customField._id);
|
||||
}
|
||||
Popup.close();
|
||||
}),
|
||||
}];
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
template(name="miniswimlane")
|
||||
.minicard(
|
||||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
@@ -1,14 +1,21 @@
|
||||
template(name="swimlaneHeader")
|
||||
.swimlane-header-wrap.js-swimlane-header
|
||||
+inlinedForm
|
||||
+editSwimlaneTitleForm
|
||||
.swimlane-header-wrap.js-swimlane-header(class='{{#if colorClass}}swimlane-{{colorClass}}{{/if}}')
|
||||
if this.isTemplateContainer
|
||||
+swimlaneFixedHeader(this)
|
||||
else
|
||||
.swimlane-header(
|
||||
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
||||
= title
|
||||
.swimlane-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
a.fa.fa-navicon.js-open-swimlane-menu
|
||||
+inlinedForm
|
||||
+editSwimlaneTitleForm
|
||||
else
|
||||
+swimlaneFixedHeader(this)
|
||||
|
||||
template(name="swimlaneFixedHeader")
|
||||
.swimlane-header(
|
||||
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
|
||||
= title
|
||||
.swimlane-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon
|
||||
a.fa.fa-navicon.js-open-swimlane-menu
|
||||
|
||||
template(name="editSwimlaneTitleForm")
|
||||
.list-composer
|
||||
@@ -20,4 +27,34 @@ template(name="editSwimlaneTitleForm")
|
||||
template(name="swimlaneActionPopup")
|
||||
unless currentUser.isCommentOnly
|
||||
ul.pop-over-list
|
||||
li: a.js-close-swimlane {{_ 'archive-swimlane'}}
|
||||
li: a.js-set-swimlane-color {{_ 'select-color'}}
|
||||
unless this.isTemplateContainer
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-close-swimlane {{_ 'archive-swimlane'}}
|
||||
|
||||
template(name="swimlaneAddPopup")
|
||||
unless currentUser.isCommentOnly
|
||||
form
|
||||
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
|
||||
autocomplete="off" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-swimlane-template {{_ 'template'}}
|
||||
|
||||
template(name="setSwimlaneColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors: each colors
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="swimlaneDeletePopup")
|
||||
p {{_ "swimlane-delete-pop"}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
@@ -1,3 +1,10 @@
|
||||
const { calculateIndexData } = Utils;
|
||||
|
||||
let swimlaneColors;
|
||||
Meteor.startup(() => {
|
||||
swimlaneColors = Swimlanes.simpleSchema()._schema.color.allowedValues;
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
editTitle(evt) {
|
||||
evt.preventDefault();
|
||||
@@ -11,15 +18,85 @@ BlazeComponent.extendComponent({
|
||||
events() {
|
||||
return [{
|
||||
'click .js-open-swimlane-menu': Popup.open('swimlaneAction'),
|
||||
'click .js-open-add-swimlane-menu': Popup.open('swimlaneAdd'),
|
||||
submit: this.editTitle,
|
||||
}];
|
||||
},
|
||||
}).register('swimlaneHeader');
|
||||
|
||||
Template.swimlaneActionPopup.events({
|
||||
'click .js-set-swimlane-color': Popup.open('setSwimlaneColor'),
|
||||
'click .js-close-swimlane' (evt) {
|
||||
evt.preventDefault();
|
||||
this.archive();
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentSwimlane = this.currentData();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
submit(evt) {
|
||||
evt.preventDefault();
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
const nextSwimlane = currentBoard.nextSwimlane(this.currentSwimlane);
|
||||
const titleInput = this.find('.swimlane-name-input');
|
||||
const title = titleInput.value.trim();
|
||||
const sortValue = calculateIndexData(this.currentSwimlane, nextSwimlane, 1);
|
||||
const swimlaneType = (currentBoard.isTemplatesBoard())?'template-swimlane':'swimlane';
|
||||
|
||||
if (title) {
|
||||
Swimlanes.insert({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: sortValue.base,
|
||||
type: swimlaneType,
|
||||
});
|
||||
|
||||
titleInput.value = '';
|
||||
titleInput.focus();
|
||||
}
|
||||
// XXX ideally, we should move the popup to the newly
|
||||
// created swimlane so a user can add more than one swimlane
|
||||
// with a minimum of interactions
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-swimlane-template': Popup.open('searchElement'),
|
||||
}];
|
||||
},
|
||||
}).register('swimlaneAddPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentSwimlane = this.currentData();
|
||||
this.currentColor = new ReactiveVar(this.currentSwimlane.color);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return swimlaneColors.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
'click .js-submit' () {
|
||||
this.currentSwimlane.setColor(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-remove-color'() {
|
||||
this.currentSwimlane.setColor(null);
|
||||
Popup.close();
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('setSwimlaneColorPopup');
|
||||
@@ -1,21 +1,22 @@
|
||||
template(name="swimlane")
|
||||
.swimlane.js-lists.js-swimlane
|
||||
.swimlane
|
||||
+swimlaneHeader
|
||||
.swimlane.js-lists.js-swimlane
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
if currentListIsInThisSwimlane _id
|
||||
+list(currentList)
|
||||
else
|
||||
each currentBoard.lists
|
||||
unless currentList
|
||||
each lists
|
||||
+miniList(this)
|
||||
if currentUser.isBoardMember
|
||||
+addListForm
|
||||
else
|
||||
each currentBoard.lists
|
||||
each lists
|
||||
+list(this)
|
||||
if currentCardIsInThisList _id ../_id
|
||||
+cardDetails(currentCard)
|
||||
if currentUser.isBoardMember
|
||||
+addListAndSwimlaneForm
|
||||
+addListForm
|
||||
|
||||
template(name="listsGroup")
|
||||
.swimlane.list-group.js-lists
|
||||
@@ -23,52 +24,31 @@ template(name="listsGroup")
|
||||
if currentList
|
||||
+list(currentList)
|
||||
else
|
||||
each currentBoard.lists
|
||||
each lists
|
||||
+miniList(this)
|
||||
if currentUser.isBoardMember
|
||||
+addListForm
|
||||
else
|
||||
each currentBoard.lists
|
||||
each lists
|
||||
+list(this)
|
||||
if currentCardIsInThisList _id null
|
||||
+cardDetails(currentCard)
|
||||
if currentUser.isBoardMember
|
||||
+addListForm
|
||||
|
||||
template(name="addListAndSwimlaneForm")
|
||||
.list.list-composer.js-list-composer
|
||||
.list-header
|
||||
+inlinedForm(autoclose=false)
|
||||
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
|
||||
autocomplete="off" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
else
|
||||
a.open-list-composer.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-list'}}
|
||||
.list-header
|
||||
+inlinedForm(autoclose=false)
|
||||
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
|
||||
autocomplete="off" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
else
|
||||
a.open-list-composer.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-swimlane'}}
|
||||
|
||||
template(name="addListForm")
|
||||
.list.list-composer.js-list-composer
|
||||
.list-header
|
||||
.list-header-add
|
||||
+inlinedForm(autoclose=false)
|
||||
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
|
||||
autocomplete="off" autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-list-template {{_ 'template'}}
|
||||
else
|
||||
a.open-list-composer.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
const { calculateIndex, enableClickOnTouch } = Utils;
|
||||
|
||||
function currentListIsInThisSwimlane(swimlaneId) {
|
||||
const currentList = Lists.findOne(Session.get('currentList'));
|
||||
return currentList && (currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '');
|
||||
}
|
||||
|
||||
function currentCardIsInThisList(listId, swimlaneId) {
|
||||
const currentCard = Cards.findOne(Session.get('currentCard'));
|
||||
const currentUser = Meteor.user();
|
||||
if (currentUser.profile.boardView === 'board-view-lists')
|
||||
return currentCard && currentCard.listId === listId;
|
||||
else if (currentUser.profile.boardView === 'board-view-swimlanes')
|
||||
if (currentUser && currentUser.profile.boardView === 'board-view-swimlanes')
|
||||
return currentCard && currentCard.listId === listId && currentCard.swimlaneId === swimlaneId;
|
||||
else if (currentUser.profile.boardView === 'board-view-cal')
|
||||
return currentCard;
|
||||
else
|
||||
return false;
|
||||
else // Default view: board-view-lists
|
||||
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
|
||||
// 1) If there is no view cookie, save to cookie board-view-lists
|
||||
// board-view-lists / board-view-swimlanes / board-view-cal
|
||||
// 2) If public user changes clicks board-view-lists then change view and
|
||||
// then change view and save cookie with view value
|
||||
// without using currentuser above, because currentuser is null.
|
||||
}
|
||||
|
||||
function initSortable(boardComponent, $listsDom) {
|
||||
@@ -110,6 +119,10 @@ BlazeComponent.extendComponent({
|
||||
return currentCardIsInThisList(listId, swimlaneId);
|
||||
},
|
||||
|
||||
currentListIsInThisSwimlane(swimlaneId) {
|
||||
return currentListIsInThisSwimlane(swimlaneId);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
// Click-and-drag action
|
||||
@@ -149,6 +162,12 @@ BlazeComponent.extendComponent({
|
||||
}).register('swimlane');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
this.isListTemplatesSwimlane = this.currentBoard.isTemplatesBoard() && this.currentData().isListTemplatesSwimlane();
|
||||
this.currentSwimlane = this.currentData();
|
||||
},
|
||||
|
||||
// Proxy
|
||||
open() {
|
||||
this.childComponents('inlinedForm')[0].open();
|
||||
@@ -165,58 +184,19 @@ BlazeComponent.extendComponent({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: $('.list').length,
|
||||
type: (this.isListTemplatesSwimlane)?'template-list':'list',
|
||||
swimlaneId: (this.currentBoard.isTemplatesBoard())?this.currentSwimlane._id:'',
|
||||
});
|
||||
|
||||
titleInput.value = '';
|
||||
titleInput.focus();
|
||||
}
|
||||
},
|
||||
'click .js-list-template': Popup.open('searchElement'),
|
||||
}];
|
||||
},
|
||||
}).register('addListForm');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// Proxy
|
||||
open() {
|
||||
this.childComponents('inlinedForm')[0].open();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
submit(evt) {
|
||||
evt.preventDefault();
|
||||
let titleInput = this.find('.list-name-input');
|
||||
if (titleInput) {
|
||||
const title = titleInput.value.trim();
|
||||
if (title) {
|
||||
Lists.insert({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: $('.list').length,
|
||||
});
|
||||
|
||||
titleInput.value = '';
|
||||
titleInput.focus();
|
||||
}
|
||||
} else {
|
||||
titleInput = this.find('.swimlane-name-input');
|
||||
const title = titleInput.value.trim();
|
||||
if (title) {
|
||||
Swimlanes.insert({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: $('.swimlane').length,
|
||||
});
|
||||
|
||||
titleInput.value = '';
|
||||
titleInput.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
}];
|
||||
},
|
||||
}).register('addListAndSwimlaneForm');
|
||||
|
||||
Template.swimlane.helpers({
|
||||
canSeeAddList() {
|
||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
display: flex
|
||||
flex-direction: row
|
||||
overflow: 0;
|
||||
max-height: 100%
|
||||
max-height: calc(100% - 26px)
|
||||
|
||||
&.placeholder
|
||||
background-color: rgba(0, 0, 0, .2)
|
||||
@@ -27,20 +27,15 @@
|
||||
.swimlane-header-wrap
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 0 0 50px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #CCC
|
||||
flex: 1 0 100%;
|
||||
background-color: #ccc;
|
||||
|
||||
.swimlane-header
|
||||
-ms-writing-mode: tb-rl;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-top: 50px;
|
||||
padding: 5px 5px
|
||||
font-weight: bold;
|
||||
min-height: 9px;
|
||||
width: 50px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
@@ -49,7 +44,95 @@
|
||||
|
||||
.swimlane-header-menu
|
||||
position: absolute
|
||||
padding: 20px 20px
|
||||
padding: 5px 5px
|
||||
|
||||
.swimlane-header-plus-icon
|
||||
margin-left: 5px
|
||||
margin-right: 10px
|
||||
|
||||
.list-group
|
||||
height: 100%
|
||||
|
||||
.moving-swimlane
|
||||
display: none
|
||||
|
||||
swimlane-color(background, color...)
|
||||
background: background !important
|
||||
if color
|
||||
color: color !important //overwrite text for better visibility
|
||||
|
||||
.swimlane-white
|
||||
swimlane-color(#ffffff, #4d4d4d) //Black text for better visibility
|
||||
border: 1px solid #eee
|
||||
|
||||
.swimlane-green
|
||||
swimlane-color(#3cb500, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-yellow
|
||||
swimlane-color(#fad900, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-orange
|
||||
swimlane-color(#ff9f19, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-red
|
||||
swimlane-color(#eb4646, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-purple
|
||||
swimlane-color(#a632db, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-blue
|
||||
swimlane-color(#0079bf, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-pink
|
||||
swimlane-color(#ff78cb, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-sky
|
||||
swimlane-color(#00c2e0, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-black
|
||||
swimlane-color(#4d4d4d, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-lime
|
||||
swimlane-color(#51e898, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-silver
|
||||
swimlane-color(unset, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-peachpuff
|
||||
swimlane-color(#ffdab9, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-crimson
|
||||
swimlane-color(#dc143c, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-plum
|
||||
swimlane-color(#dda0dd, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-darkgreen
|
||||
swimlane-color(#006400, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-slateblue
|
||||
swimlane-color(#6a5acd, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-magenta
|
||||
swimlane-color(#ff00ff, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-gold
|
||||
swimlane-color(#ffd700, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-navy
|
||||
swimlane-color(#000080, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-gray
|
||||
swimlane-color(#808080, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-saddlebrown
|
||||
swimlane-color(#8b4513, #ffffff) //White text for better visibility
|
||||
|
||||
.swimlane-paleturquoise
|
||||
swimlane-color(#afeeee, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-mistyrose
|
||||
swimlane-color(#ffe4e1, #4d4d4d) //Black text for better visibility
|
||||
|
||||
.swimlane-indigo
|
||||
swimlane-color(#4b0082, #ffffff) //White text for better visibility
|
||||
@@ -4,10 +4,11 @@ template(name="headerUserBar")
|
||||
.header-user-bar-avatar
|
||||
+userAvatar(userId=currentUser._id)
|
||||
unless isMiniScreen
|
||||
if currentUser.profile.fullname
|
||||
= currentUser.profile.fullname
|
||||
else
|
||||
= currentUser.username
|
||||
unless isSandstorm
|
||||
if currentUser.profile.fullname
|
||||
= currentUser.profile.fullname
|
||||
else
|
||||
= currentUser.username
|
||||
|
||||
template(name="memberMenuPopup")
|
||||
ul.pop-over-list
|
||||
@@ -15,13 +16,18 @@ template(name="memberMenuPopup")
|
||||
li: a.js-edit-profile {{_ 'edit-profile'}}
|
||||
li: a.js-change-settings {{_ 'change-settings'}}
|
||||
li: a.js-change-avatar {{_ 'edit-avatar'}}
|
||||
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
|
||||
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
|
||||
unless isSandstorm
|
||||
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
|
||||
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
|
||||
if currentUser.isAdmin
|
||||
li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-logout {{_ 'log-out'}}
|
||||
li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}}
|
||||
unless isSandstorm
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li: a.js-logout {{_ 'log-out'}}
|
||||
|
||||
template(name="editProfilePopup")
|
||||
form
|
||||
|
||||
@@ -3,6 +3,15 @@ Template.headerUserBar.events({
|
||||
'click .js-change-avatar': Popup.open('changeAvatar'),
|
||||
});
|
||||
|
||||
Template.memberMenuPopup.helpers({
|
||||
templatesBoardId() {
|
||||
return Meteor.user().getTemplatesBoardId();
|
||||
},
|
||||
templatesBoardSlug() {
|
||||
return Meteor.user().getTemplatesBoardSlug();
|
||||
},
|
||||
});
|
||||
|
||||
Template.memberMenuPopup.events({
|
||||
'click .js-edit-profile': Popup.open('editProfile'),
|
||||
'click .js-change-settings': Popup.open('changeSettings'),
|
||||
|
||||
+5
-7
@@ -4,9 +4,9 @@ window.Popup = new class {
|
||||
this.template = Template.popup;
|
||||
|
||||
// We only want to display one popup at a time and we keep the view object
|
||||
// in this `Popup._current` variable. If there is no popup currently opened
|
||||
// in this `Popup.current` variable. If there is no popup currently opened
|
||||
// the value is `null`.
|
||||
this._current = null;
|
||||
this.current = null;
|
||||
|
||||
// It's possible to open a sub-popup B from a popup A. In that case we keep
|
||||
// the data of popup A so we can return back to it. Every time we open a new
|
||||
@@ -27,11 +27,9 @@ window.Popup = new class {
|
||||
open(name) {
|
||||
const self = this;
|
||||
const popupName = `${name}Popup`;
|
||||
|
||||
function clickFromPopup(evt) {
|
||||
return $(evt.target).closest('.js-pop-over').length !== 0;
|
||||
}
|
||||
|
||||
return function(evt) {
|
||||
// If a popup is already opened, clicking again on the opener element
|
||||
// should close it -- and interrupt the current `open` function.
|
||||
@@ -57,7 +55,6 @@ window.Popup = new class {
|
||||
self._stack = [];
|
||||
openerElement = evt.currentTarget;
|
||||
}
|
||||
|
||||
$(openerElement).addClass('is-active');
|
||||
evt.preventDefault();
|
||||
|
||||
@@ -139,6 +136,7 @@ window.Popup = new class {
|
||||
const openerElement = this._getTopStack().openerElement;
|
||||
$(openerElement).removeClass('is-active');
|
||||
|
||||
|
||||
this._stack = [];
|
||||
}
|
||||
}
|
||||
@@ -186,7 +184,7 @@ window.Popup = new class {
|
||||
// positives.
|
||||
const title = TAPi18n.__(translationKey);
|
||||
// when popup showed as full of small screen, we need a default header to clearly see [X] button
|
||||
const defaultTitle = Utils.isMiniScreen() ? 'Wekan' : false;
|
||||
const defaultTitle = Utils.isMiniScreen() ? '' : false;
|
||||
return title !== translationKey ? title : defaultTitle;
|
||||
};
|
||||
}
|
||||
@@ -200,7 +198,7 @@ escapeActions.forEach((actionName) => {
|
||||
() => Popup[actionName](),
|
||||
() => Popup.isOpen(),
|
||||
{
|
||||
noClickEscapeOn: '.js-pop-over',
|
||||
noClickEscapeOn: '.js-pop-over,.js-open-card-title-popup',
|
||||
enabledOnClick: actionName === 'close',
|
||||
}
|
||||
);
|
||||
|
||||
+31
-1
@@ -145,6 +145,26 @@ Utils = {
|
||||
});
|
||||
},
|
||||
|
||||
manageCustomUI(){
|
||||
Meteor.call('getCustomUI', (err, data) => {
|
||||
if (err && err.error[0] === 'var-not-exist'){
|
||||
Session.set('customUI', false); // siteId || address server not defined
|
||||
}
|
||||
if (!err){
|
||||
Utils.setCustomUI(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setCustomUI(data){
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
if (currentBoard) {
|
||||
DocHead.setTitle(`${currentBoard.title } - ${ data.productName}`);
|
||||
} else {
|
||||
DocHead.setTitle(`${data.productName}`);
|
||||
}
|
||||
},
|
||||
|
||||
setMatomo(data){
|
||||
window._paq = window._paq || [];
|
||||
window._paq.push(['setDoNotTrack', data.doNotTrack]);
|
||||
@@ -198,10 +218,20 @@ Utils = {
|
||||
const element = tempInstance.$(triggerEls[i]);
|
||||
if (element.hasClass('trigger-text')) {
|
||||
finalString += element.text().toLowerCase();
|
||||
} else if (element.hasClass('user-details')) {
|
||||
let username = element.find('input').val();
|
||||
if(username === undefined || username === ''){
|
||||
username = '*';
|
||||
}
|
||||
finalString += `${element.find('.trigger-text').text().toLowerCase() } ${ username}`;
|
||||
} else if (element.find('select').length > 0) {
|
||||
finalString += element.find('select option:selected').text().toLowerCase();
|
||||
} else if (element.find('input').length > 0) {
|
||||
finalString += element.find('input').val();
|
||||
let inputvalue = element.find('input').val();
|
||||
if(inputvalue === undefined || inputvalue === ''){
|
||||
inputvalue = '*';
|
||||
}
|
||||
finalString += inputvalue;
|
||||
}
|
||||
// Add space
|
||||
if (i !== length - 1) {
|
||||
|
||||
+16
-11
@@ -14,6 +14,7 @@ FlowRouter.route('/', {
|
||||
Filter.reset();
|
||||
EscapeActions.executeAll();
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
@@ -40,6 +41,7 @@ FlowRouter.route('/b/:id/:slug', {
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
}
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
@@ -57,6 +59,7 @@ FlowRouter.route('/b/:boardId/:slug/:cardId', {
|
||||
Session.set('currentBoard', params.boardId);
|
||||
Session.set('currentCard', params.cardId);
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
@@ -122,6 +125,7 @@ FlowRouter.route('/setting', {
|
||||
},
|
||||
],
|
||||
action() {
|
||||
Utils.manageCustomUI();
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
headerBar: 'settingHeaderBar',
|
||||
content: 'setting',
|
||||
@@ -199,20 +203,21 @@ _.each(redirections, (newPath, oldPath) => {
|
||||
// using the `kadira:dochead` package. Currently we only use it to display the
|
||||
// board title if we are in a board page (see #364) but we may want to support
|
||||
// some <meta> tags in the future.
|
||||
const appTitle = 'Wekan';
|
||||
//const appTitle = Utils.manageCustomUI();
|
||||
|
||||
// XXX The `Meteor.startup` should not be necessary -- we don't need to wait for
|
||||
// the complete DOM to be ready to call `DocHead.setTitle`. But the problem is
|
||||
// that the global variable `Boards` is undefined when this file loads so we
|
||||
// wait a bit until hopefully all files are loaded. This will be fixed in a
|
||||
// clean way once Meteor will support ES6 modules -- hopefully in Meteor 1.3.
|
||||
Meteor.isClient && Meteor.startup(() => {
|
||||
Tracker.autorun(() => {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
const titleStack = [appTitle];
|
||||
if (currentBoard) {
|
||||
titleStack.push(currentBoard.title);
|
||||
}
|
||||
DocHead.setTitle(titleStack.reverse().join(' - '));
|
||||
});
|
||||
});
|
||||
//Meteor.isClient && Meteor.startup(() => {
|
||||
// Tracker.autorun(() => {
|
||||
|
||||
// const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
// const titleStack = [appTitle];
|
||||
// if (currentBoard) {
|
||||
// titleStack.push(currentBoard.title);
|
||||
// }
|
||||
// DocHead.setTitle(titleStack.reverse().join(' - '));
|
||||
// });
|
||||
//});
|
||||
@@ -1,238 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
# 1) Edit settings
|
||||
# 2) docker-compose up -d
|
||||
|
||||
services:
|
||||
|
||||
wekandb:
|
||||
image: mongo:3.2.21
|
||||
container_name: wekan-db
|
||||
restart: always
|
||||
command: mongod --smallfiles --oplogSize 128
|
||||
networks:
|
||||
- wekan-tier
|
||||
expose:
|
||||
- 27017
|
||||
volumes:
|
||||
- wekan-db:/data/db
|
||||
- wekan-db-dump:/dump
|
||||
|
||||
wekan:
|
||||
image: quay.io/wekan/wekan
|
||||
container_name: wekan-app
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- NODE_VERSION=${NODE_VERSION}
|
||||
- METEOR_RELEASE=${METEOR_RELEASE}
|
||||
- NPM_VERSION=${NPM_VERSION}
|
||||
- ARCHITECTURE=${ARCHITECTURE}
|
||||
- SRC_PATH=${SRC_PATH}
|
||||
- METEOR_EDGE=${METEOR_EDGE}
|
||||
- USE_EDGE=${USE_EDGE}
|
||||
ports:
|
||||
# Docker outsideport:insideport
|
||||
- 80:8080
|
||||
environment:
|
||||
- MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
- ROOT_URL=http://localhost
|
||||
# Wekan Export Board works when WITH_API=true.
|
||||
# If you disable Wekan API with false, Export Board does not work.
|
||||
- WITH_API=true
|
||||
# CORS: Set Access-Control-Allow-Origin header. Example: *
|
||||
#- CORS=*
|
||||
# Optional: Integration with Matomo https://matomo.org that is installed to your server
|
||||
# The address of the server where Matomo is hosted.
|
||||
# example: - MATOMO_ADDRESS=https://example.com/matomo
|
||||
#- MATOMO_ADDRESS=
|
||||
# The value of the site ID given in Matomo server for Wekan
|
||||
# example: - MATOMO_SITE_ID=12345
|
||||
#- MATOMO_SITE_ID=
|
||||
# The option do not track which enables users to not be tracked by matomo
|
||||
# example: - MATOMO_DO_NOT_TRACK=false
|
||||
#- MATOMO_DO_NOT_TRACK=
|
||||
# The option that allows matomo to retrieve the username:
|
||||
# example: MATOMO_WITH_USERNAME=true
|
||||
#- MATOMO_WITH_USERNAME=false
|
||||
# Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.
|
||||
# Setting this to false is not recommended, it also disables all other browser policy protections
|
||||
# and allows all iframing etc. See wekan/server/policy.js
|
||||
- BROWSER_POLICY_ENABLED=true
|
||||
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
||||
#- TRUSTED_URL=
|
||||
# What to send to Outgoing Webhook, or leave out. Example, that includes all that are default: cardId,listId,oldListId,boardId,comment,user,card,commentId .
|
||||
# example: WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
||||
#- WEBHOOKS_ATTRIBUTES=
|
||||
# Enable the OAuth2 connection
|
||||
# example: OAUTH2_ENABLED=true
|
||||
#- OAUTH2_ENABLED=false
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# OAuth2 Client ID, for example from Rocket.Chat. Example: abcde12345
|
||||
# example: OAUTH2_CLIENT_ID=abcde12345
|
||||
#- OAUTH2_CLIENT_ID=
|
||||
# OAuth2 Secret, for example from Rocket.Chat: Example: 54321abcde
|
||||
# example: OAUTH2_SECRET=54321abcde
|
||||
#- OAUTH2_SECRET=
|
||||
# OAuth2 Server URL, for example Rocket.Chat. Example: https://chat.example.com
|
||||
# example: OAUTH2_SERVER_URL=https://chat.example.com
|
||||
#- OAUTH2_SERVER_URL=
|
||||
# OAuth2 Authorization Endpoint. Example: /oauth/authorize
|
||||
# example: OAUTH2_AUTH_ENDPOINT=/oauth/authorize
|
||||
#- OAUTH2_AUTH_ENDPOINT=
|
||||
# OAuth2 Userinfo Endpoint. Example: /oauth/userinfo
|
||||
# example: OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
|
||||
#- OAUTH2_USERINFO_ENDPOINT=
|
||||
# OAuth2 Token Endpoint. Example: /oauth/token
|
||||
# example: OAUTH2_TOKEN_ENDPOINT=/oauth/token
|
||||
#- OAUTH2_TOKEN_ENDPOINT=
|
||||
# LDAP_ENABLE : Enable or not the connection by the LDAP
|
||||
# example : LDAP_ENABLE=true
|
||||
#- LDAP_ENABLE=false
|
||||
# LDAP_PORT : The port of the LDAP server
|
||||
# example : LDAP_PORT=389
|
||||
#- LDAP_PORT=389
|
||||
# LDAP_HOST : The host server for the LDAP server
|
||||
# example : LDAP_HOST=localhost
|
||||
#- LDAP_HOST=
|
||||
# LDAP_BASEDN : The base DN for the LDAP Tree
|
||||
# example : LDAP_BASEDN=ou=user,dc=example,dc=org
|
||||
#- LDAP_BASEDN=
|
||||
# LDAP_LOGIN_FALLBACK : Fallback on the default authentication method
|
||||
# example : LDAP_LOGIN_FALLBACK=true
|
||||
#- LDAP_LOGIN_FALLBACK=false
|
||||
# LDAP_RECONNECT : Reconnect to the server if the connection is lost
|
||||
# example : LDAP_RECONNECT=false
|
||||
#- LDAP_RECONNECT=true
|
||||
# LDAP_TIMEOUT : Overall timeout, in milliseconds
|
||||
# example : LDAP_TIMEOUT=12345
|
||||
#- LDAP_TIMEOUT=10000
|
||||
# LDAP_IDLE_TIMEOUT : Specifies the timeout for idle LDAP connections in milliseconds
|
||||
# example : LDAP_IDLE_TIMEOUT=12345
|
||||
#- LDAP_IDLE_TIMEOUT=10000
|
||||
# LDAP_CONNECT_TIMEOUT : Connection timeout, in milliseconds
|
||||
# example : LDAP_CONNECT_TIMEOUT=12345
|
||||
#- LDAP_CONNECT_TIMEOUT=10000
|
||||
# LDAP_AUTHENTIFICATION : If the LDAP needs a user account to search
|
||||
# example : LDAP_AUTHENTIFICATION=true
|
||||
#- LDAP_AUTHENTIFICATION=false
|
||||
# LDAP_AUTHENTIFICATION_USERDN : The search user DN
|
||||
# example : LDAP_AUTHENTIFICATION_USERDN=cn=admin,dc=example,dc=org
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=
|
||||
# LDAP_AUTHENTIFICATION_PASSWORD : The password for the search user
|
||||
# example : AUTHENTIFICATION_PASSWORD=admin
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=
|
||||
# LDAP_LOG_ENABLED : Enable logs for the module
|
||||
# example : LDAP_LOG_ENABLED=true
|
||||
#- LDAP_LOG_ENABLED=false
|
||||
# LDAP_BACKGROUND_SYNC : If the sync of the users should be done in the background
|
||||
# example : LDAP_BACKGROUND_SYNC=true
|
||||
#- LDAP_BACKGROUND_SYNC=false
|
||||
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
|
||||
# example : LDAP_BACKGROUND_SYNC_INTERVAL=12345
|
||||
#- LDAP_BACKGROUND_SYNC_INTERVAL=100
|
||||
# LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED :
|
||||
# example : LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=true
|
||||
#- LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false
|
||||
# LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS :
|
||||
# example : LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=true
|
||||
#- LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false
|
||||
# LDAP_ENCRYPTION : If using LDAPS
|
||||
# example : LDAP_ENCRYPTION=ssl
|
||||
#- LDAP_ENCRYPTION=false
|
||||
# LDAP_CA_CERT : The certification for the LDAPS server. Certificate needs to be included in this docker-compose.yml file.
|
||||
# example : LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+zCCA+OgAwIBAgIkAhwR/6TVLmdRY6hHxvUFWc0+Enmu/Hu6cj+G2FIdAgIC...-----END CERTIFICATE-----
|
||||
#- LDAP_CA_CERT=
|
||||
# LDAP_REJECT_UNAUTHORIZED : Reject Unauthorized Certificate
|
||||
# example : LDAP_REJECT_UNAUTHORIZED=true
|
||||
#- LDAP_REJECT_UNAUTHORIZED=false
|
||||
# LDAP_USER_SEARCH_FILTER : Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
|
||||
# example : LDAP_USER_SEARCH_FILTER=
|
||||
#- LDAP_USER_SEARCH_FILTER=
|
||||
# LDAP_USER_SEARCH_SCOPE : base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
|
||||
# example : LDAP_USER_SEARCH_SCOPE=one
|
||||
#- LDAP_USER_SEARCH_SCOPE=
|
||||
# LDAP_USER_SEARCH_FIELD : Which field is used to find the user
|
||||
# example : LDAP_USER_SEARCH_FIELD=uid
|
||||
#- LDAP_USER_SEARCH_FIELD=
|
||||
# LDAP_SEARCH_PAGE_SIZE : Used for pagination (0=unlimited)
|
||||
# example : LDAP_SEARCH_PAGE_SIZE=12345
|
||||
#- LDAP_SEARCH_PAGE_SIZE=0
|
||||
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
|
||||
# example : LDAP_SEARCH_SIZE_LIMIT=12345
|
||||
#- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering
|
||||
# example : LDAP_GROUP_FILTER_ENABLE=true
|
||||
#- LDAP_GROUP_FILTER_ENABLE=false
|
||||
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
|
||||
# example : LDAP_GROUP_FILTER_OBJECTCLASS=group
|
||||
#- LDAP_GROUP_FILTER_OBJECTCLASS=
|
||||
# LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
|
||||
# LDAP_GROUP_FILTER_GROUP_NAME :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_NAME=
|
||||
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier)
|
||||
# example : LDAP_UNIQUE_IDENTIFIER_FIELD=guid
|
||||
#- LDAP_UNIQUE_IDENTIFIER_FIELD=
|
||||
# LDAP_UTF8_NAMES_SLUGIFY : Convert the username to utf8
|
||||
# example : LDAP_UTF8_NAMES_SLUGIFY=false
|
||||
#- LDAP_UTF8_NAMES_SLUGIFY=true
|
||||
# LDAP_USERNAME_FIELD : Which field contains the ldap username
|
||||
# example : LDAP_USERNAME_FIELD=username
|
||||
#- LDAP_USERNAME_FIELD=
|
||||
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname
|
||||
# example : LDAP_FULLNAME_FIELD=fullname
|
||||
#- LDAP_FULLNAME_FIELD=
|
||||
# LDAP_MERGE_EXISTING_USERS :
|
||||
# example : LDAP_MERGE_EXISTING_USERS=true
|
||||
#- LDAP_MERGE_EXISTING_USERS=false
|
||||
# LDAP_SYNC_USER_DATA :
|
||||
# example : LDAP_SYNC_USER_DATA=true
|
||||
#- LDAP_SYNC_USER_DATA=false
|
||||
# LDAP_SYNC_USER_DATA_FIELDMAP :
|
||||
# example : LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
|
||||
#- LDAP_SYNC_USER_DATA_FIELDMAP=
|
||||
# LDAP_SYNC_GROUP_ROLES :
|
||||
# example :
|
||||
#- LDAP_SYNC_GROUP_ROLES=
|
||||
# LDAP_DEFAULT_DOMAIN : The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
# example :
|
||||
#- LDAP_DEFAULT_DOMAIN=
|
||||
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
|
||||
# example : LOGOUT_WITH_TIMER=true
|
||||
#- LOGOUT_WITH_TIMER=
|
||||
# LOGOUT_IN : The number of days
|
||||
# example : LOGOUT_IN=1
|
||||
#- LOGOUT_IN=
|
||||
# LOGOUT_ON_HOURS : The number of hours
|
||||
# example : LOGOUT_ON_HOURS=9
|
||||
#- LOGOUT_ON_HOURS=
|
||||
# LOGOUT_ON_MINUTES : The number of minutes
|
||||
# example : LOGOUT_ON_MINUTES=55
|
||||
#- LOGOUT_ON_MINUTES=
|
||||
|
||||
depends_on:
|
||||
- wekandb
|
||||
|
||||
volumes:
|
||||
wekan-db:
|
||||
driver: local
|
||||
wekan-db-dump:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
wekan-tier:
|
||||
driver: bridge
|
||||
@@ -1,260 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
# Docker: Wekan <=> MongoDB <=> ToroDB => PostgreSQL read-only mirroring
|
||||
# for reporting with SQL, and accessing with any programming language,
|
||||
# reporting package and Office suite that can connect to PostgreSQL.
|
||||
# https://github.com/wekan/wekan-postgresql
|
||||
|
||||
services:
|
||||
torodb-stampede:
|
||||
image: torodb/stampede:1.0.0-SNAPSHOT
|
||||
networks:
|
||||
- wekan-tier
|
||||
links:
|
||||
- postgres
|
||||
- mongodb
|
||||
environment:
|
||||
- POSTGRES_PASSWORD
|
||||
- TORODB_SETUP=true
|
||||
- TORODB_SYNC_SOURCE=mongodb:27017
|
||||
- TORODB_BACKEND_HOST=postgres
|
||||
- TORODB_BACKEND_PORT=5432
|
||||
- TORODB_BACKEND_DATABASE=wekan
|
||||
- TORODB_BACKEND_USER=wekan
|
||||
- TORODB_BACKEND_PASSWORD=wekan
|
||||
- DEBUG
|
||||
postgres:
|
||||
image: postgres:9.6
|
||||
networks:
|
||||
- wekan-tier
|
||||
environment:
|
||||
- POSTGRES_PASSWORD
|
||||
ports:
|
||||
- "15432:5432"
|
||||
mongodb:
|
||||
image: mongo:3.2
|
||||
networks:
|
||||
- wekan-tier
|
||||
ports:
|
||||
- "28017:27017"
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- "-c"
|
||||
- mongo --nodb --eval '
|
||||
var db;
|
||||
while (!db) {
|
||||
try {
|
||||
db = new Mongo("mongodb:27017").getDB("local");
|
||||
} catch(ex) {}
|
||||
sleep(3000);
|
||||
};
|
||||
rs.initiate({_id:"rs1",members:[{_id:0,host:"mongodb:27017"}]});
|
||||
' 1>/dev/null 2>&1 &
|
||||
mongod --replSet rs1
|
||||
wekan:
|
||||
image: quay.io/wekan/wekan
|
||||
container_name: wekan-app
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
ports:
|
||||
- 80:8080
|
||||
environment:
|
||||
- MONGO_URL=mongodb://mongodb:27017/wekan
|
||||
- ROOT_URL=http://localhost
|
||||
#---------------------------------------------------------------
|
||||
# == WEKAN API ==
|
||||
# Wekan Export Board works when WITH_API='true'.
|
||||
# If you disable Wekan API, Export Board does not work.
|
||||
- WITH_API=true
|
||||
# CORS: Set Access-Control-Allow-Origin header. Example: *
|
||||
#- CORS=*
|
||||
# Optional: Integration with Matomo https://matomo.org that is installed to your server
|
||||
# The address of the server where Matomo is hosted.
|
||||
# example: - MATOMO_ADDRESS=https://example.com/matomo
|
||||
#- MATOMO_ADDRESS=
|
||||
# The value of the site ID given in Matomo server for Wekan
|
||||
# example: - MATOMO_SITE_ID=12345
|
||||
#- MATOMO_SITE_ID=
|
||||
# The option do not track which enables users to not be tracked by matomo
|
||||
# example: - MATOMO_DO_NOT_TRACK=false
|
||||
#- MATOMO_DO_NOT_TRACK=
|
||||
# The option that allows matomo to retrieve the username:
|
||||
# example: MATOMO_WITH_USERNAME=true
|
||||
#- MATOMO_WITH_USERNAME=false
|
||||
# Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.
|
||||
# Setting this to false is not recommended, it also disables all other browser policy protections
|
||||
# and allows all iframing etc. See wekan/server/policy.js
|
||||
- BROWSER_POLICY_ENABLED=true
|
||||
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
||||
#- TRUSTED_URL=
|
||||
# What to send to Outgoing Webhook, or leave out. Example, that includes all that are default: cardId,listId,oldListId,boardId,comment,user,card,commentId .
|
||||
# example: WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
||||
#- WEBHOOKS_ATTRIBUTES=
|
||||
# Enable the OAuth2 connection
|
||||
# example: OAUTH2_ENABLED=true
|
||||
#- OAUTH2_ENABLED=false
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# OAuth2 Client ID, for example from Rocket.Chat. Example: abcde12345
|
||||
# example: OAUTH2_CLIENT_ID=abcde12345
|
||||
#- OAUTH2_CLIENT_ID=
|
||||
# OAuth2 Secret, for example from Rocket.Chat: Example: 54321abcde
|
||||
# example: OAUTH2_SECRET=54321abcde
|
||||
#- OAUTH2_SECRET=
|
||||
# OAuth2 Server URL, for example Rocket.Chat. Example: https://chat.example.com
|
||||
# example: OAUTH2_SERVER_URL=https://chat.example.com
|
||||
#- OAUTH2_SERVER_URL=
|
||||
# OAuth2 Authorization Endpoint. Example: /oauth/authorize
|
||||
# example: OAUTH2_AUTH_ENDPOINT=/oauth/authorize
|
||||
#- OAUTH2_AUTH_ENDPOINT=
|
||||
# OAuth2 Userinfo Endpoint. Example: /oauth/userinfo
|
||||
# example: OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
|
||||
#- OAUTH2_USERINFO_ENDPOINT=
|
||||
# OAuth2 Token Endpoint. Example: /oauth/token
|
||||
# example: OAUTH2_TOKEN_ENDPOINT=/oauth/token
|
||||
#- OAUTH2_TOKEN_ENDPOINT=
|
||||
# LDAP_ENABLE : Enable or not the connection by the LDAP
|
||||
# example : LDAP_ENABLE=true
|
||||
#- LDAP_ENABLE=false
|
||||
# LDAP_PORT : The port of the LDAP server
|
||||
# example : LDAP_PORT=389
|
||||
#- LDAP_PORT=389
|
||||
# LDAP_HOST : The host server for the LDAP server
|
||||
# example : LDAP_HOST=localhost
|
||||
#- LDAP_HOST=
|
||||
# LDAP_BASEDN : The base DN for the LDAP Tree
|
||||
# example : LDAP_BASEDN=ou=user,dc=example,dc=org
|
||||
#- LDAP_BASEDN=
|
||||
# LDAP_LOGIN_FALLBACK : Fallback on the default authentication method
|
||||
# example : LDAP_LOGIN_FALLBACK=true
|
||||
#- LDAP_LOGIN_FALLBACK=false
|
||||
# LDAP_RECONNECT : Reconnect to the server if the connection is lost
|
||||
# example : LDAP_RECONNECT=false
|
||||
#- LDAP_RECONNECT=true
|
||||
# LDAP_TIMEOUT : Overall timeout, in milliseconds
|
||||
# example : LDAP_TIMEOUT=12345
|
||||
#- LDAP_TIMEOUT=10000
|
||||
# LDAP_IDLE_TIMEOUT : Specifies the timeout for idle LDAP connections in milliseconds
|
||||
# example : LDAP_IDLE_TIMEOUT=12345
|
||||
#- LDAP_IDLE_TIMEOUT=10000
|
||||
# LDAP_CONNECT_TIMEOUT : Connection timeout, in milliseconds
|
||||
# example : LDAP_CONNECT_TIMEOUT=12345
|
||||
#- LDAP_CONNECT_TIMEOUT=10000
|
||||
# LDAP_AUTHENTIFICATION : If the LDAP needs a user account to search
|
||||
# example : LDAP_AUTHENTIFICATION=true
|
||||
#- LDAP_AUTHENTIFICATION=false
|
||||
# LDAP_AUTHENTIFICATION_USERDN : The search user DN
|
||||
# example : LDAP_AUTHENTIFICATION_USERDN=cn=admin,dc=example,dc=org
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=
|
||||
# LDAP_AUTHENTIFICATION_PASSWORD : The password for the search user
|
||||
# example : AUTHENTIFICATION_PASSWORD=admin
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=
|
||||
# LDAP_LOG_ENABLED : Enable logs for the module
|
||||
# example : LDAP_LOG_ENABLED=true
|
||||
#- LDAP_LOG_ENABLED=false
|
||||
# LDAP_BACKGROUND_SYNC : If the sync of the users should be done in the background
|
||||
# example : LDAP_BACKGROUND_SYNC=true
|
||||
#- LDAP_BACKGROUND_SYNC=false
|
||||
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
|
||||
# example : LDAP_BACKGROUND_SYNC_INTERVAL=12345
|
||||
#- LDAP_BACKGROUND_SYNC_INTERVAL=100
|
||||
# LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED :
|
||||
# example : LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=true
|
||||
#- LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false
|
||||
# LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS :
|
||||
# example : LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=true
|
||||
#- LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false
|
||||
# LDAP_ENCRYPTION : If using LDAPS
|
||||
# example : LDAP_ENCRYPTION=ssl
|
||||
#- LDAP_ENCRYPTION=false
|
||||
# LDAP_CA_CERT : The certification for the LDAPS server. Certificate needs to be included in this docker-compose.yml file.
|
||||
# example : LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+zCCA+OgAwIBAgIkAhwR/6TVLmdRY6hHxvUFWc0+Enmu/Hu6cj+G2FIdAgIC...-----END CERTIFICATE-----
|
||||
#- LDAP_CA_CERT=
|
||||
# LDAP_REJECT_UNAUTHORIZED : Reject Unauthorized Certificate
|
||||
# example : LDAP_REJECT_UNAUTHORIZED=true
|
||||
#- LDAP_REJECT_UNAUTHORIZED=false
|
||||
# LDAP_USER_SEARCH_FILTER : Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
|
||||
# example : LDAP_USER_SEARCH_FILTER=
|
||||
#- LDAP_USER_SEARCH_FILTER=
|
||||
# LDAP_USER_SEARCH_SCOPE : base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
|
||||
# example : LDAP_USER_SEARCH_SCOPE=one
|
||||
#- LDAP_USER_SEARCH_SCOPE=
|
||||
# LDAP_USER_SEARCH_FIELD : Which field is used to find the user
|
||||
# example : LDAP_USER_SEARCH_FIELD=uid
|
||||
#- LDAP_USER_SEARCH_FIELD=
|
||||
# LDAP_SEARCH_PAGE_SIZE : Used for pagination (0=unlimited)
|
||||
# example : LDAP_SEARCH_PAGE_SIZE=12345
|
||||
#- LDAP_SEARCH_PAGE_SIZE=0
|
||||
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
|
||||
# example : LDAP_SEARCH_SIZE_LIMIT=12345
|
||||
#- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering
|
||||
# example : LDAP_GROUP_FILTER_ENABLE=true
|
||||
#- LDAP_GROUP_FILTER_ENABLE=false
|
||||
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
|
||||
# example : LDAP_GROUP_FILTER_OBJECTCLASS=group
|
||||
#- LDAP_GROUP_FILTER_OBJECTCLASS=
|
||||
# LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
|
||||
# LDAP_GROUP_FILTER_GROUP_NAME :
|
||||
# example :
|
||||
#- LDAP_GROUP_FILTER_GROUP_NAME=
|
||||
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier)
|
||||
# example : LDAP_UNIQUE_IDENTIFIER_FIELD=guid
|
||||
#- LDAP_UNIQUE_IDENTIFIER_FIELD=
|
||||
# LDAP_UTF8_NAMES_SLUGIFY : Convert the username to utf8
|
||||
# example : LDAP_UTF8_NAMES_SLUGIFY=false
|
||||
#- LDAP_UTF8_NAMES_SLUGIFY=true
|
||||
# LDAP_USERNAME_FIELD : Which field contains the ldap username
|
||||
# example : LDAP_USERNAME_FIELD=username
|
||||
#- LDAP_USERNAME_FIELD=
|
||||
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname
|
||||
# example : LDAP_FULLNAME_FIELD=fullname
|
||||
#- LDAP_FULLNAME_FIELD=
|
||||
# LDAP_MERGE_EXISTING_USERS :
|
||||
# example : LDAP_MERGE_EXISTING_USERS=true
|
||||
#- LDAP_MERGE_EXISTING_USERS=false
|
||||
# LDAP_SYNC_USER_DATA :
|
||||
# example : LDAP_SYNC_USER_DATA=true
|
||||
#- LDAP_SYNC_USER_DATA=false
|
||||
# LDAP_SYNC_USER_DATA_FIELDMAP :
|
||||
# example : LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
|
||||
#- LDAP_SYNC_USER_DATA_FIELDMAP=
|
||||
# LDAP_SYNC_GROUP_ROLES :
|
||||
# example :
|
||||
#- LDAP_SYNC_GROUP_ROLES=
|
||||
# LDAP_DEFAULT_DOMAIN : The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
# example :
|
||||
#- LDAP_DEFAULT_DOMAIN=
|
||||
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
|
||||
# example : LOGOUT_WITH_TIMER=true
|
||||
#- LOGOUT_WITH_TIMER=
|
||||
# LOGOUT_IN : The number of days
|
||||
# example : LOGOUT_IN=1
|
||||
#- LOGOUT_IN=
|
||||
# LOGOUT_ON_HOURS : The number of hours
|
||||
# example : LOGOUT_ON_HOURS=9
|
||||
#- LOGOUT_ON_HOURS=
|
||||
# LOGOUT_ON_MINUTES : The number of minutes
|
||||
# example : LOGOUT_ON_MINUTES=55
|
||||
#- LOGOUT_ON_MINUTES=
|
||||
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
volumes:
|
||||
mongodb:
|
||||
driver: local
|
||||
mongodb-dump:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
wekan-tier:
|
||||
driver: bridge
|
||||
+462
-130
@@ -1,13 +1,101 @@
|
||||
version: '2'
|
||||
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
# 1) Edit settings
|
||||
# 2) docker-compose up -d
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== CREATING USERS AND LOGGING IN TO WEKAN ====
|
||||
# https://github.com/wekan/wekan/wiki/Adding-users
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== FORGOT PASSWORD ====
|
||||
# https://github.com/wekan/wekan/wiki/Forgot-Password
|
||||
#---------------------------------------------------------------------------------------------------------
|
||||
# ==== Upgrading Wekan to new version =====
|
||||
# 1) Stop Wekan:
|
||||
# docker-compose stop
|
||||
# 2) Download new version:
|
||||
# docker-compose pull wekan
|
||||
# 3) If you have more networks for VPN etc as described at bottom of
|
||||
# this config, download for them too:
|
||||
# docker-compose pull wekan2
|
||||
# 4) Start Wekan:
|
||||
# docker-compose start
|
||||
#----------------------------------------------------------------------------------
|
||||
# ==== OPTIONAL: DEDICATED DOCKER USER ====
|
||||
# 1) Optionally create a dedicated user for Wekan, for example:
|
||||
# sudo useradd -d /home/wekan -m -s /bin/bash wekan
|
||||
# 2) Add this user to the docker group, then logout+login or reboot:
|
||||
# sudo usermod -aG docker wekan
|
||||
# 3) Then login as user wekan.
|
||||
# 4) Create this file /home/wekan/docker-compose.yml with your modifications.
|
||||
#----------------------------------------------------------------------------------
|
||||
# ==== RUN DOCKER AS SERVICE ====
|
||||
# 1a) Running Docker as service, on Systemd like Debian 9, Ubuntu 16.04, CentOS 7:
|
||||
# sudo systemctl enable docker
|
||||
# sudo systemctl start docker
|
||||
# 1b) Running Docker as service, on init.d like Debian 8, Ubuntu 14.04, CentOS 6:
|
||||
# sudo update-rc.d docker defaults
|
||||
# sudo service docker start
|
||||
# ----------------------------------------------------------------------------------
|
||||
# ==== USAGE OF THIS docker-compose.yml ====
|
||||
# 1) For seeing does Wekan work, try this and check with your webbroser:
|
||||
# docker-compose up
|
||||
# 2) Stop Wekan and start Wekan in background:
|
||||
# docker-compose stop
|
||||
# docker-compose up -d
|
||||
# 3) See running Docker containers:
|
||||
# docker ps
|
||||
# 4) Stop Docker containers:
|
||||
# docker-compose stop
|
||||
# ----------------------------------------------------------------------------------
|
||||
# ===== INSIDE DOCKER CONTAINERS, AND BACKUP/RESTORE ====
|
||||
# https://github.com/wekan/wekan/wiki/Backup
|
||||
# If really necessary, repair MongoDB: https://github.com/wekan/wekan-mongodb/issues/6#issuecomment-424004116
|
||||
# 1) Going inside containers:
|
||||
# a) Wekan app, does not contain data
|
||||
# docker exec -it wekan-app bash
|
||||
# b) MongoDB, contains all data
|
||||
# docker exec -it wekan-db bash
|
||||
# 2) Copying database to outside of container:
|
||||
# docker exec -it wekan-db bash
|
||||
# cd /data
|
||||
# mongodump
|
||||
# exit
|
||||
# docker cp wekan-db:/data/dump .
|
||||
# 3) Restoring database
|
||||
# # 1) Stop wekan
|
||||
# docker stop wekan-app
|
||||
# # 2) Go inside database container
|
||||
# docker exec -it wekan-db bash
|
||||
# # 3) and data directory
|
||||
# cd /data
|
||||
# # 4) Remove previos dump
|
||||
# rm -rf dump
|
||||
# # 5) Exit db container
|
||||
# exit
|
||||
# # 6) Copy dump to inside docker container
|
||||
# docker cp dump wekan-db:/data/
|
||||
# # 7) Go inside database container
|
||||
# docker exec -it wekan-db bash
|
||||
# # 8) and data directory
|
||||
# cd /data
|
||||
# # 9) Restore
|
||||
# mongorestore --drop
|
||||
# # 10) Exit db container
|
||||
# exit
|
||||
# # 11) Start wekan
|
||||
# docker start wekan-app
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
services:
|
||||
|
||||
wekandb:
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== MONGODB AND METEOR VERSION ====
|
||||
# a) For Wekan Meteor 1.8.x version at meteor-1.8 branch, use mongo 4.x
|
||||
#image: mongo:4.0.4
|
||||
# b) For Wekan Meteor 1.6.x version at master/devel/edge branches.
|
||||
# Only for Snap and Sandstorm while they are not upgraded yet to Meteor 1.8.x
|
||||
image: mongo:3.2.21
|
||||
#-------------------------------------------------------------------------------------
|
||||
container_name: wekan-db
|
||||
restart: always
|
||||
command: mongod --smallfiles --oplogSize 128
|
||||
@@ -20,202 +108,446 @@ services:
|
||||
- wekan-db-dump:/dump
|
||||
|
||||
wekan:
|
||||
image: quay.io/wekan/wekan
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== MONGODB AND METEOR VERSION ====
|
||||
# a) For Wekan Meteor 1.8.x version at meteor-1.8 branch,
|
||||
# using https://quay.io/wekan/wekan automatic builds
|
||||
#image: quay.io/wekan/wekan:meteor-1.8
|
||||
# b) For Wekan Meteor 1.6.x version at master/devel/edge branches.
|
||||
# Only for Snap and Sandstorm while they are not upgraded yet to Meteor 1.8.x
|
||||
image: quay.io/wekan/wekan:master
|
||||
# c) Using specific Meteor 1.6.x version tag:
|
||||
# image: quay.io/wekan/wekan:v1.95
|
||||
# c) Using Docker Hub automatic builds https://hub.docker.com/r/wekanteam/wekan
|
||||
# image: wekanteam/wekan:meteor-1.8
|
||||
# image: wekanteam/wekan:v1.95
|
||||
#-------------------------------------------------------------------------------------
|
||||
container_name: wekan-app
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== BUILD wekan-app DOCKER CONTAINER FROM SOURCE, if you uncomment these ====
|
||||
# ==== and use commands: docker-compose up -d --build
|
||||
#build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# args:
|
||||
# - NODE_VERSION=${NODE_VERSION}
|
||||
# - METEOR_RELEASE=${METEOR_RELEASE}
|
||||
# - NPM_VERSION=${NPM_VERSION}
|
||||
# - ARCHITECTURE=${ARCHITECTURE}
|
||||
# - SRC_PATH=${SRC_PATH}
|
||||
# - METEOR_EDGE=${METEOR_EDGE}
|
||||
# - USE_EDGE=${USE_EDGE}
|
||||
#-------------------------------------------------------------------------------------
|
||||
ports:
|
||||
# Docker outsideport:insideport
|
||||
# Docker outsideport:insideport. Do not add anything extra here.
|
||||
# For example, if you want to have wekan on port 3001,
|
||||
# use 3001:8080 . Do not add any extra address etc here, that way it does not work.
|
||||
# remove port mapping if you use nginx reverse proxy, port 8080 is already exposed to wekan-tier network
|
||||
- 80:8080
|
||||
environment:
|
||||
- MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
- ROOT_URL=http://localhost
|
||||
#---------------------------------------------------------------
|
||||
# ==== ROOT_URL SETTING ====
|
||||
# Change ROOT_URL to your real Wekan URL, for example:
|
||||
# If you have Caddy/Nginx/Apache providing SSL
|
||||
# - https://example.com
|
||||
# - https://boards.example.com
|
||||
# This can be problematic with avatars https://github.com/wekan/wekan/issues/1776
|
||||
# - https://example.com/wekan
|
||||
# If without https, can be only wekan node, no need for Caddy/Nginx/Apache if you don't need them
|
||||
# - http://example.com
|
||||
# - http://boards.example.com
|
||||
# - http://192.168.1.100 <=== using at local LAN
|
||||
- ROOT_URL=http://localhost # <=== using only at same laptop/desktop where Wekan is installed
|
||||
#---------------------------------------------------------------
|
||||
# ==== EMAIL SETTINGS ====
|
||||
# Email settings are required in both MAIL_URL and Admin Panel,
|
||||
# see https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
|
||||
# For SSL in email, change smtp:// to smtps://
|
||||
# NOTE: Special characters need to be url-encoded in MAIL_URL.
|
||||
# You can encode those characters for example at: https://www.urlencoder.org
|
||||
#- MAIL_URL=smtp://user:pass@mailserver.example.com:25/
|
||||
- MAIL_URL=smtp://<mail_url>:25/?ignoreTLS=true&tls={rejectUnauthorized:false}
|
||||
- MAIL_FROM=Wekan Notifications <noreply.wekan@mydomain.com>
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: MONGO OPLOG SETTINGS =====
|
||||
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-378343587
|
||||
# We've fixed our CPU usage problem today with an environment
|
||||
# change around Wekan. I wasn't aware during implementation
|
||||
# that if you're using more than 1 instance of Wekan
|
||||
# (or any MeteorJS based tool) you're supposed to set
|
||||
# MONGO_OPLOG_URL as an environment variable.
|
||||
# Without setting it, Meteor will perform a pull-and-diff
|
||||
# update of it's dataset. With it, Meteor will update from
|
||||
# the OPLOG. See here
|
||||
# https://blog.meteor.com/tuning-meteor-mongo-livedata-for-scalability-13fe9deb8908
|
||||
# After setting
|
||||
# MONGO_OPLOG_URL=mongodb://<username>:<password>@<mongoDbURL>/local?authSource=admin&replicaSet=rsWekan
|
||||
# the CPU usage for all Wekan instances dropped to an average
|
||||
# of less than 10% with only occasional spikes to high usage
|
||||
# (I guess when someone is doing a lot of work)
|
||||
# - MONGO_OPLOG_URL=mongodb://<username>:<password>@<mongoDbURL>/local?authSource=admin&replicaSet=rsWekan
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
|
||||
# https://github.com/edemaine/kadira-compose
|
||||
# https://github.com/meteor/meteor-apm-agent
|
||||
# https://blog.meteor.com/kadira-apm-is-now-open-source-490469ffc85f
|
||||
#- APM_OPTIONS_ENDPOINT=http://<kadira-ip>:11011
|
||||
#- APM_APP_ID=
|
||||
#- APM_APP_SECRET=
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: LOGS AND STATS ====
|
||||
# https://github.com/wekan/wekan/wiki/Logs
|
||||
#
|
||||
# Daily export of Wekan changes as JSON to Logstash and ElasticSearch / Kibana (ELK)
|
||||
# https://github.com/wekan/wekan-logstash
|
||||
#
|
||||
# Statistics Python script for Wekan Dashboard
|
||||
# https://github.com/wekan/wekan-stats
|
||||
#
|
||||
# Console, file, and zulip logger on database changes https://github.com/wekan/wekan/pull/1010
|
||||
# with fix to replace console.log by winston logger https://github.com/wekan/wekan/pull/1033
|
||||
# but there could be bug https://github.com/wekan/wekan/issues/1094
|
||||
#
|
||||
# There is Feature Request: Logging date and time of all activity with summary reports,
|
||||
# and requesting reason for changing card to other column https://github.com/wekan/wekan/issues/1598
|
||||
#---------------------------------------------------------------
|
||||
# ==== WEKAN API AND EXPORT BOARD ====
|
||||
# Wekan Export Board works when WITH_API=true.
|
||||
# https://github.com/wekan/wekan/wiki/REST-API
|
||||
# https://github.com/wekan/wekan-gogs
|
||||
# If you disable Wekan API with false, Export Board does not work.
|
||||
- WITH_API=true
|
||||
# CORS: Set Access-Control-Allow-Origin header. Example: *
|
||||
#---------------------------------------------------------------
|
||||
# ==== PASSWORD BRUTE FORCE PROTECTION ====
|
||||
#https://atmospherejs.com/lucasantoniassi/accounts-lockout
|
||||
#Defaults below. Uncomment to change. wekan/server/accounts-lockout.js
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60
|
||||
#- ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60
|
||||
#- ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15
|
||||
#---------------------------------------------------------------
|
||||
# ==== EMAIL NOTIFICATION TIMEOUT, ms =====
|
||||
# Defaut: 30000 ms = 30s
|
||||
#- EMAIL_NOTIFICATION_TIMEOUT=30000
|
||||
#-----------------------------------------------------------------
|
||||
# ==== CORS =====
|
||||
# CORS: Set Access-Control-Allow-Origin header.
|
||||
#- CORS=*
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MATOMO INTEGRATION ====
|
||||
# Optional: Integration with Matomo https://matomo.org that is installed to your server
|
||||
# The address of the server where Matomo is hosted.
|
||||
# example: - MATOMO_ADDRESS=https://example.com/matomo
|
||||
#- MATOMO_ADDRESS=
|
||||
#- MATOMO_ADDRESS=https://example.com/matomo
|
||||
# The value of the site ID given in Matomo server for Wekan
|
||||
# example: - MATOMO_SITE_ID=12345
|
||||
#- MATOMO_SITE_ID=
|
||||
#- MATOMO_SITE_ID=1
|
||||
# The option do not track which enables users to not be tracked by matomo
|
||||
# example: - MATOMO_DO_NOT_TRACK=false
|
||||
#- MATOMO_DO_NOT_TRACK=
|
||||
#- MATOMO_DO_NOT_TRACK=true
|
||||
# The option that allows matomo to retrieve the username:
|
||||
# example: MATOMO_WITH_USERNAME=true
|
||||
#- MATOMO_WITH_USERNAME=false
|
||||
#- MATOMO_WITH_USERNAME=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== BROWSER POLICY AND TRUSTED IFRAME URL ====
|
||||
# Enable browser policy and allow one trusted URL that can have iframe that has Wekan embedded inside.
|
||||
# Setting this to false is not recommended, it also disables all other browser policy protections
|
||||
# and allows all iframing etc. See wekan/server/policy.js
|
||||
- BROWSER_POLICY_ENABLED=true
|
||||
# When browser policy is enabled, HTML code at this Trusted URL can have iframe that embeds Wekan inside.
|
||||
#- TRUSTED_URL=
|
||||
#- TRUSTED_URL=https://intra.example.com
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OUTGOING WEBHOOKS ====
|
||||
# What to send to Outgoing Webhook, or leave out. Example, that includes all that are default: cardId,listId,oldListId,boardId,comment,user,card,commentId .
|
||||
# example: WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
||||
#- WEBHOOKS_ATTRIBUTES=
|
||||
#- WEBHOOKS_ATTRIBUTES=cardId,listId,oldListId,boardId,comment,user,card,commentId
|
||||
#-----------------------------------------------------------------
|
||||
# ==== Debug OIDC OAuth2 etc ====
|
||||
#- DEBUG=true
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 AZURE ====
|
||||
# https://github.com/wekan/wekan/wiki/Azure
|
||||
# 1) Register the application with Azure. Make sure you capture
|
||||
# the application ID as well as generate a secret key.
|
||||
# 2) Configure the environment variables. This differs slightly
|
||||
# by installation type, but make sure you have the following:
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# Application GUID captured during app registration:
|
||||
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
|
||||
# Secret key generated during app registration:
|
||||
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
#- OAUTH2_SERVER_URL=https://login.microsoftonline.com/
|
||||
#- OAUTH2_AUTH_ENDPOINT=/oauth2/v2.0/authorize
|
||||
#- OAUTH2_USERINFO_ENDPOINT=https://graph.microsoft.com/oidc/userinfo
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/oauth2/v2.0/token
|
||||
# The claim name you want to map to the unique ID field:
|
||||
#- OAUTH2_ID_MAP=email
|
||||
# The claim name you want to map to the username field:
|
||||
#- OAUTH2_USERNAME_MAP=email
|
||||
# The claim name you want to map to the full name field:
|
||||
#- OAUTH2_FULLNAME_MAP=name
|
||||
# Tthe claim name you want to map to the email field:
|
||||
#- OAUTH2_EMAIL_MAP=email
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 KEYCLOAK ====
|
||||
# https://github.com/wekan/wekan/wiki/Keycloak <== MAPPING INFO, REQUIRED
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
#- OAUTH2_CLIENT_ID=<Keycloak create Client ID>
|
||||
#- OAUTH2_SERVER_URL=<Keycloak server name>/auth
|
||||
#- OAUTH2_AUTH_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/auth
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/userinfo
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/token
|
||||
#- OAUTH2_SECRET=<keycloak client secret>
|
||||
#-----------------------------------------------------------------
|
||||
# ==== OAUTH2 DOORKEEPER ====
|
||||
# https://github.com/wekan/wekan/issues/1874
|
||||
# https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# Enable the OAuth2 connection
|
||||
# example: OAUTH2_ENABLED=true
|
||||
#- OAUTH2_ENABLED=false
|
||||
#- OAUTH2_ENABLED=true
|
||||
# OAuth2 docs: https://github.com/wekan/wekan/wiki/OAuth2
|
||||
# OAuth2 Client ID, for example from Rocket.Chat. Example: abcde12345
|
||||
# example: OAUTH2_CLIENT_ID=abcde12345
|
||||
#- OAUTH2_CLIENT_ID=
|
||||
# OAuth2 Secret, for example from Rocket.Chat: Example: 54321abcde
|
||||
# example: OAUTH2_SECRET=54321abcde
|
||||
#- OAUTH2_SECRET=
|
||||
# OAuth2 Server URL, for example Rocket.Chat. Example: https://chat.example.com
|
||||
# example: OAUTH2_SERVER_URL=https://chat.example.com
|
||||
#- OAUTH2_SERVER_URL=
|
||||
# OAuth2 Authorization Endpoint. Example: /oauth/authorize
|
||||
# example: OAUTH2_AUTH_ENDPOINT=/oauth/authorize
|
||||
#- OAUTH2_AUTH_ENDPOINT=
|
||||
# OAuth2 Userinfo Endpoint. Example: /oauth/userinfo
|
||||
# example: OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
|
||||
#- OAUTH2_USERINFO_ENDPOINT=
|
||||
# OAuth2 Token Endpoint. Example: /oauth/token
|
||||
# example: OAUTH2_TOKEN_ENDPOINT=/oauth/token
|
||||
#- OAUTH2_TOKEN_ENDPOINT=
|
||||
# LDAP_ENABLE : Enable or not the connection by the LDAP
|
||||
# example : LDAP_ENABLE=true
|
||||
#- LDAP_ENABLE=false
|
||||
# LDAP_PORT : The port of the LDAP server
|
||||
# example : LDAP_PORT=389
|
||||
# OAuth2 login style: popup or redirect.
|
||||
#- OAUTH2_LOGIN_STYLE=redirect
|
||||
# OAuth2 Client ID.
|
||||
#- OAUTH2_CLIENT_ID=abcde12345
|
||||
# OAuth2 Secret.
|
||||
#- OAUTH2_SECRET=54321abcde
|
||||
# OAuth2 Server URL.
|
||||
#- OAUTH2_SERVER_URL=https://chat.example.com
|
||||
# OAuth2 Authorization Endpoint.
|
||||
#- OAUTH2_AUTH_ENDPOINT=/oauth/authorize
|
||||
# OAuth2 Userinfo Endpoint.
|
||||
#- OAUTH2_USERINFO_ENDPOINT=/oauth/userinfo
|
||||
# OAuth2 Token Endpoint.
|
||||
#- OAUTH2_TOKEN_ENDPOINT=/oauth/token
|
||||
# OAuth2 ID Mapping
|
||||
#- OAUTH2_ID_MAP=
|
||||
# OAuth2 Username Mapping
|
||||
#- OAUTH2_USERNAME_MAP=
|
||||
# OAuth2 Fullname Mapping
|
||||
#- OAUTH2_FULLNAME_MAP=
|
||||
# OAuth2 Email Mapping
|
||||
#- OAUTH2_EMAIL_MAP=
|
||||
#-----------------------------------------------------------------
|
||||
# ==== LDAP: UNCOMMENT ALL TO ENABLE LDAP ====
|
||||
# https://github.com/wekan/wekan/wiki/LDAP
|
||||
# For Snap settings see https://github.com/wekan/wekan-snap/wiki/Supported-settings-keys
|
||||
# Most settings work both on Snap and Docker below.
|
||||
# Note: Do not add single quotes '' to variables. Having spaces still works without quotes where required.
|
||||
#
|
||||
# The default authentication method used if a user does not exist to create and authenticate. Can be set as ldap.
|
||||
#- DEFAULT_AUTHENTICATION_METHOD=ldap
|
||||
#
|
||||
# Enable or not the connection by the LDAP
|
||||
#- LDAP_ENABLE=true
|
||||
#
|
||||
# The port of the LDAP server
|
||||
#- LDAP_PORT=389
|
||||
# LDAP_HOST : The host server for the LDAP server
|
||||
# example : LDAP_HOST=localhost
|
||||
#- LDAP_HOST=
|
||||
# LDAP_BASEDN : The base DN for the LDAP Tree
|
||||
# example : LDAP_BASEDN=ou=user,dc=example,dc=org
|
||||
#- LDAP_BASEDN=
|
||||
# LDAP_LOGIN_FALLBACK : Fallback on the default authentication method
|
||||
# example : LDAP_LOGIN_FALLBACK=true
|
||||
#
|
||||
# The host server for the LDAP server
|
||||
#- LDAP_HOST=localhost
|
||||
#
|
||||
# The base DN for the LDAP Tree
|
||||
#- LDAP_BASEDN=ou=user,dc=example,dc=org
|
||||
#
|
||||
# Fallback on the default authentication method
|
||||
#- LDAP_LOGIN_FALLBACK=false
|
||||
# LDAP_RECONNECT : Reconnect to the server if the connection is lost
|
||||
# example : LDAP_RECONNECT=false
|
||||
#
|
||||
# Reconnect to the server if the connection is lost
|
||||
#- LDAP_RECONNECT=true
|
||||
# LDAP_TIMEOUT : Overall timeout, in milliseconds
|
||||
# example : LDAP_TIMEOUT=12345
|
||||
#
|
||||
# Overall timeout, in milliseconds
|
||||
#- LDAP_TIMEOUT=10000
|
||||
# LDAP_IDLE_TIMEOUT : Specifies the timeout for idle LDAP connections in milliseconds
|
||||
# example : LDAP_IDLE_TIMEOUT=12345
|
||||
#
|
||||
# Specifies the timeout for idle LDAP connections in milliseconds
|
||||
#- LDAP_IDLE_TIMEOUT=10000
|
||||
# LDAP_CONNECT_TIMEOUT : Connection timeout, in milliseconds
|
||||
# example : LDAP_CONNECT_TIMEOUT=12345
|
||||
#
|
||||
# Connection timeout, in milliseconds
|
||||
#- LDAP_CONNECT_TIMEOUT=10000
|
||||
# LDAP_AUTHENTIFICATION : If the LDAP needs a user account to search
|
||||
# example : LDAP_AUTHENTIFICATION=true
|
||||
#- LDAP_AUTHENTIFICATION=false
|
||||
# LDAP_AUTHENTIFICATION_USERDN : The search user DN
|
||||
# example : LDAP_AUTHENTIFICATION_USERDN=cn=admin,dc=example,dc=org
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=
|
||||
# LDAP_AUTHENTIFICATION_PASSWORD : The password for the search user
|
||||
# example : AUTHENTIFICATION_PASSWORD=admin
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=
|
||||
# LDAP_LOG_ENABLED : Enable logs for the module
|
||||
# example : LDAP_LOG_ENABLED=true
|
||||
#- LDAP_LOG_ENABLED=false
|
||||
# LDAP_BACKGROUND_SYNC : If the sync of the users should be done in the background
|
||||
# example : LDAP_BACKGROUND_SYNC=true
|
||||
#
|
||||
# If the LDAP needs a user account to search
|
||||
#- LDAP_AUTHENTIFICATION=true
|
||||
#
|
||||
# The search user DN
|
||||
#- LDAP_AUTHENTIFICATION_USERDN=cn=wekan_adm,ou=serviceaccounts,ou=admin,ou=prod,dc=mydomain,dc=com
|
||||
#
|
||||
# The password for the search user
|
||||
#- LDAP_AUTHENTIFICATION_PASSWORD=pwd
|
||||
#
|
||||
# Enable logs for the module
|
||||
#- LDAP_LOG_ENABLED=true
|
||||
#
|
||||
# If the sync of the users should be done in the background
|
||||
#- LDAP_BACKGROUND_SYNC=false
|
||||
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
|
||||
# example : LDAP_BACKGROUND_SYNC_INTERVAL=12345
|
||||
#
|
||||
# At which interval does the background task sync in milliseconds
|
||||
#- LDAP_BACKGROUND_SYNC_INTERVAL=100
|
||||
# LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED :
|
||||
# example : LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=true
|
||||
#
|
||||
#- LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false
|
||||
# LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS :
|
||||
# example : LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=true
|
||||
#
|
||||
#- LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false
|
||||
# LDAP_ENCRYPTION : If using LDAPS
|
||||
# example : LDAP_ENCRYPTION=ssl
|
||||
#
|
||||
# If using LDAPS: LDAP_ENCRYPTION=ssl
|
||||
#- LDAP_ENCRYPTION=false
|
||||
# LDAP_CA_CERT : The certification for the LDAPS server. Certificate needs to be included in this docker-compose.yml file.
|
||||
# example : LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+zCCA+OgAwIBAgIkAhwR/6TVLmdRY6hHxvUFWc0+Enmu/Hu6cj+G2FIdAgIC...-----END CERTIFICATE-----
|
||||
#- LDAP_CA_CERT=
|
||||
# LDAP_REJECT_UNAUTHORIZED : Reject Unauthorized Certificate
|
||||
# example : LDAP_REJECT_UNAUTHORIZED=true
|
||||
#
|
||||
# The certification for the LDAPS server. Certificate needs to be included in this docker-compose.yml file.
|
||||
#- LDAP_CA_CERT=-----BEGIN CERTIFICATE-----MIIE+G2FIdAgIC...-----END CERTIFICATE-----
|
||||
#
|
||||
# Reject Unauthorized Certificate
|
||||
#- LDAP_REJECT_UNAUTHORIZED=false
|
||||
# LDAP_USER_SEARCH_FILTER : Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
|
||||
# example : LDAP_USER_SEARCH_FILTER=
|
||||
#
|
||||
# Optional extra LDAP filters. Don't forget the outmost enclosing parentheses if needed
|
||||
#- LDAP_USER_SEARCH_FILTER=
|
||||
# LDAP_USER_SEARCH_SCOPE : base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
|
||||
# example : LDAP_USER_SEARCH_SCOPE=one
|
||||
#- LDAP_USER_SEARCH_SCOPE=
|
||||
# LDAP_USER_SEARCH_FIELD : Which field is used to find the user
|
||||
# example : LDAP_USER_SEARCH_FIELD=uid
|
||||
#- LDAP_USER_SEARCH_FIELD=
|
||||
# LDAP_SEARCH_PAGE_SIZE : Used for pagination (0=unlimited)
|
||||
# example : LDAP_SEARCH_PAGE_SIZE=12345
|
||||
#
|
||||
# base (search only in the provided DN), one (search only in the provided DN and one level deep), or sub (search the whole subtree)
|
||||
#- LDAP_USER_SEARCH_SCOPE=one
|
||||
#
|
||||
# Which field is used to find the user, like uid / sAMAccountName
|
||||
#- LDAP_USER_SEARCH_FIELD=sAMAccountName
|
||||
#
|
||||
# Used for pagination (0=unlimited)
|
||||
#- LDAP_SEARCH_PAGE_SIZE=0
|
||||
# LDAP_SEARCH_SIZE_LIMIT : The limit number of entries (0=unlimited)
|
||||
# example : LDAP_SEARCH_SIZE_LIMIT=12345
|
||||
#
|
||||
# The limit number of entries (0=unlimited)
|
||||
#- LDAP_SEARCH_SIZE_LIMIT=0
|
||||
# LDAP_GROUP_FILTER_ENABLE : Enable group filtering
|
||||
# example : LDAP_GROUP_FILTER_ENABLE=true
|
||||
#
|
||||
# Enable group filtering
|
||||
#- LDAP_GROUP_FILTER_ENABLE=false
|
||||
# LDAP_GROUP_FILTER_OBJECTCLASS : The object class for filtering
|
||||
# example : LDAP_GROUP_FILTER_OBJECTCLASS=group
|
||||
#
|
||||
# The object class for filtering. Example: group
|
||||
#- LDAP_GROUP_FILTER_OBJECTCLASS=
|
||||
# LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE :
|
||||
# example :
|
||||
#
|
||||
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE :
|
||||
# example :
|
||||
#
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
|
||||
# LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT :
|
||||
# example :
|
||||
#
|
||||
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
|
||||
# LDAP_GROUP_FILTER_GROUP_NAME :
|
||||
# example :
|
||||
#
|
||||
#- LDAP_GROUP_FILTER_GROUP_NAME=
|
||||
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier)
|
||||
# example : LDAP_UNIQUE_IDENTIFIER_FIELD=guid
|
||||
#
|
||||
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier). Example: guid
|
||||
#- LDAP_UNIQUE_IDENTIFIER_FIELD=
|
||||
#
|
||||
# LDAP_UTF8_NAMES_SLUGIFY : Convert the username to utf8
|
||||
# example : LDAP_UTF8_NAMES_SLUGIFY=false
|
||||
#- LDAP_UTF8_NAMES_SLUGIFY=true
|
||||
# LDAP_USERNAME_FIELD : Which field contains the ldap username
|
||||
# example : LDAP_USERNAME_FIELD=username
|
||||
#- LDAP_USERNAME_FIELD=
|
||||
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname
|
||||
# example : LDAP_FULLNAME_FIELD=fullname
|
||||
#- LDAP_FULLNAME_FIELD=
|
||||
# LDAP_MERGE_EXISTING_USERS :
|
||||
# example : LDAP_MERGE_EXISTING_USERS=true
|
||||
#
|
||||
# LDAP_USERNAME_FIELD : Which field contains the ldap username. username / sAMAccountName
|
||||
#- LDAP_USERNAME_FIELD=sAMAccountName
|
||||
#
|
||||
# LDAP_FULLNAME_FIELD : Which field contains the ldap fullname. fullname / sAMAccountName
|
||||
#- LDAP_FULLNAME_FIELD=fullname
|
||||
#
|
||||
#- LDAP_MERGE_EXISTING_USERS=false
|
||||
# LDAP_SYNC_USER_DATA :
|
||||
# example : LDAP_SYNC_USER_DATA=true
|
||||
#
|
||||
# Allow existing account matching by e-mail address when username does not match
|
||||
#- LDAP_EMAIL_MATCH_ENABLE=true
|
||||
#
|
||||
# LDAP_EMAIL_MATCH_REQUIRE : require existing account matching by e-mail address when username does match
|
||||
#- LDAP_EMAIL_MATCH_REQUIRE=true
|
||||
#
|
||||
# LDAP_EMAIL_MATCH_VERIFIED : require existing account email address to be verified for matching
|
||||
#- LDAP_EMAIL_MATCH_VERIFIED=true
|
||||
#
|
||||
# LDAP_EMAIL_FIELD : which field contains the LDAP e-mail address
|
||||
#- LDAP_EMAIL_FIELD=mail
|
||||
#-----------------------------------------------------------------
|
||||
#- LDAP_SYNC_USER_DATA=false
|
||||
# LDAP_SYNC_USER_DATA_FIELDMAP :
|
||||
# example : LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
|
||||
#- LDAP_SYNC_USER_DATA_FIELDMAP=
|
||||
# LDAP_SYNC_GROUP_ROLES :
|
||||
#
|
||||
#- LDAP_SYNC_USER_DATA_FIELDMAP={"cn":"name", "mail":"email"}
|
||||
#
|
||||
#- LDAP_SYNC_GROUP_ROLES=''
|
||||
#
|
||||
# The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
# example :
|
||||
#- LDAP_SYNC_GROUP_ROLES=
|
||||
# LDAP_DEFAULT_DOMAIN : The default domain of the ldap it is used to create email if the field is not map correctly with the LDAP_SYNC_USER_DATA_FIELDMAP
|
||||
# example :
|
||||
#- LDAP_DEFAULT_DOMAIN=
|
||||
#- LDAP_DEFAULT_DOMAIN=mydomain.com
|
||||
#
|
||||
# Enable/Disable syncing of admin status based on ldap groups:
|
||||
#- LDAP_SYNC_ADMIN_STATUS=true
|
||||
#
|
||||
# Comma separated list of admin group names to sync.
|
||||
#- LDAP_SYNC_ADMIN_GROUPS=group1,group2
|
||||
#---------------------------------------------------------------------
|
||||
# Login to LDAP automatically with HTTP header.
|
||||
# In below example for siteminder, at right side of = is header name.
|
||||
#- HEADER_LOGIN_ID=HEADERUID
|
||||
#- HEADER_LOGIN_FIRSTNAME=HEADERFIRSTNAME
|
||||
#- HEADER_LOGIN_LASTNAME=HEADERLASTNAME
|
||||
#- HEADER_LOGIN_EMAIL=HEADEREMAILADDRESS
|
||||
#---------------------------------------------------------------------
|
||||
# ==== LOGOUT TIMER, probably does not work yet ====
|
||||
# LOGOUT_WITH_TIMER : Enables or not the option logout with timer
|
||||
# example : LOGOUT_WITH_TIMER=true
|
||||
#- LOGOUT_WITH_TIMER=
|
||||
#
|
||||
# LOGOUT_IN : The number of days
|
||||
# example : LOGOUT_IN=1
|
||||
#- LOGOUT_IN=
|
||||
#
|
||||
# LOGOUT_ON_HOURS : The number of hours
|
||||
# example : LOGOUT_ON_HOURS=9
|
||||
#- LOGOUT_ON_HOURS=
|
||||
#
|
||||
# LOGOUT_ON_MINUTES : The number of minutes
|
||||
# example : LOGOUT_ON_MINUTES=55
|
||||
#- LOGOUT_ON_MINUTES=
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
depends_on:
|
||||
- wekandb
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
|
||||
# When using Wekan both at office LAN and remote VPN:
|
||||
# 1) Have above Wekan docker container config with LAN IP address
|
||||
# 2) Copy all of above wekan container config below, look above of this part above and all config below it,
|
||||
# before above depends_on: part:
|
||||
#
|
||||
# wekan:
|
||||
# #-------------------------------------------------------------------------------------
|
||||
# # ==== MONGODB AND METEOR VERSION ====
|
||||
# # a) For Wekan Meteor 1.8.x version at meteor-1.8 branch, .....
|
||||
#
|
||||
#
|
||||
# and change name to different name like wekan2 or wekanvpn, and change ROOT_URL to server VPN IP
|
||||
# address.
|
||||
# 3) This way both Wekan containers can use same MongoDB database
|
||||
# and see the same Wekan boards.
|
||||
# 4) You could also add 3rd Wekan container for 3rd network etc.
|
||||
# EXAMPLE:
|
||||
# wekan2:
|
||||
# ....COPY CONFIG FROM ABOVE TO HERE...
|
||||
# environment:
|
||||
# - ROOT_URL='http://10.10.10.10'
|
||||
# ...COPY CONFIG FROM ABOVE TO HERE...
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
# OPTIONAL NGINX CONFIG FOR REVERSE PROXY
|
||||
# nginx:
|
||||
# image: nginx
|
||||
# container_name: nginx
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# depends_on:
|
||||
# - wekan
|
||||
# ports:
|
||||
# - 80:80
|
||||
# - 443:443
|
||||
# volumes:
|
||||
# - ./nginx/ssl:/etc/nginx/ssl/:ro
|
||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
## Alternative volume config:
|
||||
## volumes:
|
||||
## - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
## - ./nginx/ssl/ssl.conf:/etc/nginx/conf.d/ssl/ssl.conf:ro
|
||||
## - ./nginx/ssl/testvm-ehu.crt:/etc/nginx/conf.d/ssl/certs/mycert.crt:ro
|
||||
## - ./nginx/ssl/testvm-ehu.key:/etc/nginx/conf.d/ssl/certs/mykey.key:ro
|
||||
## - ./nginx/ssl/pphrase:/etc/nginx/conf.d/ssl/pphrase:ro
|
||||
|
||||
volumes:
|
||||
wekan-db:
|
||||
driver: local
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Find text from all subdirectories
|
||||
# and ignore all temporary directories:
|
||||
# - node-modules = installed node modules
|
||||
# - .build = Wekan bundle that is combined from source. Do not edit these, these are deleted and recreated.
|
||||
# - .meteor = Meteor version, packages etc at .meteor/local
|
||||
# - .git = git history
|
||||
|
||||
# If less or more that 1 parameter, show usage.
|
||||
if (( $# != 1 )); then
|
||||
echo 'Usage: ./find.sh text-to-find'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
find . | grep -v node_modules | grep -v .build | grep -v .meteor | grep -v .git | xargs grep --no-messages $1 | less
|
||||
@@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
OWNERS
|
||||
@@ -0,0 +1,13 @@
|
||||
name: wekan
|
||||
version: 1.0.0
|
||||
appVersion: 2.x.x
|
||||
kubeVersion: "^1.8.0-0"
|
||||
description: Open Source kanban
|
||||
home: https://wekan.github.io/
|
||||
icon: https://wekan.github.io/wekan-logo.svg
|
||||
sources:
|
||||
- https://github.com/wekan/wekan
|
||||
maintainers:
|
||||
- name: technotaff
|
||||
email: github@randall.cc
|
||||
engine: gotpl
|
||||
@@ -0,0 +1,4 @@
|
||||
approvers:
|
||||
- technotaff
|
||||
reviewers:
|
||||
- technotaff
|
||||
@@ -0,0 +1,58 @@
|
||||
# Helm Chart for Wekan
|
||||
|
||||
## Features
|
||||
|
||||
o Uses a MongoDB replica set by default - this allows fault-tolerant
|
||||
and scalable MongoDB deployment (or just set the replicas to 1 for
|
||||
a single server install)
|
||||
|
||||
o Optional Horizontal Pod Autoscaler (HPA), so that your Wekan pods
|
||||
will scale automatically with increased CPU load.
|
||||
|
||||
## The configurable values (values.yaml)
|
||||
|
||||
Scaling Wekan:
|
||||
|
||||
```yaml
|
||||
## Configuration for wekan component
|
||||
##
|
||||
|
||||
replicaCount: 1
|
||||
```
|
||||
**replicaCount** Will set the initial number of replicas for the Wekan pod (and container)
|
||||
|
||||
```yaml
|
||||
## Configure an horizontal pod autoscaler
|
||||
##
|
||||
autoscaling:
|
||||
enabled: true
|
||||
config:
|
||||
minReplicas: 1
|
||||
maxReplicas: 16
|
||||
## Note: when setting this, a `resources.request.cpu` is required. You
|
||||
## likely want to set it to `1` or some lower value.
|
||||
##
|
||||
targetCPUUtilizationPercentage: 80
|
||||
```
|
||||
This section (if *enabled* is set to **true**) will enable the Kubernetes Horizontal Pod Autoscaler (HPA).
|
||||
|
||||
**minReplicas:** this is the minimum number of pods to scale down to (We recommend setting this to the same value as **replicaCount**).
|
||||
|
||||
**maxReplicas:** this is the maximum number of pods to scale up to.
|
||||
|
||||
**targetCPUUtilizationPercentage:** This is the CPU at which the HPA will scale-out the number of Wekan pods.
|
||||
|
||||
```yaml
|
||||
mongodb-replicaset:
|
||||
enabled: true
|
||||
replicas: 3
|
||||
replicaSetName: rs0
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
runAsNonRoot: true
|
||||
```
|
||||
|
||||
This section controls the scale of the MongoDB redundant Replica Set.
|
||||
|
||||
**replicas:** This is the number of MongoDB instances to include in the set. You can set this to 1 for a single server - this will still allow you to scale-up later with a helm upgrade.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
- name: mongodb-replicaset
|
||||
version: 3.6.x
|
||||
repository: "https://kubernetes-charts.storage.googleapis.com/"
|
||||
condition: mongodb-replicaset.enabled
|
||||
@@ -0,0 +1,19 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "wekan.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get svc -w {{ template "wekan.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "wekan.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "wekan.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:8080
|
||||
{{- end }}
|
||||
@@ -0,0 +1,82 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "wekan.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "wekan.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified name for the wekan data app.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "wekan.localdata.fullname" -}}
|
||||
{{- if .Values.localdata.fullnameOverride -}}
|
||||
{{- .Values.localdata.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- printf "%s-localdata" .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s-localdata" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "wekan.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use for the api component
|
||||
*/}}
|
||||
{{- define "wekan.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccounts.create -}}
|
||||
{{ default (include "wekan.fullname" .) .Values.serviceAccounts.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.serviceAccounts.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified mongodb-replicaset name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "wekan.mongodb-replicaset.fullname" -}}
|
||||
{{- $name := default "mongodb-replicaset" (index .Values "mongodb-replicaset" "nameOverride") -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the MongoDB URL. If MongoDB is installed as part of this chart, use k8s service discovery,
|
||||
else use user-provided URL.
|
||||
*/}}
|
||||
{{- define "mongodb-replicaset.url" -}}
|
||||
{{- if (index .Values "mongodb-replicaset" "enabled") -}}
|
||||
{{- $count := (int (index .Values "mongodb-replicaset" "replicas")) -}}
|
||||
{{- $release := .Release.Name -}}
|
||||
mongodb://{{- range $v := until $count }}{{ $release }}-mongodb-replicaset-{{ $v }}.{{ $release }}-mongodb-replicaset:27017{{ if ne $v (sub $count 1) }},{{- end -}}{{- end -}}?replicaSet={{ index .Values "mongodb-replicaset" "replicaSetName" }}
|
||||
{{- else -}}
|
||||
{{- index .Values "mongodb-replicaset" "url" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,58 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
component: wekan
|
||||
release: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
component: wekan
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
serviceAccountName: {{ template "wekan.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
terminationMessagePolicy: FallbackToLogsOnError
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: ROOT_URL
|
||||
value: {{ .Values.root_url | default "https://wekan.local" | quote }}
|
||||
- name: MONGO_URL
|
||||
value: "{{ template "mongodb-replicaset.url" . }}"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 60
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.affinity }}
|
||||
affinity:
|
||||
{{ toYaml .Values.affinity | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if .Values.autoscaling.enabled -}}
|
||||
apiVersion: autoscaling/v1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
{{ toYaml .Values.autoscaling.config | indent 2 }}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "wekan.fullname" . -}}
|
||||
{{- $servicePort := .Values.service.port -}}
|
||||
{{- $ingressPath := .Values.ingress.path -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ . }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $ingressPath }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
type: Opaque
|
||||
data:
|
||||
accessKey: {{ .Values.credentials.accessKey | b64enc }}
|
||||
secretKey: {{ .Values.credentials.secretKey | b64enc }}
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
{{- if .Values.service.annotations }}
|
||||
annotations:
|
||||
{{ toYaml .Values.service.annotations | indent 4 }}
|
||||
{{- end }}
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: {{ template "wekan.name" . }}
|
||||
component: wekan
|
||||
release: {{ .Release.Name }}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccounts.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
chart: {{ template "wekan.chart" . }}
|
||||
component: wekan
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
name: {{ template "wekan.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: {{ template "wekan.fullname" . }}-test
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ template "wekan.fullname" . }}-test
|
||||
imagePullPolicy: IfNotPresent
|
||||
image: "docker.io/mesosphere/aws-cli:1.14.5"
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- aws s3 --endpoint-url=http://{{ include "wekan.fullname" . }} --region=us-east-1 ls
|
||||
env:
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
key: accessKey
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "wekan.fullname" . }}
|
||||
key: secretKey
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,110 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Wekan:
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
## Define serviceAccount names to create or use. Defaults to component's fully
|
||||
## qualified name.
|
||||
##
|
||||
serviceAccounts:
|
||||
create: true
|
||||
name: ""
|
||||
|
||||
## Wekan image configuration
|
||||
##
|
||||
image:
|
||||
repository: quay.io/wekan/wekan
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Configuration for wekan component
|
||||
##
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
## Specify wekan credentials
|
||||
##
|
||||
credentials:
|
||||
accessKey: access-key
|
||||
secretKey: secret-key
|
||||
|
||||
## Specify additional environmental variables for the Deployment
|
||||
##
|
||||
env: {}
|
||||
|
||||
service:
|
||||
type: NodePort
|
||||
port: 80
|
||||
annotations: {}
|
||||
# prometheus.io/scrape: "true"
|
||||
# prometheus.io/port: "8000"
|
||||
# prometheus.io/path: "/_/monitoring/metrics"
|
||||
|
||||
## Comma-separated string of allowed virtual hosts for external access.
|
||||
## This should match the ingress hosts
|
||||
##
|
||||
endpoint: wekan.local
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /*
|
||||
# This must match 'endpoint', unless your client supports different
|
||||
# hostnames.
|
||||
hosts: [ wekan.local ]
|
||||
# - wekan.local
|
||||
tls: []
|
||||
# - secretName: wekan-example-tls
|
||||
# hosts:
|
||||
# - wekan-example.local
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 300m
|
||||
limits:
|
||||
memory: 1Gi
|
||||
cpu: 500m
|
||||
|
||||
## Node labels for pod assignment
|
||||
## ref: https://kubernetes.io/docs/user-guide/node-selection/
|
||||
##
|
||||
nodeSelector: {}
|
||||
|
||||
## Tolerations for pod assignment
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
##
|
||||
tolerations: []
|
||||
|
||||
## Affinity for pod assignment
|
||||
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
|
||||
##
|
||||
affinity: {}
|
||||
|
||||
## Configure an horizontal pod autoscaler
|
||||
##
|
||||
autoscaling:
|
||||
enabled: true
|
||||
config:
|
||||
minReplicas: 1
|
||||
maxReplicas: 16
|
||||
## Note: when setting this, a `resources.request.cpu` is required. You
|
||||
## likely want to set it to `1` or some lower value.
|
||||
##
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# MongoDB:
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
mongodb-replicaset:
|
||||
enabled: true
|
||||
replicas: 3
|
||||
replicaSetName: rs0
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
runAsNonRoot: true
|
||||
#image:
|
||||
# tag: 3.2.21
|
||||
Loaded 100 of 565 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user