forked from vikunja/frontend
Compare commits
784 Commits
Author | SHA1 | Date | |
---|---|---|---|
d0efca194f | |||
d19a5d9714 | |||
90cad1c8dd | |||
|
057017c8eb | ||
d7ce8dd320 | |||
25b110ce48 | |||
33fe5e4f20 | |||
129ef769a3 | |||
9030a9f7c1 | |||
|
3748a496d5 | ||
890e7e1f52 | |||
9e0f2b0249 | |||
9a34c522b2 | |||
60dd698fad | |||
15ecafdf04 | |||
8902c15f7e | |||
d5358793de | |||
33798b8d88 | |||
c686e8677b | |||
5acc1696a9 | |||
c4976b6a22 | |||
d88ff594e1 | |||
66f0df0333 | |||
b742c55287 | |||
82c9a91d39 | |||
cd820a6cb2 | |||
2c4da79c1b | |||
c4f6465569 | |||
7d84601f6d | |||
8f94b7490c | |||
cbce7cd142 | |||
28e775be42 | |||
24ad2f892d | |||
7c1934aad0 | |||
ae2b0f97c4 | |||
70e0696300 | |||
|
60e49468cf | ||
10e566164e | |||
3c1b54c1a3 | |||
b80c6cf326 | |||
d2e6ab4505 | |||
32ed4c7da9 | |||
70ae19a903 | |||
cc2e0e79d3 | |||
|
49bdd00133 | ||
43eb742352 | |||
34b6692f25 | |||
152aefd365 | |||
2297872879 | |||
75ca7ecd61 | |||
|
0909d2cfe5 | ||
a60662b72b | |||
|
478b2c043e | ||
6bc54d7a92 | |||
e8e664b256 | |||
|
72fd932020 | ||
e5090b117f | |||
1b498a238c | |||
2a14325f62 | |||
ac6c4cf2bc | |||
66bad4b2b1 | |||
cdac38eabc | |||
79918620c3 | |||
dd1ae53d00 | |||
9894490616 | |||
|
a218eab609 | ||
c046bb95b3 | |||
12ad0d2ed3 | |||
8e505b6f51 | |||
3589251f55 | |||
b3b0d8d6e6 | |||
06572e8f0a | |||
fe7e06079e | |||
c2b04a2b81 | |||
d2148df6c8 | |||
|
305c5b32ee | ||
a446310986 | |||
8a22d1811e | |||
|
d34a872d40 | ||
06126de139 | |||
047435bb68 | |||
4be631888a | |||
|
be11397163 | ||
f3986c710b | |||
9c8266fb0d | |||
7148b56eea | |||
ff6645d2ab | |||
bd17afe466 | |||
25bd26bea6 | |||
83c0ef4e8b | |||
f55c42f124 | |||
4189fdadd9 | |||
2670cecf70 | |||
e54b5e88cc | |||
03c6e343c2 | |||
9acde8d017 | |||
813d2b56a0 | |||
1005182a50 | |||
4eba9479b0 | |||
dbe1ad9353 | |||
b6cd424aa3 | |||
6651adf6de | |||
3aa502e07d | |||
78a268ab07 | |||
4661c2e90e | |||
fbfa265dbf | |||
c1ad1b0639 | |||
c27661107f | |||
464cc0ed8c | |||
f8e2ef210f | |||
|
61379ed4c6 | ||
f1f99d065c | |||
da3eaf0d35 | |||
34182b8bbb | |||
20660564c1 | |||
c2ffe3a9dc | |||
a33e2f6c00 | |||
0ce150af23 | |||
06a1ff6f4b | |||
7c964c29d4 | |||
b9f0635d9f | |||
61baf02e26 | |||
59b05e9836 | |||
f68bb2625e | |||
25f9f5dceb | |||
35d68619f4 | |||
929d4f4023 | |||
a92eb31ab3 | |||
2006abd0a6 | |||
e4504748c4 | |||
854228034d | |||
88ce29aa77 | |||
0ca1b3a7f5 | |||
a118580704 | |||
771aad5420 | |||
411ae58e59 | |||
c3501f5060 | |||
5ca31d00ee | |||
1e2039325f | |||
472cca8ab8 | |||
2fad45e016 | |||
68a137acf9 | |||
95ba8b8a11 | |||
96c9407414 | |||
653415e764 | |||
73947f0ba4 | |||
389ca1b692 | |||
9c0e140e2e | |||
51d08a1637 | |||
35de8a40d8 | |||
80772f7578 | |||
faa62985df | |||
154d43a392 | |||
7fe5565654 | |||
1fcd1cdd4b | |||
ba057f3527 | |||
dd7b77e12d | |||
3845a45934 | |||
564808bd35 | |||
c0a66e4746 | |||
28242cfb23 | |||
818fb2b524 | |||
|
ad95bdd039 | ||
3faed19298 | |||
9114a86813 | |||
bae9a5c9be | |||
fe2d6d4467 | |||
|
96acea90ed | ||
21c98d5166 | |||
b3cb36c1e1 | |||
79ceaf6a2b | |||
5694b39489 | |||
32e5f9f757 | |||
928b338cf2 | |||
1a792e0667 | |||
6407644138 | |||
2db88b583b | |||
bef25c49d5 | |||
f01ea20a38 | |||
3c9083b90d | |||
169feaaf0f | |||
5d59392566 | |||
6593380013 | |||
69e94e58c4 | |||
cd8e497b24 | |||
aab2020e68 | |||
a050419fdf | |||
f0c3980700 | |||
68597c9709 | |||
5325f6d7d9 | |||
8c687350a0 | |||
bac679caf7 | |||
4f8ff17138 | |||
|
83e7138a18 | ||
8e44b87d07 | |||
4b0022664a | |||
d8ad934643 | |||
77ee1bfc3e | |||
8728647f00 | |||
bd7d09c17c | |||
77bedbd1cf | |||
2773612420 | |||
48cfdddff7 | |||
3f8e457d52 | |||
098b5fa2b1 | |||
5e4eb4a728 | |||
8930f61548 | |||
9a736cf65f | |||
2677f6254d | |||
bfcb36e093 | |||
9ec29cad30 | |||
c4f609a0c8 | |||
7e7535b860 | |||
df9181b34e | |||
e6a56f2822 | |||
3633d68269 | |||
dd3a5fe6b5 | |||
04642ae1ec | |||
eac19e28d6 | |||
11f94e4037 | |||
39cc7a00d8 | |||
02da1e171e | |||
ae177c73ea | |||
e6c4c18974 | |||
95487d7569 | |||
7b2a688b6e | |||
|
f5b3b21ce0 | ||
979561342a | |||
ad27f588a2 | |||
c7a989d7dc | |||
0e674d8300 | |||
121fd70235 | |||
d4cd90da45 | |||
c74612f24a | |||
64f9f4fd88 | |||
b50adaf4b5 | |||
7b92028e67 | |||
08d84f7994 | |||
f95b138b9f | |||
|
e6aecbd8dc | ||
eab0600f63 | |||
46f5dcb4dc | |||
0dc7e83dc4 | |||
82c10b87c8 | |||
5888946861 | |||
e24607ed3a | |||
|
d1ae6a8b84 | ||
fc052cf8f5 | |||
d9f608e8b4 | |||
a988565227 | |||
|
b76fffb788 | ||
25c3b7bcbf | |||
dfa6cd777b | |||
21ad8301f2 | |||
7110c9a5ce | |||
a4c8fccb11 | |||
c294f9d28d | |||
422d7fc693 | |||
abb5128426 | |||
2174608801 | |||
a6cdf6c4bd | |||
2c9693a83e | |||
6989558963 | |||
7fb85dacec | |||
57218d1454 | |||
9df6950d1a | |||
cd2b7fe185 | |||
52987060b1 | |||
aeb73a374f | |||
bc416f282f | |||
f88c373742 | |||
10ac1ff66a | |||
ae025e30c6 | |||
a1dd1d6664 | |||
57c64bbf71 | |||
218a19d907 | |||
7b6a13dd52 | |||
4ff0c81e37 | |||
6a15489610 | |||
59c942af73 | |||
302ba2bec7 | |||
34d1e4bddd | |||
02c24a4814 | |||
0724776ccb | |||
11979cbee0 | |||
2a490bf8ef | |||
b5d3d1a7b7 | |||
554ffe3b9d | |||
0f57be107b | |||
269aa6b426 | |||
b316b8f2ba | |||
cad68e269c | |||
efb3407b87 | |||
6f1ff02c04 | |||
93c66b0613 | |||
c14644a300 | |||
02d2300608 | |||
ff918608c5 | |||
aa591ee2ed | |||
f4a7943680 | |||
68fd4698ac | |||
|
dd039f31fe | ||
6c2dc483a2 | |||
811254e6a9 | |||
85ffed4d9a | |||
5fb45afb12 | |||
fb14eca634 | |||
14e2698833 | |||
0d6c0c8399 | |||
5d38b8327f | |||
f747d5b2fc | |||
8a75790453 | |||
acb212ab24 | |||
4ba02ebbb6 | |||
244da46e38 | |||
f40035dc79 | |||
5f71e406fc | |||
|
3d11a4f03a | ||
1dfd2dc4b7 | |||
e9701660d3 | |||
c8dbb4c7ef | |||
1241d90268 | |||
3de5b65977 | |||
4a353553c3 | |||
1240f31c0a | |||
01ac84ce1e | |||
4c969f0a42 | |||
8e2c76a33e | |||
b3666ec27e | |||
2c6862c509 | |||
9f8c43818c | |||
0debca91c8 | |||
7b6c9fcd24 | |||
55675bf41b | |||
bb24b06031 | |||
dbce0376d5 | |||
40db144a41 | |||
f7ba3bd08f | |||
ac1d374191 | |||
391992effb | |||
2e9ade11c3 | |||
f11a8c543b | |||
e30a4452f2 | |||
6cc11e64ab | |||
7b05ed9d3d | |||
|
dba35c0107 | ||
|
bfbc874b1d | ||
dbccdb239a | |||
f13db9268a | |||
ed8de7e3eb | |||
b34118485c | |||
9c3259c660 | |||
a3e289c06c | |||
31b7c1f217 | |||
c30dcff451 | |||
086f50d4fe | |||
46e825820c | |||
a3e2cbeb27 | |||
a342ae67de | |||
e4d97e0520 | |||
b69a05689b | |||
6b824a49ab | |||
652db56d42 | |||
afaf1846ec | |||
ba452ab883 | |||
39f699a61a | |||
4ab547810c | |||
bbaddb9406 | |||
a2cc9ddc88 | |||
175e31ca62 | |||
d414b65e7d | |||
78158bcba5 | |||
9402344b7e | |||
3eca9f6180 | |||
26e3d42ed5 | |||
6e095436e9 | |||
1344026494 | |||
1a94496801 | |||
48570808e5 | |||
a7440ed296 | |||
12ebefd86a | |||
6c9cbaadc8 | |||
9b10693172 | |||
db1c6d6a41 | |||
c56787443f | |||
cb218ec0c3 | |||
0dd6f82a0e | |||
225091864f | |||
ebd9c4702e | |||
4ad9773022 | |||
0a17df87e9 | |||
b567146d69 | |||
65522a57f1 | |||
1d936618fa | |||
76814a2d3f | |||
4134fcbd75 | |||
49fac7db1c | |||
e25273df48 | |||
638f6bea24 | |||
ddcd6a17dc | |||
4e21b463df | |||
3db4e011d4 | |||
a0d39e6081 | |||
a803bc637e | |||
d4e452545a | |||
9d73ac661f | |||
55e912221b | |||
d85be26761 | |||
ac78e85e17 | |||
131022da42 | |||
336db56316 | |||
b5d9afd0f7 | |||
0be83db40f | |||
03f4d0b8bc | |||
ee8f80cc70 | |||
ce887c38f3 | |||
799c0be830 | |||
760efa854d | |||
26bec05174 | |||
c32a198a34 | |||
6a8c656dbb | |||
63ba2982c9 | |||
9d9fb959d8 | |||
8ed201c83f | |||
bfb40c9166 | |||
5ea450844c | |||
36bec9e64f | |||
a95014dc5d | |||
2579c33ee1 | |||
6f1baa3219 | |||
4dee3a90e9 | |||
326b6eda6f | |||
85e882cc59 | |||
e4379f0a22 | |||
2bb7ff1803 | |||
5dd6e9a077 | |||
f7629c28f4 | |||
be2a38b48e | |||
3ba5f531bb | |||
10f1e69bc3 | |||
fd7d90b017 | |||
d898316918 | |||
a6f524e7af | |||
5e65814b8c | |||
aaa9d553d0 | |||
5685890493 | |||
2e336150e0 | |||
749dcdcd70 | |||
ab94343d07 | |||
fa71cec5c8 | |||
c6f3829387 | |||
7171b63947 | |||
06c4c0d921 | |||
f2ca2d850d | |||
638d187a24 | |||
b188d40d3c | |||
3ad948305f | |||
be1f1d94c9 | |||
06e8cdb9d2 | |||
10311b79df | |||
ad2690b21c | |||
1bd17d6e50 | |||
a5e710bfe5 | |||
e1bdabc8d6 | |||
c6ef99dde2 | |||
49b508a783 | |||
52128925f5 | |||
cf0c7f9d08 | |||
57d5140301 | |||
dbd9106621 | |||
e4fef0e88e | |||
7ef0074ecc | |||
17c35f6d42 | |||
3a0844adba | |||
5b5b9022e0 | |||
|
0b0bd7dff6 | ||
079e3782d1 | |||
a0ae9ae54c | |||
a1b9a0ec4c | |||
1fa690670d | |||
3f0a87a5ec | |||
caf02f78bf | |||
9b9fd14d27 | |||
7f77efbfab | |||
53967d20cc | |||
ef3411f39a | |||
66e63f1363 | |||
2fe21f6b28 | |||
67df372636 | |||
df80e9da23 | |||
13ab2efd0f | |||
0ffe96cf59 | |||
1808d0971d | |||
ec83a28d78 | |||
f0320b3a58 | |||
d93a1a4f4f | |||
a9f9ddf6b9 | |||
6a8fe35fcf | |||
94661e9e09 | |||
318f63d098 | |||
e2c9e83c2a | |||
cd434a0e3e | |||
9f293af804 | |||
b175e00cfe | |||
|
19dd82d62a | ||
b3ddc9465a | |||
6b38f17d32 | |||
86449d4912 | |||
145d756251 | |||
838a11a2f6 | |||
3bfd3210b0 | |||
e933bfa99e | |||
f6a37a54d0 | |||
e00c9bb1af | |||
018707c3d5 | |||
386727f6c5 | |||
a29ce36d6c | |||
7aed16bd6f | |||
b1f3ca6e59 | |||
4c0b8a06c5 | |||
60647c50ac | |||
59eaf1849e | |||
fb57339050 | |||
f9831a6ad8 | |||
f25c67f80a | |||
d3b0b97192 | |||
fa3be219a8 | |||
c22702d911 | |||
b25c5ff547 | |||
2e0a097806 | |||
c2083f7924 | |||
8923261e5b | |||
5391df56b0 | |||
d2b1f5780e | |||
1717e968e1 | |||
2c29bb3971 | |||
37b8218a0a | |||
ca7bbb5b91 | |||
2f3c008d2b | |||
c2722b7c3d | |||
312abd907f | |||
1b73c1ed64 | |||
d442d6653b | |||
758b8d6e2b | |||
416fd2e2a7 | |||
15a8335f1a | |||
c689583669 | |||
7a43a7acc9 | |||
8843418161 | |||
7c1eab13ae | |||
5a69036da7 | |||
2ad3458873 | |||
eb464343e8 | |||
2f18d0cbad | |||
6cd463a514 | |||
05b70632c5 | |||
ca9fe6ff21 | |||
e5754300de | |||
65134048bf | |||
8339a99747 | |||
3e1ae41e70 | |||
f757ba3441 | |||
6499c9cb5b | |||
28e5440d8b | |||
fef8c4d0f4 | |||
99e5059c64 | |||
5df4f39d95 | |||
7ec5a70ccb | |||
72fcab6e78 | |||
292c90425e | |||
b80f070431 | |||
03936c0403 | |||
62825d2e64 | |||
5cd5caef45 | |||
798e8b529d | |||
0e3766c5a5 | |||
90207a4427 | |||
60993a886a | |||
a6b42f9181 | |||
98fbd7c53c | |||
8d533f50e8 | |||
707459ec77 | |||
faf7db649e | |||
202e71be48 | |||
d6e8b418d3 | |||
a9f41f6114 | |||
f6f0d52518 | |||
ccb9be42c2 | |||
179009bfe3 | |||
8c2bd94a9f | |||
7757166d75 | |||
7f03002972 | |||
8555006d9e | |||
713ad64658 | |||
0713d481e3 | |||
ace0cf3588 | |||
bba3bbfe89 | |||
754afc5496 | |||
f1e8892ab5 | |||
c11e192c4e | |||
e9c704075d | |||
35edcb5672 | |||
4695798176 | |||
7a323fd170 | |||
1d6e4b6e32 | |||
5524aa7998 | |||
15ff2008e3 | |||
9bc2e6e165 | |||
344001856c | |||
ad261fcc2f | |||
5142a0ae72 | |||
6d195f96c9 | |||
|
1917b217a8 | ||
1f6b01bc73 | |||
d47a16aa8e | |||
c57d00a74b | |||
77ea7fa0ee | |||
b92d780cda | |||
f14e721caf | |||
1ff6399112 | |||
503fb8da76 | |||
f050cb7015 | |||
3670916f36 | |||
|
838a063eaa | ||
e1b16b11d6 | |||
314cbf471f | |||
a416d26f7c | |||
795b26e1dd | |||
14666cf9d8 | |||
c938f31935 | |||
35a52ef01b | |||
3b05ce3f10 | |||
aec4fd7a2d | |||
2661af3a17 | |||
56f43bae3f | |||
84472d2e9c | |||
c5afcd63b0 | |||
9bdb257814 | |||
5ad9891b16 | |||
7c04064917 | |||
fb5383d86b | |||
68af314ec0 | |||
8b1de5ce09 | |||
724b6fe091 | |||
6648cd30c3 | |||
8b90b45739 | |||
39be67eecf | |||
|
750f0ddeab | ||
6a5ece2f24 | |||
|
4ce33abfe6 | ||
5b7e1af87d | |||
59c6605b14 | |||
820d598ecd | |||
a263ec1273 | |||
b68892492c | |||
7c97695cec | |||
e764f34a2d | |||
6892a28bb6 | |||
74d688b8d2 | |||
ed84651046 | |||
7468ed21fa | |||
d8015913c3 | |||
|
78789834f0 | ||
739fe0caa1 | |||
4703f9c4d5 | |||
fd699ad777 | |||
0acf44778d | |||
8fc254d2db | |||
|
7d3b97d422 | ||
4a34f245db | |||
973ea39a64 | |||
f94a65ce7a | |||
432fbbea78 | |||
e483f1cd2e | |||
eb34f6e136 | |||
91e9eef582 | |||
dea1789a00 | |||
30adad5ae6 | |||
3ed6f939e5 | |||
a337d22c1f | |||
addfcf2510 | |||
303034f02c | |||
0fd44e9484 | |||
04040f20ba | |||
6c999ad148 | |||
cc519e6773 | |||
f9dcae4f65 | |||
ade6c2cb18 | |||
4566b62a93 | |||
37d3ef24d2 | |||
71265769ce | |||
a13c16ca03 | |||
a33fb72ef8 | |||
c5776264c0 | |||
078d8b39a9 | |||
b77c7c2f45 | |||
e369473dd0 | |||
70501f9da1 | |||
9bb7019b09 | |||
df4fe7a644 | |||
2f009d0b27 | |||
70d7def7d7 | |||
0033407f96 | |||
b10a2329ca | |||
6870db4a72 | |||
3643ffe0d0 | |||
02971f6ff9 | |||
7d3c34b004 | |||
f3ea6fd4dc | |||
bed6b81a58 | |||
f9bf9139b8 | |||
96e2c81b7e | |||
e62c00a187 | |||
611419888a | |||
5cc7e282bf | |||
de0b71103c | |||
537e9e8044 | |||
ac95c1fdc8 | |||
b36da9e4d9 | |||
e11ee3c136 | |||
887719ea24 | |||
14f1c3b26e | |||
2142729d38 | |||
9dcc2baae2 | |||
37c88d2974 | |||
36fd0deec4 | |||
4a4438d431 | |||
28a6745346 | |||
9ae0470879 | |||
927aed1161 | |||
7f3d7a656d | |||
040a8ce095 | |||
|
8974939bf2 | ||
|
846de369f2 | ||
|
4d865af423 | ||
62ad01fc8f | |||
da0164b97d | |||
fc8711d6d8 | |||
03cef1f831 | |||
ee4974a494 | |||
bfbfd6a421 | |||
49954abbbe | |||
2f618512cb | |||
|
0086ebed0d | ||
1523ed9a47 | |||
79c7cbedcc | |||
3e128f3966 | |||
fb45483ffc | |||
a9bc7d7a38 | |||
78f032d678 | |||
d73b71a097 | |||
2bdc6155d7 | |||
f60cebf42c | |||
2e4c6673d4 | |||
6e3d64d6ef | |||
2deb66855b | |||
a64c0c19e5 | |||
24b4576c00 | |||
34ad889d90 | |||
af523cfcd7 | |||
842f204123 | |||
9162002e55 | |||
985f998a82 | |||
b93639e14e | |||
a4be973e29 | |||
060a573fe9 | |||
7c43b7385d | |||
befa6f27bb | |||
b9d3b5c756 | |||
ee732684bc | |||
360b530dd5 | |||
713c3a1a08 | |||
81b1e4035d | |||
2cde9341d4 | |||
cdf0690da6 | |||
80335e7b95 | |||
dbc2de14c9 | |||
c0f711d27f | |||
df24522490 | |||
6cf2e574bf | |||
e7b89ae44f | |||
72a1aaa654 | |||
c70c3b6080 | |||
70ea1f2301 |
38
.drone.yml
38
.drone.yml
|
@ -15,6 +15,7 @@ trigger:
|
||||||
services:
|
services:
|
||||||
- name: api
|
- name: api
|
||||||
image: vikunja/api:unstable
|
image: vikunja/api:unstable
|
||||||
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||||
VIKUNJA_LOG_LEVEL: DEBUG
|
VIKUNJA_LOG_LEVEL: DEBUG
|
||||||
|
@ -41,11 +42,12 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
CYPRESS_CACHE_FOLDER: .cache/cypress
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: true
|
||||||
commands:
|
commands:
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
- pnpm install --fetch-timeout 100000
|
- pnpm install --fetch-timeout 100000
|
||||||
|
@ -53,7 +55,7 @@ steps:
|
||||||
# - restore-cache
|
# - restore-cache
|
||||||
|
|
||||||
- name: lint
|
- name: lint
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -64,7 +66,7 @@ steps:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: build-prod
|
- name: build-prod
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -75,7 +77,7 @@ steps:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: test-unit
|
- name: test-unit
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
|
@ -85,7 +87,7 @@ steps:
|
||||||
|
|
||||||
- name: typecheck
|
- name: typecheck
|
||||||
failure: ignore
|
failure: ignore
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -135,8 +137,9 @@ steps:
|
||||||
# - dependencies
|
# - dependencies
|
||||||
|
|
||||||
- name: deploy-preview
|
- name: deploy-preview
|
||||||
image: node:18-alpine
|
image: williamjackson/netlify-cli
|
||||||
pull: always
|
pull: always
|
||||||
|
user: root # The rest runs as root and thus the permissions wouldn't work
|
||||||
environment:
|
environment:
|
||||||
NETLIFY_AUTH_TOKEN:
|
NETLIFY_AUTH_TOKEN:
|
||||||
from_secret: netlify_auth_token
|
from_secret: netlify_auth_token
|
||||||
|
@ -199,10 +202,14 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
SENTRY_AUTH_TOKEN:
|
||||||
|
from_secret: sentry_auth_token
|
||||||
|
SENTRY_ORG: vikunja
|
||||||
|
SENTRY_PROJECT: frontend-oss
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- apk add git
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
|
@ -276,10 +283,14 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:18-alpine
|
image: node:20-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
SENTRY_AUTH_TOKEN:
|
||||||
|
from_secret: sentry_auth_token
|
||||||
|
SENTRY_ORG: vikunja
|
||||||
|
SENTRY_PROJECT: frontend-oss
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- apk add git
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
|
@ -346,8 +357,7 @@ type: docker
|
||||||
name: docker-release
|
name: docker-release
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- release-latest
|
- build
|
||||||
- release-version
|
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
ref:
|
ref:
|
||||||
|
@ -375,8 +385,7 @@ steps:
|
||||||
repo: vikunja/frontend
|
repo: vikunja/frontend
|
||||||
tags: unstable
|
tags: unstable
|
||||||
build_args:
|
build_args:
|
||||||
- USE_RELEASE=true
|
- USE_RELEASE=false
|
||||||
- RELEASE_VERSION=unstable
|
|
||||||
platforms:
|
platforms:
|
||||||
- linux/386
|
- linux/386
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
@ -410,8 +419,7 @@ steps:
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
repo: vikunja/frontend
|
repo: vikunja/frontend
|
||||||
build_args:
|
build_args:
|
||||||
- USE_RELEASE=true
|
- USE_RELEASE=false
|
||||||
- RELEASE_VERSION=${DRONE_TAG##v}
|
|
||||||
platforms:
|
platforms:
|
||||||
- linux/386
|
- linux/386
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
@ -521,6 +529,6 @@ steps:
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: 971875b90c7bb1649d1b00d022d0b594ba9b68f927bf8f0dbe840190816d676b
|
hmac: 6a566550cac03e9f3f9bbccab95fda4b342233bd63a1409cb5f634b1c744c326
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
14
.npmrc
14
.npmrc
|
@ -1,2 +1,14 @@
|
||||||
|
fetch-timeout=100000
|
||||||
|
|
||||||
|
# pnpm settings
|
||||||
|
# The following settings prepare for the new default value of pnpm 8
|
||||||
|
# they can be removed directly after having moved to pnpm 8
|
||||||
auto-install-peers=true
|
auto-install-peers=true
|
||||||
fetch-timeout=100000
|
dedupe-peer-dependents=true
|
||||||
|
resolve-peers-from-workspace-root=true
|
||||||
|
save-workspace-protocol=rolling
|
||||||
|
resolution-mode=lowest-direct
|
||||||
|
publishConfig.linkDirectory=true
|
||||||
|
|
||||||
|
# remove some time after having moved to pnpm 8
|
||||||
|
use-lockfile-v6=true
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"lokalise.i18n-ally",
|
"lokalise.i18n-ally",
|
||||||
"mgmcdermott.vscode-language-babel",
|
"mgmcdermott.vscode-language-babel",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"Syler.sass-indented"
|
"Syler.sass-indented",
|
||||||
|
"zixuanchen.vitest-explorer"
|
||||||
]
|
]
|
||||||
}
|
}
|
750
CHANGELOG.md
750
CHANGELOG.md
|
@ -9,6 +9,755 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
||||||
|
|
||||||
The releases aim at the api versions which is why there are missing versions.
|
The releases aim at the api versions which is why there are missing versions.
|
||||||
|
|
||||||
|
## [0.21.0] - 2023-07-07
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* *(Expandable)* Spelling
|
||||||
|
* *(building)* Let the compiler ignore props interface
|
||||||
|
* *(ci)* Always pull latest unstable api image for testing
|
||||||
|
* *(ci)* Directly build docker images and not use releases to avoid caching issues
|
||||||
|
* *(ci)* Disable puppeteer chrome download
|
||||||
|
* *(docker)* Copy patches prior to installing dependencies so that the installation actually works
|
||||||
|
* *(docker)* Don't set nginx worker rlimit
|
||||||
|
* *(filters)* Load projects after creating a filter
|
||||||
|
* *(filters)* Load projects after deleting a filter
|
||||||
|
* *(filters)* Load projects after updating a filter
|
||||||
|
* *(gantt)* Only update today value when changing to the gantt chart view
|
||||||
|
* *(i18n)* OrderedList translationid
|
||||||
|
* *(i18n)* Typo
|
||||||
|
* *(kanban)* Decrease task count per bucket when deleting a task
|
||||||
|
* *(kanban)* Don't export buckets as readonly because that makes it impossible to update them, even from within the store
|
||||||
|
* *(link share)* Default share view should be list, not project
|
||||||
|
* *(link share)* Redirect to list view after authenticating
|
||||||
|
* *(navigation)* Favorites project
|
||||||
|
* *(navigation)* Hide archived subprojects
|
||||||
|
* *(navigation)* Hide left ul border
|
||||||
|
* *(navigation)* Highlight saved filters in project view and prevent them from being dragged around
|
||||||
|
* *(navigation)* Hover state of other menu items
|
||||||
|
* *(navigation)* Make marking a project as favorite work
|
||||||
|
* *(navigation)* Make sure the Favorites project shows up when marking or unmarking a task as favorite
|
||||||
|
* *(navigation)* Make sure updating a project's state works for sub projects as well.
|
||||||
|
* *(navigation)* Make the styles work again
|
||||||
|
* *(navigation)* Menu item overflow
|
||||||
|
* *(navigation)* Nav item width for items without sub projects
|
||||||
|
* *(navigation)* Show text ellipsis for very long project titles
|
||||||
|
* *(navigation)* Sidebar top spacing
|
||||||
|
* *(navigation)* Watcher
|
||||||
|
* *(project)* Correctly load background when switching from or to a project view
|
||||||
|
* *(project)* Don't try to read title of undefined project
|
||||||
|
* *(project)* Duplicate a project without new parent
|
||||||
|
* *(project)* Make sure the correct tasks are loaded when switching between projects
|
||||||
|
* *(project)* Set maxRight on projects after opening a task
|
||||||
|
* *(projects)* Make sure the project hierarchy is properly updated when moving projects between parents
|
||||||
|
* *(projects)* Update project duplicate api definitions
|
||||||
|
* *(quick add magic)* Cleanup all assignee properties
|
||||||
|
* *(quick add magic)* Date parsing with a date at the beginning
|
||||||
|
* *(quick add magic)* Don't replace the prefix in every occurrence when it is present in the matched part
|
||||||
|
* *(quick add magic)* Use the project user service to find assignees for quick add magic
|
||||||
|
* *(reminders)* Align remove icon with the rest
|
||||||
|
* *(reminders)* Assignment to const when changing a reminder
|
||||||
|
* *(reminders)* Custom relative highlight now only when a custom relative reminder was actually selected
|
||||||
|
* *(reminders)* Don't assigne the task
|
||||||
|
* *(reminders)* Don't assume 30 days are always a month
|
||||||
|
* *(reminders)* Don't sync negative relative reminder amounts in ui
|
||||||
|
* *(reminders)* Duplicate reminder for each change
|
||||||
|
* *(reminders)* Flatpickr styling improvements
|
||||||
|
* *(reminders)* Properly parse relative reminders which don't have an amount
|
||||||
|
* *(reminders)* Set date over relative reminder
|
||||||
|
* *(reminders)* Style flatpickr so that it blends in more
|
||||||
|
* *(repeat)* Prevent disappearing repeat mode settings when modes other than default repeat mode were selected
|
||||||
|
* *(sentry)* Don't fail the build when sentry upload fails
|
||||||
|
* *(sentry)* Use correct environment from vite env mode
|
||||||
|
* *(settings)* Don't try to sort timezones if there are none
|
||||||
|
* *(task detail view)* Make project display show the task's project
|
||||||
|
* *(task)* Break long task titles after 4 lines only
|
||||||
|
* *(task)* Call getting task identifier directly instead of using model function
|
||||||
|
* *(task)* Make an attachment cover image
|
||||||
|
* *(task)* Repeat mode now saves correctly
|
||||||
|
* *(tests)* Make sure the task is created with a bucket
|
||||||
|
* *(tests)* New project input field
|
||||||
|
* *(tests)* Project archived filter checkbox selector
|
||||||
|
* *(tests)* Wait for request instead of fixed time
|
||||||
|
* *(user)* Fix flickering of default settings
|
||||||
|
* *(user)* Lint* Fix comment
|
||||||
|
* *(user)* Set the language when saving
|
||||||
|
* Add await ([9d9fb95](9d9fb959d8f1c4a12110f1a988115116085b6aaf))
|
||||||
|
* Add default for level ([9402344](9402344b7ea70359c592412b6c341897e45c6069))
|
||||||
|
* Add interval to uses of useNow so that it uses less resources ([b77c7c2](b77c7c2f45495a0fe6d132b5f569e807074c6d12))
|
||||||
|
* Add more padding to the textarea ([dfa6cd7](dfa6cd777bc5d03cf88d62db9008aa0b366aa806))
|
||||||
|
* Add spacing between checkbox and title of related task ([62825d2](62825d2e6409e08ab3229bf693ed068198e18085))
|
||||||
|
* Allow icon changes configuration via env (#3567) ([57218d1](57218d14548bf1d4cd59f6976e84cf178023305d))
|
||||||
|
* Avoid crashing browser processes during tests ([7b05ed9](7b05ed9d3d24e07a6535f2462d215c47b6650be1))
|
||||||
|
* Bottom margin of project header ([1a94496](1a9449680114212eeb93be2aba3f10c416f67e78))
|
||||||
|
* Bubble changes from the editor immediately and move the delay to callers ([f4a7943](f4a79436809d13e1d2c5337f79358c15310d08d2))
|
||||||
|
* Checkbox label size based on icon ([fd699ad](fd699ad777c47764b35345b7ec18a854957ff5d1))
|
||||||
|
* Clarify user search setting ([ae025e3](ae025e30c659d43cce1e3f8361bd1c4c7cb860da))
|
||||||
|
* Cleanup unused translation strings ([aaa9d55](aaa9d553d080a83a9fd1bcdece366fb5832831f1))
|
||||||
|
* Collapsing child projects ([2250918](225091864f9088a07120cd3d36918f3060d57d30))
|
||||||
|
* Correctly sync filters on upcoming tasks page ([faa6298](faa62985dff877afc54c3510be8d27d493717780))
|
||||||
|
* Disable autocomplete in assignee search ([64f9f4f](64f9f4fd88a513cbc401aacbeab87695bb9f55bf))
|
||||||
|
* Don't allow creating a new label from filter view ([4c969f0](4c969f0a427e98b491c49646aaf19e19cf9ec924))
|
||||||
|
* Don't require variant prop on loading component as it already has a default one set ([01ac84c](01ac84ce1eda1de79fd752792115a71cb5c15698))
|
||||||
|
* Don't set the current project when setting a project ([31b7c1f](31b7c1f217532bf388ba95a03f469508bee46f6a))
|
||||||
|
* Don't show > for top-level projects ([03f4d0b](03f4d0b8bcba90b19302d6c6d2fbb92460b59957))
|
||||||
|
* Don't show child projects when the project is only a favorite ([0a17df8](0a17df87e950b8043578dbb7e9f12d5937802169))
|
||||||
|
* Don't try to convert a null date ([4ba02eb](4ba02ebbb6be4b96a42688b3ec8f29fe923aee0b))
|
||||||
|
* Don't try to map data from empty responses ([a118580](a11858070496614c492da321fe461b72c31afe5a))
|
||||||
|
* Don't try to map non-array data ([813d2b5](813d2b56a06cbd28a1bf0d01b64685a1b49188d0))
|
||||||
|
* Don't try to set a user language if none is saved ([68fd469](68fd4698ac443345dc7dbbf8cfebf76ec467b6ec))
|
||||||
|
* Don't try to set config from non-json responses ([7c1934a](7c1934aad0e5fcd0f785896d57efc34c6df935cd))
|
||||||
|
* Ensure all matched quick add magic parts are correctly removed from the task ([7b6a13d](7b6a13dd52dfa06e6093ae30adad1b86b66610e1))
|
||||||
|
* Ensure same protocol for configured api url (#3303) ([6c999ad](6c999ad14844b4f9ec74dc225895db6a12e4a781))
|
||||||
|
* Follow the happy path ([34182b8](34182b8bbb7a7e8eeb0ce698dc6da79785d05fc9))
|
||||||
|
* Force usage of @types for flexsearch instead of integrated types ([f60cebf](f60cebf42cb73d9fd2d9fea8de5bbeb96a724d47))
|
||||||
|
* Has-pseudo-class polyfill ([4703f9c](4703f9c4d5e3902d0fc389d447aa9a7da2e2dd4a))
|
||||||
|
* Ignore ts deprecations for now ([96e2c81](96e2c81b7ef2b7a0ff515dd01d9aeb28429cc0d5))
|
||||||
|
* Improve projectView storing and add migration ([842f204](842f204123afc3b9b4633b68de58cffb3af4f912))
|
||||||
|
* Improve the "pop" sound a bit ([3643ffe](3643ffe0d0357c89cb3517fafbb0c438188ac88d))
|
||||||
|
* Improve tooltip icon contrast ([a6cdf6c](a6cdf6c4bdceb1168f20e9d049c2e66f40c98aa1))
|
||||||
|
* Improve tooltip text ([2174608](21746088012f4fe0f750ed5e5cac916d506fb17b))
|
||||||
|
* Increase default auto-save timeout to 5 seconds ([f7ba3bd](f7ba3bd08fa9181180f99f4e5ebd5ec916fbcf19))
|
||||||
|
* Indention ([e25273d](e25273df4899867ee146159d3d18125d387f8524))
|
||||||
|
* Lint ([292c904](292c90425ef96b99671702a0b28d87d660fa53dc))
|
||||||
|
* Lint ([4ff0c81](4ff0c81e373696b0505c2c080d558a20071562f3))
|
||||||
|
* Lint ([5d59392](5d593925666a09cbfda2f62577deb670033f93fb))
|
||||||
|
* Lint ([9ec29ca](9ec29cad300fe1c25cb355fb86e165ca920df511))
|
||||||
|
* Lint ([c294f9d](c294f9d28d3e793f8151265d5a16ed2fc53aea92))
|
||||||
|
* Lint ([c74612f](c74612f24adeb4aceafe9fc9b3264b1dfe84d128))
|
||||||
|
* Lint ([cd2b7fe](cd2b7fe185632253290838e405b8a2666b15ce24))
|
||||||
|
* Lint ([ed8de7e](ed8de7e3eb78f6723d5f675cca18c014b252ed64))
|
||||||
|
* List view: don't sort tasks after marking one "done" (#3285) ([6870db4](6870db4a72568f183134a6dd2d4af687dd7c839d))
|
||||||
|
* Load the correct language ([6593380](6593380013ff6043b846126ac67e6f96442a1c5b))
|
||||||
|
* Make check if projects are available work again ([5e65814](5e65814b8c5b37f3962856f2809f7cc85756da1e))
|
||||||
|
* Make computed side-effect free ([26bec05](26bec0517417afc52db93f0fdc4c48d47ed5c131))
|
||||||
|
* Make sure redirects to a saved view work as intended ([a64c0c1](a64c0c19e5a7da36ae4993fce443e4f37e3a4572))
|
||||||
|
* Make sure the unread notifications indicator is correctly positioned ([8b90b45](8b90b45739418f447b885fc9b37438e325f61b32))
|
||||||
|
* Make tests work again ([5685890](56858904938126dbfa8ade2e88e3ec6c4fff3a6f))
|
||||||
|
* Make type singular ([bc416f2](bc416f282f13d2b81782aba4b0d68b71f26c83e8))
|
||||||
|
* Make update available button use the correct text color all the time ([ae2b0f9](ae2b0f97c4bb50d4ff493af2af132f9740d16d49))
|
||||||
|
* Missing await ([391992e](391992effbb424a107ff060e7175884740a28c62))
|
||||||
|
* Missing variant prop for loading component ([2e9ade1](2e9ade11c3a3b6cb531d053f82a598a5ab851a93))
|
||||||
|
* Move parent project child id mutation to store ([26e3d42](26e3d42ed527afd6bf695ba3ad291e1c2b545bba))
|
||||||
|
* Move parent project handling out of useProject ([ba452ab](ba452ab88339b9ace987f1a18584a7950e00a776))
|
||||||
|
* Move the collapsable placeholder to the button ([1344026](1344026494fe47ac5604bff07b537a2765e840f6))
|
||||||
|
* Move types to dev dependencies ([739fe0c](739fe0caa13dc946e1801f290d8ab5f18cdc5faf))
|
||||||
|
* Only bind child projects data down ([3eca9f6](3eca9f6180e64f892e94d27eaa192cea780563a0))
|
||||||
|
* Only update daytime salutation when switching to home view ([c577626](c5776264c069000efbb62c64dfc2143d5fc4e0df))
|
||||||
|
* Passing readonly projects data to navigation ([d85be26](d85be26761240164b6bdcbe0601b46585b74fafa))
|
||||||
|
* Properly determine if there are projects ([a2cc9dd](a2cc9ddc8821a4b9b1ee1dd6109d1b3958a06ba6))
|
||||||
|
* Rebase readd CustomTransition ([b93639e](b93639e14ecab06496086c3d2cc14f51d8f9f672))
|
||||||
|
* Recreate project instead of editing before ([175e31c](175e31ca629660d8d683b35b8e7c8052a62cd17d))
|
||||||
|
* Redundant ) ([6c2dc48](6c2dc483a20213f1f238e6224b9ecfb87faa2461))
|
||||||
|
* Remove getProjectById and replace all usages of it ([78158bc](78158bcba52d152a2ebf465242e25a55e6764470))
|
||||||
|
* Remove leftover suspense ([9d73ac6](9d73ac661fbf9315995c8a1f633708021591d2db))
|
||||||
|
* Remove leftovers of childIds ([bbaddb9](bbaddb9406910106b7d476a6550acff025e72655))
|
||||||
|
* Remove namespace routes ([10311b7](10311b79df36db44a8e96a446234c3c6d6aa6ec7))
|
||||||
|
* Remove namespace store reference ([ad2690b](ad2690b21cfc9ccc658737a726cc6b110089b635))
|
||||||
|
* Remove unnecessary fallback ([d414b65](d414b65e7d591f567067ce8085b9934207dc938a))
|
||||||
|
* Rename getParentProjects method to make it clear what it does ([39f699a](39f699a61ae91eb93c364137f76b595e7cad7561))
|
||||||
|
* Rename list to project for parsing subtasks via indention ([fc8711d](fc8711d6d841d11847cd8567999373145ce3398d))
|
||||||
|
* Rename resolveRef ([f14e721](f14e721caf9434ac119f32c5e7f107bfbdd6746c))
|
||||||
|
* Return redirect ([7c964c2](7c964c29d487b5bcd2c125f81731e3b37374641a))
|
||||||
|
* Return updated project instead of the old one ([4ab5478](4ab547810c77e747e701ea865c13157d51aba461))
|
||||||
|
* Review findings ([5fb45af](5fb45afb12479eb135323299409bd91d8be24e39))
|
||||||
|
* Review findings ([85ffed4](85ffed4d9a26fc054fee51608bb83ccf2e3032f9))
|
||||||
|
* Review findings ([fb14eca](fb14eca6340ac4c761b8f61027662328bf55ade4))
|
||||||
|
* Route to create new project ([a5e710b](a5e710bfe594e06262b9ef46fa6b56ad637b8156))
|
||||||
|
* Set and use correct type for destructured props ([dbe1ad9](dbe1ad9353e165fd1e314cc72c7a4dece1c47d38))
|
||||||
|
* Set vue-ignore ([b6cd424](b6cd424aa30be3bd715c0b7555032fc80149ae7b))
|
||||||
|
* Show favorite on hover ([0be83db](0be83db40fa96478bfdb4a69e8a995d6debb6f52))
|
||||||
|
* Simplify sort ([85e882c](85e882cc5940067414004dabd01916f559fbd0ff))
|
||||||
|
* Sort in store ([46e8258](46e825820c465ebb9f8087e3afe6d74fad8d5159))
|
||||||
|
* SortBy type import ([d73b71a](d73b71a097755cdb075955a824c26bbaba222aaf))
|
||||||
|
* Spacing ([9162002](9162002e55d9ebfd0a6c8dbe28aab0c15f95b7e2))
|
||||||
|
* Style: "favorite" button being shown on projects without hovering ([ee4974a](ee4974a4948012b03adcedd956c0d907c57431c9))
|
||||||
|
* Switching to view type now ([060a573](060a573fe9006441131fd98c4618c5d294cf39b7))
|
||||||
|
* Tests ([69e94e5](69e94e58c451a5115c713696798f8fcbf8f787b3))
|
||||||
|
* Translation string ([f13db92](f13db9268a8862204522a8d68ad7b51edb9d91e1))
|
||||||
|
* Tsconfig as per https://github.com/vuejs/tsconfig#configuration-for-node-environments ([05b7063](05b70632c55ce34ee6471cc372334fc6e14c99a0))
|
||||||
|
* Tsconfig as per https://github.com/vuejs/tsconfig#configuration-for-node-environments ([ca9fe6f](ca9fe6ff215351c3f4c8de65a333f5cfd5876488))
|
||||||
|
* Undefined parent project when none was selected ([6cc11e6](6cc11e64ab392f8e8e69070000a748c04746e550))
|
||||||
|
* Undo further nesting of interactive items ([0acf447](0acf44778d0ab2a317bcfb3e89aa0292e2d5c2ed))
|
||||||
|
* Update logo change only every hour ([7126576](71265769cefb91be9a51fceff0a04095a9dd7e72))
|
||||||
|
* Use correct shortcut to open projects overview ([326b6ed](326b6eda6fce6554ea6e215c681466374657902a))
|
||||||
|
* Use menu tag everywhere ([0dd6f82](0dd6f82a0e198056724821c2bb56c6b9807ea451))
|
||||||
|
* Use onActivated ([a33fb72](a33fb72ef86112c6f29017bb951ff4e1ee611ed6))
|
||||||
|
* Use props destructuring everywhere ([3aa502e](3aa502e07d89314e885c252e1e3d4668fa64059b))
|
||||||
|
* Use strict comparison ([91e9eef](91e9eef5829d2a5ae27099fbd54029ed0ca46818))
|
||||||
|
* Use the color bubble as handle if the project has a color ([4857080](48570808e55e51751734ddaf4532ad651920d622))
|
||||||
|
* Use time constant ([a13c16c](a13c16ca03698a24860f8453cdb231c420d0077b))
|
||||||
|
* Wording ([985f998](985f998a821229d03c7d40d1a81f7fbe5121d585))
|
||||||
|
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* *(deps)* Install dependencies after rebase
|
||||||
|
* *(deps)* Pin dependency @tsconfig/node18 to 2.0.0
|
||||||
|
* *(deps)* Update all dev dependencies at once per day
|
||||||
|
* *(deps)* Update caniuse-and-related
|
||||||
|
* *(deps)* Update caniuse-and-related
|
||||||
|
* *(deps)* Update dependency @4tw/cypress-drag-drop to v2.2.4
|
||||||
|
* *(deps)* Update dependency @cypress/vite-dev-server to v5.0.5
|
||||||
|
* *(deps)* Update dependency @cypress/vue to v5.0.5
|
||||||
|
* *(deps)* Update dependency @faker-js/faker to v8
|
||||||
|
* *(deps)* Update dependency @faker-js/faker to v8.0.1
|
||||||
|
* *(deps)* Update dependency @faker-js/faker to v8.0.2
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.10.0
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.11.0
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.12.0
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.12.1
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.9.2
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.9.3
|
||||||
|
* *(deps)* Update dependency @kyvg/vue3-notification to v2.9.1
|
||||||
|
* *(deps)* Update dependency @rushstack/eslint-patch to v1.3.0
|
||||||
|
* *(deps)* Update dependency @rushstack/eslint-patch to v1.3.1
|
||||||
|
* *(deps)* Update dependency @rushstack/eslint-patch to v1.3.2
|
||||||
|
* *(deps)* Update dependency @tsconfig/node18 to v18
|
||||||
|
* *(deps)* Update dependency @tsconfig/node18 to v2.0.1
|
||||||
|
* *(deps)* Update dependency @types/codemirror to v5.60.8
|
||||||
|
* *(deps)* Update dependency @types/dompurify to v3
|
||||||
|
* *(deps)* Update dependency @types/dompurify to v3.0.1
|
||||||
|
* *(deps)* Update dependency @types/dompurify to v3.0.2
|
||||||
|
* *(deps)* Update dependency @types/marked to v4.3.0
|
||||||
|
* *(deps)* Update dependency @types/marked to v4.3.1
|
||||||
|
* *(deps)* Update dependency @types/marked to v5
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.1
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.10
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.11
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.12
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.13
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.2
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.3
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.5
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.6
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.7
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.8
|
||||||
|
* *(deps)* Update dependency @types/node to v18.15.9
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.0
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.1
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.10
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.11
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.14
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.16
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.17
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.18
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.19
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.2
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.3
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.4
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.5
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.6
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.7
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.8
|
||||||
|
* *(deps)* Update dependency @types/node to v18.16.9
|
||||||
|
* *(deps)* Update dependency @types/sortablejs to v1.15.1
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v4.0.2
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v4.0.3
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v4.0.4
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v4.0.5
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4.1.0
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4.2.0
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4.2.1
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4.2.2
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4.2.3
|
||||||
|
* *(deps)* Update dependency @vue/eslint-config-typescript to v11.0.3
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.3.2
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.4.0
|
||||||
|
* *(deps)* Update dependency @vue/tsconfig to v0.3.2
|
||||||
|
* *(deps)* Update dependency @vue/tsconfig to v0.4.0
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10.0.2
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10.1.0
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10.1.2
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10.2.0
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v10.2.1
|
||||||
|
* *(deps)* Update dependency axios to v1.3.5
|
||||||
|
* *(deps)* Update dependency axios to v1.3.6
|
||||||
|
* *(deps)* Update dependency axios to v1.4.0
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001465
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001468
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001470
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001473
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001477
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001479
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001481
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001486
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001487
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001489
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001500
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001508
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001511
|
||||||
|
* *(deps)* Update dependency codemirror to v5.65.13
|
||||||
|
* *(deps)* Update dependency css-has-pseudo to v6
|
||||||
|
* *(deps)* Update dependency csstype to v3.1.2
|
||||||
|
* *(deps)* Update dependency cypress to v12.10.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.11.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.12.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.13.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.14.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.15.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.16.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.8.0
|
||||||
|
* *(deps)* Update dependency cypress to v12.8.1
|
||||||
|
* *(deps)* Update dependency cypress to v12.9.0
|
||||||
|
* *(deps)* Update dependency date-fns to v2.30.0
|
||||||
|
* *(deps)* Update dependency dayjs to v1.11.8
|
||||||
|
* *(deps)* Update dependency dayjs to v1.11.9
|
||||||
|
* *(deps)* Update dependency dompurify to v3.0.2
|
||||||
|
* *(deps)* Update dependency dompurify to v3.0.3
|
||||||
|
* *(deps)* Update dependency dompurify to v3.0.4
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.12
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.13
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.14
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.15
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.16
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.17
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.18
|
||||||
|
* *(deps)* Update dependency esbuild to v0.17.19
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.0
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.1
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.10
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.11
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.2
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.3
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.4
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.5
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.6
|
||||||
|
* *(deps)* Update dependency esbuild to v0.18.9
|
||||||
|
* *(deps)* Update dependency eslint to v8.37.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.38.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.39.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.40.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.41.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.42.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.43.0
|
||||||
|
* *(deps)* Update dependency eslint to v8.44.0
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.10.0
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.11.0
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.11.1
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.12.0
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.13.0
|
||||||
|
* *(deps)* Update dependency flexsearch to v0.7.31
|
||||||
|
* *(deps)* Update dependency floating-vue to v2.0.0-beta.21
|
||||||
|
* *(deps)* Update dependency floating-vue to v2.0.0-beta.22
|
||||||
|
* *(deps)* Update dependency floating-vue to v2.0.0-beta.24
|
||||||
|
* *(deps)* Update dependency happy-dom to v9
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.1.9
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.10.1
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.10.9
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.18.3
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.20.1
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.20.3
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.7.1
|
||||||
|
* *(deps)* Update dependency happy-dom to v9.9.2
|
||||||
|
* *(deps)* Update dependency highlight.js to v11.8.0
|
||||||
|
* *(deps)* Update dependency histoire to v0.16.2
|
||||||
|
* *(deps)* Update dependency marked to v4.3.0
|
||||||
|
* *(deps)* Update dependency marked to v5
|
||||||
|
* *(deps)* Update dependency marked to v5.0.1
|
||||||
|
* *(deps)* Update dependency marked to v5.0.2
|
||||||
|
* *(deps)* Update dependency marked to v5.0.3
|
||||||
|
* *(deps)* Update dependency marked to v5.0.4
|
||||||
|
* *(deps)* Update dependency marked to v5.0.5
|
||||||
|
* *(deps)* Update dependency marked to v5.1.0
|
||||||
|
* *(deps)* Update dependency netlify-cli to v13.1.2
|
||||||
|
* *(deps)* Update dependency netlify-cli to v13.1.6
|
||||||
|
* *(deps)* Update dependency netlify-cli to v13.2.1
|
||||||
|
* *(deps)* Update dependency netlify-cli to v13.2.2
|
||||||
|
* *(deps)* Update dependency netlify-cli to v14
|
||||||
|
* *(deps)* Update dependency netlify-cli to v14.3.1
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.34
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.35
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.36
|
||||||
|
* *(deps)* Update dependency pinia to v2.1.4
|
||||||
|
* *(deps)* Update dependency postcss to v8.4.22
|
||||||
|
* *(deps)* Update dependency postcss to v8.4.23
|
||||||
|
* *(deps)* Update dependency postcss to v8.4.24
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.1.0
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.2.0
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.3.0
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.3.1
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.3.2
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.4.1
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.4.2
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.5.0
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v8.5.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.2
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.3
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.4
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.5
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.6
|
||||||
|
* *(deps)* Update dependency rollup to v3.20.7
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.2
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.3
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.4
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.5
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.6
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.7
|
||||||
|
* *(deps)* Update dependency rollup to v3.21.8
|
||||||
|
* *(deps)* Update dependency rollup to v3.22.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.23.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.23.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.24.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.24.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.25.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.25.1
|
||||||
|
* *(deps)* Update dependency rollup to v3.25.2
|
||||||
|
* *(deps)* Update dependency rollup to v3.25.3
|
||||||
|
* *(deps)* Update dependency rollup to v3.26.0
|
||||||
|
* *(deps)* Update dependency rollup-plugin-visualizer to v5.9.2
|
||||||
|
* *(deps)* Update dependency sass to v1.59.3
|
||||||
|
* *(deps)* Update dependency sass to v1.60.0
|
||||||
|
* *(deps)* Update dependency sass to v1.61.0
|
||||||
|
* *(deps)* Update dependency sass to v1.62.0
|
||||||
|
* *(deps)* Update dependency sass to v1.62.1
|
||||||
|
* *(deps)* Update dependency sass to v1.63.0
|
||||||
|
* *(deps)* Update dependency sass to v1.63.2
|
||||||
|
* *(deps)* Update dependency sass to v1.63.3
|
||||||
|
* *(deps)* Update dependency sass to v1.63.4
|
||||||
|
* *(deps)* Update dependency sass to v1.63.5
|
||||||
|
* *(deps)* Update dependency sass to v1.63.6
|
||||||
|
* *(deps)* Update dependency typescript to v5
|
||||||
|
* *(deps)* Update dependency typescript to v5.0.3
|
||||||
|
* *(deps)* Update dependency typescript to v5.0.4
|
||||||
|
* *(deps)* Update dependency typescript to v5.1.3
|
||||||
|
* *(deps)* Update dependency typescript to v5.1.5
|
||||||
|
* *(deps)* Update dependency typescript to v5.1.6
|
||||||
|
* *(deps)* Update dependency ufo to v1.1.2
|
||||||
|
* *(deps)* Update dependency vite to v4.2.0
|
||||||
|
* *(deps)* Update dependency vite to v4.2.1
|
||||||
|
* *(deps)* Update dependency vite to v4.2.2
|
||||||
|
* *(deps)* Update dependency vite to v4.3.0
|
||||||
|
* *(deps)* Update dependency vite to v4.3.1
|
||||||
|
* *(deps)* Update dependency vite to v4.3.2
|
||||||
|
* *(deps)* Update dependency vite to v4.3.3
|
||||||
|
* *(deps)* Update dependency vite to v4.3.4
|
||||||
|
* *(deps)* Update dependency vite to v4.3.5
|
||||||
|
* *(deps)* Update dependency vite to v4.3.6
|
||||||
|
* *(deps)* Update dependency vite to v4.3.7
|
||||||
|
* *(deps)* Update dependency vite to v4.3.8
|
||||||
|
* *(deps)* Update dependency vite to v4.3.9
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.14.5
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.14.6
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.14.7
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.15.0
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.15.1
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.15.2
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.16.1
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.16.3
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.16.4
|
||||||
|
* *(deps)* Update dependency vite-plugin-sentry to v1.3.0
|
||||||
|
* *(deps)* Update dependency vitest to v0.29.3
|
||||||
|
* *(deps)* Update dependency vitest to v0.29.4
|
||||||
|
* *(deps)* Update dependency vitest to v0.29.5
|
||||||
|
* *(deps)* Update dependency vitest to v0.29.7
|
||||||
|
* *(deps)* Update dependency vitest to v0.29.8
|
||||||
|
* *(deps)* Update dependency vitest to v0.30.0
|
||||||
|
* *(deps)* Update dependency vitest to v0.30.1
|
||||||
|
* *(deps)* Update dependency vitest to v0.31.0
|
||||||
|
* *(deps)* Update dependency vitest to v0.31.1
|
||||||
|
* *(deps)* Update dependency vitest to v0.31.2
|
||||||
|
* *(deps)* Update dependency vitest to v0.31.4
|
||||||
|
* *(deps)* Update dependency vitest to v0.32.0
|
||||||
|
* *(deps)* Update dependency vitest to v0.32.1
|
||||||
|
* *(deps)* Update dependency vitest to v0.32.2
|
||||||
|
* *(deps)* Update dependency vitest to v0.32.3
|
||||||
|
* *(deps)* Update dependency vue to v3.3.4
|
||||||
|
* *(deps)* Update dependency vue to v3.3.4
|
||||||
|
* *(deps)* Update dependency vue-flatpickr-component to v11.0.3
|
||||||
|
* *(deps)* Update dependency vue-router to v4.2.0
|
||||||
|
* *(deps)* Update dependency vue-router to v4.2.1
|
||||||
|
* *(deps)* Update dependency vue-router to v4.2.2
|
||||||
|
* *(deps)* Update dependency vue-router to v4.2.3
|
||||||
|
* *(deps)* Update dependency vue-router to v4.2.4
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.4.0
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.4.1
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.4.2
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.4.3
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.4.4
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.0
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.1
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.2
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.3
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.4
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.6.5
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.8.0
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.8.1
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.8.2
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.8.3
|
||||||
|
* *(deps)* Update dev-dependencies
|
||||||
|
* *(deps)* Update dev-dependencies
|
||||||
|
* *(deps)* Update dev-dependencies
|
||||||
|
* *(deps)* Update dev-dependencies
|
||||||
|
* *(deps)* Update dev-dependencies
|
||||||
|
* *(deps)* Update flake
|
||||||
|
* *(deps)* Update font awesome to v6.4.0
|
||||||
|
* *(deps)* Update histoire to v0.15.9
|
||||||
|
* *(deps)* Update histoire to v0.16.0
|
||||||
|
* *(deps)* Update histoire to v0.16.1
|
||||||
|
* *(deps)* Update lockfile
|
||||||
|
* *(deps)* Update node.js to v18.16.0
|
||||||
|
* *(deps)* Update node.js to v18.16.1
|
||||||
|
* *(deps)* Update node.js to v20 (#3411)
|
||||||
|
* *(deps)* Update pnpm to v7.29.3
|
||||||
|
* *(deps)* Update pnpm to v7.30.0
|
||||||
|
* *(deps)* Update pnpm to v7.30.1
|
||||||
|
* *(deps)* Update pnpm to v7.30.2
|
||||||
|
* *(deps)* Update pnpm to v7.30.3
|
||||||
|
* *(deps)* Update pnpm to v7.30.5
|
||||||
|
* *(deps)* Update pnpm to v7.31.0
|
||||||
|
* *(deps)* Update pnpm to v7.32.0
|
||||||
|
* *(deps)* Update pnpm to v8
|
||||||
|
* *(deps)* Update pnpm to v8.3.0
|
||||||
|
* *(deps)* Update pnpm to v8.3.1
|
||||||
|
* *(deps)* Update pnpm to v8.4.0
|
||||||
|
* *(deps)* Update pnpm to v8.5.0
|
||||||
|
* *(deps)* Update pnpm to v8.5.1
|
||||||
|
* *(deps)* Update pnpm to v8.6.0
|
||||||
|
* *(deps)* Update pnpm to v8.6.1
|
||||||
|
* *(deps)* Update pnpm to v8.6.2
|
||||||
|
* *(deps)* Update pnpm to v8.6.3
|
||||||
|
* *(deps)* Update pnpm to v8.6.4
|
||||||
|
* *(deps)* Update pnpm to v8.6.5
|
||||||
|
* *(deps)* Update pnpm to v8.6.6
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.43.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.44.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.44.1
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.44.2
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.45.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.46.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.47.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.48.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.49.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.50.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.51.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.51.2
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.52.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.52.1
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.53.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.53.1
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.54.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.55.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.55.2
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.56.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.57.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.55.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.56.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.57.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.57.1
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.58.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.1
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.11
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.2
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.5
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.6
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.7
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.8
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.59.9
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.60.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.60.1
|
||||||
|
* *(deps)* Update workbox monorepo to v6.6.0 (#3548)
|
||||||
|
* *(deps)* Update workbox monorepo to v6.6.1 (#3553)
|
||||||
|
* *(deps)* Update workbox monorepo to v7 (major) (#3556)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* *(assignees)* Show user avatar in search results
|
||||||
|
* *(datepicker)* Separate datepicker popup and datepicker logic in different components
|
||||||
|
* *(i18n)* Enable Danish translation
|
||||||
|
* *(i18n)* Enable Japanese translation
|
||||||
|
* *(i18n)* Enable Spanish translation
|
||||||
|
* *(i18n)* Use chinese name for chinese translation
|
||||||
|
* *(kanban)* Use total task count from the api instead of manually calculating it per bucket
|
||||||
|
* *(link share)* Add e2e tests for link share hash
|
||||||
|
* *(navigation)* Add hiding child projects
|
||||||
|
* *(navigation)* Allow dragging a project out from its parent project
|
||||||
|
* *(navigation)* Correctly show child projects
|
||||||
|
* *(navigation)* Make dragging a project to a parent work
|
||||||
|
* *(navigation)* Make dragging a project under another project work
|
||||||
|
* *(navigation)* Show favorite projects on top
|
||||||
|
* *(projects)* Allow setting a saved filter for tasks shown on the overview page
|
||||||
|
* *(projects)* Move hasProjects check to store
|
||||||
|
* *(quick add magic)* Allow fuzzy matching of assignees when the api results are unambigous
|
||||||
|
* *(reminders)* Add confirm button
|
||||||
|
* *(reminders)* Add e2e tests for task reminders
|
||||||
|
* *(reminders)* Add more spacing
|
||||||
|
* *(reminders)* Add on the due / start / end date as a reminder preset
|
||||||
|
* *(reminders)* Add preset two hours before due / start / end date
|
||||||
|
* *(reminders)* Add proper time picker for relative dates
|
||||||
|
* *(reminders)* Highlight which preset or custom date is selected
|
||||||
|
* *(reminders)* Make adding new reminders less confusing
|
||||||
|
* *(reminders)* Make relative presets actually work
|
||||||
|
* *(reminders)* Move reminder settings to a popup
|
||||||
|
* *(reminders)* Only show relative reminders when there's a date to relate them to
|
||||||
|
* *(reminders)* Show resolved reminder time in a tooltip and properly bubble updated task down to the reminder component
|
||||||
|
* *(reminders)* Translate all reminder form strings
|
||||||
|
* *(sentry)* Only load sentry when it's enabled
|
||||||
|
* *(tests)* Add project tests derived from old namespace tests
|
||||||
|
* *(user)* Migrate color scheme settings to persistance in db
|
||||||
|
* *(user)* Migrate pop sound setting to store in api
|
||||||
|
* *(user)* Persist frontend settings in the api (#3594)* Rename files with list to project ([b9d3b5c](b9d3b5c75635577321acc1791219aed40c6c14a4))
|
||||||
|
* *(user)* Save quick add magic mode in api
|
||||||
|
* *(user)* Set default settings when loading persisted
|
||||||
|
* *(user)* Use user language from store after logging in
|
||||||
|
* Abstract BaseCheckbox ([8fc254d](8fc254d2db5738e5d370c9f346c8d0d1e31bb9d0))
|
||||||
|
* Add hotkeys for priority, delete and favorite on the `TaskDetailView` (#3400) ([e00c9bb](e00c9bb1afc8491039b5ffb50d4d8d9b38e6e086))
|
||||||
|
* Add message to add to home screen on mobile ([3c9083b](3c9083b90dd3e5f97109ba2a23d2f2f8cc7d6c7c))
|
||||||
|
* Add redirect for old list routes ([af523cf](af523cfcd71528c7e8d0b50874f4766f40f958d2))
|
||||||
|
* Add setting for infinite nesting ([cb218ec](cb218ec0c31a41ba41a713a3757f71ad550dd71c))
|
||||||
|
* Add transition to input icons ([abb5128](abb51284269d84de14d0a156c386c63dc596b9ab))
|
||||||
|
* Add vite-plugin sentry (#1991) ([5ca31d0](5ca31d00eeff28f4728a4d07b96d761a6f174207))
|
||||||
|
* Add vite-plugin sentry ([73947f0](73947f0ba4031cb0f9aff78f8a7e3316a36d59b4))
|
||||||
|
* Allow creating a new project directly as a child project from another one ([b341184](b34118485cc056146682cd4592c90e4662b307eb))
|
||||||
|
* Allow disabling icon changes ([efb3407](efb3407b8769a23f4352161d6db6267ce4b30eee))
|
||||||
|
* Allow hiding the quick add magic help tooltip with a button ([7fb85da](7fb85dacecdae597180553036243ab845d50ede5))
|
||||||
|
* Allow selecting a parent project when creating a project ([ce887c3](ce887c38f3a9e84c832bfbf62efa455df37a1a4f))
|
||||||
|
* Allow selecting a parent project when duplicating a project ([799c0be](799c0be8306cfc5150611153c59701e96d56893a))
|
||||||
|
* Allow selecting a parent project when editing a project ([ee8f80c](ee8f80cc70109a496959da167d14ffda4e2a6175))
|
||||||
|
* Allow to edit existing relative reminders ([5d38b83](5d38b8327fc323c571fced33442bdb923d6d3baa))
|
||||||
|
* Better vscode vitest integration ([314cbf4](314cbf471f8e9cff2a3fca6bbd969807401b5cda))
|
||||||
|
* Change the link share hash name ([2066056](20660564c16283c77029bc3c3125c6c3febde47e))
|
||||||
|
* Check link share auth from store instead ([c2ffe3a](c2ffe3a9dcfd1e067b8d92e1d69183c2a8acfa8f))
|
||||||
|
* Don't handle child projects and instead only save the ids ([760efa8](760efa854dcc83e74f96782339b79b8d27b853b2))
|
||||||
|
* Don't use child_projects property from api ([ebd9c47](ebd9c4702ed1c6920d47e5e42294e6d4fa3c73c0))
|
||||||
|
* Edit relative reminders (#3248) ([3f8e457](3f8e457d5250df0b3af34d8f3bb0c053b15a97be))
|
||||||
|
* Edit relative reminders ([14e2698](14e26988331ca72afae01b8264969458cdb4a509))
|
||||||
|
* Hide quick add magic help behind a tooltip (#3353) ([a988565](a988565227f57dfc728319d433532f71e61d6424))
|
||||||
|
* Highlight hint icon when hovering the input ([422d7fc](422d7fc693caf886a49d03ff48b56ae6ce825356))
|
||||||
|
* Improve datemathHelp.vue ([795b26e](795b26e1dde781e152ab03fc31fd95f9f106a452))
|
||||||
|
* Improve handling of an invalid api url ([24ad2f8](24ad2f892db0fce3458624c9dad8735130253fa0))
|
||||||
|
* Improve user assignments via quick add magic (#3348) ([d9f608e](d9f608e8b4be4da380a535edcce1782c6d21926d))
|
||||||
|
* Improve variable naming for ProjectCardGrid ([a4be973](a4be973e29e81db4e244427fc46a11b4c8c95f4c))
|
||||||
|
* Load all projects earlier than in the navigation and use the loading state of the store ([1d93661](1d936618faecb0ddcb10f7c900096a3705614dbd))
|
||||||
|
* Mark undone if task moved from isDoneBucket (#3291) ([30adad5](30adad5ae6568b5ef1125f206989d447fb999eee))
|
||||||
|
* Move namespaces list to projects list ([e1bdabc](e1bdabc8d670f7342f4f0777a30a961e3fd4601d))
|
||||||
|
* Move navigation item to component ([3db4e01](3db4e011d4b625cee940c58ee32d065b8c43f1bb))
|
||||||
|
* Move quick add magic to a popup behind an icon ([6989558](69895589636ee6369c9778f529bf1df953acb7b1))
|
||||||
|
* New image for the unauthenticated views ([bef25c4](bef25c49d535ff3940a0112a715f5b351e816468))
|
||||||
|
* Optimize print view for project views ([8e2c76a](8e2c76a33eec573afab0b754d0707f84e2cca962))
|
||||||
|
* Persist link share auth rule in url hash (#3336) ([da3eaf0](da3eaf0d357c24775ba8a4cf8f089e5042f73c00))
|
||||||
|
* Persist link share auth rule in url hash ([f68bb26](f68bb2625e5f619f365fdd421aeda2b8af879aab))
|
||||||
|
* Prepare for pnpm 8 (#3331) ([7d3b97d](7d3b97d422896e17ab9231c66e49da6c07967d7e))
|
||||||
|
* Rebuild main navigation so that it works recursively with projects ([06e8cdb](06e8cdb9d2907c846ca7c555b31571b5c1798433))
|
||||||
|
* Remove all namespace leftovers ([1bd17d6](1bd17d6e50034c159150095f1c51a966293a6726))
|
||||||
|
* Remove namespaces, make projects infinitely nestable (#3323) ([ac1d374](ac1d374191fca764a70d9851d9828a78ae27c075))
|
||||||
|
* Rename link share hash prefix ([b9f0635](b9f0635d9fcc764c7ee188c95ce59ac358f735cf))
|
||||||
|
* Rename list to project everywhere ([befa6f2](befa6f27bb607a57eb8ed49d0152b85cdab4cb95))
|
||||||
|
* Replace color dot with handle icon on hover ([a3e2cbe](a3e2cbeb27ad8b0d052df62c9f24f8dd3808ddda))
|
||||||
|
* Set the current language to the one saved by the user on login ([acb212a](acb212ab241e1ed873c943e9c5fa3bcfb2c83a91))
|
||||||
|
* Show all parent projects in project search ([6a8c656](6a8c656dbb0a4729035468aedc60fd06e80c17ed))
|
||||||
|
* Show all parent projects in task detail view ([63ba298](63ba2982c92d495de6c7e3526c3693dcfe0e3fba))
|
||||||
|
* Show avatar and full name in team overview ([b80f070](b80f07043104868d134761b34582b234d12274e1))
|
||||||
|
* Show initial list of users when opening the assignees view ([59c942a](59c942af735a40f68cfd01caadb22694113da8ae))
|
||||||
|
* Start adding relative reminder picker with more options ([9df6950](9df6950d1a4a361c075020319ce3037e19e0912d))
|
||||||
|
* Translate inbox project title ([f2ca2d8](f2ca2d850de5b4b3b3d90e3a5c41adebca2dc1a5))
|
||||||
|
* Type i18n improvements ([dea1789](dea1789a00981fb496f0c1f4c19a6f0749e4de70))
|
||||||
|
* Use new Reminders API instead of reminder_dates ([f747d5b](f747d5b2fcadb7459c389372dca4507b75cdd4fa))
|
||||||
|
* Wrap projects navigation in a <Suspense> so that we can use top level await ([2579c33](2579c33ee1d07234c3ad42d75f2c7a1f7bfdb149))
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
* *(ci)* Remove netlify dependency (#3459)
|
||||||
|
* *(ci)* Sign drone config
|
||||||
|
* *(editor)* Disable deprecated marked options
|
||||||
|
* *(i18n)* Clarify translation string
|
||||||
|
* *(parseSubtasksViaIndention)* Fix comment (#3259)
|
||||||
|
* *(reminders)* Remove reminderDates property
|
||||||
|
* *(sentry)* Alwys use the same version
|
||||||
|
* *(sentry)* Ignore missing commits
|
||||||
|
* *(sentry)* Only load sentry when enabled
|
||||||
|
* *(sentry)* Remove debug options
|
||||||
|
* *(sentry)* Remove sourcemaps after upload via plugin
|
||||||
|
* *(sentry)* Use correct chunks option
|
||||||
|
* *(task)* Move toggleFavorite to store
|
||||||
|
* *(task)* Use ref for task instead of reactive
|
||||||
|
* *(tests)* Enable experimental memory managment for cypress tests
|
||||||
|
* *(user)* Cleanup* Update JSDoc example ([bfbfd6a](bfbfd6a4212d493912406c1c505b6c0a24f0f014))
|
||||||
|
* Add comment on overriding ([21ad830](21ad8301f28ba838c577acb72cb66ea00e176876))
|
||||||
|
* Add types for emit ([c567874](c56787443f6f9f6be0f8d8501dd4e6e7a768648a))
|
||||||
|
* Better function naming in password components ([a416d26](a416d26f7cfd163cadb0b6ded107b217ecad5d7c))
|
||||||
|
* Catch error when trying to play pop sound ([929d4f4](929d4f402342de309dd8e453252d22fcb9f362a6))
|
||||||
|
* Chore; extract code to reminder-period.vue ([0d6c0c8](0d6c0c8399c9fc73843bdbeb84ff19467edcaa90))
|
||||||
|
* Clarify users when can still be found even if they disabled it ([302ba2b](302ba2bec7f592f6b0b1fba84a5a1a9fd5f994de))
|
||||||
|
* Cleanup namespace leftovers ([2e33615](2e336150e086354b1623569aa98ab9c5be48c59a))
|
||||||
|
* Don't recalculate everything ([9c3259c](9c3259c660e8436f41b5494c9567319090c03bd6))
|
||||||
|
* Don't set the current project to null if it's undefined already ([e4d97e0](e4d97e05205e2c36143319ccf07ccac03f5de408))
|
||||||
|
* Don't show selection for parent project when no projects are available ([c30dcff](c30dcff45157e5b89b7bb6c2442271c15da33fc4))
|
||||||
|
* Don't wrap a computed in another computed ([afaf184](afaf1846ece65b8b2bbee971fafb31a535a4381b))
|
||||||
|
* Export favorite projects from store ([131022d](131022da427616765f8109ca8ac8f6bad1bdcbbb))
|
||||||
|
* Export not archived root projects ([b5d9afd](b5d9afd0f72aaf28b89f4877ce3ad2eabe6c3d7b))
|
||||||
|
* Export projects as array directly from projects store ([e4379f0](e4379f0a229b7b8572fddb029658713a0bbfca1d))
|
||||||
|
* Follow the happy path ([a33e2f6](a33e2f6c00f35f36aabb6b4d6e823396d29cdf3d))
|
||||||
|
* Format ([4ad9773](4ad9773022b5873fff09b7afade02c026ac5332f))
|
||||||
|
* Format ([638d187](638d187a24020d698327b0a0d04a5897672d3b79))
|
||||||
|
* Formatting ([b92d780](b92d780cda3ab7222c4a6ab7323d1dd3f679b514))
|
||||||
|
* Group return parameter ([5298706](52987060b11ac0418b6a88f1beabaee59165117d))
|
||||||
|
* Import const instead of redeclaring it ([61baf02](61baf02e26b292e3f02816a483eb7d92fb49d8ab))
|
||||||
|
* Improve prop type definition ([638f6be](638f6bea24980658d0f5fb3432d7b64c2ae06f75))
|
||||||
|
* Make fuzzy matching a paramater ([aeb73a3](aeb73a374f84f6b01d4be4cc784336a214a4cdfa))
|
||||||
|
* Move ProjectsNavigationWrapper back to navigation.vue ([65522a5](65522a57f1ceddfabeba235e17f8f81ee6bae47b))
|
||||||
|
* Move all options to component props ([db1c6d6](db1c6d6a41591c8ee5df2d2ee400aaaeda0d02bb))
|
||||||
|
* Move const ([0ce150a](0ce150af237985dda0cf44f24179ebae332e7585))
|
||||||
|
* Move duplicate project logic to composable ([b69a056](b69a05689be6e2c833c838cde052702600d245c7))
|
||||||
|
* Move loader class ([ac78e85](ac78e85e1726b6d7047db72ccbbaf29ac11d1696))
|
||||||
|
* Move loading styles to variant into the component ([76814a2](76814a2d3f68876934c5791bb4901fca5f95c00f))
|
||||||
|
* Move more logic to ProjectsNavigationItem.vue ([b567146](b567146d69f1c6a1eba6e37061bde7f627ff8654))
|
||||||
|
* Move positioning css ([7110c9a](7110c9a5ceb58e6e9675c0f91ddb34c9ab8f2cbc))
|
||||||
|
* Move styles to components ([25c3b7b](25c3b7bcbfb4ddc2163092ed7c1d5e4758967f1b))
|
||||||
|
* Move v-if ([12ebefd](12ebefd86a61ca5c82b104b4155a4989c8622713))
|
||||||
|
* Only apply padding where needed ([ddcd6a1](ddcd6a17dc659611910c2d4ed84fcff575e0ca3a))
|
||||||
|
* Re-add top menu spacing ([086f50d](086f50d4feed90aac0c458d3f53cbe59ae7402c8))
|
||||||
|
* Redirect to new project after creating from store ([6b824a4](6b824a49abe8854045c7670fcd6da50539c9fce5))
|
||||||
|
* Reduce nesting ([06a1ff6](06a1ff6f4bea4cc7447d528423de54f14583dca4))
|
||||||
|
* Refactor get parents project and move to projects store ([c32a198](c32a198a34edd3db7d6967010ce9dde401d1c864))
|
||||||
|
* Remove nesting ([a4c8fcc](a4c8fccb115f019840025659c7a8a4bac31eee04))
|
||||||
|
* Remove old comment ([4134fcb](4134fcbd752ab4cc7691907264b04cf64e11d012))
|
||||||
|
* Remove old todo ([4e21b46](4e21b463df9af5aec9a5b45c8331f5a9f9e8aeb9))
|
||||||
|
* Remove triggered notifications as it's not supported anywhere ([8a75790](8a75790453427287dc5a57ff3b59cd2b9cabd3f4))
|
||||||
|
* Remove type annotation for computed ([a3e289c](a3e289c06c992b24dcff21b1c4f8871676101d98))
|
||||||
|
* Remove unnecessary map ([336db56](336db56316dec7aeacf2174f5945764dc350769c))
|
||||||
|
* Remove unused class ([d4e4525](d4e452545afe94ed2e860cd14982462e080a4d49))
|
||||||
|
* Remove unused code ([652db56](652db56d42b39c05385ff7484fa43b0baa769759))
|
||||||
|
* Remove user margin from the component ([57c64bb](57c64bbf71342b4e9e2e9e3808412b5e0cf01006))
|
||||||
|
* Remove user margin from the component ([a1dd1d6](a1dd1d6664479e125e2f8ae87a9d2a57bf94fc9e))
|
||||||
|
* Remove wrapper div ([2c9693a](2c9693a83eeca832d49d519c5676ae30569628ca))
|
||||||
|
* Rename alias ([a803bc6](a803bc637e44893aa6921b70215a3206acdc5a91))
|
||||||
|
* Rename archived message key ([4dee3a9](4dee3a90e9a76cdd190eb28b3327bef1bcc34787))
|
||||||
|
* Rename flag ([6e09543](6e095436e9bfb6856c6aa469fc4cad93f239bad4))
|
||||||
|
* Rename getRedirectRoute ([59b05e9](59b05e9836946ed8b9dbb3926fc694641d8508a1))
|
||||||
|
* Rename prop ([2bb7ff1](2bb7ff1803d5a35bdd61a94e7a4d6fd03d5d1492))
|
||||||
|
* Replace section with a div ([9b10693](9b1069317283fc20c834eac981e0b2a500e32dba))
|
||||||
|
* Set project id from the outside ([6c9cbaa](6c9cbaadc821ab92e85b1f8e3fcb3fa85ea99670))
|
||||||
|
* Update nix flake ([f40035d](f40035dc7943e8199c553acfec838f21ea212c3e))
|
||||||
|
* Use <menu> instead of <ul> ([49fac7d](49fac7db1cbefce49712797869b956f31e8f541c))
|
||||||
|
* Use klona to clone project objet ([55e9122](55e912221be4b4765cdb3a7bd0e3dc693478ac81))
|
||||||
|
* Use long variable name ([6f1baa3](6f1baa3219093147842efe10f92482364516c84c))
|
||||||
|
* Use long variable name ([a0d39e6](a0d39e6081f35e4ba6589b7840168b0c69b3210f))
|
||||||
|
* Use project id type ([a342ae6](a342ae67de1c884895ce3304cf6eb1757a38573a))
|
||||||
|
* Use startsWith for prefix matching ([10ac1ff](10ac1ff66a2bcd797f54c83dda13745fdf359f33))
|
||||||
|
* Use stores directly ([a7440ed](a7440ed296ec0e99c9dc81e43617b3b54fc518a7))
|
||||||
|
* [skip ci] Updated translations via Crowdin
|
||||||
|
|
||||||
|
|
||||||
|
## [0.20.5] - 2023-03-12
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* *(docker)* Add cap_net_bind to the nginx binary in the docker container
|
||||||
|
* *(docker)* Revert unprivileged user
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* *(deps)* Update dependency sass to v1.59.2
|
||||||
|
* *(deps)* Update dependency eslint to v8.36.0
|
||||||
|
|
||||||
## [0.20.4] - 2023-03-10
|
## [0.20.4] - 2023-03-10
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
@ -4778,4 +5527,3 @@ Co-committed-by: renovate <renovatebot@kolaente.de>
|
||||||
* Fixed loading tasks when the user was not authenticated
|
* Fixed loading tasks when the user was not authenticated
|
||||||
|
|
||||||
## [0.1] - 2018-09-20
|
## [0.1] - 2018-09-20
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,17 @@
|
||||||
# │─││ │││ │ │
|
# │─││ │││ │ │
|
||||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
|
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
ARG USE_RELEASE=false
|
ARG USE_RELEASE=false
|
||||||
ARG RELEASE_VERSION=main
|
ARG RELEASE_VERSION=unstable
|
||||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||||
|
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
COPY pnpm-lock.yaml ./
|
COPY pnpm-lock.yaml ./
|
||||||
|
COPY patches ./patches/
|
||||||
|
|
||||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||||
# https://pnpm.io/installation#using-corepack
|
# https://pnpm.io/installation#using-corepack
|
||||||
|
@ -54,6 +55,8 @@ ENV VIKUNJA_LOG_FORMAT main
|
||||||
ENV VIKUNJA_API_URL /api/v1
|
ENV VIKUNJA_API_URL /api/v1
|
||||||
ENV VIKUNJA_SENTRY_ENABLED false
|
ENV VIKUNJA_SENTRY_ENABLED false
|
||||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||||
|
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
||||||
|
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
||||||
|
|
||||||
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
||||||
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
||||||
|
@ -66,5 +69,3 @@ RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
|
||||||
chmod -R 0644 /etc/nginx/nginx.conf && \
|
chmod -R 0644 /etc/nginx/nginx.conf && \
|
||||||
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
||||||
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
||||||
# unprivileged user
|
|
||||||
USER nginx
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||||
[![Download](https://img.shields.io/badge/download-v0.20.4-brightgreen.svg)](https://dl.vikunja.io)
|
[![Download](https://img.shields.io/badge/download-v0.21.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||||
|
|
||||||
This is the web frontend for Vikunja, written in Vue.js.
|
This is the web frontend for Vikunja, written in Vue.js.
|
||||||
|
@ -50,4 +50,3 @@ pnpm run build
|
||||||
```shell
|
```shell
|
||||||
pnpm run lint
|
pnpm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,5 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
viewportWidth: 1600,
|
viewportWidth: 1600,
|
||||||
viewportHeight: 900,
|
viewportHeight: 900,
|
||||||
|
experimentalMemoryManagement: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {prepareLists} from './prepareLists'
|
|
||||||
|
|
||||||
describe('List History', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
prepareLists()
|
|
||||||
|
|
||||||
it('should show a list history on the home page', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/lists/*').as('loadList')
|
|
||||||
|
|
||||||
const lists = ListFactory.create(6)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.get('body')
|
|
||||||
.should('not.contain', 'Last viewed')
|
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[1].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[2].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[3].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[4].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[5].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
|
|
||||||
// cy.visit('/')
|
|
||||||
// cy.wait('@loadNamespaces')
|
|
||||||
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
|
||||||
cy.get('nav.menu.top-menu a')
|
|
||||||
.contains('Overview')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('body')
|
|
||||||
.should('contain', 'Last viewed')
|
|
||||||
cy.get('[data-cy="listCardGrid"]')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
.should('contain', lists[1].title)
|
|
||||||
.should('contain', lists[2].title)
|
|
||||||
.should('contain', lists[3].title)
|
|
||||||
.should('contain', lists[4].title)
|
|
||||||
.should('contain', lists[5].title)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,122 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {prepareLists} from './prepareLists'
|
|
||||||
|
|
||||||
describe('Lists', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
let lists
|
|
||||||
prepareLists((newLists) => (lists = newLists))
|
|
||||||
|
|
||||||
it('Should create a new list', () => {
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('.namespace-title .dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-title .dropdown .dropdown-item')
|
|
||||||
.contains('New list')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/new/1')
|
|
||||||
cy.get('.card-header-title')
|
|
||||||
.contains('New list')
|
|
||||||
cy.get('input.input')
|
|
||||||
.type('New List')
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Create')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new list is done
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/')
|
|
||||||
cy.get('.list-title')
|
|
||||||
.should('contain', 'New List')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should redirect to a specific list view after visited', () => {
|
|
||||||
cy.visit('/lists/1/kanban')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/1/kanban')
|
|
||||||
cy.visit('/lists/1')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/1/kanban')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should rename the list in all places', () => {
|
|
||||||
TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
list_id: 1,
|
|
||||||
})
|
|
||||||
const newListName = 'New list name'
|
|
||||||
|
|
||||||
cy.visit('/lists/1')
|
|
||||||
cy.get('.list-title')
|
|
||||||
.should('contain', 'First List')
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Edit')
|
|
||||||
.click()
|
|
||||||
cy.get('#title')
|
|
||||||
.type(`{selectall}${newListName}`)
|
|
||||||
cy.get('footer.card-footer .button')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.list-title')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('.card-content')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a list', () => {
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/settings/delete')
|
|
||||||
cy.get('[data-cy="modalPrimary"]')
|
|
||||||
.contains('Do it')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.location('pathname')
|
|
||||||
.should('equal', '/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should archive a list', () => {
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.list-title-dropdown')
|
|
||||||
.click()
|
|
||||||
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item')
|
|
||||||
.contains('Archive')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-content')
|
|
||||||
.should('contain.text', 'Archive this list')
|
|
||||||
cy.get('.modal-content [data-cy=modalPrimary]')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.get('main.app-content')
|
|
||||||
.should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,145 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
|
||||||
|
|
||||||
describe('Namepaces', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
let namespaces
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
namespaces = NamespaceFactory.create(1)
|
|
||||||
ListFactory.create(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be all there', () => {
|
|
||||||
cy.visit('/namespaces')
|
|
||||||
cy.get('[data-cy="namespace-title"]')
|
|
||||||
.should('contain', namespaces[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should create a new Namespace', () => {
|
|
||||||
const newNamespaceTitle = 'New Namespace'
|
|
||||||
|
|
||||||
cy.visit('/namespaces')
|
|
||||||
cy.get('[data-cy="new-namespace"]')
|
|
||||||
.should('contain', 'New namespace')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/namespaces/new')
|
|
||||||
cy.get('.card-header-title')
|
|
||||||
.should('contain', 'New namespace')
|
|
||||||
cy.get('input.input')
|
|
||||||
.type(newNamespaceTitle)
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Create')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.namespace-container')
|
|
||||||
.should('contain', newNamespaceTitle)
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/namespaces')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should rename the namespace all places', () => {
|
|
||||||
const newNamespaces = NamespaceFactory.create(5)
|
|
||||||
const newNamespaceName = 'New namespace name'
|
|
||||||
|
|
||||||
cy.visit('/namespaces')
|
|
||||||
|
|
||||||
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
|
||||||
.contains('Edit')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/settings/edit')
|
|
||||||
cy.get('#namespacetext')
|
|
||||||
.invoke('val')
|
|
||||||
.should('equal', newNamespaces[0].title) // wait until the namespace data is loaded
|
|
||||||
cy.get('#namespacetext')
|
|
||||||
.type(`{selectall}${newNamespaceName}`)
|
|
||||||
cy.get('footer.card-footer .button')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 })
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists')
|
|
||||||
.should('contain', newNamespaceName)
|
|
||||||
.should('not.contain', newNamespaces[0].title)
|
|
||||||
cy.get('[data-cy="namespaces-list"]')
|
|
||||||
.should('contain', newNamespaceName)
|
|
||||||
.should('not.contain', newNamespaces[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a namespace when deleting it', () => {
|
|
||||||
const newNamespaces = NamespaceFactory.create(5)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/settings/delete')
|
|
||||||
cy.get('[data-cy="modalPrimary"]')
|
|
||||||
.contains('Do it')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists')
|
|
||||||
.should('not.contain', newNamespaces[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show archived lists & namespaces if the filter is not checked', () => {
|
|
||||||
const n = NamespaceFactory.create(1, {
|
|
||||||
id: 2,
|
|
||||||
is_archived: true,
|
|
||||||
}, false)
|
|
||||||
ListFactory.create(1, {
|
|
||||||
id: 2,
|
|
||||||
namespace_id: n[0].id,
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
ListFactory.create(1, {
|
|
||||||
id: 3,
|
|
||||||
is_archived: true,
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
// Initial
|
|
||||||
cy.visit('/namespaces')
|
|
||||||
cy.get('.namespace')
|
|
||||||
.should('not.contain', 'Archived')
|
|
||||||
|
|
||||||
// Show archived
|
|
||||||
cy.get('[data-cy="show-archived-check"] label.check span')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('be.checked')
|
|
||||||
cy.get('.namespace')
|
|
||||||
.should('contain', 'Archived')
|
|
||||||
|
|
||||||
// Don't show archived
|
|
||||||
cy.get('[data-cy="show-archived-check"] label.check span')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('not.be.checked')
|
|
||||||
|
|
||||||
// Second time visiting after unchecking
|
|
||||||
cy.visit('/namespaces')
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('not.be.checked')
|
|
||||||
cy.get('.namespace')
|
|
||||||
.should('not.contain', 'Archived')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,19 +0,0 @@
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
|
|
||||||
export function createLists() {
|
|
||||||
NamespaceFactory.create(1)
|
|
||||||
const lists = ListFactory.create(1, {
|
|
||||||
title: 'First List'
|
|
||||||
})
|
|
||||||
TaskFactory.truncate()
|
|
||||||
return lists
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareLists(setLists = (...args: any[]) => {}) {
|
|
||||||
beforeEach(() => {
|
|
||||||
const lists = createLists()
|
|
||||||
setLists(lists)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,18 +1,18 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
|
||||||
describe('Editor', () => {
|
describe('Editor', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
NamespaceFactory.create(1)
|
ProjectFactory.create(1)
|
||||||
ListFactory.create(1)
|
BucketFactory.create(1)
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
UserListFactory.truncate()
|
UserProjectFactory.truncate()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Has a preview with checkable checkboxes', () => {
|
it('Has a preview with checkable checkboxes', () => {
|
||||||
|
@ -24,6 +24,7 @@ describe('Editor', () => {
|
||||||
* [ ] Checklist
|
* [ ] Checklist
|
||||||
* [x] Checklist checked
|
* [x] Checklist checked
|
||||||
`,
|
`,
|
||||||
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
|
@ -8,20 +8,20 @@ describe('The Menu', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Is visible by default on desktop', () => {
|
it('Is visible by default on desktop', () => {
|
||||||
cy.get('.namespace-container')
|
cy.get('.menu-container')
|
||||||
.should('have.class', 'is-active')
|
.should('have.class', 'is-active')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can be hidden on desktop', () => {
|
it('Can be hidden on desktop', () => {
|
||||||
cy.get('button.menu-show-button:visible')
|
cy.get('button.menu-show-button:visible')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.namespace-container')
|
cy.get('.menu-container')
|
||||||
.should('not.have.class', 'is-active')
|
.should('not.have.class', 'is-active')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Is hidden by default on mobile', () => {
|
it('Is hidden by default on mobile', () => {
|
||||||
cy.viewport('iphone-8')
|
cy.viewport('iphone-8')
|
||||||
cy.get('.namespace-container')
|
cy.get('.menu-container')
|
||||||
.should('not.have.class', 'is-active')
|
.should('not.have.class', 'is-active')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ describe('The Menu', () => {
|
||||||
cy.viewport('iphone-8')
|
cy.viewport('iphone-8')
|
||||||
cy.get('button.menu-show-button:visible')
|
cy.get('button.menu-show-button:visible')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.namespace-container')
|
cy.get('.menu-container')
|
||||||
.should('have.class', 'is-active')
|
.should('have.class', 'is-active')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
17
cypress/e2e/project/prepareProjects.ts
Normal file
17
cypress/e2e/project/prepareProjects.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
|
export function createProjects() {
|
||||||
|
const projects = ProjectFactory.create(1, {
|
||||||
|
title: 'First Project'
|
||||||
|
})
|
||||||
|
TaskFactory.truncate()
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareProjects(setProjects = (...args: any[]) => {}) {
|
||||||
|
beforeEach(() => {
|
||||||
|
const projects = createProjects()
|
||||||
|
setProjects(projects)
|
||||||
|
})
|
||||||
|
}
|
50
cypress/e2e/project/project-history.spec.ts
Normal file
50
cypress/e2e/project/project-history.spec.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
|
describe('Project History', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
prepareProjects()
|
||||||
|
|
||||||
|
it('should show a project history on the home page', () => {
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
||||||
|
|
||||||
|
const projects = ProjectFactory.create(6)
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
cy.wait('@loadProjectArray')
|
||||||
|
cy.get('body')
|
||||||
|
.should('not.contain', 'Last viewed')
|
||||||
|
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[1].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[2].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[3].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[4].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[5].id}`)
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
|
||||||
|
// cy.visit('/')
|
||||||
|
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
||||||
|
cy.get('nav.menu.top-menu a')
|
||||||
|
.contains('Overview')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('body')
|
||||||
|
.should('contain', 'Last viewed')
|
||||||
|
cy.get('[data-cy="projectCardGrid"]')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
.should('contain', projects[1].title)
|
||||||
|
.should('contain', projects[2].title)
|
||||||
|
.should('contain', projects[3].title)
|
||||||
|
.should('contain', projects[4].title)
|
||||||
|
.should('contain', projects[5].title)
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,15 +3,15 @@ import {formatISO, format} from 'date-fns'
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
describe('List View Gantt', () => {
|
describe('Project View Gantt', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
prepareLists()
|
prepareProjects()
|
||||||
|
|
||||||
it('Hides tasks with no dates', () => {
|
it('Hides tasks with no dates', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.contain', tasks[0].title)
|
.should('not.contain', tasks[0].title)
|
||||||
|
@ -25,7 +25,7 @@ describe('List View Gantt', () => {
|
||||||
nextMonth.setDate(1)
|
nextMonth.setDate(1)
|
||||||
nextMonth.setMonth(9)
|
nextMonth.setMonth(9)
|
||||||
|
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', format(now, 'MMMM'))
|
.should('contain', format(now, 'MMMM'))
|
||||||
|
@ -38,7 +38,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: now.toISOString(),
|
start_date: now.toISOString(),
|
||||||
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.be.empty')
|
.should('not.be.empty')
|
||||||
|
@ -50,7 +50,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: null,
|
start_date: null,
|
||||||
end_date: null,
|
end_date: null,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.gantt-options .fancycheckbox')
|
cy.get('.gantt-options .fancycheckbox')
|
||||||
.contains('Show tasks which don\'t have dates set')
|
.contains('Show tasks which don\'t have dates set')
|
||||||
|
@ -69,7 +69,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: now.toISOString(),
|
start_date: now.toISOString(),
|
||||||
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
||||||
.first()
|
.first()
|
||||||
|
@ -83,9 +83,9 @@ describe('List View Gantt', () => {
|
||||||
const now = Date.UTC(2022, 10, 9)
|
const now = Date.UTC(2022, 10, 9)
|
||||||
cy.clock(now, ['Date'])
|
cy.clock(now, ['Date'])
|
||||||
|
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
|
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
||||||
.first()
|
.first()
|
||||||
|
@ -99,13 +99,13 @@ describe('List View Gantt', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should change the date range based on date query parameters', () => {
|
it('Should change the date range based on date query parameters', () => {
|
||||||
cy.visit('/lists/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', 'September 2022')
|
.should('contain', 'September 2022')
|
||||||
.should('contain', 'October 2022')
|
.should('contain', 'October 2022')
|
||||||
.should('contain', 'November 2022')
|
.should('contain', 'November 2022')
|
||||||
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
|
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
||||||
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
|
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: formatISO(now),
|
start_date: formatISO(now),
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
||||||
.dblclick()
|
.dblclick()
|
|
@ -1,13 +1,13 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
describe('List View Kanban', () => {
|
describe('Project View Kanban', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
prepareLists()
|
prepareProjects()
|
||||||
|
|
||||||
let buckets
|
let buckets
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -16,10 +16,10 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Shows all buckets with their tasks', () => {
|
it('Shows all buckets with their tasks', () => {
|
||||||
const data = TaskFactory.create(10, {
|
const data = TaskFactory.create(10, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .title')
|
cy.get('.kanban .bucket .title')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
|
@ -34,10 +34,10 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Can add a new task to a bucket', () => {
|
it('Can add a new task to a bucket', () => {
|
||||||
TaskFactory.create(2, {
|
TaskFactory.create(2, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket')
|
cy.get('.kanban .bucket')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
|
@ -55,7 +55,7 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can create a new bucket', () => {
|
it('Can create a new bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket.new-bucket .button')
|
cy.get('.kanban .bucket.new-bucket .button')
|
||||||
.click()
|
.click()
|
||||||
|
@ -69,7 +69,7 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can set a bucket limit', () => {
|
it('Can set a bucket limit', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
|
@ -90,7 +90,7 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can rename a bucket', () => {
|
it('Can rename a bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .title')
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
.first()
|
.first()
|
||||||
|
@ -101,7 +101,7 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can delete a bucket', () => {
|
it('Can delete a bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
|
@ -125,10 +125,10 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Can drag tasks around', () => {
|
it('Can drag tasks around', () => {
|
||||||
const tasks = TaskFactory.create(2, {
|
const tasks = TaskFactory.create(2, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -144,10 +144,10 @@ describe('List View Kanban', () => {
|
||||||
it('Should navigate to the task when the task card is clicked', () => {
|
it('Should navigate to the task when the task card is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -158,18 +158,18 @@ describe('List View Kanban', () => {
|
||||||
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the kanban board when moving it to another list', () => {
|
it('Should remove a task from the kanban board when moving it to another project', () => {
|
||||||
const lists = ListFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
BucketFactory.create(2, {
|
BucketFactory.create(2, {
|
||||||
list_id: '{increment}',
|
project_id: '{increment}',
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
const task = tasks[0]
|
const task = tasks[0]
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
||||||
|
@ -180,7 +180,7 @@ describe('List View Kanban', () => {
|
||||||
.contains('Move')
|
.contains('Move')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
.type(`${lists[1].title}{enter}`)
|
.type(`${projects[1].title}{enter}`)
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
||||||
// presses enter and we can't simulate pressing on enter to select the item.
|
// presses enter and we can't simulate pressing on enter to select the item.
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||||
|
@ -197,26 +197,26 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Shows a button to filter the kanban board', () => {
|
it('Shows a button to filter the kanban board', () => {
|
||||||
const data = TaskFactory.create(10, {
|
const data = TaskFactory.create(10, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.list-kanban .filter-container .base-button')
|
cy.get('.project-kanban .filter-container .base-button')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the board when deleting it', () => {
|
it('Should remove a task from the board when deleting it', () => {
|
||||||
const lists = ListFactory.create(1)
|
const projects = ProjectFactory.create(1)
|
||||||
const buckets = BucketFactory.create(2, {
|
const buckets = BucketFactory.create(2, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const task = tasks[0]
|
const task = tasks[0]
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
|
@ -1,32 +1,32 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
describe('List View List', () => {
|
describe('Project View Project', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
prepareLists()
|
prepareProjects()
|
||||||
|
|
||||||
it('Should be an empty list', () => {
|
it('Should be an empty project', () => {
|
||||||
cy.visit('/lists/1')
|
cy.visit('/projects/1')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/lists/1/list')
|
.should('contain', '/projects/1/list')
|
||||||
cy.get('.list-title')
|
cy.get('.project-title')
|
||||||
.should('contain', 'First List')
|
.should('contain', 'First Project')
|
||||||
cy.get('.list-title-dropdown')
|
cy.get('.project-title-dropdown')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('p')
|
cy.get('p')
|
||||||
.contains('This list is currently empty.')
|
.contains('This project is currently empty.')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should create a new task', () => {
|
it('Should create a new task', () => {
|
||||||
const newTaskTitle = 'New task'
|
const newTaskTitle = 'New task'
|
||||||
|
|
||||||
cy.visit('/lists/1')
|
cy.visit('/projects/1')
|
||||||
cy.get('.task-add textarea')
|
cy.get('.task-add textarea')
|
||||||
.type(newTaskTitle+'{enter}')
|
.type(newTaskTitle+'{enter}')
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
|
@ -36,9 +36,9 @@ describe('List View List', () => {
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
|
|
||||||
cy.get('.tasks .task .tasktext')
|
cy.get('.tasks .task .tasktext')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -49,33 +49,32 @@ describe('List View List', () => {
|
||||||
.should('contain', `/tasks/${tasks[0].id}`)
|
.should('contain', `/tasks/${tasks[0].id}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not see any elements for a list which is shared read only', () => {
|
it('Should not see any elements for a project which is shared read only', () => {
|
||||||
UserFactory.create(2)
|
UserFactory.create(2)
|
||||||
UserListFactory.create(1, {
|
UserProjectFactory.create(1, {
|
||||||
list_id: 2,
|
project_id: 2,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
})
|
})
|
||||||
const lists = ListFactory.create(2, {
|
const projects = ProjectFactory.create(2, {
|
||||||
owner_id: '{increment}',
|
owner_id: '{increment}',
|
||||||
namespace_id: '{increment}',
|
|
||||||
})
|
})
|
||||||
cy.visit(`/lists/${lists[1].id}/`)
|
cy.visit(`/projects/${projects[1].id}/`)
|
||||||
|
|
||||||
cy.get('.list-title-wrapper .icon')
|
cy.get('.project-title-wrapper .icon')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should only show the color of a list in the navigation and not in the list view', () => {
|
it('Should only show the color of a project in the navigation and not in the list view', () => {
|
||||||
const lists = ListFactory.create(1, {
|
const projects = ProjectFactory.create(1, {
|
||||||
hex_color: '00db60',
|
hex_color: '00db60',
|
||||||
})
|
})
|
||||||
TaskFactory.create(10, {
|
TaskFactory.create(10, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
cy.visit(`/lists/${lists[0].id}/`)
|
cy.visit(`/projects/${projects[0].id}/`)
|
||||||
|
|
||||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||||
|
@ -87,9 +86,9 @@ describe('List View List', () => {
|
||||||
const tasks = TaskFactory.create(100, {
|
const tasks = TaskFactory.create(100, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: i => `task${i}`,
|
title: i => `task${i}`,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
|
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[1].title)
|
.should('contain', tasks[1].title)
|
|
@ -2,37 +2,37 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
describe('List View Table', () => {
|
describe('Project View Table', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
it('Should show a table with tasks', () => {
|
it('Should show a table with tasks', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.should('contain', tasks[0].title)
|
.should('contain', tasks[0].title)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have working column switches', () => {
|
it('Should have working column switches', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table .filter-container .items .button')
|
cy.get('.project-table .filter-container .items .button')
|
||||||
.contains('Columns')
|
.contains('Columns')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
|
||||||
.contains('Priority')
|
.contains('Priority')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
|
||||||
.contains('Done')
|
.contains('Done')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.list-table table.table th')
|
cy.get('.project-table table.table th')
|
||||||
.contains('Priority')
|
.contains('Priority')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.list-table table.table th')
|
cy.get('.project-table table.table th')
|
||||||
.contains('Done')
|
.contains('Done')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
@ -40,11 +40,11 @@ describe('List View Table', () => {
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
.click()
|
.click()
|
||||||
|
|
171
cypress/e2e/project/project.spec.ts
Normal file
171
cypress/e2e/project/project.spec.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
|
describe('Projects', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
|
let projects
|
||||||
|
prepareProjects((newProjects) => (projects = newProjects))
|
||||||
|
|
||||||
|
it('Should create a new project', () => {
|
||||||
|
cy.visit('/projects')
|
||||||
|
cy.get('.project-header [data-cy=new-project]')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/new')
|
||||||
|
cy.get('.card-header-title')
|
||||||
|
.contains('New project')
|
||||||
|
cy.get('input[name=projectTitle]')
|
||||||
|
.type('New Project')
|
||||||
|
cy.get('.button')
|
||||||
|
.contains('Create')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification', {timeout: 1000}) // Waiting until the request to create the new project is done
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/')
|
||||||
|
cy.get('.project-title')
|
||||||
|
.should('contain', 'New Project')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should redirect to a specific project view after visited', () => {
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/projects/*/buckets*').as('loadBuckets')
|
||||||
|
cy.visit('/projects/1/kanban')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/1/kanban')
|
||||||
|
cy.wait('@loadBuckets')
|
||||||
|
cy.visit('/projects/1')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/1/kanban')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should rename the project in all places', () => {
|
||||||
|
TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
project_id: 1,
|
||||||
|
})
|
||||||
|
const newProjectName = 'New project name'
|
||||||
|
|
||||||
|
cy.visit('/projects/1')
|
||||||
|
cy.get('.project-title')
|
||||||
|
.should('contain', 'First Project')
|
||||||
|
|
||||||
|
cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Edit')
|
||||||
|
.click()
|
||||||
|
cy.get('#title')
|
||||||
|
.type(`{selectall}${newProjectName}`)
|
||||||
|
cy.get('footer.card-footer .button')
|
||||||
|
.contains('Save')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.project-title')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.get('.menu-container .menu-list li:first-child')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('.project-grid')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a project when deleting it', () => {
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Delete')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/settings/delete')
|
||||||
|
cy.get('[data-cy="modalPrimary"]')
|
||||||
|
.contains('Do it')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.menu-container .menu-list')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.location('pathname')
|
||||||
|
.should('equal', '/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should archive a project', () => {
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.project-title-dropdown')
|
||||||
|
.click()
|
||||||
|
cy.get('.project-title-dropdown .dropdown-menu .dropdown-item')
|
||||||
|
.contains('Archive')
|
||||||
|
.click()
|
||||||
|
cy.get('.modal-content')
|
||||||
|
.should('contain.text', 'Archive this project')
|
||||||
|
cy.get('.modal-content [data-cy=modalPrimary]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.menu-container .menu-list')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.get('main.app-content')
|
||||||
|
.should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should show all projects on the projects page', () => {
|
||||||
|
const projects = ProjectFactory.create(10)
|
||||||
|
|
||||||
|
cy.visit('/projects')
|
||||||
|
|
||||||
|
projects.forEach(p => {
|
||||||
|
cy.get('[data-cy="projects-list"]')
|
||||||
|
.should('contain', p.title)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not show archived projects if the filter is not checked', () => {
|
||||||
|
ProjectFactory.create(1, {
|
||||||
|
id: 2,
|
||||||
|
}, false)
|
||||||
|
ProjectFactory.create(1, {
|
||||||
|
id: 3,
|
||||||
|
is_archived: true,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// Initial
|
||||||
|
cy.visit('/projects')
|
||||||
|
cy.get('.project-grid')
|
||||||
|
.should('not.contain', 'Archived')
|
||||||
|
|
||||||
|
// Show archived
|
||||||
|
cy.get('[data-cy="show-archived-check"] label span')
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('be.checked')
|
||||||
|
cy.get('.project-grid')
|
||||||
|
.should('contain', 'Archived')
|
||||||
|
|
||||||
|
// Don't show archived
|
||||||
|
cy.get('[data-cy="show-archived-check"] label span')
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('not.be.checked')
|
||||||
|
|
||||||
|
// Second time visiting after unchecking
|
||||||
|
cy.visit('/projects')
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('not.be.checked')
|
||||||
|
cy.get('.project-grid')
|
||||||
|
.should('not.contain', 'Archived')
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,25 +1,59 @@
|
||||||
import {LinkShareFactory} from '../../factories/link_sharing'
|
import {LinkShareFactory} from '../../factories/link_sharing'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
|
function prepareLinkShare() {
|
||||||
|
const projects = ProjectFactory.create(1)
|
||||||
|
const tasks = TaskFactory.create(10, {
|
||||||
|
project_id: projects[0].id
|
||||||
|
})
|
||||||
|
const linkShares = LinkShareFactory.create(1, {
|
||||||
|
project_id: projects[0].id,
|
||||||
|
right: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
share: linkShares[0],
|
||||||
|
project: projects[0],
|
||||||
|
tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('Link shares', () => {
|
describe('Link shares', () => {
|
||||||
it('Can view a link share', () => {
|
it('Can view a link share', () => {
|
||||||
const lists = ListFactory.create(1)
|
const {share, project, tasks} = prepareLinkShare()
|
||||||
const tasks = TaskFactory.create(10, {
|
|
||||||
list_id: lists[0].id
|
|
||||||
})
|
|
||||||
const linkShares = LinkShareFactory.create(1, {
|
|
||||||
list_id: lists[0].id,
|
|
||||||
right: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/share/${linkShares[0].hash}/auth`)
|
cy.visit(`/share/${share.hash}/auth`)
|
||||||
|
|
||||||
cy.get('h1.title')
|
cy.get('h1.title')
|
||||||
.should('contain', lists[0].title)
|
.should('contain', project.title)
|
||||||
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.tasks')
|
||||||
|
.should('contain', tasks[0].title)
|
||||||
|
|
||||||
|
cy.url().should('contain', `/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should work when directly viewing a project with share hash present', () => {
|
||||||
|
const {share, project, tasks} = prepareLinkShare()
|
||||||
|
|
||||||
|
cy.visit(`/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
||||||
|
|
||||||
|
cy.get('h1.title')
|
||||||
|
.should('contain', project.title)
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[0].title)
|
.should('contain', tasks[0].title)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should work when directly viewing a task with share hash present', () => {
|
||||||
|
const {share, project, tasks} = prepareLinkShare()
|
||||||
|
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}#share-auth-token=${share.hash}`)
|
||||||
|
|
||||||
|
cy.get('h1.title')
|
||||||
|
.should('contain', tasks[0].title)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {seed} from '../../support/seed'
|
import {seed} from '../../support/seed'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
import {updateUserSettings} from '../../support/updateUserSettings'
|
||||||
|
|
||||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||||
NamespaceFactory.create(1)
|
const project = ProjectFactory.create()[0]
|
||||||
const list = ListFactory.create()[0]
|
|
||||||
BucketFactory.create(1, {
|
BucketFactory.create(1, {
|
||||||
list_id: list.id,
|
project_id: project.id,
|
||||||
})
|
})
|
||||||
const tasks = []
|
const tasks = []
|
||||||
let dueDate = startDueDate
|
let dueDate = startDueDate
|
||||||
|
@ -20,7 +18,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||||
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
||||||
tasks.push({
|
tasks.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
list_id: list.id,
|
project_id: project.id,
|
||||||
done: false,
|
done: false,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
title: 'Test Task ' + i,
|
title: 'Test Task ' + i,
|
||||||
|
@ -31,7 +29,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
seed(TaskFactory.table, tasks)
|
seed(TaskFactory.table, tasks)
|
||||||
return {tasks, list}
|
return {tasks, project}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Home Page Task Overview', () => {
|
describe('Home Page Task Overview', () => {
|
||||||
|
@ -73,7 +71,7 @@ describe('Home Page Task Overview', () => {
|
||||||
due_date: new Date().toISOString(),
|
due_date: new Date().toISOString(),
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
||||||
cy.get('.tasks .task')
|
cy.get('.tasks .task')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
|
@ -90,7 +88,7 @@ describe('Home Page Task Overview', () => {
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
||||||
cy.get('.task-add textarea')
|
cy.get('.task-add textarea')
|
||||||
.type(newTaskTitle+'{enter}')
|
.type(newTaskTitle+'{enter}')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
@ -113,10 +111,10 @@ describe('Home Page Task Overview', () => {
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should show a task without a due date added via default list at the bottom', () => {
|
it('Should show a task without a due date added via default project at the bottom', () => {
|
||||||
const {list} = seedTasks(40)
|
const {project} = seedTasks(40)
|
||||||
updateUserSettings({
|
updateUserSettings({
|
||||||
default_list_id: list.id,
|
default_project_id: project.id,
|
||||||
overdue_tasks_reminders_time: '9:00',
|
overdue_tasks_reminders_time: '9:00',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -131,23 +129,22 @@ describe('Home Page Task Overview', () => {
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should show the cta buttons for new list when there are no tasks', () => {
|
it('Should show the cta buttons for new project when there are no tasks', () => {
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
cy.get('.home.app-content .content')
|
||||||
.should('contain.text', 'You can create a new list for your new tasks:')
|
.should('contain.text', 'Import your projects and tasks from other services into Vikunja:')
|
||||||
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show the cta buttons for new list when there are tasks', () => {
|
it('Should not show the cta buttons for new project when there are tasks', () => {
|
||||||
seedTasks()
|
seedTasks()
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
cy.get('.home.app-content .content')
|
||||||
.should('not.contain.text', 'You can create a new list for your new tasks:')
|
.should('not.contain.text', 'You can create a new project for your new tasks:')
|
||||||
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
.should('not.contain.text', 'Or import your projects and tasks from other services into Vikunja:')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskCommentFactory} from '../../factories/task_comment'
|
import {TaskCommentFactory} from '../../factories/task_comment'
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
|
||||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||||
import {LabelFactory} from '../../factories/labels'
|
import {LabelFactory} from '../../factories/labels'
|
||||||
import {LabelTaskFactory} from '../../factories/label_task'
|
import {LabelTaskFactory} from '../../factories/label_task'
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
|
||||||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
||||||
|
import {TaskReminderFactory} from '../../factories/task_reminders'
|
||||||
|
|
||||||
function addLabelToTaskAndVerify(labelTitle: string) {
|
function addLabelToTaskAndVerify(labelTitle: string) {
|
||||||
cy.get('.task-view .action-buttons .button')
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
@ -47,23 +47,21 @@ function uploadAttachmentAndVerify(taskId: number) {
|
||||||
describe('Task', () => {
|
describe('Task', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
let namespaces
|
let projects
|
||||||
let lists
|
|
||||||
let buckets
|
let buckets
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// UserFactory.create(1)
|
// UserFactory.create(1)
|
||||||
namespaces = NamespaceFactory.create(1)
|
projects = ProjectFactory.create(1)
|
||||||
lists = ListFactory.create(1)
|
|
||||||
buckets = BucketFactory.create(1, {
|
buckets = BucketFactory.create(1, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
UserListFactory.truncate()
|
UserProjectFactory.truncate()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should be created new', () => {
|
it('Should be created new', () => {
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
.type('New Task')
|
.type('New Task')
|
||||||
cy.get('.button')
|
cy.get('.button')
|
||||||
|
@ -74,11 +72,11 @@ describe('Task', () => {
|
||||||
.should('contain', 'New Task')
|
.should('contain', 'New Task')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Inserts new tasks at the top of the list', () => {
|
it('Inserts new tasks at the top of the project', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
cy.get('.list-is-empty-notice')
|
cy.get('.project-is-empty-notice')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
.type('New Task')
|
.type('New Task')
|
||||||
|
@ -95,8 +93,8 @@ describe('Task', () => {
|
||||||
it('Marks a task as done', () => {
|
it('Marks a task as done', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
cy.get('.tasks .task .fancycheckbox label.check')
|
cy.get('.tasks .task .fancycheckbox')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
|
@ -106,11 +104,11 @@ describe('Task', () => {
|
||||||
it('Can add a task to favorites', () => {
|
it('Can add a task to favorites', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/list')
|
||||||
cy.get('.tasks .task .favorite')
|
cy.get('.tasks .task .favorite')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
cy.get('.menu.namespaces-lists')
|
cy.get('.menu-container')
|
||||||
.should('contain', 'Favorites')
|
.should('contain', 'Favorites')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -133,8 +131,7 @@ describe('Task', () => {
|
||||||
cy.get('.task-view h1.title.task-id')
|
cy.get('.task-view h1.title.task-id')
|
||||||
.should('contain', '#1')
|
.should('contain', '#1')
|
||||||
cy.get('.task-view h6.subtitle')
|
cy.get('.task-view h6.subtitle')
|
||||||
.should('contain', namespaces[0].title)
|
.should('contain', projects[0].title)
|
||||||
.should('contain', lists[0].title)
|
|
||||||
cy.get('.task-view .details.content.description')
|
cy.get('.task-view .details.content.description')
|
||||||
.should('contain', tasks[0].description)
|
.should('contain', tasks[0].description)
|
||||||
cy.get('.task-view .action-buttons p.created')
|
cy.get('.task-view .action-buttons p.created')
|
||||||
|
@ -179,21 +176,21 @@ describe('Task', () => {
|
||||||
.should('contain', 'Mark as undone')
|
.should('contain', 'Mark as undone')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Shows a task identifier since the list has one', () => {
|
it('Shows a task identifier since the project has one', () => {
|
||||||
const lists = ListFactory.create(1, {
|
const projects = ProjectFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
identifier: 'TEST',
|
identifier: 'TEST',
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
index: 1,
|
index: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
cy.get('.task-view h1.title.task-id')
|
cy.get('.task-view h1.title.task-id')
|
||||||
.should('contain', `${lists[0].identifier}-${tasks[0].index}`)
|
.should('contain', `${projects[0].identifier}-${tasks[0].index}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can edit the description', () => {
|
it('Can edit the description', () => {
|
||||||
|
@ -236,14 +233,14 @@ describe('Task', () => {
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can move a task to another list', () => {
|
it('Can move a task to another project', () => {
|
||||||
const lists = ListFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
BucketFactory.create(2, {
|
BucketFactory.create(2, {
|
||||||
list_id: '{increment}'
|
project_id: '{increment}'
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
@ -251,7 +248,7 @@ describe('Task', () => {
|
||||||
.contains('Move')
|
.contains('Move')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
.type(`${lists[1].title}{enter}`)
|
.type(`${projects[1].title}{enter}`)
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
||||||
// presses enter and we can't simulate pressing on enter to select the item.
|
// presses enter and we can't simulate pressing on enter to select the item.
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||||
|
@ -260,8 +257,7 @@ describe('Task', () => {
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.task-view h6.subtitle')
|
cy.get('.task-view h6.subtitle')
|
||||||
.should('contain', namespaces[0].title)
|
.should('contain', projects[1].title)
|
||||||
.should('contain', lists[1].title)
|
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
})
|
})
|
||||||
|
@ -269,7 +265,7 @@ describe('Task', () => {
|
||||||
it('Can delete a task', () => {
|
it('Can delete a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
@ -286,17 +282,17 @@ describe('Task', () => {
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', `/lists/${tasks[0].list_id}/`)
|
.should('contain', `/projects/${tasks[0].project_id}/`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can add an assignee to a task', () => {
|
it('Can add an assignee to a task', () => {
|
||||||
const users = UserFactory.create(5)
|
const users = UserFactory.create(5)
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
UserListFactory.create(5, {
|
UserProjectFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
user_id: '{increment}',
|
user_id: '{increment}',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -321,10 +317,10 @@ describe('Task', () => {
|
||||||
const users = UserFactory.create(2)
|
const users = UserFactory.create(2)
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
UserListFactory.create(5, {
|
UserProjectFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
user_id: '{increment}',
|
user_id: '{increment}',
|
||||||
})
|
})
|
||||||
TaskAssigneeFactory.create(1, {
|
TaskAssigneeFactory.create(1, {
|
||||||
|
@ -347,7 +343,7 @@ describe('Task', () => {
|
||||||
it('Can add a new label to a task', () => {
|
it('Can add a new label to a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
LabelFactory.truncate()
|
LabelFactory.truncate()
|
||||||
const newLabelText = 'some new label'
|
const newLabelText = 'some new label'
|
||||||
|
@ -375,7 +371,7 @@ describe('Task', () => {
|
||||||
it('Can add an existing label to a task', () => {
|
it('Can add an existing label to a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
@ -388,13 +384,13 @@ describe('Task', () => {
|
||||||
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
|
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -412,7 +408,7 @@ describe('Task', () => {
|
||||||
it('Can remove a label from a task', () => {
|
it('Can remove a label from a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.create(1, {
|
LabelTaskFactory.create(1, {
|
||||||
|
@ -466,6 +462,154 @@ describe('Task', () => {
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Can set a reminder', () => {
|
||||||
|
TaskReminderFactory.truncate()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
done: false,
|
||||||
|
})
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Set Reminders')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .columns.details .column button')
|
||||||
|
.contains('Add a new reminder')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.contains('Tomorrow')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.reminder-options-popup')
|
||||||
|
.should('not.be.visible')
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows to set a relative reminder when the task already has a due date', () => {
|
||||||
|
TaskReminderFactory.truncate()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
done: false,
|
||||||
|
due_date: (new Date()).toISOString(),
|
||||||
|
})
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Set Reminders')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .columns.details .column button')
|
||||||
|
.contains('Add a new reminder')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.should('contain', '1 day before Due Date')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.contains('1 day before Due Date')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.reminder-options-popup')
|
||||||
|
.should('not.be.visible')
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows to set a relative reminder when the task already has a start date', () => {
|
||||||
|
TaskReminderFactory.truncate()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
done: false,
|
||||||
|
start_date: (new Date()).toISOString(),
|
||||||
|
})
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Set Reminders')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .columns.details .column button')
|
||||||
|
.contains('Add a new reminder')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.should('contain', '1 day before Start Date')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.contains('1 day before Start Date')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.reminder-options-popup')
|
||||||
|
.should('not.be.visible')
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows to set a custom relative reminder when the task already has a due date', () => {
|
||||||
|
TaskReminderFactory.truncate()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
done: false,
|
||||||
|
due_date: (new Date()).toISOString(),
|
||||||
|
})
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Set Reminders')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .columns.details .column button')
|
||||||
|
.contains('Add a new reminder')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.contains('Custom')
|
||||||
|
.click()
|
||||||
|
cy.get('.reminder-options-popup .card-content .reminder-period input')
|
||||||
|
.first()
|
||||||
|
.type('10')
|
||||||
|
cy.get('.reminder-options-popup .card-content .reminder-period select')
|
||||||
|
.first()
|
||||||
|
.select('days')
|
||||||
|
cy.get('.reminder-options-popup .card-content button')
|
||||||
|
.contains('Confirm')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.reminder-options-popup')
|
||||||
|
.should('not.be.visible')
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allows to set a fixed reminder when the task already has a due date', () => {
|
||||||
|
TaskReminderFactory.truncate()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
done: false,
|
||||||
|
due_date: (new Date()).toISOString(),
|
||||||
|
})
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Set Reminders')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .columns.details .column button')
|
||||||
|
.contains('Add a new reminder')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.reminder-options-popup .card-content')
|
||||||
|
.contains('Date and time')
|
||||||
|
.click()
|
||||||
|
cy.get('.datepicker__quick-select-date')
|
||||||
|
.contains('Tomorrow')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.reminder-options-popup')
|
||||||
|
.should('not.be.visible')
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
})
|
||||||
|
|
||||||
it('Can set a priority for a task', () => {
|
it('Can set a priority for a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -527,13 +671,13 @@ describe('Task', () => {
|
||||||
TaskAttachmentFactory.truncate()
|
TaskAttachmentFactory.truncate()
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"isolatedModules": false,
|
"isolatedModules": false,
|
||||||
"target": "ES2015",
|
"target": "ES2015",
|
||||||
"lib": ["ESNext", "dom"],
|
"lib": ["ESNext", "dom"],
|
||||||
"types": ["cypress"]
|
"types": ["cypress"],
|
||||||
|
"ignoreDeprecations": "5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
|
|
||||||
const testAndAssertFailed = fixture => {
|
const testAndAssertFailed = fixture => {
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
||||||
|
|
||||||
cy.visit('/login')
|
cy.visit('/login')
|
||||||
cy.get('input[id=username]').type(fixture.username)
|
cy.get('input[id=username]').type(fixture.username)
|
||||||
cy.get('input[id=password]').type(fixture.password)
|
cy.get('input[id=password]').type(fixture.password)
|
||||||
cy.get('.button').contains('Login').click()
|
cy.get('.button').contains('Login').click()
|
||||||
|
|
||||||
cy.wait(5000) // It can take waaaayy too long to log the user in
|
cy.wait('@login')
|
||||||
cy.url().should('include', '/')
|
cy.url().should('include', '/')
|
||||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
import {createLists} from '../list/prepareLists'
|
import {createProjects} from '../project/prepareProjects'
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
cy.get('.navbar .username-dropdown-trigger')
|
cy.get('.navbar .username-dropdown-trigger')
|
||||||
|
@ -26,21 +26,21 @@ describe('Log out', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('Should clear the list history after logging the user out', () => {
|
it.skip('Should clear the project history after logging the user out', () => {
|
||||||
const lists = createLists()
|
const projects = createProjects()
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(localStorage.getItem('listHistory')).to.not.eq(null)
|
expect(localStorage.getItem('projectHistory')).to.not.eq(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
logout()
|
logout()
|
||||||
|
|
||||||
cy.wait(1000) // This makes re-loading of the list and associated entities (and the resulting error) visible
|
cy.wait(1000) // This makes re-loading of the project and associated entities (and the resulting error) visible
|
||||||
|
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/login')
|
.should('contain', '/login')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(localStorage.getItem('listHistory')).to.eq(null)
|
expect(localStorage.getItem('projectHistory')).to.eq(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class BucketFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: now.toISOString(),
|
||||||
updated: now.toISOString(),
|
updated: now.toISOString(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class LinkShareFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
hash: faker.random.word(32),
|
hash: faker.random.word(32),
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
sharing_type: 0,
|
sharing_type: 0,
|
||||||
shared_by_id: 1,
|
shared_by_id: 1,
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
import {faker} from '@faker-js/faker'
|
|
||||||
|
|
||||||
export class ListFactory extends Factory {
|
|
||||||
static table = 'lists'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
title: faker.lorem.words(3),
|
|
||||||
owner_id: 1,
|
|
||||||
namespace_id: 1,
|
|
||||||
created: now.toISOString(),
|
|
||||||
updated: now.toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {faker} from '@faker-js/faker'
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
export class NamespaceFactory extends Factory {
|
export class ProjectFactory extends Factory {
|
||||||
static table = 'namespaces'
|
static table = 'projects'
|
||||||
|
|
||||||
static factory() {
|
static factory() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
@ -15,4 +15,4 @@ export class NamespaceFactory extends Factory {
|
||||||
updated: now.toISOString(),
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ export class TaskFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
done: false,
|
done: false,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
index: '{increment}',
|
index: '{increment}',
|
||||||
position: '{increment}',
|
position: '{increment}',
|
||||||
|
|
18
cypress/factories/task_reminders.ts
Normal file
18
cypress/factories/task_reminders.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {Factory} from '../support/factory'
|
||||||
|
|
||||||
|
export class TaskReminderFactory extends Factory {
|
||||||
|
static table = 'task_reminders'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
task_id: 1,
|
||||||
|
reminder: now.toISOString(),
|
||||||
|
created: now.toISOString(),
|
||||||
|
relative_to: '',
|
||||||
|
relative_period: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
|
||||||
export class UserListFactory extends Factory {
|
export class UserProjectFactory extends Factory {
|
||||||
static table = 'users_lists'
|
static table = 'users_projects'
|
||||||
|
|
||||||
static factory() {
|
static factory() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
created: now.toISOString(),
|
created: now.toISOString(),
|
|
@ -4,7 +4,7 @@ import {seed} from './seed'
|
||||||
* A factory makes it easy to seed the database with data.
|
* A factory makes it easy to seed the database with data.
|
||||||
*/
|
*/
|
||||||
export class Factory {
|
export class Factory {
|
||||||
static table = null
|
static table: string | null = null
|
||||||
|
|
||||||
static factory() {
|
static factory() {
|
||||||
return {}
|
return {}
|
||||||
|
|
2
docker/injector.sh
Normal file → Executable file
2
docker/injector.sh
Normal file → Executable file
|
@ -11,5 +11,7 @@ VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
|
||||||
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
|
||||||
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
|
||||||
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
||||||
|
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
|
||||||
|
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
date -uIseconds | xargs echo 'info: started at'
|
date -uIseconds | xargs echo 'info: started at'
|
||||||
|
|
0
docker/ipv6-disable.sh
Normal file → Executable file
0
docker/ipv6-disable.sh
Normal file → Executable file
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
pid /tmp/nginx.pid;
|
pid /tmp/nginx.pid;
|
||||||
worker_processes auto;
|
worker_processes auto;
|
||||||
worker_rlimit_nofile 65535;
|
|
||||||
|
|
||||||
events {
|
events {
|
||||||
multi_accept on;
|
multi_accept on;
|
||||||
|
|
|
@ -30,21 +30,21 @@ A basic service can look like this:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import AbstractService from './abstractService'
|
import AbstractService from './abstractService'
|
||||||
import ListModel from '../models/list'
|
import ProjectModel from '../models/project'
|
||||||
|
|
||||||
export default class ListService extends AbstractService {
|
export default class ProjectService extends AbstractService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
getAll: '/lists',
|
getAll: '/projects',
|
||||||
get: '/lists/{id}',
|
get: '/projects/{id}',
|
||||||
create: '/namespaces/{namespaceID}/lists',
|
create: '/namespaces/{namespaceID}/projects',
|
||||||
update: '/lists/{id}',
|
update: '/projects/{id}',
|
||||||
delete: '/lists/{id}',
|
delete: '/projects/{id}',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modelFactory(data) {
|
modelFactory(data) {
|
||||||
return new ListModel(data)
|
return new ProjectModel(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -132,7 +132,7 @@ import AbstractModel from './abstractModel'
|
||||||
import TaskModel from './task'
|
import TaskModel from './task'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
|
|
||||||
export default class ListModel extends AbstractModel {
|
export default class ProjectModel extends AbstractModel {
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
// The constructor of AbstractModel handles all the default parsing.
|
// The constructor of AbstractModel handles all the default parsing.
|
||||||
|
|
26
env.d.ts
vendored
26
env.d.ts
vendored
|
@ -3,10 +3,32 @@
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
/// <reference types="@histoire/plugin-vue/components" />
|
/// <reference types="@histoire/plugin-vue/components" />
|
||||||
|
|
||||||
|
declare module 'postcss-focus-within/browser' {
|
||||||
|
import focusWithinInit from 'postcss-focus-within/browser'
|
||||||
|
export default focusWithinInit
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'css-has-pseudo/browser' {
|
||||||
|
import cssHasPseudo from 'css-has-pseudo/browser'
|
||||||
|
export default cssHasPseudo
|
||||||
|
}
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_IS_ONLINE: boolean
|
readonly VIKUNJA_API_URL?: string
|
||||||
|
readonly VIKUNJA_HTTP_PORT?: number
|
||||||
|
readonly VIKUNJA_HTTPS_PORT?: number
|
||||||
|
|
||||||
|
readonly VIKUNJA_SENTRY_ENABLED?: boolean
|
||||||
|
readonly VIKUNJA_SENTRY_DSN?: string
|
||||||
|
|
||||||
|
readonly SENTRY_AUTH_TOKEN?: string
|
||||||
|
readonly SENTRY_ORG?: string
|
||||||
|
readonly SENTRY_PROJECT?: string
|
||||||
|
|
||||||
|
readonly VITE_WORKBOX_DEBUG?: boolean
|
||||||
|
readonly VITE_IS_ONLINE: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1664753041,
|
"lastModified": 1685498995,
|
||||||
"narHash": "sha256-0ogaD8PaGHluARFeupofvk1Nq9gpVeZdlFM0Kcwguys=",
|
"narHash": "sha256-rdyjnkq87tJp+T2Bm1OD/9NXKSsh/vLlPeqCc/mm7qs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a62844b302507c7531ad68a86cb7aa54704c9cb4",
|
"rev": "9cfaa8a1a00830d17487cb60a19bb86f96f09b27",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -18,15 +18,20 @@
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<script>
|
<script>
|
||||||
//
|
//
|
||||||
// This variable points the frontend to the api.
|
// This variable points the frontend to the api.
|
||||||
// It has to be the full url, including the last /api/v1 part and port.
|
// It has to be the full url, including the last /api/v1 part and port.
|
||||||
// You can change this if your api is not reachable on the same port as the frontend.
|
// You can change this if your api is not reachable on the same port as the frontend.
|
||||||
window.API_URL = 'http://localhost:3456/api/v1'
|
window.API_URL = 'http://localhost:3456/api/v1'
|
||||||
// Enable error tracking with sentry. If this is set to true, will send anonymized data to
|
// Enable error tracking with sentry. If this is set to true, will send anonymized data to
|
||||||
// our sentry instance to notify us of potential problems.
|
// our sentry instance to notify us of potential problems.
|
||||||
window.SENTRY_ENABLED = false
|
window.SENTRY_ENABLED = false
|
||||||
window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480'
|
window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480'
|
||||||
|
// If enabled, allows the user to nest projects infinitely, instead of the default 2 levels.
|
||||||
|
// This setting might change in the future or be removed completely.
|
||||||
|
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
||||||
|
// Allow changing the logo and other icons based on various occasions throughout the year.
|
||||||
|
window.ALLOW_ICON_CHANGES = true
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
143
package.json
143
package.json
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://vikunja.io/",
|
"homepage": "https://vikunja.io/",
|
||||||
"funding": "https://opencollective.com/vikunja",
|
"funding": "https://opencollective.com/vikunja",
|
||||||
"packageManager": "pnpm@7.29.1",
|
"packageManager": "pnpm@8.6.7",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"todo",
|
"todo",
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -45,102 +45,107 @@
|
||||||
"story:preview": "histoire preview"
|
"story:preview": "histoire preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@github/hotkey": "2.0.1",
|
"@github/hotkey": "2.0.1",
|
||||||
"@infectoone/vue-ganttastic": "2.1.4",
|
"@infectoone/vue-ganttastic": "2.1.4",
|
||||||
"@intlify/unplugin-vue-i18n": "0.9.1",
|
"@intlify/unplugin-vue-i18n": "0.12.2",
|
||||||
"@kyvg/vue3-notification": "2.9.0",
|
"@kyvg/vue3-notification": "2.9.1",
|
||||||
"@sentry/tracing": "7.42.0",
|
"@sentry/tracing": "7.58.1",
|
||||||
"@sentry/vue": "7.42.0",
|
"@sentry/vue": "7.58.1",
|
||||||
"@types/is-touch-device": "1.0.0",
|
"@vueuse/core": "10.2.1",
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"axios": "1.4.0",
|
||||||
"@types/sortablejs": "1.15.0",
|
|
||||||
"@vueuse/core": "9.13.0",
|
|
||||||
"axios": "1.3.4",
|
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
"codemirror": "5.65.12",
|
"codemirror": "5.65.13",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.30.0",
|
||||||
"dayjs": "1.11.7",
|
"dayjs": "1.11.9",
|
||||||
"dompurify": "3.0.1",
|
"dompurify": "3.0.5",
|
||||||
"easymde": "2.18.0",
|
"easymde": "2.18.0",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.31",
|
||||||
"floating-vue": "2.0.0-beta.20",
|
"floating-vue": "2.0.0-beta.24",
|
||||||
"focus-within": "3.0.2",
|
"highlight.js": "11.8.0",
|
||||||
"highlight.js": "11.7.0",
|
|
||||||
"is-touch-device": "1.0.1",
|
"is-touch-device": "1.0.1",
|
||||||
"klona": "2.0.6",
|
"klona": "2.0.6",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"marked": "4.2.12",
|
"marked": "5.1.1",
|
||||||
"pinia": "2.0.33",
|
"pinia": "2.1.4",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"ufo": "1.1.1",
|
"ufo": "1.1.2",
|
||||||
"vue": "3.2.47",
|
"vue": "3.3.4",
|
||||||
"vue-advanced-cropper": "2.8.8",
|
"vue-advanced-cropper": "2.8.8",
|
||||||
"vue-flatpickr-component": "11.0.2",
|
"vue-flatpickr-component": "11.0.3",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
"vue-router": "4.1.6",
|
"vue-router": "4.2.4",
|
||||||
"workbox-precaching": "6.5.4",
|
"workbox-precaching": "7.0.0",
|
||||||
"zhyswan-vuedraggable": "4.1.3"
|
"zhyswan-vuedraggable": "4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@4tw/cypress-drag-drop": "2.2.3",
|
"@4tw/cypress-drag-drop": "2.2.4",
|
||||||
"@cypress/vite-dev-server": "5.0.4",
|
"@cypress/vite-dev-server": "5.0.5",
|
||||||
"@cypress/vue": "5.0.4",
|
"@cypress/vue": "5.0.5",
|
||||||
"@faker-js/faker": "7.6.0",
|
"@faker-js/faker": "8.0.2",
|
||||||
"@histoire/plugin-screenshot": "0.15.8",
|
"@histoire/plugin-screenshot": "0.16.1",
|
||||||
"@histoire/plugin-vue": "0.15.8",
|
"@histoire/plugin-vue": "0.16.1",
|
||||||
"@rushstack/eslint-patch": "1.2.0",
|
"@rushstack/eslint-patch": "1.3.2",
|
||||||
"@types/codemirror": "5.60.7",
|
"@tsconfig/node18": "18.2.0",
|
||||||
"@types/dompurify": "2.4.0",
|
"@types/codemirror": "5.60.8",
|
||||||
|
"@types/dompurify": "3.0.2",
|
||||||
"@types/flexsearch": "0.7.3",
|
"@types/flexsearch": "0.7.3",
|
||||||
"@types/focus-within": "1.0.1",
|
"@types/is-touch-device": "1.0.0",
|
||||||
"@types/lodash.debounce": "4.0.7",
|
"@types/lodash.debounce": "4.0.7",
|
||||||
"@types/marked": "4.0.8",
|
"@types/marked": "5.0.1",
|
||||||
"@types/node": "18.15.0",
|
"@types/node": "18.16.19",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
"@types/sortablejs": "1.15.1",
|
||||||
"@typescript-eslint/parser": "5.54.1",
|
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||||
"@vitejs/plugin-legacy": "4.0.1",
|
"@typescript-eslint/parser": "6.0.0",
|
||||||
"@vitejs/plugin-vue": "4.0.0",
|
"@vitejs/plugin-legacy": "4.1.0",
|
||||||
"@vue/eslint-config-typescript": "11.0.2",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"@vue/test-utils": "2.3.1",
|
"@vue/eslint-config-typescript": "11.0.3",
|
||||||
"@vue/tsconfig": "0.1.3",
|
"@vue/test-utils": "2.4.0",
|
||||||
|
"@vue/tsconfig": "0.4.0",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"browserslist": "4.21.5",
|
"browserslist": "4.21.9",
|
||||||
"caniuse-lite": "1.0.30001460",
|
"caniuse-lite": "1.0.30001515",
|
||||||
"csstype": "3.1.1",
|
"css-has-pseudo": "6.0.0",
|
||||||
"cypress": "12.7.0",
|
"csstype": "3.1.2",
|
||||||
"esbuild": "0.17.11",
|
"cypress": "12.17.1",
|
||||||
"eslint": "8.35.0",
|
"esbuild": "0.18.12",
|
||||||
"eslint-plugin-vue": "9.9.0",
|
"eslint": "8.45.0",
|
||||||
"happy-dom": "8.9.0",
|
"eslint-plugin-vue": "9.15.1",
|
||||||
"histoire": "0.15.8",
|
"happy-dom": "10.3.2",
|
||||||
"netlify-cli": "13.0.1",
|
"histoire": "0.16.2",
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.26",
|
||||||
"postcss-easing-gradients": "3.0.1",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"postcss-easings": "3.0.1",
|
"postcss-easings": "4.0.0",
|
||||||
"postcss-preset-env": "8.0.1",
|
"postcss-focus-within": "8.0.0",
|
||||||
"rollup": "3.19.1",
|
"postcss-preset-env": "9.0.0",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup": "3.26.2",
|
||||||
"sass": "1.58.3",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
|
"sass": "1.63.6",
|
||||||
"start-server-and-test": "2.0.0",
|
"start-server-and-test": "2.0.0",
|
||||||
"typescript": "4.9.5",
|
"typescript": "5.1.6",
|
||||||
"vite": "4.1.4",
|
"vite": "4.4.4",
|
||||||
"vite-plugin-inject-preload": "1.3.1",
|
"vite-plugin-inject-preload": "1.3.1",
|
||||||
"vite-plugin-pwa": "0.14.4",
|
"vite-plugin-pwa": "0.16.4",
|
||||||
|
"vite-plugin-sentry": "1.3.0",
|
||||||
"vite-svg-loader": "4.0.0",
|
"vite-svg-loader": "4.0.0",
|
||||||
"vitest": "0.29.2",
|
"vitest": "0.33.0",
|
||||||
"vue-tsc": "1.2.0",
|
"vue-tsc": "1.8.5",
|
||||||
"wait-on": "7.0.1",
|
"wait-on": "7.0.1",
|
||||||
"workbox-cli": "6.5.4"
|
"workbox-cli": "7.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
patches/flexsearch@0.7.31.patch
Normal file
16
patches/flexsearch@0.7.31.patch
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
diff --git a/index.d.ts b/index.d.ts
|
||||||
|
deleted file mode 100644
|
||||||
|
index 9f39f41073864b83968bdaa242ac4e3c3149685a..0000000000000000000000000000000000000000
|
||||||
|
diff --git a/package.json b/package.json
|
||||||
|
index 8968f5bf8010ff194240591c8b83299f7328e79d..6d84b6f590a841b129ed8b3860cb786df5a185c0 100644
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@ -22,8 +22,6 @@
|
||||||
|
},
|
||||||
|
"main": "dist/flexsearch.bundle.js",
|
||||||
|
"browser": "dist/flexsearch.bundle.js",
|
||||||
|
- "module": "dist/module/index.js",
|
||||||
|
- "types": "./index.d.ts",
|
||||||
|
"preferGlobal": false,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
14360
pnpm-lock.yaml
14360
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["netlify-cli", "happy-dom"],
|
"matchPackageNames": ["happy-dom"],
|
||||||
"extends": ["schedule:weekly"]
|
"extends": ["schedule:weekly"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,9 +29,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchDepTypes": ["devDependencies"],
|
"matchDepTypes": ["devDependencies"],
|
||||||
"automerge": true,
|
"groupName": "dev-dependencies",
|
||||||
"automergeStrategy": "squash",
|
"extends": ["schedule:daily"]
|
||||||
"automergeType": "pr"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -33,9 +33,9 @@ const promiseExec = cmd => {
|
||||||
}
|
}
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
let stdout = await promiseExec(`./node_modules/.bin/netlify link --id ${siteId}`)
|
let stdout = await promiseExec(`/home/node/docker-netlify-cli/node_modules/.bin/netlify link --id ${siteId}`)
|
||||||
console.log(stdout)
|
console.log(stdout)
|
||||||
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
|
stdout = await promiseExec(`/home/node/docker-netlify-cli/node_modules/.bin/netlify deploy --alias ${alias}`)
|
||||||
console.log(stdout)
|
console.log(stdout)
|
||||||
|
|
||||||
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
|
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
57af69409e66bc87f4f2fc5822dd8d3c2eb47c601f81af1ac4a56f3e2d80837b1a2de06f4ff57695ec379b7c15b881e3 ./scripts/deploy-preview-netlify.mjs
|
4a7c1293c7b12e9ab476cdf35251a407c6a1cd005d22c06df994222cccfb25cde5f47d15866a098c9d739778fee4dc19 ./scripts/deploy-preview-netlify.mjs
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
|
<AddToHomeScreen/>
|
||||||
<UpdateNotification/>
|
<UpdateNotification/>
|
||||||
<Notification/>
|
<Notification/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
@ -43,6 +44,7 @@ import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
import {useColorScheme} from '@/composables/useColorScheme'
|
import {useColorScheme} from '@/composables/useColorScheme'
|
||||||
import {useBodyClass} from '@/composables/useBodyClass'
|
import {useBodyClass} from '@/composables/useBodyClass'
|
||||||
|
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
@ -92,7 +94,7 @@ watch(userEmailConfirm, (userEmailConfirm) => {
|
||||||
router.push({name: 'user.login'})
|
router.push({name: 'user.login'})
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
setLanguage()
|
setLanguage(authStore.settings.language)
|
||||||
useColorScheme()
|
useColorScheme()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Binary file not shown.
4
src/assets/checkbox.svg
Normal file
4
src/assets/checkbox.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 18 18" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z" stroke-dasharray="60"></path>
|
||||||
|
<polyline points="1 9 7 14 15 4" stroke-dasharray="22" stroke-dashoffset="66"></polyline>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 420 B |
Binary file not shown.
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 313 KiB |
|
@ -63,7 +63,7 @@ import {unrefElement} from '@vueuse/core'
|
||||||
import {ref, type HTMLAttributes} from 'vue'
|
import {ref, type HTMLAttributes} from 'vue'
|
||||||
import type {RouteLocationRaw} from 'vue-router'
|
import type {RouteLocationRaw} from 'vue-router'
|
||||||
|
|
||||||
export interface BaseButtonProps extends HTMLAttributes {
|
export interface BaseButtonProps extends /* @vue-ignore */ HTMLAttributes {
|
||||||
type?: BaseButtonTypes
|
type?: BaseButtonTypes
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
to?: RouteLocationRaw
|
to?: RouteLocationRaw
|
||||||
|
|
54
src/components/base/BaseCheckbox.vue
Normal file
54
src/components/base/BaseCheckbox.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<div class="base-checkbox" v-cy="'checkbox'">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:id="checkboxId"
|
||||||
|
class="is-sr-only"
|
||||||
|
:checked="modelValue"
|
||||||
|
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
|
||||||
|
:disabled="disabled || undefined"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot name="label" :checkboxId="checkboxId">
|
||||||
|
<label :for="checkboxId" class="base-checkbox__label">
|
||||||
|
<slot/>
|
||||||
|
</label>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const checkboxId = ref(`fancycheckbox_${createRandomID()}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.base-checkbox__label {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-checkbox:has(input:disabled) .base-checkbox__label {
|
||||||
|
cursor:not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,7 +32,7 @@ import {computed, ref} from 'vue'
|
||||||
import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor'
|
import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/** Wheather the Expandable is open or not */
|
/** Whether the Expandable is open or not */
|
||||||
open: {
|
open: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
11
src/components/date/datemathHelp.story.vue
Normal file
11
src/components/date/datemathHelp.story.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import datemathHelp from './datemathHelp.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story>
|
||||||
|
<Variant title="Default">
|
||||||
|
<datemathHelp />
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<card
|
<card
|
||||||
class="has-no-shadow how-it-works-modal"
|
class="has-no-shadow how-it-works-modal"
|
||||||
:title="$t('input.datemathHelp.title')">
|
:title="$t('input.datemathHelp.title')"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ $t('input.datemathHelp.intro') }}
|
{{ $t('input.datemathHelp.intro') }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -27,11 +28,11 @@
|
||||||
</p>
|
</p>
|
||||||
<p>{{ $t('misc.forExample') }}</p>
|
<p>{{ $t('misc.forExample') }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>+1d</code>{{ $t('input.datemathHelp.add1Day') }}</li>
|
<li><code>+1d</code> {{ $t('input.datemathHelp.add1Day') }}</li>
|
||||||
<li><code>-1d</code>{{ $t('input.datemathHelp.minus1Day') }}</li>
|
<li><code>-1d</code> {{ $t('input.datemathHelp.minus1Day') }}</li>
|
||||||
<li><code>/d</code>{{ $t('input.datemathHelp.roundDay') }}</li>
|
<li><code>/d</code> {{ $t('input.datemathHelp.roundDay') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>{{ $t('input.datemathHelp.supportedUnits') }}</p>
|
<h3>{{ $t('input.datemathHelp.supportedUnits') }}</h3>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -69,7 +70,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>{{ $t('input.datemathHelp.someExamples') }}</p>
|
<h3>{{ $t('input.datemathHelp.someExamples') }}</h3>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
<td><code>{{ exampleDate }}||+1M/d</code></td>
|
<td><code>{{ exampleDate }}||+1M/d</code></td>
|
||||||
<td>
|
<td>
|
||||||
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
|
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
|
||||||
<code>{{ exampleDate }}</code>
|
<strong>{{ exampleDate }}</strong>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -110,13 +111,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {formatDate} from '@/helpers/time/formatDate'
|
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
const exampleDate = formatDate(new Date(), 'yyyy-MM-dd')
|
const exampleDate = formatDateShort(new Date())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
// FIXME: Remove style overwrites
|
||||||
.how-it-works-modal {
|
.how-it-works-modal {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
80
src/components/home/AddToHomeScreen.vue
Normal file
80
src/components/home/AddToHomeScreen.vue
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowMessage"
|
||||||
|
class="add-to-home-screen"
|
||||||
|
:class="{'has-update-available': hasUpdateAvailable}"
|
||||||
|
>
|
||||||
|
<icon icon="arrow-up-from-bracket" class="add-icon"/>
|
||||||
|
<p>
|
||||||
|
{{ $t('home.addToHomeScreen') }}
|
||||||
|
</p>
|
||||||
|
<BaseButton @click="() => hideMessage = true" class="hide-button">
|
||||||
|
<icon icon="x"/>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import {useLocalStorage} from '@vueuse/core'
|
||||||
|
import {computed} from 'vue'
|
||||||
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
|
const baseStore = useBaseStore()
|
||||||
|
|
||||||
|
const hideMessage = useLocalStorage('hideAddToHomeScreenMessage', false)
|
||||||
|
const hasUpdateAvailable = computed(() => baseStore.updateAvailable)
|
||||||
|
|
||||||
|
const shouldShowMessage = computed(() => {
|
||||||
|
if (hideMessage.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia('(display-mode: standalone)').matches) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.add-to-home-screen {
|
||||||
|
position: fixed;
|
||||||
|
// FIXME: We should prevent usage of z-index or
|
||||||
|
// at least define it centrally
|
||||||
|
// the highest z-index of a modal is .hint-modal with 4500
|
||||||
|
z-index: 5000;
|
||||||
|
bottom: 1rem;
|
||||||
|
inset-inline: 1rem;
|
||||||
|
max-width: max-content;
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
background: var(--grey-900);
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: .9rem;
|
||||||
|
color: var(--grey-200);
|
||||||
|
|
||||||
|
@media screen and (min-width: $tablet) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-update-available {
|
||||||
|
bottom: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-button {
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,9 +4,12 @@ import { useNow } from '@vueuse/core'
|
||||||
|
|
||||||
import LogoFull from '@/assets/logo-full.svg?component'
|
import LogoFull from '@/assets/logo-full.svg?component'
|
||||||
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
|
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
|
||||||
|
import {MILLISECONDS_A_HOUR} from '@/constants/date'
|
||||||
|
|
||||||
const now = useNow()
|
const now = useNow({
|
||||||
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
interval: MILLISECONDS_A_HOUR,
|
||||||
|
})
|
||||||
|
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
109
src/components/home/ProjectsNavigation.vue
Normal file
109
src/components/home/ProjectsNavigation.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<draggable
|
||||||
|
v-model="availableProjects"
|
||||||
|
animation="100"
|
||||||
|
ghostClass="ghost"
|
||||||
|
group="projects"
|
||||||
|
@start="() => drag = true"
|
||||||
|
@end="saveProjectPosition"
|
||||||
|
handle=".handle"
|
||||||
|
tag="menu"
|
||||||
|
item-key="id"
|
||||||
|
:disabled="!canEditOrder"
|
||||||
|
filter=".drag-disabled"
|
||||||
|
:component-data="{
|
||||||
|
type: 'transition-group',
|
||||||
|
name: !drag ? 'flip-list' : null,
|
||||||
|
class: [
|
||||||
|
'menu-list can-be-hidden',
|
||||||
|
{ 'dragging-disabled': !canEditOrder }
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #item="{element: project}">
|
||||||
|
<ProjectsNavigationItem
|
||||||
|
:class="{'drag-disabled': project.id < 0}"
|
||||||
|
:project="project"
|
||||||
|
:is-loading="projectUpdating[project.id]"
|
||||||
|
:can-collapse="canCollapse"
|
||||||
|
:level="level"
|
||||||
|
:data-project-id="project.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, watch} from 'vue'
|
||||||
|
import draggable from 'zhyswan-vuedraggable'
|
||||||
|
import type {SortableEvent} from 'sortablejs'
|
||||||
|
|
||||||
|
import ProjectsNavigationItem from '@/components/home/ProjectsNavigationItem.vue'
|
||||||
|
|
||||||
|
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: IProject[],
|
||||||
|
canEditOrder: boolean,
|
||||||
|
canCollapse?: boolean,
|
||||||
|
level?: number,
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', projects: IProject[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const drag = ref(false)
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
|
// Vue draggable will modify the projects list as it changes their position which will not work on a prop.
|
||||||
|
// Hence, we'll clone the prop and work on the clone.
|
||||||
|
const availableProjects = ref<IProject[]>([])
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
projects => {
|
||||||
|
availableProjects.value = projects || []
|
||||||
|
},
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectUpdating = ref<{ [id: IProject['id']]: boolean }>({})
|
||||||
|
|
||||||
|
async function saveProjectPosition(e: SortableEvent) {
|
||||||
|
if (!e.newIndex && e.newIndex !== 0) return
|
||||||
|
|
||||||
|
const projectsActive = availableProjects.value
|
||||||
|
// If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
|
||||||
|
// array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
|
||||||
|
// To work around that we're explicitly checking that case here and decrease the index.
|
||||||
|
const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
|
||||||
|
|
||||||
|
const projectId = parseInt(e.item.dataset.projectId)
|
||||||
|
const project = projectStore.projects[projectId]
|
||||||
|
|
||||||
|
const parentProjectId = e.to.parentNode.dataset.projectId ? parseInt(e.to.parentNode.dataset.projectId) : 0
|
||||||
|
const projectBefore = projectsActive[newIndex - 1] ?? null
|
||||||
|
const projectAfter = projectsActive[newIndex + 1] ?? null
|
||||||
|
projectUpdating.value[project.id] = true
|
||||||
|
|
||||||
|
const position = calculateItemPosition(
|
||||||
|
projectBefore !== null ? projectBefore.position : null,
|
||||||
|
projectAfter !== null ? projectAfter.position : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// create a copy of the project in order to not violate pinia manipulation
|
||||||
|
await projectStore.updateProject({
|
||||||
|
...project,
|
||||||
|
position,
|
||||||
|
parentProjectId,
|
||||||
|
})
|
||||||
|
emit('update:modelValue', availableProjects.value)
|
||||||
|
} finally {
|
||||||
|
projectUpdating.value[project.id] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
178
src/components/home/ProjectsNavigationItem.vue
Normal file
178
src/components/home/ProjectsNavigationItem.vue
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
class="list-menu loader-container is-loading-small"
|
||||||
|
:class="{'is-loading': isLoading}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<BaseButton
|
||||||
|
v-if="canCollapse && childProjects?.length > 0"
|
||||||
|
@click="childProjectsOpen = !childProjectsOpen"
|
||||||
|
class="collapse-project-button"
|
||||||
|
>
|
||||||
|
<icon icon="chevron-down" :class="{ 'project-is-collapsed': !childProjectsOpen }"/>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
:to="{ name: 'project.index', params: { projectId: project.id} }"
|
||||||
|
class="list-menu-link"
|
||||||
|
:class="{'router-link-exact-active': currentProject?.id === project.id}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!canCollapse || childProjects?.length === 0"
|
||||||
|
class="collapse-project-button-placeholder"
|
||||||
|
></span>
|
||||||
|
<div class="color-bubble-handle-wrapper" :class="{'is-draggable': project.id > 0}">
|
||||||
|
<ColorBubble
|
||||||
|
v-if="project.hexColor !== ''"
|
||||||
|
:color="project.hexColor"
|
||||||
|
/>
|
||||||
|
<span v-else-if="project.id < -1" class="saved-filter-icon icon menu-item-icon">
|
||||||
|
<icon icon="filter"/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="project.id > 0"
|
||||||
|
class="icon menu-item-icon handle lines-handle"
|
||||||
|
:class="{'has-color-bubble': project.hexColor !== ''}"
|
||||||
|
>
|
||||||
|
<icon icon="grip-lines"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="project-menu-title">{{ getProjectTitle(project) }}</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
v-if="project.id > 0"
|
||||||
|
class="favorite"
|
||||||
|
:class="{'is-favorite': project.isFavorite}"
|
||||||
|
@click="projectStore.toggleProjectFavorite(project)"
|
||||||
|
>
|
||||||
|
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
|
||||||
|
</BaseButton>
|
||||||
|
<ProjectSettingsDropdown
|
||||||
|
v-if="project.id > 0"
|
||||||
|
class="menu-list-dropdown"
|
||||||
|
:project="project"
|
||||||
|
:level="level"
|
||||||
|
>
|
||||||
|
<template #trigger="{toggleOpen}">
|
||||||
|
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||||
|
<icon icon="ellipsis-h" class="icon"/>
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
</ProjectSettingsDropdown>
|
||||||
|
<span class="list-setting-spacer" v-else></span>
|
||||||
|
</div>
|
||||||
|
<ProjectsNavigation
|
||||||
|
v-if="canNestDeeper && childProjectsOpen && canCollapse"
|
||||||
|
:model-value="childProjects"
|
||||||
|
:can-edit-order="true"
|
||||||
|
:can-collapse="canCollapse"
|
||||||
|
:level="level + 1"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref} from 'vue'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||||
|
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||||
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
|
||||||
|
import {canNestProjectDeeper} from '@/helpers/canNestProjectDeeper'
|
||||||
|
|
||||||
|
const {
|
||||||
|
project,
|
||||||
|
isLoading,
|
||||||
|
canCollapse,
|
||||||
|
level = 0,
|
||||||
|
} = defineProps<{
|
||||||
|
project: IProject,
|
||||||
|
isLoading?: boolean,
|
||||||
|
canCollapse?: boolean,
|
||||||
|
level?: number,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
const baseStore = useBaseStore()
|
||||||
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
|
|
||||||
|
const childProjectsOpen = ref(true)
|
||||||
|
|
||||||
|
const childProjects = computed(() => {
|
||||||
|
if (!canNestDeeper.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectStore.getChildProjects(project.id)
|
||||||
|
.filter(p => !p.isArchived)
|
||||||
|
.sort((a, b) => a.position - b.position)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canNestDeeper = computed(() => canNestProjectDeeper(level))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-setting-spacer {
|
||||||
|
width: 5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-is-collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-favorite {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu:hover > div > .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu:hover > div > a > .color-bubble-handle-wrapper.is-draggable > {
|
||||||
|
.saved-filter-icon,
|
||||||
|
.color-bubble {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-bubble-handle-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-right: .25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.color-bubble, .icon {
|
||||||
|
transition: all $transition;
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-menu-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-filter-icon {
|
||||||
|
color: var(--grey-300) !important;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,67 +1,51 @@
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
|
||||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
class="navbar d-print-none">
|
||||||
aria-label="main navigation"
|
<router-link :to="{ name: 'home' }" class="logo-link">
|
||||||
class="navbar d-print-none"
|
<Logo width="164" height="48" />
|
||||||
>
|
|
||||||
<router-link :to="{name: 'home'}" class="logo-link">
|
|
||||||
<Logo width="164" height="48"/>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<MenuButton class="menu-button"/>
|
<MenuButton class="menu-button" />
|
||||||
|
|
||||||
<div
|
<div v-if="currentProject?.id" class="project-title-wrapper">
|
||||||
v-if="currentList.id"
|
<h1 class="project-title">
|
||||||
class="list-title-wrapper"
|
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
|
||||||
>
|
</h1>
|
||||||
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1>
|
|
||||||
|
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
|
||||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
|
<icon icon="circle-info" />
|
||||||
<icon icon="circle-info"/>
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<list-settings-dropdown
|
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
|
||||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
class="project-title-dropdown" :project="currentProject">
|
||||||
class="list-title-dropdown"
|
<template #trigger="{ toggleOpen }">
|
||||||
:list="currentList"
|
<BaseButton class="project-title-button" @click="toggleOpen">
|
||||||
>
|
<icon icon="ellipsis-h" class="icon" />
|
||||||
<template #trigger="{toggleOpen}">
|
|
||||||
<BaseButton class="list-title-button" @click="toggleOpen">
|
|
||||||
<icon icon="ellipsis-h" class="icon"/>
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
</list-settings-dropdown>
|
</project-settings-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<BaseButton
|
<BaseButton @click="openQuickActions" class="trigger-button" v-shortcut="'Control+k'"
|
||||||
@click="openQuickActions"
|
:title="$t('keyboardShortcuts.quickSearch')">
|
||||||
class="trigger-button"
|
<icon icon="search" />
|
||||||
v-shortcut="'Control+k'"
|
|
||||||
:title="$t('keyboardShortcuts.quickSearch')"
|
|
||||||
>
|
|
||||||
<icon icon="search"/>
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<dropdown>
|
<dropdown>
|
||||||
<template #trigger="{toggleOpen, open}">
|
<template #trigger="{ toggleOpen, open }">
|
||||||
<BaseButton
|
<BaseButton class="username-dropdown-trigger" @click="toggleOpen" variant="secondary" :shadow="false">
|
||||||
class="username-dropdown-trigger"
|
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40" />
|
||||||
@click="toggleOpen"
|
|
||||||
variant="secondary"
|
|
||||||
:shadow="false"
|
|
||||||
>
|
|
||||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
|
||||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||||
<span class="icon is-small" :style="{
|
<span class="icon is-small" :style="{
|
||||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||||
}">
|
}">
|
||||||
<icon icon="chevron-down"/>
|
<icon icon="chevron-down" />
|
||||||
</span>
|
</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<dropdown-item :to="{name: 'user.settings'}">
|
<dropdown-item :to="{ name: 'user.settings' }">
|
||||||
{{ $t('user.settings.title') }}
|
{{ $t('user.settings.title') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||||
|
@ -73,7 +57,7 @@
|
||||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||||
{{ $t('keyboardShortcuts.title') }}
|
{{ $t('keyboardShortcuts.title') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item :to="{name: 'about'}">
|
<dropdown-item :to="{ name: 'about' }">
|
||||||
{{ $t('about.title') }}
|
{{ $t('about.title') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item @click="authStore.logout()">
|
<dropdown-item @click="authStore.logout()">
|
||||||
|
@ -85,11 +69,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import {RIGHTS as Rights} from '@/constants/rights'
|
import { RIGHTS as Rights } from '@/constants/rights'
|
||||||
|
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||||
import Notifications from '@/components/notifications/notifications.vue'
|
import Notifications from '@/components/notifications/notifications.vue'
|
||||||
|
@ -97,16 +81,16 @@ import Logo from '@/components/home/Logo.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import MenuButton from '@/components/home/MenuButton.vue'
|
import MenuButton from '@/components/home/MenuButton.vue'
|
||||||
|
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import { useBaseStore } from '@/stores/base'
|
||||||
import {useConfigStore} from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
|
const canWriteCurrentProject = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
|
||||||
const menuActive = computed(() => baseStore.menuActive)
|
const menuActive = computed(() => baseStore.menuActive)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
@ -166,7 +150,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||||
|
|
||||||
.logo-link {
|
.logo-link {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media screen and (min-width: $tablet) {
|
@media screen and (min-width: $tablet) {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -185,12 +169,12 @@ $user-dropdown-width-mobile: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title-wrapper {
|
.project-title-wrapper {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
// this makes the truncated text of the list title work
|
// this makes the truncated text of the project title work
|
||||||
// inside the flexbox parent
|
// inside the flexbox parent
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
@ -199,7 +183,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title {
|
.project-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
// We need the following for overflowing ellipsis to work
|
// We need the following for overflowing ellipsis to work
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -211,15 +195,15 @@ $user-dropdown-width-mobile: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title-dropdown {
|
.project-title-dropdown {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|
||||||
.list-title-button {
|
.project-title-button {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title-button {
|
.project-title-button {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
min-width: var(--navbar-button-min-width);
|
min-width: var(--navbar-button-min-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -235,7 +219,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
> * {
|
>* {
|
||||||
min-width: var(--navbar-button-min-width);
|
min-width: var(--navbar-button-min-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
const updateAvailable = ref(false)
|
const baseStore = useBaseStore()
|
||||||
|
|
||||||
|
const updateAvailable = computed(() => baseStore.updateAvailable)
|
||||||
const registration = ref(null)
|
const registration = ref(null)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
|
|
||||||
|
@ -31,11 +34,11 @@ navigator?.serviceWorker?.addEventListener(
|
||||||
function showRefreshUI(e: Event) {
|
function showRefreshUI(e: Event) {
|
||||||
console.log('recieved refresh event', e)
|
console.log('recieved refresh event', e)
|
||||||
registration.value = e.detail
|
registration.value = e.detail
|
||||||
updateAvailable.value = true
|
baseStore.setUpdateAvailable(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshApp() {
|
function refreshApp() {
|
||||||
updateAvailable.value = false
|
baseStore.setUpdateAvailable(false)
|
||||||
if (!registration.value || !registration.value.waiting) {
|
if (!registration.value || !registration.value.waiting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -60,12 +63,11 @@ function refreshApp() {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: .5rem;
|
padding: .5rem .5rem .5rem 1rem;
|
||||||
background: $warning;
|
background: $warning;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
color: var(--grey-900);
|
color: hsl(220.9, 39.3%, 11%); // color copied to avoid it changing in dark mode
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-notification__message {
|
.update-notification__message {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<quick-actions/>
|
<quick-actions/>
|
||||||
|
|
||||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
|
||||||
<component :is="Component"/>
|
<component :is="Component"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
@ -69,6 +69,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useLabelStore} from '@/stores/labels'
|
import {useLabelStore} from '@/stores/labels'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||||
|
@ -87,26 +88,25 @@ function showKeyboardShortcuts() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// FIXME: this is really error prone
|
// FIXME: this is really error prone
|
||||||
// Reset the current list highlight in menu if the current route is not list related.
|
// Reset the current project highlight in menu if the current route is not project related.
|
||||||
watch(() => route.name as string, (routeName) => {
|
watch(() => route.name as string, (routeName) => {
|
||||||
if (
|
if (
|
||||||
routeName &&
|
routeName &&
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
'home',
|
'home',
|
||||||
'namespace.edit',
|
|
||||||
'teams.index',
|
'teams.index',
|
||||||
'teams.edit',
|
'teams.edit',
|
||||||
'tasks.range',
|
'tasks.range',
|
||||||
'labels.index',
|
'labels.index',
|
||||||
'migrate.start',
|
'migrate.start',
|
||||||
'migrate.wunderlist',
|
'migrate.wunderlist',
|
||||||
'namespaces.index',
|
'projects.index',
|
||||||
].includes(routeName) ||
|
].includes(routeName) ||
|
||||||
routeName.startsWith('user.settings')
|
routeName.startsWith('user.settings')
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
baseStore.handleSetCurrentList({list: null})
|
baseStore.handleSetCurrentProject({project: null})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -116,6 +116,9 @@ useRenewTokenOnFocus()
|
||||||
|
|
||||||
const labelStore = useLabelStore()
|
const labelStore = useLabelStore()
|
||||||
labelStore.loadAllLabels()
|
labelStore.loadAllLabels()
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
projectStore.loadProjects()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
<Logo class="logo" v-if="logoVisible"/>
|
<Logo class="logo" v-if="logoVisible"/>
|
||||||
<h1
|
<h1
|
||||||
:class="{'m-0': !logoVisible}"
|
:class="{'m-0': !logoVisible}"
|
||||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
:style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
|
||||||
class="title">
|
class="title">
|
||||||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
{{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="box has-text-left view">
|
<div class="box has-text-left view">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
@ -31,7 +31,7 @@ import Logo from '@/components/home/Logo.vue'
|
||||||
import PoweredByLink from './PoweredByLink.vue'
|
import PoweredByLink from './PoweredByLink.vue'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
const logoVisible = computed(() => baseStore.logoVisible)
|
const logoVisible = computed(() => baseStore.logoVisible)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<aside :class="{'is-active': menuActive}" class="namespace-container">
|
<aside :class="{'is-active': baseStore.menuActive}" class="menu-container">
|
||||||
<nav class="menu top-menu">
|
<nav class="menu top-menu">
|
||||||
<router-link :to="{name: 'home'}" class="logo">
|
<router-link :to="{name: 'home'}" class="logo">
|
||||||
<Logo width="164" height="48"/>
|
<Logo width="164" height="48"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<ul class="menu-list">
|
<menu class="menu-list other-menu-items">
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
||||||
<span class="menu-item-icon icon">
|
<span class="menu-item-icon icon">
|
||||||
|
@ -22,11 +22,11 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
|
<router-link :to="{ name: 'projects.index'}" v-shortcut="'g p'">
|
||||||
<span class="menu-item-icon icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon icon="layer-group"/>
|
<icon icon="layer-group"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('namespace.title') }}
|
{{ $t('project.projects') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -45,238 +45,55 @@
|
||||||
{{ $t('team.title') }}
|
{{ $t('team.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</menu>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
<Loading
|
||||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
v-if="projectStore.isLoading"
|
||||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
variant="small"
|
||||||
<BaseButton
|
/>
|
||||||
@click="toggleLists(n.id)"
|
<template v-else>
|
||||||
class="menu-label"
|
<nav class="menu" v-if="favoriteProjects">
|
||||||
v-tooltip="namespaceTitles[nk]"
|
<ProjectsNavigation
|
||||||
>
|
:model-value="favoriteProjects"
|
||||||
<ColorBubble
|
:can-edit-order="false"
|
||||||
v-if="n.hexColor !== ''"
|
:can-collapse="false"
|
||||||
:color="n.hexColor"
|
/>
|
||||||
class="mr-1"
|
</nav>
|
||||||
/>
|
|
||||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
<nav class="menu">
|
||||||
<div
|
<ProjectsNavigation
|
||||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
:model-value="projects"
|
||||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
:can-edit-order="true"
|
||||||
>
|
:can-collapse="true"
|
||||||
<icon icon="chevron-down"/>
|
:level="1"
|
||||||
</div>
|
/>
|
||||||
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
</nav>
|
||||||
({{ namespaceListsCount[nk] }})
|
</template>
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
|
||||||
triggered by the change needs to have access to the current namespace
|
|
||||||
-->
|
|
||||||
<draggable
|
|
||||||
v-if="listsVisible[n.id] ?? true"
|
|
||||||
v-bind="dragOptions"
|
|
||||||
:modelValue="activeLists[nk]"
|
|
||||||
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
|
||||||
group="namespace-lists"
|
|
||||||
@start="() => drag = true"
|
|
||||||
@end="saveListPosition"
|
|
||||||
handle=".handle"
|
|
||||||
:disabled="n.id < 0 || undefined"
|
|
||||||
tag="ul"
|
|
||||||
item-key="id"
|
|
||||||
:data-namespace-id="n.id"
|
|
||||||
:data-namespace-index="nk"
|
|
||||||
:component-data="{
|
|
||||||
type: 'transition-group',
|
|
||||||
name: !drag ? 'flip-list' : null,
|
|
||||||
class: [
|
|
||||||
'menu-list can-be-hidden',
|
|
||||||
{ 'dragging-disabled': n.id < 0 }
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #item="{element: l}">
|
|
||||||
<li
|
|
||||||
class="list-menu loader-container is-loading-small"
|
|
||||||
:class="{'is-loading': listUpdating[l.id]}"
|
|
||||||
>
|
|
||||||
<BaseButton
|
|
||||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
|
||||||
class="list-menu-link"
|
|
||||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
|
||||||
>
|
|
||||||
<span class="icon menu-item-icon handle">
|
|
||||||
<icon icon="grip-lines"/>
|
|
||||||
</span>
|
|
||||||
<ColorBubble
|
|
||||||
v-if="l.hexColor !== ''"
|
|
||||||
:color="l.hexColor"
|
|
||||||
class="mr-1"
|
|
||||||
/>
|
|
||||||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-if="l.id > 0"
|
|
||||||
class="favorite"
|
|
||||||
:class="{'is-favorite': l.isFavorite}"
|
|
||||||
@click="listStore.toggleListFavorite(l)"
|
|
||||||
>
|
|
||||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
|
||||||
</BaseButton>
|
|
||||||
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0">
|
|
||||||
<template #trigger="{toggleOpen}">
|
|
||||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
|
||||||
<icon icon="ellipsis-h" class="icon"/>
|
|
||||||
</BaseButton>
|
|
||||||
</template>
|
|
||||||
</list-settings-dropdown>
|
|
||||||
<span class="list-setting-spacer" v-else></span>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
</template>
|
|
||||||
</nav>
|
|
||||||
<PoweredByLink/>
|
<PoweredByLink/>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onBeforeMount} from 'vue'
|
import {computed} from 'vue'
|
||||||
import draggable from 'zhyswan-vuedraggable'
|
|
||||||
import type {SortableEvent} from 'sortablejs'
|
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
|
||||||
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
|
||||||
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||||
import Logo from '@/components/home/Logo.vue'
|
import Logo from '@/components/home/Logo.vue'
|
||||||
|
import Loading from '@/components/misc/loading.vue'
|
||||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
|
||||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
|
||||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
|
||||||
|
|
||||||
const drag = ref(false)
|
|
||||||
const dragOptions = {
|
|
||||||
animation: 100,
|
|
||||||
ghostClass: 'ghost',
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const namespaceStore = useNamespaceStore()
|
const projectStore = useProjectStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
|
||||||
const menuActive = computed(() => baseStore.menuActive)
|
|
||||||
const loading = computed(() => namespaceStore.isLoading)
|
|
||||||
|
|
||||||
|
const projects = computed(() => projectStore.notArchivedRootProjects)
|
||||||
const namespaces = computed(() => {
|
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
||||||
return namespaceStore.namespaces.filter(n => !n.isArchived)
|
|
||||||
})
|
|
||||||
const activeLists = computed(() => {
|
|
||||||
return namespaces.value.map(({lists}) => {
|
|
||||||
return lists?.filter(item => {
|
|
||||||
return typeof item !== 'undefined' && !item.isArchived
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const namespaceTitles = computed(() => {
|
|
||||||
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
|
||||||
})
|
|
||||||
|
|
||||||
const namespaceListsCount = computed(() => {
|
|
||||||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const listStore = useListStore()
|
|
||||||
|
|
||||||
function toggleLists(namespaceId: INamespace['id']) {
|
|
||||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
|
||||||
}
|
|
||||||
|
|
||||||
const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
|
|
||||||
// FIXME: async action will be unfinished when component mounts
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
const namespaces = await namespaceStore.loadNamespaces()
|
|
||||||
namespaces.forEach(n => {
|
|
||||||
if (typeof listsVisible.value[n.id] === 'undefined') {
|
|
||||||
listsVisible.value[n.id] = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
|
|
||||||
// This is a bit hacky: since we do have to filter out the archived items from the list
|
|
||||||
// for vue draggable updating it is not as simple as replacing it.
|
|
||||||
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
|
|
||||||
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
|
|
||||||
// later when showing them anyway, and it makes the merging happening here a lot easier.
|
|
||||||
const lists = [
|
|
||||||
...activeLists,
|
|
||||||
...namespace.lists.filter(l => l.isArchived),
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaceStore.setNamespaceById({
|
|
||||||
...namespace,
|
|
||||||
lists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
|
||||||
|
|
||||||
async function saveListPosition(e: SortableEvent) {
|
|
||||||
if (!e.newIndex && e.newIndex !== 0) return
|
|
||||||
|
|
||||||
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
|
|
||||||
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
|
|
||||||
|
|
||||||
const listsActive = activeLists.value[newNamespaceIndex]
|
|
||||||
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
|
|
||||||
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
|
|
||||||
// To work around that we're explicitly checking that case here and decrease the index.
|
|
||||||
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
|
|
||||||
|
|
||||||
const list = listsActive[newIndex]
|
|
||||||
const listBefore = listsActive[newIndex - 1] ?? null
|
|
||||||
const listAfter = listsActive[newIndex + 1] ?? null
|
|
||||||
listUpdating.value[list.id] = true
|
|
||||||
|
|
||||||
const position = calculateItemPosition(
|
|
||||||
listBefore !== null ? listBefore.position : null,
|
|
||||||
listAfter !== null ? listAfter.position : null,
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// create a copy of the list in order to not violate pinia manipulation
|
|
||||||
await listStore.updateList({
|
|
||||||
...list,
|
|
||||||
position,
|
|
||||||
namespaceId,
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
listUpdating.value[list.id] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$navbar-padding: 2rem;
|
|
||||||
$vikunja-nav-background: var(--site-background);
|
|
||||||
$vikunja-nav-color: var(--grey-700);
|
|
||||||
$vikunja-nav-selected-width: 0.4rem;
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
@ -289,10 +106,10 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.namespace-container {
|
.menu-container {
|
||||||
background: $vikunja-nav-background;
|
background: var(--site-background);
|
||||||
color: $vikunja-nav-color;
|
color: $vikunja-nav-color;
|
||||||
padding: 0 0 1rem;
|
padding: 1rem 0;
|
||||||
transition: transform $transition-duration ease-in;
|
transition: transform $transition-duration ease-in;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: $navbar-height;
|
top: $navbar-height;
|
||||||
|
@ -314,252 +131,24 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are general menu styles
|
.top-menu .menu-list {
|
||||||
// should be in own components
|
li {
|
||||||
.menu {
|
|
||||||
.menu-label,
|
|
||||||
.menu-list .list-menu-link,
|
|
||||||
.menu-list a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.color-bubble {
|
|
||||||
height: 12px;
|
|
||||||
flex: 0 0 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-list {
|
|
||||||
li {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-list-dropdown {
|
|
||||||
opacity: 1;
|
|
||||||
transition: $transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(hover: hover) and (pointer: fine) {
|
|
||||||
.menu-list-dropdown {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .menu-list-dropdown {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item-icon {
|
|
||||||
color: var(--grey-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-list-dropdown-trigger {
|
|
||||||
display: flex;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-list-move {
|
|
||||||
transition: transform $transition-duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost {
|
|
||||||
background: var(--grey-200);
|
|
||||||
|
|
||||||
* {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-menu-link,
|
|
||||||
li > a {
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
border-radius: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
border-left: $vikunja-nav-selected-width solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.router-link-exact-active {
|
|
||||||
color: var(--primary);
|
|
||||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
height: 1rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.router-link-exact-active .icon:not(.handle) {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.handle {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity $transition;
|
|
||||||
margin-right: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:not(.dragging-disabled) .handle {
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-menu {
|
|
||||||
margin-top: math.div($navbar-padding, 2);
|
|
||||||
|
|
||||||
.menu-list {
|
|
||||||
li {
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: $vikunja-font;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-menu-link,
|
|
||||||
li > a {
|
|
||||||
padding-left: 2rem;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
padding-bottom: .25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespaces-lists {
|
|
||||||
padding-top: math.div($navbar-padding, 2);
|
|
||||||
|
|
||||||
.menu-label {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: $vikunja-font;
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
min-height: 2.5rem;
|
font-family: $vikunja-font;
|
||||||
padding-top: 0;
|
|
||||||
padding-left: $navbar-padding;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count {
|
|
||||||
color: var(--grey-500);
|
|
||||||
margin-right: .5rem;
|
|
||||||
// align brackets with number
|
|
||||||
font-feature-settings: "case";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.favorite {
|
.list-menu-link,
|
||||||
margin-left: .25rem;
|
li > a {
|
||||||
transition: opacity $transition, color $transition;
|
padding-left: 2rem;
|
||||||
opacity: 1;
|
display: inline-block;
|
||||||
|
|
||||||
&.is-favorite {
|
.icon {
|
||||||
color: var(--warning);
|
padding-bottom: .25rem;
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media(hover: hover) and (pointer: fine) {
|
|
||||||
.list-menu .favorite {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-menu:hover .favorite,
|
|
||||||
.favorite.is-favorite {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-menu-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-bubble {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-archived {
|
|
||||||
min-width: 85px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.namespace-title {
|
.menu + .menu {
|
||||||
display: flex;
|
padding-top: math.div($navbar-padding, 2);
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
padding: 0 .25rem;
|
|
||||||
|
|
||||||
.toggle-lists-icon {
|
|
||||||
svg {
|
|
||||||
transition: all $transition;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active svg {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .toggle-lists-icon svg {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.has-menu) .toggle-lists-icon {
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-setting-spacer {
|
|
||||||
width: 2.5rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespaces-list.loader-container.is-loading {
|
|
||||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, toRef, watch} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
import {createRandomID} from '@/helpers/randomId'
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
import XButton from '@/components/input/button.vue'
|
import XButton from '@/components/input/button.vue'
|
||||||
|
|
||||||
|
@ -53,22 +53,16 @@ const lastChangeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const defaultColors = ref(DEFAULT_COLORS)
|
const defaultColors = ref(DEFAULT_COLORS)
|
||||||
const colorListID = ref(createRandomID())
|
const colorListID = ref(createRandomID())
|
||||||
|
|
||||||
const props = defineProps({
|
const {
|
||||||
modelValue: {
|
modelValue,
|
||||||
type: String,
|
} = defineProps<{
|
||||||
required: true,
|
modelValue: string,
|
||||||
},
|
}>()
|
||||||
menuPosition: {
|
|
||||||
type: String,
|
|
||||||
default: 'top',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const modelValue = toRef(props, 'modelValue')
|
|
||||||
watch(
|
watch(
|
||||||
modelValue,
|
() => modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
color.value = newValue
|
color.value = newValue
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<multiselect
|
|
||||||
v-model="selectedLists"
|
|
||||||
:search-results="foundLists"
|
|
||||||
:loading="listService.loading"
|
|
||||||
:multiple="true"
|
|
||||||
:placeholder="$t('list.search')"
|
|
||||||
label="title"
|
|
||||||
@search="findLists"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
|
||||||
|
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
|
|
||||||
import ListService from '@/services/list'
|
|
||||||
import {includesById} from '@/helpers/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Array as PropType<IList[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: IList[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const lists = ref<IList[]>([])
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
lists.value = props.modelValue
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedLists = computed({
|
|
||||||
get() {
|
|
||||||
return lists.value
|
|
||||||
},
|
|
||||||
set: (value) => {
|
|
||||||
lists.value = value
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const listService = shallowReactive(new ListService())
|
|
||||||
const foundLists = ref<IList[]>([])
|
|
||||||
|
|
||||||
async function findLists(query: string) {
|
|
||||||
if (query === '') {
|
|
||||||
foundLists.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await listService.getAll({}, {s: query}) as IList[]
|
|
||||||
|
|
||||||
// Filter selected items from the results
|
|
||||||
foundLists.value = response.filter(({id}) => !includesById(lists.value, id))
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<multiselect
|
|
||||||
v-model="selectedNamespaces"
|
|
||||||
:search-results="foundNamespaces"
|
|
||||||
:loading="namespaceService.loading"
|
|
||||||
:multiple="true"
|
|
||||||
:placeholder="$t('namespace.search')"
|
|
||||||
label="namespace"
|
|
||||||
@search="findNamespaces"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
|
||||||
|
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
|
||||||
|
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
|
||||||
|
|
||||||
import NamespaceService from '@/services/namespace'
|
|
||||||
import {includesById} from '@/helpers/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Array as PropType<INamespace[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: INamespace[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const namespaces = ref<INamespace[]>([])
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
namespaces.value = props.modelValue
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedNamespaces = computed({
|
|
||||||
get() {
|
|
||||||
return namespaces.value
|
|
||||||
},
|
|
||||||
set: (value) => {
|
|
||||||
namespaces.value = value
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const namespaceService = shallowReactive(new NamespaceService())
|
|
||||||
const foundNamespaces = ref<INamespace[]>([])
|
|
||||||
|
|
||||||
async function findNamespaces(query: string) {
|
|
||||||
if (query === '') {
|
|
||||||
foundNamespaces.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await namespaceService.getAll({}, {s: query}) as INamespace[]
|
|
||||||
|
|
||||||
// Filter selected items from the results
|
|
||||||
foundNamespaces.value = response.filter(({id}) => !includesById(namespaces.value, id))
|
|
||||||
}
|
|
||||||
</script>
|
|
63
src/components/input/SelectProject.vue
Normal file
63
src/components/input/SelectProject.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedProjects"
|
||||||
|
:search-results="foundProjects"
|
||||||
|
:loading="projectService.loading"
|
||||||
|
:multiple="true"
|
||||||
|
:placeholder="$t('project.search')"
|
||||||
|
label="title"
|
||||||
|
@search="findProjects"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||||
|
|
||||||
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
|
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
import ProjectService from '@/services/project'
|
||||||
|
import {includesById} from '@/helpers/utils'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<IProject[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: IProject[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projects = ref<IProject[]>([])
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
projects.value = props.modelValue
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedProjects = computed({
|
||||||
|
get() {
|
||||||
|
return projects.value
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
projects.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectService = shallowReactive(new ProjectService())
|
||||||
|
const foundProjects = ref<IProject[]>([])
|
||||||
|
|
||||||
|
async function findProjects(query: string) {
|
||||||
|
if (query === '') {
|
||||||
|
foundProjects.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await projectService.getAll({}, {s: query}) as IProject[]
|
||||||
|
|
||||||
|
// Filter selected items from the results
|
||||||
|
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
|
||||||
|
}
|
||||||
|
</script>
|
26
src/components/input/SimpleButton.vue
Normal file
26
src/components/input/SimpleButton.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<BaseButton class="simple-button">
|
||||||
|
<slot/>
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.simple-button {
|
||||||
|
color: var(--text);
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
transition: background-color $transition;
|
||||||
|
border-radius: $radius;
|
||||||
|
display: block;
|
||||||
|
margin: .1rem 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -47,7 +47,7 @@ import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue
|
||||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
// extending the props of the BaseButton
|
// extending the props of the BaseButton
|
||||||
export interface ButtonProps extends BaseButtonProps {
|
export interface ButtonProps extends /* @vue-ignore */ BaseButtonProps {
|
||||||
variant?: ButtonTypes
|
variant?: ButtonTypes
|
||||||
icon?: IconProp
|
icon?: IconProp
|
||||||
iconColor?: string
|
iconColor?: string
|
||||||
|
|
|
@ -1,78 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="datepicker">
|
<div class="datepicker">
|
||||||
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
<SimpleButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
||||||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||||
</BaseButton>
|
</SimpleButton>
|
||||||
|
|
||||||
<CustomTransition name="fade">
|
<CustomTransition name="fade">
|
||||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||||
|
|
||||||
<BaseButton
|
<DatepickerInline
|
||||||
v-if="(new Date()).getHours() < 21"
|
v-model="date"
|
||||||
class="datepicker__quick-select-date"
|
@update:model-value="updateData"
|
||||||
@click.stop="setDate('today')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.today') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
class="datepicker__quick-select-date"
|
|
||||||
@click.stop="setDate('tomorrow')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon :icon="['far', 'sun']"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.tomorrow') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
class="datepicker__quick-select-date"
|
|
||||||
@click.stop="setDate('nextMonday')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon icon="coffee"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.nextMonday') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
class="datepicker__quick-select-date"
|
|
||||||
@click.stop="setDate('thisWeekend')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon icon="cocktail"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
class="datepicker__quick-select-date"
|
|
||||||
@click.stop="setDate('laterThisWeek')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon icon="chess-knight"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
class="datepicker__quick-select-date"
|
|
||||||
@click.stop="setDate('nextWeek')"
|
|
||||||
>
|
|
||||||
<span class="icon"><icon icon="forward"/></span>
|
|
||||||
<span class="text">
|
|
||||||
<span>{{ $t('input.datepicker.nextWeek') }}</span>
|
|
||||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
|
|
||||||
</span>
|
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<flat-pickr
|
|
||||||
:config="flatPickerConfig"
|
|
||||||
class="input"
|
|
||||||
v-model="flatPickrDate"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<x-button
|
<x-button
|
||||||
|
@ -89,19 +26,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, onMounted, onBeforeUnmount, toRef, watch, computed, type PropType} from 'vue'
|
import {ref, onMounted, onBeforeUnmount, toRef, watch, type PropType} from 'vue'
|
||||||
import flatPickr from 'vue-flatpickr-component'
|
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
|
||||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
import DatepickerInline from '@/components/input/datepickerInline.vue'
|
||||||
|
import SimpleButton from '@/components/input/SimpleButton.vue'
|
||||||
|
|
||||||
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
|
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
|
||||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
|
||||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -125,8 +58,6 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
|
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
|
||||||
|
|
||||||
const date = ref<Date | null>()
|
const date = ref<Date | null>()
|
||||||
const show = ref(false)
|
const show = ref(false)
|
||||||
const changed = ref(false)
|
const changed = ref(false)
|
||||||
|
@ -141,37 +72,6 @@ watch(
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
)
|
)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const weekStart = computed(() => authStore.settings.weekStart)
|
|
||||||
const flatPickerConfig = computed(() => ({
|
|
||||||
altFormat: t('date.altFormatLong'),
|
|
||||||
altInput: true,
|
|
||||||
dateFormat: 'Y-m-d H:i',
|
|
||||||
enableTime: true,
|
|
||||||
time_24hr: true,
|
|
||||||
inline: true,
|
|
||||||
locale: {
|
|
||||||
firstDayOfWeek: weekStart.value,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
|
||||||
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
|
||||||
const flatPickrDate = computed({
|
|
||||||
set(newValue: string | Date) {
|
|
||||||
date.value = createDateFromString(newValue)
|
|
||||||
updateData()
|
|
||||||
},
|
|
||||||
get() {
|
|
||||||
if (!date.value) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatDate(date.value, 'yyy-LL-dd H:mm')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
function setDateValue(dateString: string | Date | null) {
|
function setDateValue(dateString: string | Date | null) {
|
||||||
if (dateString === null) {
|
if (dateString === null) {
|
||||||
date.value = null
|
date.value = null
|
||||||
|
@ -212,29 +112,6 @@ function close() {
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDate(dateString: string) {
|
|
||||||
if (date.value === null) {
|
|
||||||
date.value = new Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = calculateDayInterval(dateString)
|
|
||||||
const newDate = new Date()
|
|
||||||
newDate.setDate(newDate.getDate() + interval)
|
|
||||||
newDate.setHours(calculateNearestHours(newDate))
|
|
||||||
newDate.setMinutes(0)
|
|
||||||
newDate.setSeconds(0)
|
|
||||||
date.value = newDate
|
|
||||||
flatPickrDate.value = newDate
|
|
||||||
updateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWeekdayFromStringInterval(dateString: string) {
|
|
||||||
const interval = calculateDayInterval(dateString)
|
|
||||||
const newDate = new Date()
|
|
||||||
newDate.setDate(newDate.getDate() + interval)
|
|
||||||
return formatDate(newDate, 'E')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -257,42 +134,6 @@ function getWeekdayFromStringInterval(dateString: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.datepicker__quick-select-date {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 .5rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 2.25rem;
|
|
||||||
color: var(--text);
|
|
||||||
transition: all $transition;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-radius: $radius $radius 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--grey-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
width: 100%;
|
|
||||||
font-size: .85rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-right: .25rem;
|
|
||||||
|
|
||||||
.weekday {
|
|
||||||
color: var(--text-light);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker__close-button {
|
.datepicker__close-button {
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
|
|
228
src/components/input/datepickerInline.vue
Normal file
228
src/components/input/datepickerInline.vue
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
<template>
|
||||||
|
<BaseButton
|
||||||
|
v-if="(new Date()).getHours() < 21"
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('today')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.today') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('tomorrow')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon :icon="['far', 'sun']"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.tomorrow') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('nextMonday')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon icon="coffee"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.nextMonday') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('thisWeekend')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon icon="cocktail"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('laterThisWeek')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon icon="chess-knight"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
class="datepicker__quick-select-date"
|
||||||
|
@click.stop="setDate('nextWeek')"
|
||||||
|
>
|
||||||
|
<span class="icon"><icon icon="forward"/></span>
|
||||||
|
<span class="text">
|
||||||
|
<span>{{ $t('input.datepicker.nextWeek') }}</span>
|
||||||
|
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
|
||||||
|
</span>
|
||||||
|
</BaseButton>
|
||||||
|
|
||||||
|
<div class="flatpickr-container">
|
||||||
|
<flat-pickr
|
||||||
|
:config="flatPickerConfig"
|
||||||
|
v-model="flatPickrDate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, toRef, watch, computed, type PropType} from 'vue'
|
||||||
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
|
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
|
import {formatDate} from '@/helpers/time/formatDate'
|
||||||
|
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||||
|
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||||
|
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [Date, null, String] as PropType<Date | null | string>,
|
||||||
|
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'close-on-change'])
|
||||||
|
|
||||||
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
|
const date = ref<Date | null>()
|
||||||
|
const changed = ref(false)
|
||||||
|
|
||||||
|
const modelValue = toRef(props, 'modelValue')
|
||||||
|
watch(
|
||||||
|
modelValue,
|
||||||
|
setDateValue,
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const weekStart = computed(() => authStore.settings.weekStart)
|
||||||
|
const flatPickerConfig = computed(() => ({
|
||||||
|
altFormat: t('date.altFormatLong'),
|
||||||
|
altInput: true,
|
||||||
|
dateFormat: 'Y-m-d H:i',
|
||||||
|
enableTime: true,
|
||||||
|
time_24hr: true,
|
||||||
|
inline: true,
|
||||||
|
locale: {
|
||||||
|
firstDayOfWeek: weekStart.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||||
|
// To make that work, we need a separate variable since flatpickr does not have a change event.
|
||||||
|
const flatPickrDate = computed({
|
||||||
|
set(newValue: string | Date | null) {
|
||||||
|
if (newValue === null) {
|
||||||
|
date.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
date.value = createDateFromString(newValue)
|
||||||
|
updateData()
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
if (!date.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatDate(date.value, 'yyy-LL-dd H:mm')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function setDateValue(dateString: string | Date | null) {
|
||||||
|
if (dateString === null) {
|
||||||
|
date.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
date.value = createDateFromString(dateString)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateData() {
|
||||||
|
changed.value = true
|
||||||
|
emit('update:modelValue', date.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDate(dateString: string) {
|
||||||
|
if (date.value === null) {
|
||||||
|
date.value = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = calculateDayInterval(dateString)
|
||||||
|
const newDate = new Date()
|
||||||
|
newDate.setDate(newDate.getDate() + interval)
|
||||||
|
newDate.setHours(calculateNearestHours(newDate))
|
||||||
|
newDate.setMinutes(0)
|
||||||
|
newDate.setSeconds(0)
|
||||||
|
date.value = newDate
|
||||||
|
flatPickrDate.value = newDate
|
||||||
|
updateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekdayFromStringInterval(dateString: string) {
|
||||||
|
const interval = calculateDayInterval(dateString)
|
||||||
|
const newDate = new Date()
|
||||||
|
newDate.setDate(newDate.getDate() + interval)
|
||||||
|
return formatDate(newDate, 'E')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.datepicker__quick-select-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 2.25rem;
|
||||||
|
color: var(--text);
|
||||||
|
transition: all $transition;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: $radius $radius 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
width: 100%;
|
||||||
|
font-size: .85rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: .25rem;
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
color: var(--text-light);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-container {
|
||||||
|
:deep(.flatpickr-calendar) {
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.input) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<vue-easymde
|
<vue-easymde
|
||||||
:configs="config"
|
:configs="config"
|
||||||
@change="() => bubble()"
|
@change="() => bubbleNow()"
|
||||||
@update:modelValue="handleInput"
|
@update:modelValue="handleInput"
|
||||||
class="content"
|
class="content"
|
||||||
v-if="isEditActive"
|
v-if="isEditActive"
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-else-if="isEditActive"
|
v-else-if="isEditActive"
|
||||||
@click="toggleEdit"
|
@click="bubbleSaveClick"
|
||||||
class="done-edit">
|
class="done-edit">
|
||||||
{{ $t('misc.save') }}
|
{{ $t('misc.save') }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
<x-button
|
<x-button
|
||||||
v-else-if="isEditActive"
|
v-else-if="isEditActive"
|
||||||
@click="toggleEdit"
|
@click="bubbleSaveClick"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
v-cy="'saveEditor'">
|
v-cy="'saveEditor'">
|
||||||
|
@ -84,8 +84,8 @@ import {createRandomID} from '@/helpers/randomId'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import ButtonLink from '@/components/misc/ButtonLink.vue'
|
import ButtonLink from '@/components/misc/ButtonLink.vue'
|
||||||
import type { IAttachment } from '@/modelTypes/IAttachment'
|
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||||
import type { ITask } from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -115,7 +115,7 @@ const props = defineProps({
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
bottomActions: {
|
bottomActions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
|
@ -134,10 +134,9 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'save'])
|
||||||
|
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
||||||
const isEditActive = ref(false)
|
const isEditActive = ref(false)
|
||||||
const isPreviewActive = ref(true)
|
const isPreviewActive = ref(true)
|
||||||
|
|
||||||
|
@ -148,7 +147,7 @@ const preview = ref('')
|
||||||
const attachmentService = new AttachmentService()
|
const attachmentService = new AttachmentService()
|
||||||
|
|
||||||
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
|
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
|
||||||
const loadedAttachments = ref<{[key: CacheKey]: string}>({})
|
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
|
||||||
const config = ref(createEasyMDEConfig({
|
const config = ref(createEasyMDEConfig({
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
uploadImage: props.uploadEnabled,
|
uploadImage: props.uploadEnabled,
|
||||||
|
@ -175,7 +174,7 @@ watch(
|
||||||
if (oldVal === '' && text.value === modelValue.value) {
|
if (oldVal === '' && text.value === modelValue.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bubble()
|
bubbleNow()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -208,17 +207,11 @@ function handleInput(val: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
text.value = val
|
text.value = val
|
||||||
bubble(1000)
|
bubbleNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
function bubble(timeout = 500) {
|
function bubbleNow() {
|
||||||
if (changeTimeout.value !== null) {
|
emit('update:modelValue', text.value)
|
||||||
clearTimeout(changeTimeout.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTimeout.value = setTimeout(() => {
|
|
||||||
emit('update:modelValue', text.value)
|
|
||||||
}, timeout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAt(str: string, index: number, replacement: string) {
|
function replaceAt(str: string, index: number, replacement: string) {
|
||||||
|
@ -233,7 +226,10 @@ function findNthIndex(str: string, n: number) {
|
||||||
function renderPreview() {
|
function renderPreview() {
|
||||||
setupMarkdownRenderer(checkboxId.value)
|
setupMarkdownRenderer(checkboxId.value)
|
||||||
|
|
||||||
preview.value = DOMPurify.sanitize(marked(text.value), {ADD_ATTR: ['target']})
|
preview.value = DOMPurify.sanitize(marked(text.value, {
|
||||||
|
mangle: false,
|
||||||
|
headerIds: false,
|
||||||
|
}), {ADD_ATTR: ['target']})
|
||||||
|
|
||||||
// Since the render function is synchronous, we can't do async http requests in it.
|
// Since the render function is synchronous, we can't do async http requests in it.
|
||||||
// Therefore, we can't resolve the blob url at (markdown) compile time.
|
// Therefore, we can't resolve the blob url at (markdown) compile time.
|
||||||
|
@ -286,25 +282,27 @@ function handleCheckboxClick(e: Event) {
|
||||||
console.debug('no index found')
|
console.debug('no index found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const listPrefix = text.value.substring(index, index + 1)
|
const projectPrefix = text.value.substring(index, index + 1)
|
||||||
|
|
||||||
console.debug({index, listPrefix, checked, text: text.value})
|
|
||||||
|
|
||||||
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
|
console.debug({index, projectPrefix, checked, text: text.value})
|
||||||
bubble()
|
|
||||||
|
text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
|
||||||
|
bubbleNow()
|
||||||
|
emit('save', text.value)
|
||||||
renderPreview()
|
renderPreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEdit() {
|
function toggleEdit() {
|
||||||
if (isEditActive.value) {
|
isPreviewActive.value = false
|
||||||
isPreviewActive.value = true
|
isEditActive.value = true
|
||||||
isEditActive.value = false
|
}
|
||||||
renderPreview()
|
|
||||||
bubble(0) // save instantly
|
function bubbleSaveClick() {
|
||||||
} else {
|
isPreviewActive.value = true
|
||||||
isPreviewActive.value = false
|
isEditActive.value = false
|
||||||
isEditActive.value = true
|
renderPreview()
|
||||||
}
|
bubbleNow()
|
||||||
|
emit('save', text.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
71
src/components/input/fancycheckbox.story.vue
Normal file
71
src/components/input/fancycheckbox.story.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {logEvent} from 'histoire/client'
|
||||||
|
import FancyCheckbox from './fancycheckbox.vue'
|
||||||
|
|
||||||
|
const isDisabled = ref<boolean | undefined>()
|
||||||
|
|
||||||
|
const isChecked = ref(false)
|
||||||
|
|
||||||
|
const isCheckedInitiallyEnabled = ref(true)
|
||||||
|
|
||||||
|
const isCheckedDisabled = ref(false)
|
||||||
|
|
||||||
|
const withoutInitialState = ref<boolean | undefined>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story :layout="{ type: 'grid', width: '200px' }">
|
||||||
|
<Variant title="Default">
|
||||||
|
<FancyCheckbox
|
||||||
|
v-model="isChecked"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
>
|
||||||
|
This is probably not important
|
||||||
|
</FancyCheckbox>
|
||||||
|
|
||||||
|
Visualisation
|
||||||
|
<input type="checkbox" v-model="isChecked">
|
||||||
|
{{ isChecked }}
|
||||||
|
</Variant>
|
||||||
|
<Variant title="Enabled Initially">
|
||||||
|
<FancyCheckbox
|
||||||
|
:disabled="isDisabled"
|
||||||
|
v-model="isCheckedInitiallyEnabled"
|
||||||
|
>
|
||||||
|
We want you to use this option
|
||||||
|
</FancyCheckbox>
|
||||||
|
|
||||||
|
Visualisation
|
||||||
|
<input type="checkbox" v-model="isCheckedInitiallyEnabled">
|
||||||
|
{{ isCheckedInitiallyEnabled }}
|
||||||
|
</Variant>
|
||||||
|
<Variant title="Disabled">
|
||||||
|
<FancyCheckbox
|
||||||
|
disabled
|
||||||
|
:modelValue="isCheckedDisabled"
|
||||||
|
@update:model-value="logEvent('Setting disabled: This should never happen', $event)"
|
||||||
|
>
|
||||||
|
You can't change this
|
||||||
|
</FancyCheckbox>
|
||||||
|
|
||||||
|
Visualisation
|
||||||
|
<input type="checkbox" v-model="isCheckedDisabled" disabled>
|
||||||
|
{{ isCheckedDisabled }}
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Undefined initial State">
|
||||||
|
<FancyCheckbox
|
||||||
|
v-model="withoutInitialState"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
>
|
||||||
|
Not sure what the value should be
|
||||||
|
</FancyCheckbox>
|
||||||
|
|
||||||
|
Visualisation
|
||||||
|
<input type="checkbox" v-model="withoutInitialState" disabled>
|
||||||
|
{{ withoutInitialState }}
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
|
@ -1,66 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'is-disabled': disabled}" class="fancycheckbox">
|
<BaseCheckbox
|
||||||
<input
|
class="fancycheckbox"
|
||||||
:checked="checked"
|
:class="{
|
||||||
:disabled="disabled || undefined"
|
'is-disabled': disabled,
|
||||||
:id="checkBoxId"
|
'is-block': isBlock,
|
||||||
@change="(event: Event) => updateData((event.target as HTMLInputElement).checked)"
|
}"
|
||||||
type="checkbox"
|
:disabled="disabled"
|
||||||
/>
|
:model-value="modelValue"
|
||||||
<label :for="checkBoxId" class="check" @click.prevent="check">
|
@update:model-value="value => emit('update:modelValue', value)"
|
||||||
<svg height="18px" viewBox="0 0 18 18" width="18px">
|
>
|
||||||
<path
|
<CheckboxIcon class="fancycheckbox__icon" />
|
||||||
d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
<span v-if="$slots.default" class="fancycheckbox__content">
|
||||||
<polyline points="1 9 7 14 15 4"></polyline>
|
<slot/>
|
||||||
</svg>
|
</span>
|
||||||
<span>
|
</BaseCheckbox>
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, toRef, watch} from 'vue'
|
import CheckboxIcon from '@/assets/checkbox.svg?component'
|
||||||
|
|
||||||
import {createRandomID} from '@/helpers/randomId'
|
import BaseCheckbox from '@/components/base/BaseCheckbox.vue'
|
||||||
|
|
||||||
const checked = ref(false)
|
defineProps({
|
||||||
const checkBoxId = `fancycheckbox_${createRandomID()}`
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
},
|
||||||
|
isBlock: {
|
||||||
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
|
||||||
|
|
||||||
const modelValue = toRef(props, 'modelValue')
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
watch(
|
}>()
|
||||||
modelValue,
|
|
||||||
newValue => {
|
|
||||||
checked.value = newValue
|
|
||||||
},
|
|
||||||
{immediate: true},
|
|
||||||
)
|
|
||||||
|
|
||||||
function updateData(newChecked: boolean) {
|
|
||||||
checked.value = newChecked
|
|
||||||
emit('update:modelValue', newChecked)
|
|
||||||
emit('change', newChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
function check() {
|
|
||||||
checked.value = !checked.value
|
|
||||||
updateData(checked.value)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,75 +46,54 @@ function check() {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
|
|
||||||
// FIXME: should be a prop
|
|
||||||
&.is-block {
|
&.is-block {
|
||||||
|
display: block;
|
||||||
margin: .5rem .2rem;
|
margin: .5rem .2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=checkbox] {
|
.fancycheckbox__content {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
margin: auto;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
padding-left: .5rem;
|
padding-left: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
.fancycheckbox__icon:deep() {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
fill: none;
|
stroke: var(--stroke-color, #c8ccd4);
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke: #c8ccd4;
|
|
||||||
stroke-width: 1.5;
|
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
|
||||||
|
|
||||||
.check:hover svg {
|
path,
|
||||||
stroke: var(--primary);
|
polyline {
|
||||||
}
|
transition: all 0.2s linear, color 0.2s ease;
|
||||||
|
|
||||||
.is-disabled .check:hover svg {
|
|
||||||
stroke: #c8ccd4;
|
|
||||||
}
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke-dasharray: 60;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
polyline {
|
|
||||||
stroke-dasharray: 22;
|
|
||||||
stroke-dashoffset: 66;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=checkbox]:checked + .check {
|
|
||||||
svg {
|
|
||||||
stroke: var(--primary);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancycheckbox:not(:has(input:disabled)):hover .fancycheckbox__icon,
|
||||||
|
.fancycheckbox:has(input:checked) .fancycheckbox__icon {
|
||||||
|
--stroke-color: var(--primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// Since css-has-pseudo doesn't work with deep classes,
|
||||||
|
// the following rules can't be scoped
|
||||||
|
|
||||||
|
.fancycheckbox:has(:not(input:checked)) .fancycheckbox__icon {
|
||||||
|
path {
|
||||||
|
transition-delay: 0.05s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancycheckbox:has(input:checked) .fancycheckbox__icon {
|
||||||
path {
|
path {
|
||||||
stroke-dashoffset: 60;
|
stroke-dashoffset: 60;
|
||||||
transition: all 0.3s linear;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
polyline {
|
polyline {
|
||||||
stroke-dashoffset: 42;
|
stroke-dashoffset: 42;
|
||||||
transition: all 0.2s linear;
|
|
||||||
transition-delay: 0.15s;
|
transition-delay: 0.15s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
@keydown.down.exact.prevent="() => preSelect(0)"
|
@keydown.down.exact.prevent="() => preSelect(0)"
|
||||||
ref="searchInput"
|
ref="searchInput"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
|
:autocomplete="autocompleteEnabled ? undefined : 'off'"
|
||||||
|
:spellcheck="autocompleteEnabled ? undefined : 'false'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,6 +198,13 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* If false, the search input will get the autocomplete="off" attributes attached to it.
|
||||||
|
*/
|
||||||
|
autocompleteEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
|
|
||||||
class="loader-container"
|
|
||||||
>
|
|
||||||
<div class="switch-view-container">
|
|
||||||
<div class="switch-view">
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g l'"
|
|
||||||
:title="$t('keyboardShortcuts.list.switchToListView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'list'}"
|
|
||||||
:to="{ name: 'list.list', params: { listId } }"
|
|
||||||
>
|
|
||||||
{{ $t('list.list.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g g'"
|
|
||||||
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'gantt'}"
|
|
||||||
:to="{ name: 'list.gantt', params: { listId } }"
|
|
||||||
>
|
|
||||||
{{ $t('list.gantt.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g t'"
|
|
||||||
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'table'}"
|
|
||||||
:to="{ name: 'list.table', params: { listId } }"
|
|
||||||
>
|
|
||||||
{{ $t('list.table.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g k'"
|
|
||||||
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'kanban'}"
|
|
||||||
:to="{ name: 'list.kanban', params: { listId } }"
|
|
||||||
>
|
|
||||||
{{ $t('list.kanban.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
<slot name="header" />
|
|
||||||
</div>
|
|
||||||
<CustomTransition name="fade">
|
|
||||||
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
|
|
||||||
{{ $t('list.archived') }}
|
|
||||||
</Message>
|
|
||||||
</CustomTransition>
|
|
||||||
|
|
||||||
<slot v-if="loadedListId"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {ref, computed, watch} from 'vue'
|
|
||||||
import {useRoute} from 'vue-router'
|
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
|
||||||
import Message from '@/components/misc/message.vue'
|
|
||||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
|
||||||
import ListService from '@/services/list'
|
|
||||||
|
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
|
||||||
import {saveListToHistory} from '@/modules/listHistory'
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
|
||||||
import {useListStore} from '@/stores/lists'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
listId: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
viewName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
|
||||||
const listStore = useListStore()
|
|
||||||
const listService = ref(new ListService())
|
|
||||||
const loadedListId = ref(0)
|
|
||||||
|
|
||||||
const currentList = computed(() => {
|
|
||||||
return typeof baseStore.currentList === 'undefined' ? {
|
|
||||||
id: 0,
|
|
||||||
title: '',
|
|
||||||
isArchived: false,
|
|
||||||
maxRight: null,
|
|
||||||
} : baseStore.currentList
|
|
||||||
})
|
|
||||||
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
|
|
||||||
|
|
||||||
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
|
||||||
// This resulted in loading and setting the list multiple times, even when navigating away from it.
|
|
||||||
// This caused wired bugs where the list background would be set on the home page but only right after setting a new
|
|
||||||
// list background and then navigating to home. It also highlighted the list in the menu and didn't allow changing any
|
|
||||||
// of it, most likely due to the rights not being properly populated.
|
|
||||||
watch(
|
|
||||||
() => props.listId,
|
|
||||||
// loadList
|
|
||||||
async (listIdToLoad: number) => {
|
|
||||||
const listData = {id: listIdToLoad}
|
|
||||||
saveListToHistory(listData)
|
|
||||||
|
|
||||||
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
|
|
||||||
// the currently loaded list has the right set.
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
listIdToLoad === loadedListId.value ||
|
|
||||||
typeof listIdToLoad === 'undefined' ||
|
|
||||||
listIdToLoad === currentList.value.id
|
|
||||||
)
|
|
||||||
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
|
|
||||||
) {
|
|
||||||
loadedListId.value = props.listId
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`Loading list, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
|
|
||||||
|
|
||||||
// Set the current list to the one we're about to load so that the title is already shown at the top
|
|
||||||
loadedListId.value = 0
|
|
||||||
const listFromStore = listStore.getListById(listData.id)
|
|
||||||
if (listFromStore !== null) {
|
|
||||||
baseStore.setBackground(null)
|
|
||||||
baseStore.setBlurHash(null)
|
|
||||||
baseStore.handleSetCurrentList({list: listFromStore})
|
|
||||||
}
|
|
||||||
|
|
||||||
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
|
|
||||||
const list = new ListModel(listData)
|
|
||||||
try {
|
|
||||||
const loadedList = await listService.value.get(list)
|
|
||||||
baseStore.handleSetCurrentList({list: loadedList})
|
|
||||||
} finally {
|
|
||||||
loadedListId.value = props.listId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true},
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.switch-view-container {
|
|
||||||
@media screen and (max-width: $tablet) {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-view {
|
|
||||||
background: var(--white);
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: $radius;
|
|
||||||
font-size: .75rem;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
height: $switch-view-height;
|
|
||||||
margin: 0 auto 1rem;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-view-button {
|
|
||||||
padding: .25rem .5rem;
|
|
||||||
display: block;
|
|
||||||
border-radius: $radius;
|
|
||||||
transition: all 100ms;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--switch-view-color);
|
|
||||||
background: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
color: var(--switch-view-color);
|
|
||||||
background: var(--primary);
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: var(--shadow-xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: this should be in notification and set via a prop
|
|
||||||
.is-archived .notification.is-warning {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,77 +0,0 @@
|
||||||
<template>
|
|
||||||
<ul class="list-grid">
|
|
||||||
<li
|
|
||||||
v-for="(item, index) in filteredLists"
|
|
||||||
:key="`list_${item.id}_${index}`"
|
|
||||||
class="list-grid-item"
|
|
||||||
>
|
|
||||||
<ListCard :list="item" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {computed, type PropType} from 'vue'
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
|
|
||||||
import ListCard from './ListCard.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
lists: {
|
|
||||||
type: Array as PropType<IList[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showArchived: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
itemLimit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredLists = computed(() => {
|
|
||||||
return props.showArchived
|
|
||||||
? props.lists
|
|
||||||
: props.lists.filter(l => !l.isArchived)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$list-height: 150px;
|
|
||||||
$list-spacing: 1rem;
|
|
||||||
|
|
||||||
.list-grid {
|
|
||||||
margin: 0; // reset li
|
|
||||||
list-style-type: none;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--list-columns), 1fr);
|
|
||||||
grid-auto-rows: $list-height;
|
|
||||||
gap: $list-spacing;
|
|
||||||
|
|
||||||
@media screen and (min-width: $mobile) {
|
|
||||||
--list-rows: 4;
|
|
||||||
--list-columns: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $mobile) and (max-width: $tablet) {
|
|
||||||
--list-columns: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $tablet) and (max-width: $widescreen) {
|
|
||||||
--list-columns: 3;
|
|
||||||
--list-rows: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $widescreen) {
|
|
||||||
--list-columns: 5;
|
|
||||||
--list-rows: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-grid-item {
|
|
||||||
display: grid;
|
|
||||||
margin-top: 0; // remove padding coming form .content li + li
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,55 +0,0 @@
|
||||||
import {ref, watch, type Ref} from 'vue'
|
|
||||||
import ListService from '@/services/list'
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
|
||||||
|
|
||||||
export function useListBackground(list: Ref<IList>) {
|
|
||||||
const background = ref<string | null>(null)
|
|
||||||
const backgroundLoading = ref(false)
|
|
||||||
const blurHashUrl = ref('')
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
|
||||||
async ([listId, blurHash], oldValue) => {
|
|
||||||
if (
|
|
||||||
list.value === null ||
|
|
||||||
!list.value.backgroundInformation ||
|
|
||||||
backgroundLoading.value
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const [oldListId, oldBlurHash] = oldValue || []
|
|
||||||
if (
|
|
||||||
oldValue !== undefined &&
|
|
||||||
listId === oldListId && blurHash === oldBlurHash
|
|
||||||
) {
|
|
||||||
// list hasn't changed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const blurHashPromise = getBlobFromBlurHash(blurHash).then((blurHash) => {
|
|
||||||
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const listService = new ListService()
|
|
||||||
const backgroundPromise = listService.background(list.value).then((result) => {
|
|
||||||
background.value = result
|
|
||||||
})
|
|
||||||
await Promise.all([blurHashPromise, backgroundPromise])
|
|
||||||
} finally {
|
|
||||||
backgroundLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
background,
|
|
||||||
blurHashUrl,
|
|
||||||
backgroundLoading,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
faAngleRight,
|
faAngleRight,
|
||||||
faArchive,
|
faArchive,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
|
faArrowUpFromBracket,
|
||||||
faBars,
|
faBars,
|
||||||
faBell,
|
faBell,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
|
@ -56,7 +57,7 @@ import {
|
||||||
faTimes,
|
faTimes,
|
||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers, faX,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faBellSlash,
|
faBellSlash,
|
||||||
|
@ -67,10 +68,11 @@ import {
|
||||||
faStar,
|
faStar,
|
||||||
faSun,
|
faSun,
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
|
faCircleQuestion,
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome'
|
import type {FontAwesomeIcon as FontAwesomeIconFixedTypes} from '@/types/vue-fontawesome'
|
||||||
|
|
||||||
library.add(faAlignLeft)
|
library.add(faAlignLeft)
|
||||||
library.add(faAngleRight)
|
library.add(faAngleRight)
|
||||||
|
@ -86,6 +88,7 @@ library.add(faCheckDouble)
|
||||||
library.add(faChessKnight)
|
library.add(faChessKnight)
|
||||||
library.add(faChevronDown)
|
library.add(faChevronDown)
|
||||||
library.add(faCircleInfo)
|
library.add(faCircleInfo)
|
||||||
|
library.add(faCircleQuestion)
|
||||||
library.add(faClock)
|
library.add(faClock)
|
||||||
library.add(faCloudDownloadAlt)
|
library.add(faCloudDownloadAlt)
|
||||||
library.add(faCloudUploadAlt)
|
library.add(faCloudUploadAlt)
|
||||||
|
@ -137,6 +140,8 @@ library.add(faTimesCircle)
|
||||||
library.add(faTrashAlt)
|
library.add(faTrashAlt)
|
||||||
library.add(faUser)
|
library.add(faUser)
|
||||||
library.add(faUsers)
|
library.add(faUsers)
|
||||||
|
library.add(faArrowUpFromBracket)
|
||||||
|
library.add(faX)
|
||||||
|
|
||||||
// overwriting the wrong types
|
// overwriting the wrong types
|
||||||
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
|
@ -24,12 +24,12 @@
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div :class="{'content': hasContent}">
|
<div :class="{'content': hasContent}">
|
||||||
<slot />
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="$slots.footer" class="card-footer">
|
<footer v-if="$slots.footer" class="card-footer">
|
||||||
<slot name="footer" />
|
<slot name="footer"/>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,22 +76,27 @@ defineEmits(['close'])
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border: 1px solid var(--card-border-color);
|
border: 1px solid var(--card-border-color);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-bottom: 1px solid var(--card-border-color);
|
border-bottom: 1px solid var(--card-border-color);
|
||||||
border-radius: $radius $radius 0 0;
|
border-radius: $radius $radius 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
background-color: var(--grey-50);
|
background-color: var(--grey-50);
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
padding: var(--modal-card-head-padding);
|
padding: var(--modal-card-head-padding);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.
|
||||||
import Icon from '@/components/misc/Icon'
|
import Icon from '@/components/misc/Icon'
|
||||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
export interface DropDownItemProps extends BaseButtonProps {
|
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
|
||||||
icon?: IconProp,
|
icon?: IconProp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.navigation.namespaces',
|
title: 'keyboardShortcuts.navigation.projects',
|
||||||
keys: ['g', 'n'],
|
keys: ['g', 'p'],
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -61,8 +61,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'list.kanban.title',
|
title: 'project.kanban.title',
|
||||||
available: (route) => route.name === 'list.kanban',
|
available: (route) => route.name === 'project.kanban',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.task.done',
|
title: 'keyboardShortcuts.task.done',
|
||||||
|
@ -71,26 +71,26 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.list.title',
|
title: 'keyboardShortcuts.project.title',
|
||||||
available: (route) => (route.name as string)?.startsWith('list.'),
|
available: (route) => (route.name as string)?.startsWith('project.'),
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.list.switchToListView',
|
title: 'keyboardShortcuts.project.switchToListView',
|
||||||
keys: ['g', 'l'],
|
keys: ['g', 'l'],
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.list.switchToGanttView',
|
title: 'keyboardShortcuts.project.switchToGanttView',
|
||||||
keys: ['g', 'g'],
|
keys: ['g', 'g'],
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.list.switchToTableView',
|
title: 'keyboardShortcuts.project.switchToTableView',
|
||||||
keys: ['g', 't'],
|
keys: ['g', 't'],
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.list.switchToKanbanView',
|
title: 'keyboardShortcuts.project.switchToKanbanView',
|
||||||
keys: ['g', 'k'],
|
keys: ['g', 'k'],
|
||||||
combination: 'then',
|
combination: 'then',
|
||||||
},
|
},
|
||||||
|
@ -140,6 +140,18 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||||
title: 'keyboardShortcuts.task.description',
|
title: 'keyboardShortcuts.task.description',
|
||||||
keys: ['e'],
|
keys: ['e'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.priority',
|
||||||
|
keys: ['p'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.delete',
|
||||||
|
keys: ['shift', 'delete'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.favorite',
|
||||||
|
keys: ['s'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
|
@ -1,13 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loader-container is-loading"></div>
|
<div class="loader-container is-loading" :class="{'is-small': variant === 'small'}"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
inheritAttrs: false,
|
inheritAttrs: true,
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const {
|
||||||
|
variant = 'default',
|
||||||
|
} = defineProps<{
|
||||||
|
variant?: 'default' | 'small'
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.loader-container {
|
.loader-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -20,5 +28,18 @@ export default {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
min-width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
|
||||||
|
&.is-loading::after {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
top: calc(50% - 1.5rem);
|
||||||
|
left: calc(50% - 1.5rem);
|
||||||
|
border-width: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -75,17 +75,19 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import {ref, useAttrs, watchEffect} from 'vue'
|
import {ref, useAttrs, watchEffect} from 'vue'
|
||||||
import {useScrollLock} from '@vueuse/core'
|
import {useScrollLock} from '@vueuse/core'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const {
|
||||||
|
enabled = true,
|
||||||
|
overflow,
|
||||||
|
wide,
|
||||||
|
transitionName = 'modal',
|
||||||
|
variant = 'default',
|
||||||
|
} = defineProps<{
|
||||||
enabled?: boolean,
|
enabled?: boolean,
|
||||||
overflow?: boolean,
|
overflow?: boolean,
|
||||||
wide?: boolean,
|
wide?: boolean,
|
||||||
transitionName?: 'modal' | 'fade',
|
transitionName?: 'modal' | 'fade',
|
||||||
variant?: 'default' | 'hint-modal' | 'scrolling',
|
variant?: 'default' | 'hint-modal' | 'scrolling',
|
||||||
}>(), {
|
}>()
|
||||||
enabled: true,
|
|
||||||
transitionName: 'modal',
|
|
||||||
variant: 'default',
|
|
||||||
})
|
|
||||||
|
|
||||||
defineEmits(['close', 'submit'])
|
defineEmits(['close', 'submit'])
|
||||||
|
|
||||||
|
@ -95,7 +97,7 @@ const modal = ref<HTMLElement | null>(null)
|
||||||
const scrollLock = useScrollLock(modal)
|
const scrollLock = useScrollLock(modal)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
scrollLock.value = props.enabled
|
scrollLock.value = enabled
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -200,10 +202,10 @@ $modal-width: 1024px;
|
||||||
right: $close-button-padding;
|
right: $close-button-padding;
|
||||||
color: var(--grey-900);
|
color: var(--grey-900);
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|
||||||
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||||
top: calc(5px + $modal-margin);
|
top: calc(5px + $modal-margin);
|
||||||
right: 50%;
|
right: 50%;
|
||||||
// we align the close button to the modal until there is enough space outside for it
|
// we align the close button to the modal until there is enough space outside for it
|
||||||
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
|
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<section class="content">
|
<section class="content">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="title" v-if="title">{{ title }}</h2>
|
<h2 class="title" v-if="title">{{ title }}</h2>
|
||||||
<api-config/>
|
<api-config v-if="showApiConfig"/>
|
||||||
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
|
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
|
||||||
{{ motd }}
|
{{ motd }}
|
||||||
</Message>
|
</Message>
|
||||||
|
@ -45,6 +45,12 @@ const route = useRoute()
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const title = computed(() => t(route.meta?.title as string || ''))
|
const title = computed(() => t(route.meta?.title as string || ''))
|
||||||
useTitle(() => title.value)
|
useTitle(() => title.value)
|
||||||
|
|
||||||
|
const {
|
||||||
|
showApiConfig = true,
|
||||||
|
} = defineProps<{
|
||||||
|
showApiConfig?: boolean
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
}"
|
}"
|
||||||
ref="popup"
|
ref="popup"
|
||||||
>
|
>
|
||||||
<slot name="content" :isOpen="open"/>
|
<slot name="content" :isOpen="open" :toggle="toggle"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -23,11 +23,14 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
const popup = ref<HTMLElement | null>(null)
|
const popup = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
open.value = false
|
open.value = false
|
||||||
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
|
|
@ -11,22 +11,20 @@
|
||||||
<slot/>
|
<slot/>
|
||||||
</template>
|
</template>
|
||||||
<section v-else-if="error !== ''">
|
<section v-else-if="error !== ''">
|
||||||
<no-auth-wrapper>
|
<no-auth-wrapper :show-api-config="false">
|
||||||
<card>
|
<p v-if="error === ERROR_NO_API_URL">
|
||||||
<p v-if="error === ERROR_NO_API_URL">
|
{{ $t('ready.noApiUrlConfigured') }}
|
||||||
{{ $t('ready.noApiUrlConfigured') }}
|
</p>
|
||||||
|
<message variant="danger" v-else class="mb-4">
|
||||||
|
<p>
|
||||||
|
{{ $t('ready.errorOccured') }}<br/>
|
||||||
|
{{ error }}
|
||||||
</p>
|
</p>
|
||||||
<message variant="danger" v-else>
|
<p>
|
||||||
<p>
|
{{ $t('ready.checkApiUrl') }}
|
||||||
{{ $t('ready.errorOccured') }}<br/>
|
</p>
|
||||||
{{ error }}
|
</message>
|
||||||
</p>
|
<api-config :configure-open="true" @found-api="load"/>
|
||||||
<p>
|
|
||||||
{{ $t('ready.checkApiUrl') }}
|
|
||||||
</p>
|
|
||||||
</message>
|
|
||||||
<api-config :configure-open="true" @found-api="load"/>
|
|
||||||
</card>
|
|
||||||
</no-auth-wrapper>
|
</no-auth-wrapper>
|
||||||
</section>
|
</section>
|
||||||
<CustomTransition name="fade">
|
<CustomTransition name="fade">
|
||||||
|
@ -56,11 +54,13 @@ import {useOnline} from '@/composables/useOnline'
|
||||||
import {getAuthForRoute} from '@/router'
|
import {getAuthForRoute} from '@/router'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const ready = computed(() => baseStore.ready)
|
const ready = computed(() => baseStore.ready)
|
||||||
const online = useOnline()
|
const online = useOnline()
|
||||||
|
@ -72,7 +72,7 @@ async function load() {
|
||||||
try {
|
try {
|
||||||
await baseStore.loadApp()
|
await baseStore.loadApp()
|
||||||
baseStore.setReady(true)
|
baseStore.setReady(true)
|
||||||
const redirectTo = await getAuthForRoute(route)
|
const redirectTo = await getAuthForRoute(route, authStore)
|
||||||
if (typeof redirectTo !== 'undefined') {
|
if (typeof redirectTo !== 'undefined') {
|
||||||
await router.push(redirectTo)
|
await router.push(redirectTo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ import {success} from '@/message'
|
||||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
entity: String,
|
entity: String as ISubscription['entity'],
|
||||||
entityId: Number,
|
entityId: Number,
|
||||||
isButton: {
|
isButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -73,28 +73,18 @@ const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const tooltipText = computed(() => {
|
const tooltipText = computed(() => {
|
||||||
if (disabled.value) {
|
if (disabled.value) {
|
||||||
if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
|
if (props.entity === 'task' && subscriptionEntity.value === 'project') {
|
||||||
return t('task.subscription.subscribedListThroughParentNamespace')
|
return t('task.subscription.subscribedTaskThroughParentProject')
|
||||||
}
|
|
||||||
if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
|
|
||||||
return t('task.subscription.subscribedTaskThroughParentNamespace')
|
|
||||||
}
|
|
||||||
if (props.entity === 'task' && subscriptionEntity.value === 'list') {
|
|
||||||
return t('task.subscription.subscribedTaskThroughParentList')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.entity) {
|
switch (props.entity) {
|
||||||
case 'namespace':
|
case 'project':
|
||||||
return props.modelValue !== null ?
|
return props.modelValue !== null ?
|
||||||
t('task.subscription.subscribedNamespace') :
|
t('task.subscription.subscribedProject') :
|
||||||
t('task.subscription.notSubscribedNamespace')
|
t('task.subscription.notSubscribedProject')
|
||||||
case 'list':
|
|
||||||
return props.modelValue !== null ?
|
|
||||||
t('task.subscription.subscribedList') :
|
|
||||||
t('task.subscription.notSubscribedList')
|
|
||||||
case 'task':
|
case 'task':
|
||||||
return props.modelValue !== null ?
|
return props.modelValue !== null ?
|
||||||
t('task.subscription.subscribedTask') :
|
t('task.subscription.subscribedTask') :
|
||||||
|
@ -130,11 +120,8 @@ async function subscribe() {
|
||||||
|
|
||||||
let message = ''
|
let message = ''
|
||||||
switch (props.entity) {
|
switch (props.entity) {
|
||||||
case 'namespace':
|
case 'project':
|
||||||
message = t('task.subscription.subscribeSuccessNamespace')
|
message = t('task.subscription.subscribeSuccessProject')
|
||||||
break
|
|
||||||
case 'list':
|
|
||||||
message = t('task.subscription.subscribeSuccessList')
|
|
||||||
break
|
break
|
||||||
case 'task':
|
case 'task':
|
||||||
message = t('task.subscription.subscribeSuccessTask')
|
message = t('task.subscription.subscribeSuccessTask')
|
||||||
|
@ -153,11 +140,8 @@ async function unsubscribe() {
|
||||||
|
|
||||||
let message = ''
|
let message = ''
|
||||||
switch (props.entity) {
|
switch (props.entity) {
|
||||||
case 'namespace':
|
case 'project':
|
||||||
message = t('task.subscription.unsubscribeSuccessNamespace')
|
message = t('task.subscription.unsubscribeSuccessProject')
|
||||||
break
|
|
||||||
case 'list':
|
|
||||||
message = t('task.subscription.unsubscribeSuccessList')
|
|
||||||
break
|
break
|
||||||
case 'task':
|
case 'task':
|
||||||
message = t('task.subscription.unsubscribeSuccessTask')
|
message = t('task.subscription.unsubscribeSuccessTask')
|
||||||
|
|
|
@ -48,10 +48,11 @@ const displayName = computed(() => getDisplayName(props.user))
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.user {
|
.user {
|
||||||
margin: .5rem;
|
display: flex;
|
||||||
|
justify-items: center;
|
||||||
|
|
||||||
&.is-inline {
|
&.is-inline {
|
||||||
display: inline;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<dropdown>
|
|
||||||
<template #trigger="triggerProps">
|
|
||||||
<slot name="trigger" v-bind="triggerProps">
|
|
||||||
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
|
|
||||||
<icon icon="ellipsis-h" class="icon"/>
|
|
||||||
</BaseButton>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="namespace.isArchived">
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
|
||||||
icon="archive"
|
|
||||||
>
|
|
||||||
{{ $t('menu.unarchive') }}
|
|
||||||
</dropdown-item>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id } }"
|
|
||||||
icon="pen"
|
|
||||||
>
|
|
||||||
{{ $t('menu.edit') }}
|
|
||||||
</dropdown-item>
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
|
|
||||||
icon="share-alt"
|
|
||||||
>
|
|
||||||
{{ $t('menu.share') }}
|
|
||||||
</dropdown-item>
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
|
|
||||||
icon="plus"
|
|
||||||
>
|
|
||||||
{{ $t('menu.newList') }}
|
|
||||||
</dropdown-item>
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
|
||||||
icon="archive"
|
|
||||||
>
|
|
||||||
{{ $t('menu.archive') }}
|
|
||||||
</dropdown-item>
|
|
||||||
<Subscription
|
|
||||||
class="has-no-shadow"
|
|
||||||
:is-button="false"
|
|
||||||
entity="namespace"
|
|
||||||
:entity-id="namespace.id"
|
|
||||||
:model-value="subscription"
|
|
||||||
@update:model-value="setSubscriptionInStore"
|
|
||||||
type="dropdown"
|
|
||||||
/>
|
|
||||||
<dropdown-item
|
|
||||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
|
||||||
icon="trash-alt"
|
|
||||||
class="has-text-danger"
|
|
||||||
>
|
|
||||||
{{ $t('menu.delete') }}
|
|
||||||
</dropdown-item>
|
|
||||||
</template>
|
|
||||||
</dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {ref, onMounted, type PropType} from 'vue'
|
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
|
||||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
|
||||||
import Subscription from '@/components/misc/subscription.vue'
|
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
|
||||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
namespace: {
|
|
||||||
type: Object as PropType<INamespace>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const namespaceStore = useNamespaceStore()
|
|
||||||
|
|
||||||
const subscription = ref<ISubscription | null>(null)
|
|
||||||
onMounted(() => {
|
|
||||||
subscription.value = props.namespace.subscription
|
|
||||||
})
|
|
||||||
|
|
||||||
function setSubscriptionInStore(sub: ISubscription) {
|
|
||||||
subscription.value = sub
|
|
||||||
namespaceStore.setNamespaceById({
|
|
||||||
...props.namespace,
|
|
||||||
subscription: sub,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.dropdown-trigger {
|
|
||||||
padding: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -20,7 +20,8 @@
|
||||||
:user="n.notification.doer"
|
:user="n.notification.doer"
|
||||||
:show-username="false"
|
:show-username="false"
|
||||||
:avatar-size="16"
|
:avatar-size="16"
|
||||||
v-if="n.notification.doer"/>
|
v-if="n.notification.doer"
|
||||||
|
/>
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
<div>
|
<div>
|
||||||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||||
|
@ -117,9 +118,9 @@ function to(n, index) {
|
||||||
case names.TASK_DELETED:
|
case names.TASK_DELETED:
|
||||||
// Nothing
|
// Nothing
|
||||||
break
|
break
|
||||||
case names.LIST_CREATED:
|
case names.PROJECT_CREATED:
|
||||||
to.name = 'task.index'
|
to.name = 'task.index'
|
||||||
to.params.listId = n.notification.list.id
|
to.params.projectId = n.notification.project.id
|
||||||
break
|
break
|
||||||
case names.TEAM_MEMBER_ADDED:
|
case names.TEAM_MEMBER_ADDED:
|
||||||
to.name = 'teams.edit'
|
to.name = 'teams.edit'
|
||||||
|
@ -145,12 +146,13 @@ function to(n, index) {
|
||||||
|
|
||||||
.trigger-button {
|
.trigger-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unread-indicator {
|
.unread-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: .75rem;
|
top: 1rem;
|
||||||
right: 1.15rem;
|
right: .5rem;
|
||||||
width: .75rem;
|
width: .75rem;
|
||||||
height: .75rem;
|
height: .75rem;
|
||||||
|
|
||||||
|
|
213
src/components/project/ProjectWrapper.vue
Normal file
213
src/components/project/ProjectWrapper.vue
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ 'is-loading': projectService.loading, 'is-archived': currentProject?.isArchived}"
|
||||||
|
class="loader-container"
|
||||||
|
>
|
||||||
|
<h1 class="project-title-print">
|
||||||
|
{{ getProjectTitle(currentProject) }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="switch-view-container d-print-none">
|
||||||
|
<div class="switch-view">
|
||||||
|
<BaseButton
|
||||||
|
v-shortcut="'g l'"
|
||||||
|
:title="$t('keyboardShortcuts.project.switchToListView')"
|
||||||
|
class="switch-view-button"
|
||||||
|
:class="{'is-active': viewName === 'project'}"
|
||||||
|
:to="{ name: 'project.list', params: { projectId } }"
|
||||||
|
>
|
||||||
|
{{ $t('project.list.title') }}
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
v-shortcut="'g g'"
|
||||||
|
:title="$t('keyboardShortcuts.project.switchToGanttView')"
|
||||||
|
class="switch-view-button"
|
||||||
|
:class="{'is-active': viewName === 'gantt'}"
|
||||||
|
:to="{ name: 'project.gantt', params: { projectId } }"
|
||||||
|
>
|
||||||
|
{{ $t('project.gantt.title') }}
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
v-shortcut="'g t'"
|
||||||
|
:title="$t('keyboardShortcuts.project.switchToTableView')"
|
||||||
|
class="switch-view-button"
|
||||||
|
:class="{'is-active': viewName === 'table'}"
|
||||||
|
:to="{ name: 'project.table', params: { projectId } }"
|
||||||
|
>
|
||||||
|
{{ $t('project.table.title') }}
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton
|
||||||
|
v-shortcut="'g k'"
|
||||||
|
:title="$t('keyboardShortcuts.project.switchToKanbanView')"
|
||||||
|
class="switch-view-button"
|
||||||
|
:class="{'is-active': viewName === 'kanban'}"
|
||||||
|
:to="{ name: 'project.kanban', params: { projectId } }"
|
||||||
|
>
|
||||||
|
{{ $t('project.kanban.title') }}
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
<CustomTransition name="fade">
|
||||||
|
<Message variant="warning" v-if="currentProject?.isArchived" class="mb-4">
|
||||||
|
{{ $t('project.archivedMessage') }}
|
||||||
|
</Message>
|
||||||
|
</CustomTransition>
|
||||||
|
|
||||||
|
<slot v-if="loadedProjectId"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed, watch} from 'vue'
|
||||||
|
import {useRoute} from 'vue-router'
|
||||||
|
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import Message from '@/components/misc/message.vue'
|
||||||
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
|
||||||
|
import ProjectModel from '@/models/project'
|
||||||
|
import ProjectService from '@/services/project'
|
||||||
|
|
||||||
|
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||||
|
import {saveProjectToHistory} from '@/modules/projectHistory'
|
||||||
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
projectId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
viewName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const baseStore = useBaseStore()
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
const projectService = ref(new ProjectService())
|
||||||
|
const loadedProjectId = ref(0)
|
||||||
|
|
||||||
|
const currentProject = computed(() => {
|
||||||
|
return typeof baseStore.currentProject === 'undefined' ? {
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
isArchived: false,
|
||||||
|
maxRight: null,
|
||||||
|
} : baseStore.currentProject
|
||||||
|
})
|
||||||
|
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
|
||||||
|
|
||||||
|
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
||||||
|
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||||
|
// This caused wired bugs where the project background would be set on the home page but only right after setting a new
|
||||||
|
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
|
||||||
|
// of it, most likely due to the rights not being properly populated.
|
||||||
|
watch(
|
||||||
|
() => props.projectId,
|
||||||
|
// loadProject
|
||||||
|
async (projectIdToLoad: number) => {
|
||||||
|
const projectData = {id: projectIdToLoad}
|
||||||
|
saveProjectToHistory(projectData)
|
||||||
|
|
||||||
|
// Don't load the project if we either already loaded it or aren't dealing with a project at all currently and
|
||||||
|
// the currently loaded project has the right set.
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
projectIdToLoad === loadedProjectId.value ||
|
||||||
|
typeof projectIdToLoad === 'undefined' ||
|
||||||
|
projectIdToLoad === currentProject.value?.id
|
||||||
|
)
|
||||||
|
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
|
||||||
|
) {
|
||||||
|
loadedProjectId.value = props.projectId
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`Loading project, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
|
||||||
|
|
||||||
|
// Set the current project to the one we're about to load so that the title is already shown at the top
|
||||||
|
loadedProjectId.value = 0
|
||||||
|
const projectFromStore = projectStore.projects[projectData.id]
|
||||||
|
if (projectFromStore) {
|
||||||
|
baseStore.handleSetCurrentProject({project: projectFromStore})
|
||||||
|
}
|
||||||
|
|
||||||
|
// We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
|
||||||
|
const project = new ProjectModel(projectData)
|
||||||
|
try {
|
||||||
|
const loadedProject = await projectService.value.get(project)
|
||||||
|
baseStore.handleSetCurrentProject({project: loadedProject})
|
||||||
|
} finally {
|
||||||
|
loadedProjectId.value = props.projectId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.switch-view-container {
|
||||||
|
@media screen and (max-width: $tablet) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-view {
|
||||||
|
background: var(--white);
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: .75rem;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
height: $switch-view-height;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-view-button {
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
display: block;
|
||||||
|
border-radius: $radius;
|
||||||
|
transition: all 100ms;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--switch-view-color);
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--switch-view-color);
|
||||||
|
background: var(--primary);
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this should be in notification and set via a prop
|
||||||
|
.is-archived .notification.is-warning {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title-print {
|
||||||
|
display: none;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,70 +1,72 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list-card"
|
class="project-card"
|
||||||
:class="{
|
:class="{
|
||||||
'has-light-text': background !== null,
|
'has-light-text': background !== null,
|
||||||
'has-background': blurHashUrl !== '' || background !== null
|
'has-background': blurHashUrl !== '' || background !== null
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
|
'border-left': project.hexColor ? `0.25rem solid ${project.hexColor}` : undefined,
|
||||||
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
|
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="list-background background-fade-in"
|
class="project-background background-fade-in"
|
||||||
:class="{'is-visible': background}"
|
:class="{'is-visible': background}"
|
||||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||||
/>
|
/>
|
||||||
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
<span v-if="project.isArchived" class="is-archived" >{{ $t('project.archived') }}</span>
|
||||||
|
|
||||||
<div class="list-title" aria-hidden="true">{{ list.title }}</div>
|
<div class="project-title" aria-hidden="true">
|
||||||
|
<span v-if="project.id < -1" class="saved-filter-icon icon">
|
||||||
|
<icon icon="filter"/>
|
||||||
|
</span>
|
||||||
|
{{ project.title }}
|
||||||
|
</div>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
class="list-button"
|
class="project-button"
|
||||||
:aria-label="list.title"
|
:aria-label="project.title"
|
||||||
:title="list.description"
|
:title="project.description"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'list.index',
|
name: 'project.index',
|
||||||
params: { listId: list.id}
|
params: { projectId: project.id}
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="!list.isArchived"
|
v-if="!project.isArchived"
|
||||||
class="favorite"
|
class="favorite"
|
||||||
:class="{'is-favorite': list.isFavorite}"
|
:class="{'is-favorite': project.isFavorite}"
|
||||||
@click.prevent.stop="listStore.toggleListFavorite(list)"
|
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
||||||
>
|
>
|
||||||
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
|
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {toRef, type PropType} from 'vue'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
|
||||||
import {useListBackground} from './useListBackground'
|
import {useProjectBackground} from './useProjectBackground'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
const props = defineProps({
|
const {
|
||||||
list: {
|
project,
|
||||||
type: Object as PropType<IList>,
|
} = defineProps<{
|
||||||
required: true,
|
project: IProject,
|
||||||
},
|
}>()
|
||||||
})
|
|
||||||
|
|
||||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
const {background, blurHashUrl} = useProjectBackground(project)
|
||||||
|
|
||||||
const listStore = useListStore()
|
const projectStore = useProjectStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.list-card {
|
.project-card {
|
||||||
--list-card-padding: 1rem;
|
--project-card-padding: 1rem;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
padding: var(--list-card-padding);
|
padding: var(--project-card-padding);
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
transition: box-shadow $transition;
|
transition: box-shadow $transition;
|
||||||
|
@ -91,14 +93,14 @@ const listStore = useListStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-background,
|
.has-background,
|
||||||
.list-background {
|
.project-background {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-background,
|
.project-background,
|
||||||
.list-button {
|
.project-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -111,7 +113,7 @@ const listStore = useListStore()
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title {
|
.project-title {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
font-family: $vikunja-font;
|
font-family: $vikunja-font;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -120,7 +122,7 @@ const listStore = useListStore()
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
max-height: calc(100% - (var(--list-card-padding) + 1rem)); // padding & height of the "is archived" badge
|
max-height: calc(100% - (var(--project-card-padding) + 1rem)); // padding & height of the "is archived" badge
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
@ -130,11 +132,11 @@ const listStore = useListStore()
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-light-text .list-title {
|
.has-light-text .project-title {
|
||||||
color: var(--grey-100);
|
color: var(--grey-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-background .list-title {
|
.has-background .project-title {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 10px var(--black),
|
0 0 10px var(--black),
|
||||||
1px 1px 5px var(--grey-700),
|
1px 1px 5px var(--grey-700),
|
||||||
|
@ -144,8 +146,8 @@ const listStore = useListStore()
|
||||||
|
|
||||||
.favorite {
|
.favorite {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--list-card-padding);
|
top: var(--project-card-padding);
|
||||||
right: var(--list-card-padding);
|
right: var(--project-card-padding);
|
||||||
transition: opacity $transition, color $transition;
|
transition: opacity $transition, color $transition;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
|
@ -161,11 +163,11 @@ const listStore = useListStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(hover: hover) and (pointer: fine) {
|
@media(hover: hover) and (pointer: fine) {
|
||||||
.list-card .favorite {
|
.project-card .favorite {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-card:hover .favorite {
|
.project-card:hover .favorite {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,4 +181,9 @@ const listStore = useListStore()
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saved-filter-icon {
|
||||||
|
color: var(--grey-300);
|
||||||
|
font-size: .75em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
73
src/components/project/partials/ProjectCardGrid.vue
Normal file
73
src/components/project/partials/ProjectCardGrid.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<ul class="project-grid">
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in filteredProjects"
|
||||||
|
:key="`project_${item.id}_${index}`"
|
||||||
|
class="project-grid-item"
|
||||||
|
>
|
||||||
|
<ProjectCard :project="item" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed, type PropType} from 'vue'
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
import ProjectCard from './ProjectCard.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
projects: {
|
||||||
|
type: Array as PropType<IProject[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showArchived: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
itemLimit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
return props.showArchived
|
||||||
|
? props.projects
|
||||||
|
: props.projects.filter(l => !l.isArchived)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.project-grid {
|
||||||
|
--project-grid-item-height: 150px;
|
||||||
|
--project-grid-gap: 1rem;
|
||||||
|
margin: 0; // reset li
|
||||||
|
list-style-type: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--project-grid-columns), 1fr);
|
||||||
|
grid-auto-rows: var(--project-grid-item-height);
|
||||||
|
gap: var(--project-grid-gap);
|
||||||
|
|
||||||
|
@media screen and (min-width: $mobile) {
|
||||||
|
--project-grid-columns: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $mobile) and (max-width: $tablet) {
|
||||||
|
--project-grid-columns: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $tablet) and (max-width: $widescreen) {
|
||||||
|
--project-grid-columns: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $widescreen) {
|
||||||
|
--project-grid-columns: 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-grid-item {
|
||||||
|
display: grid;
|
||||||
|
margin-top: 0; // remove padding coming form .content li + li
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,7 +32,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, watch} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
|
|
||||||
import Filters from '@/components/list/partials/filters.vue'
|
import Filters from '@/components/project/partials/filters.vue'
|
||||||
|
|
||||||
import {getDefaultParams} from '@/composables/useTaskList'
|
import {getDefaultParams} from '@/composables/useTaskList'
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user