mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-10-30 05:57:10 +00:00
Compare commits
615 Commits
v1.66.6
...
355beb8394
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
355beb8394 | ||
|
|
d82eeba792 | ||
|
|
0396b05e58 | ||
|
|
9494a9eb37 | ||
|
|
8238f84ac7 | ||
|
|
1cf19b2a50 | ||
|
|
e8543fd2f8 | ||
|
|
c7f360e1fa | ||
|
|
a2b83f7f0c | ||
|
|
ae5689a403 | ||
|
|
c46af1d286 | ||
|
|
d17ba2168c | ||
|
|
80c14716eb | ||
|
|
8541fcd29b | ||
|
|
65d6acd7fb | ||
|
|
8b5f83d6b2 | ||
|
|
5e18891f4b | ||
|
|
74e0b85748 | ||
|
|
81d3495b65 | ||
|
|
d1959ca09f | ||
|
|
ec7a5ecf10 | ||
|
|
70b9ec99ba | ||
|
|
7147d79166 | ||
|
|
1dad9316bd | ||
|
|
872f7dcf48 | ||
|
|
02b450325e | ||
|
|
136bc4cbb9 | ||
|
|
dab49df9aa | ||
|
|
6286087f3e | ||
|
|
4ce7160f79 | ||
|
|
2913bf19b0 | ||
|
|
7bd6be6fd0 | ||
|
|
705daa286b | ||
|
|
614d06be66 | ||
|
|
dec3e9a7ce | ||
|
|
0017ac3373 | ||
|
|
ae34744578 | ||
|
|
76885058ef | ||
|
|
fccb007036 | ||
|
|
a9f8901bd5 | ||
|
|
8ae968b5be | ||
|
|
beffd45e4f | ||
|
|
4488d81e8d | ||
|
|
618cc8c5f1 | ||
|
|
3fb22a877e | ||
|
|
8759409b82 | ||
|
|
245647ae6a | ||
|
|
eb7d33ea07 | ||
|
|
3575b16def | ||
|
|
a6fb680be8 | ||
|
|
9e17df5544 | ||
|
|
683fde7be4 | ||
|
|
ee68ce92a2 | ||
|
|
8b4e38d795 | ||
|
|
4995202627 | ||
|
|
986b997a6a | ||
|
|
9a957af759 | ||
|
|
c5a0a96016 | ||
|
|
8715a6c0ac | ||
|
|
c9810095aa | ||
|
|
69eb888469 | ||
|
|
748347df9a | ||
|
|
aa4d49027c | ||
|
|
a9d7387e40 | ||
|
|
dc4d260c9b | ||
|
|
dc430491bf | ||
|
|
42cd261ea7 | ||
|
|
35af4fdc09 | ||
|
|
8bb4998e59 | ||
|
|
c825a3f4c4 | ||
|
|
5343c34488 | ||
|
|
4878be1a53 | ||
|
|
1ff689c26c | ||
|
|
79b660e899 | ||
|
|
665a679bd5 | ||
|
|
7bd634eb95 | ||
|
|
c3b5a77a86 | ||
|
|
8498846d9c | ||
|
|
12f39a0133 | ||
|
|
ffc2a86e95 | ||
|
|
82babf41a2 | ||
|
|
81055b4fbd | ||
|
|
5070a59f88 | ||
|
|
65d5bf960d | ||
|
|
8fc4cb190e | ||
|
|
c6c065dbb9 | ||
|
|
8ba34533d7 | ||
|
|
095a4b2362 | ||
|
|
fafc631e49 | ||
|
|
e56383c8b1 | ||
|
|
b9c26bdb04 | ||
|
|
8aeaa81752 | ||
|
|
b16ec0490f | ||
|
|
eceaf1d73b | ||
|
|
34cf668a33 | ||
|
|
c22d410c9f | ||
|
|
fc6af867f2 | ||
|
|
2d96114984 | ||
|
|
fd7e19e490 | ||
|
|
f7d996f5b2 | ||
|
|
f8ab1383ab | ||
|
|
e1559aac94 | ||
|
|
2e17cce5cd | ||
|
|
8fb831f171 | ||
|
|
cb84f34515 | ||
|
|
272cce1a77 | ||
|
|
e0e3ed1580 | ||
|
|
c4c848cf37 | ||
|
|
d33a2e3a5b | ||
|
|
5c8753fb96 | ||
|
|
32d25d86eb | ||
|
|
863adccac1 | ||
|
|
2d527e1d16 | ||
|
|
9a64ad6fa7 | ||
|
|
5ce472ebff | ||
|
|
76588af12f | ||
|
|
134f169eb9 | ||
|
|
7c2d731c4c | ||
|
|
c7e2a290cf | ||
|
|
5ea966892a | ||
|
|
b879db76b7 | ||
|
|
d13a628029 | ||
|
|
7c1e2595e3 | ||
|
|
a99e8a915e | ||
|
|
36f424da0b | ||
|
|
c0a65d5a23 | ||
|
|
02e31333d2 | ||
|
|
d69616119d | ||
|
|
f89cd5f44f | ||
|
|
abe4951251 | ||
|
|
dbc770d40b | ||
|
|
e69a8fece5 | ||
|
|
cf20be8a77 | ||
|
|
450bcb649c | ||
|
|
a00395351e | ||
|
|
3b24c760ff | ||
|
|
3801f0be18 | ||
|
|
5508fbee2f | ||
|
|
51ff4e7f36 | ||
|
|
f3104db2e4 | ||
|
|
602b1028c3 | ||
|
|
4f98e979a2 | ||
|
|
e0f46c4af7 | ||
|
|
bc8a9a2b85 | ||
|
|
c2b03f925d | ||
|
|
efa2e52054 | ||
|
|
cedf5761f8 | ||
|
|
f601bb8751 | ||
|
|
c39d2a56d2 | ||
|
|
34d3d92afd | ||
|
|
805722bbe8 | ||
|
|
fe3e38343b | ||
|
|
616e82c497 | ||
|
|
ab7e47b91f | ||
|
|
cf1c103a46 | ||
|
|
71202a4a29 | ||
|
|
a7e0ceac4c | ||
|
|
6bce701aab | ||
|
|
f8b9e206a5 | ||
|
|
4c1ec2004b | ||
|
|
ebed74d239 | ||
|
|
c789b69b54 | ||
|
|
24c32511d8 | ||
|
|
302fb0642d | ||
|
|
06e7b6e3eb | ||
|
|
33acd55eaa | ||
|
|
dec82e89c2 | ||
|
|
f5ac5bc4ec | ||
|
|
b6c680e802 | ||
|
|
5fa57c13b4 | ||
|
|
acc81fda44 | ||
|
|
7ab5acf45f | ||
|
|
0d4ffbcc22 | ||
|
|
a9253ac2df | ||
|
|
d00b4843a7 | ||
|
|
6068de2c71 | ||
|
|
73da427c6b | ||
|
|
9b7ec0ddfe | ||
|
|
c2f5f14c44 | ||
|
|
0b7c3588d5 | ||
|
|
a51fac5736 | ||
|
|
726c3d0704 | ||
|
|
8dd564dbd0 | ||
|
|
e33c65cddc | ||
|
|
f2fbd2ead0 | ||
|
|
123a2e45eb | ||
|
|
f8d2d9c680 | ||
|
|
9dcbef9a79 | ||
|
|
0b14857a12 | ||
|
|
bd3d516f60 | ||
|
|
40d0bd8cea | ||
|
|
873946a1a6 | ||
|
|
968deeb254 | ||
|
|
959041be52 | ||
|
|
3319520179 | ||
|
|
580fcf3657 | ||
|
|
53dae7c520 | ||
|
|
6d59d709f1 | ||
|
|
4343e9070c | ||
|
|
b62373fb5f | ||
|
|
3da98f8e56 | ||
|
|
494d24952e | ||
|
|
8a6b17bd7b | ||
|
|
d2e859a74e | ||
|
|
4a78d55d22 | ||
|
|
dc252b8c4b | ||
|
|
c433205e89 | ||
|
|
d6bc5b57b1 | ||
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce | ||
|
|
46c1ccdfcc | ||
|
|
8817536038 | ||
|
|
c3bb23a6ee | ||
|
|
7e9c4c575e | ||
|
|
5a70eee91e | ||
|
|
228f6990a1 | ||
|
|
d80ed0e70e | ||
|
|
4576c75737 | ||
|
|
67764faaa7 | ||
|
|
91dd0b27ae | ||
|
|
99dcf49fbc | ||
|
|
6fb3edbfd6 | ||
|
|
26f13ce857 | ||
|
|
e9b475c0a8 | ||
|
|
7752010092 | ||
|
|
d3705b3ed7 | ||
|
|
1394e2897e | ||
|
|
5117a1c5af | ||
|
|
3c62403f33 | ||
|
|
a4760f5162 | ||
|
|
b071070431 | ||
|
|
3bcb9628e7 | ||
|
|
e62c4cf5bf | ||
|
|
af46962ce4 | ||
|
|
0b0967830b | ||
|
|
172251a208 | ||
|
|
8a6fb63d55 | ||
|
|
9652959e5e | ||
|
|
825ef46d41 | ||
|
|
ad9f7c6b95 | ||
|
|
b960b5c149 | ||
|
|
0f092d21f9 | ||
|
|
031576caa6 | ||
|
|
7a97a96c42 | ||
|
|
2efb2daba0 | ||
|
|
4374c39924 | ||
|
|
15711495c7 | ||
|
|
236f803427 | ||
|
|
6772130f2a | ||
|
|
ddd72f3fac | ||
|
|
6e262835ef | ||
|
|
2f3b8ddc5f | ||
|
|
cea3a74b34 | ||
|
|
867941a233 | ||
|
|
3ff388a16d | ||
|
|
f4248e9ab9 | ||
|
|
507b3289c7 | ||
|
|
9e1dfc48d5 | ||
|
|
518cbc7b5d | ||
|
|
ccc8db0620 | ||
|
|
7cfb663efd | ||
|
|
e5103cc925 | ||
|
|
26458f5a19 | ||
|
|
79d5ec6caf | ||
|
|
034d461ab6 | ||
|
|
2e9c1c170c | ||
|
|
24ad3b2c61 | ||
|
|
288f55dc2f | ||
|
|
78dbea6267 | ||
|
|
6a9e53141d | ||
|
|
05e6994520 | ||
|
|
1a4dc67eb9 | ||
|
|
31d87a116b | ||
|
|
c47796d590 | ||
|
|
c7138a41ee | ||
|
|
96f04c70a9 | ||
|
|
87a8bc09ab | ||
|
|
5f5661d559 | ||
|
|
35ca87790e | ||
|
|
ae43e4a57c | ||
|
|
b91712a01a | ||
|
|
b20007b341 | ||
|
|
6a24e1188b | ||
|
|
5894efc1aa | ||
|
|
a05612d243 | ||
|
|
48de874d6b | ||
|
|
91e6da316f | ||
|
|
fa60bd81a1 | ||
|
|
a08a69c5be | ||
|
|
18d450a41a | ||
|
|
36cdee61c0 | ||
|
|
797e188259 | ||
|
|
91b581668a | ||
|
|
ad01fec28f | ||
|
|
357d3a0df6 | ||
|
|
5ce6022761 | ||
|
|
235a0c5aea | ||
|
|
9b81fa6ebb | ||
|
|
8792d5ab0e | ||
|
|
d46ed0c078 | ||
|
|
73c433fcd2 | ||
|
|
02b5239220 | ||
|
|
0ed3bdfcb0 | ||
|
|
bdeb89011f | ||
|
|
1523b625bc | ||
|
|
fb91eeb692 | ||
|
|
601d2e02cb | ||
|
|
0a662d34eb | ||
|
|
5cd4693e9d | ||
|
|
f3f0f860e3 | ||
|
|
93a5cf8a79 | ||
|
|
7cf15cbc21 | ||
|
|
30bc6d20b2 | ||
|
|
b39f99fde4 | ||
|
|
0e8aa9efa4 | ||
|
|
e1fcde36e3 | ||
|
|
7aafe077d3 | ||
|
|
5b8cab5e76 | ||
|
|
4ab56af40a | ||
|
|
e8cea86a76 | ||
|
|
d0a6e0b358 | ||
|
|
8831b3e970 | ||
|
|
f6db6f0914 | ||
|
|
ab8baeedd1 | ||
|
|
eccee5e72e | ||
|
|
4d93055bda | ||
|
|
c60c16e56a | ||
|
|
99b1de5647 | ||
|
|
7efe11a421 | ||
|
|
954108856a | ||
|
|
cbca745ec4 | ||
|
|
e15e7c8f8d | ||
|
|
65e8a520e5 | ||
|
|
3926af5a6d | ||
|
|
556fb33223 | ||
|
|
82295adeab | ||
|
|
efabf060c7 | ||
|
|
96e434ebf5 | ||
|
|
d81e2567cc | ||
|
|
f8d487639f | ||
|
|
cecfbb5375 | ||
|
|
8d35500e2f | ||
|
|
5dad5d8e03 | ||
|
|
9d7d4fad2e | ||
|
|
7be64bde02 | ||
|
|
48eb7552a9 | ||
|
|
5347b0060c | ||
|
|
b826c03226 | ||
|
|
1c211a8667 | ||
|
|
fd4d5b90e2 | ||
|
|
1ee9f26b34 | ||
|
|
da1762934b | ||
|
|
511457c761 | ||
|
|
29b4cedb81 | ||
|
|
585de15e6b | ||
|
|
74f7ad155d | ||
|
|
a9bf118f3a | ||
|
|
6d5a432bad | ||
|
|
f1f12abd16 | ||
|
|
09880a54e9 | ||
|
|
0f6847b16d | ||
|
|
ce82ed97f5 | ||
|
|
36b393dbde | ||
|
|
524c283a0d | ||
|
|
afda53a9bc | ||
|
|
1310d75012 | ||
|
|
80bbde549d | ||
|
|
2451487593 | ||
|
|
ecd626f105 | ||
|
|
123b312965 | ||
|
|
e94de8e629 | ||
|
|
956a5a04ca | ||
|
|
affeb7c624 | ||
|
|
e457d94df8 | ||
|
|
e9583c928e | ||
|
|
89c14628e1 | ||
|
|
ffba407eaf | ||
|
|
33f710127c | ||
|
|
7a82b2c102 | ||
|
|
2db2a47186 | ||
|
|
63faa43c1d | ||
|
|
eabb0e8470 | ||
|
|
9f75ae6b03 | ||
|
|
a1f28cd245 | ||
|
|
90a04b517e | ||
|
|
9f6e6a333f | ||
|
|
7b9e2c4fd9 | ||
|
|
63f13711cc | ||
|
|
c65b8e5ebd | ||
|
|
bfed1480ae | ||
|
|
5ff902f185 | ||
|
|
8d38345c7f | ||
|
|
14be9dbb8a | ||
|
|
720c26db94 | ||
|
|
6d0b8b845d | ||
|
|
b2767e000e | ||
|
|
169f23c2ca | ||
|
|
81f70eafff | ||
|
|
650170498a | ||
|
|
8b6f600989 | ||
|
|
fe3617b39f | ||
|
|
0f466c51ba | ||
|
|
a1a641bce3 | ||
|
|
4764c25eb1 | ||
|
|
d390455cf2 | ||
|
|
f58ebad0ec | ||
|
|
7ca4eb3b8f | ||
|
|
7fb8d24d73 | ||
|
|
472dbaa68b | ||
|
|
f03448007d | ||
|
|
c317a8bff9 | ||
|
|
618cca39a4 | ||
|
|
fe7a98098f | ||
|
|
df49939990 | ||
|
|
f23f2776f4 | ||
|
|
4419c86164 | ||
|
|
9848f49b49 | ||
|
|
679bd782a8 | ||
|
|
6a316e3906 | ||
|
|
c129db8474 | ||
|
|
10035b4c91 | ||
|
|
5839271de7 | ||
|
|
47db8ef709 | ||
|
|
2656491aaa | ||
|
|
a7637c9cae | ||
|
|
7b83ed8205 | ||
|
|
00cbc77f1d | ||
|
|
4d75b256c4 | ||
|
|
5aeff7c40c | ||
|
|
6a543bf644 | ||
|
|
dfb035525d | ||
|
|
4c23069a0a | ||
|
|
4a1d7be44c | ||
|
|
798aec1b74 | ||
|
|
7914d7e151 | ||
|
|
26d0392da1 | ||
|
|
83b1406cce | ||
|
|
8b579d6837 | ||
|
|
c0fd20dfff | ||
|
|
dd6b67c6e6 | ||
|
|
fa83185cf5 | ||
|
|
97d5010d41 | ||
|
|
e73ad93920 | ||
|
|
425af246fb | ||
|
|
a2912ba0ff | ||
|
|
48ff2f4413 | ||
|
|
61cd281a18 | ||
|
|
6e28134282 | ||
|
|
d1377f44d2 | ||
|
|
d261c6186b | ||
|
|
2a72a2612d | ||
|
|
66bb4f87d4 | ||
|
|
977b1ad083 | ||
|
|
94db18d42b | ||
|
|
7e0375108d | ||
|
|
094a5d7b62 | ||
|
|
8f947a5f04 | ||
|
|
5580ad62f9 | ||
|
|
c0953dc954 | ||
|
|
1df93da564 | ||
|
|
e2252a9d72 | ||
|
|
7cdba4b507 | ||
|
|
b110d5afec | ||
|
|
6112b3e399 | ||
|
|
af0869a39b | ||
|
|
d44c339990 | ||
|
|
0304f92ad9 | ||
|
|
4a41d6e5d5 | ||
|
|
30893ca68e | ||
|
|
1edd02fa5c | ||
|
|
10a957ff0d | ||
|
|
35b1b3619d | ||
|
|
777ebd0c41 | ||
|
|
9e7d8c08e1 | ||
|
|
ad3f4cda09 | ||
|
|
98502cc6ae | ||
|
|
0bac671eb0 | ||
|
|
09c9a1e752 | ||
|
|
41e77e8336 | ||
|
|
a6e9fee2a0 | ||
|
|
c403a1cee5 | ||
|
|
02d25b370a | ||
|
|
e5a3eec8a1 | ||
|
|
910352d66c | ||
|
|
c4f02e7d55 | ||
|
|
6a1197ad83 | ||
|
|
84c31bbb88 | ||
|
|
33f6c32306 | ||
|
|
5c71304d41 | ||
|
|
bbaf04e977 | ||
|
|
ad5b2d2eb3 | ||
|
|
3c064baf8a | ||
|
|
9e11b10d74 | ||
|
|
2fc45e00b4 | ||
|
|
45eb08fc3a | ||
|
|
fbcfae0200 | ||
|
|
9c4ce013ec | ||
|
|
5dba7c12f0 | ||
|
|
db793c80c8 | ||
|
|
aa4a3f1aa9 | ||
|
|
3424639309 | ||
|
|
0f309a29ba | ||
|
|
e13b8846b8 | ||
|
|
d5ea4d6129 | ||
|
|
9d50bfedbd | ||
|
|
b03410083c | ||
|
|
a314b1e448 | ||
|
|
e8a51a85c4 | ||
|
|
d4074f966c | ||
|
|
1413b41824 | ||
|
|
379c1edec3 | ||
|
|
58b5bade9e | ||
|
|
71aee4cd3e | ||
|
|
10bab0cfa1 | ||
|
|
698350b0f7 | ||
|
|
a97cf25031 | ||
|
|
8302d088bd | ||
|
|
64788e73de | ||
|
|
114fd471e8 | ||
|
|
b24a3120d3 | ||
|
|
c5f93b3d0a | ||
|
|
79290e4721 | ||
|
|
984e126f23 | ||
|
|
cd1ad31aed | ||
|
|
1e3f6cf9e7 | ||
|
|
9c6ccd9a8a | ||
|
|
681ba21d39 | ||
|
|
aef62189ee | ||
|
|
09f70ac817 | ||
|
|
1eacb22143 | ||
|
|
8524bad377 | ||
|
|
9d899243d1 | ||
|
|
9acf20a639 | ||
|
|
71ef6b2e82 | ||
|
|
5e34d95dd2 | ||
|
|
25a809c064 | ||
|
|
f760498150 | ||
|
|
328301a375 | ||
|
|
f28e7ebbbb | ||
|
|
bfa84af71e | ||
|
|
42cd1ba976 | ||
|
|
88cba866fd | ||
|
|
af2876a84b | ||
|
|
c5b15bfa78 | ||
|
|
85f00a63c2 | ||
|
|
05f427bcd7 | ||
|
|
69f4c41534 | ||
|
|
30b9239a8b | ||
|
|
2061a83c59 | ||
|
|
24e723de07 | ||
|
|
27b5694885 | ||
|
|
4093f28cee | ||
|
|
08aaf2f2dd | ||
|
|
5a927e5ba5 | ||
|
|
10fafcf59f | ||
|
|
be87591801 | ||
|
|
086d4378d3 | ||
|
|
e982275905 | ||
|
|
77c02703e9 | ||
|
|
0ef27d4f95 | ||
|
|
5edc27744e | ||
|
|
02ff887fee | ||
|
|
3a30eeb59f | ||
|
|
79af8fb601 | ||
|
|
f9f00faa0e | ||
|
|
a3c41e84e4 | ||
|
|
7f21f33351 | ||
|
|
568f682cee | ||
|
|
901c4c8ca4 | ||
|
|
3dbba97f9c | ||
|
|
3475620267 | ||
|
|
8936a5e5d8 | ||
|
|
719e34f9bc | ||
|
|
df955ff8b0 | ||
|
|
4dc74022b4 | ||
|
|
c2b7d07208 | ||
|
|
21e76a6f05 | ||
|
|
89c0d6fad6 | ||
|
|
b74d15b1ee | ||
|
|
10313438bf | ||
|
|
a764217948 | ||
|
|
d81d6fb937 | ||
|
|
4c0f7ab7f9 | ||
|
|
1c48945a96 | ||
|
|
850901f62f | ||
|
|
4822854e30 | ||
|
|
f580538331 | ||
|
|
0d70c555e6 | ||
|
|
c5f6cf0080 | ||
|
|
6ff7b3bc9a | ||
|
|
346c2c65b0 | ||
|
|
a6445fd500 | ||
|
|
358e43e508 | ||
|
|
20c5ba6b63 | ||
|
|
661658a6e8 | ||
|
|
0a6f224ed3 | ||
|
|
e7bb29693f | ||
|
|
32dfd50461 | ||
|
|
bfec385dce | ||
|
|
04278f99d7 | ||
|
|
064a36fcbb | ||
|
|
854524a03c |
13
.check.exs
13
.check.exs
@@ -13,8 +13,8 @@
|
||||
|
||||
## list of tools (see `mix check` docs for a list of default curated tools)
|
||||
tools: [
|
||||
## curated tools may be disabled (e.g. the check for compilation warnings)
|
||||
{:compiler, false},
|
||||
## Allow compilation warnings for now (error budget: unlimited warnings)
|
||||
{:compiler, "mix compile"},
|
||||
|
||||
## ...or have command & args adjusted (e.g. enable skip comments for sobelow)
|
||||
# {:sobelow, "mix sobelow --exit --skip"},
|
||||
@@ -22,10 +22,15 @@
|
||||
## ...or reordered (e.g. to see output from dialyzer before others)
|
||||
# {:dialyzer, order: -1},
|
||||
|
||||
## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
|
||||
## Credo with relaxed error budget: max 200 issues
|
||||
{:credo, "mix credo --strict --max-issues 200"},
|
||||
|
||||
## Dialyzer but don't halt on exit (allow warnings)
|
||||
{:dialyzer, "mix dialyzer"},
|
||||
|
||||
## Tests without warnings-as-errors for now
|
||||
{:ex_unit, "mix test"},
|
||||
{:doctor, false},
|
||||
{:ex_unit, false},
|
||||
{:npm_test, false},
|
||||
{:sobelow, false}
|
||||
|
||||
|
||||
18
.credo.exs
18
.credo.exs
@@ -82,8 +82,6 @@
|
||||
# You can customize the priority of any check
|
||||
# Priority values are: `low, normal, high, higher`
|
||||
#
|
||||
{Credo.Check.Design.AliasUsage,
|
||||
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||
# You can also customize the exit_status of each check.
|
||||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
@@ -99,10 +97,9 @@
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleDoc, false},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
@@ -121,14 +118,12 @@
|
||||
#
|
||||
{Credo.Check.Refactor.Apply, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, []},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.MapJoin, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
@@ -196,10 +191,19 @@
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||
{Credo.Check.Warning.MixEnv, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||
{Credo.Check.Warning.UnsafeToAtom, []},
|
||||
|
||||
# {Credo.Check.Refactor.MapInto, []},
|
||||
|
||||
#
|
||||
# Temporarily disable checks that generate too many issues
|
||||
# to get under the 200 issue budget
|
||||
#
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Design.AliasUsage, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []}
|
||||
|
||||
#
|
||||
# Custom checks can be created using `mix credo.gen.check`.
|
||||
#
|
||||
|
||||
127
.credo.test.exs
Normal file
127
.credo.test.exs
Normal file
@@ -0,0 +1,127 @@
|
||||
# Credo configuration specific to test files
|
||||
# This enforces stricter quality standards for test code
|
||||
|
||||
%{
|
||||
configs: [
|
||||
%{
|
||||
name: "test",
|
||||
files: %{
|
||||
included: ["test/"],
|
||||
excluded: ["test/support/"]
|
||||
},
|
||||
requires: [],
|
||||
strict: true,
|
||||
color: true,
|
||||
checks: [
|
||||
# Consistency checks
|
||||
{Credo.Check.Consistency.ExceptionNames, []},
|
||||
{Credo.Check.Consistency.LineEndings, []},
|
||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||
|
||||
# Design checks - stricter for tests
|
||||
{Credo.Check.Design.AliasUsage, priority: :high},
|
||||
# Lower threshold for tests
|
||||
{Credo.Check.Design.DuplicatedCode, mass_threshold: 25},
|
||||
{Credo.Check.Design.TagTODO, []},
|
||||
{Credo.Check.Design.TagFIXME, []},
|
||||
|
||||
# Readability checks - very important for tests
|
||||
{Credo.Check.Readability.AliasOrder, []},
|
||||
{Credo.Check.Readability.FunctionNames, []},
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
# Slightly longer for test descriptions
|
||||
{Credo.Check.Readability.MaxLineLength, max_length: 120},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
# Not required for test modules
|
||||
{Credo.Check.Readability.ModuleDoc, false},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||
{Credo.Check.Readability.Semicolons, []},
|
||||
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||
{Credo.Check.Readability.StringSigils, []},
|
||||
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||
{Credo.Check.Readability.VariableNames, []},
|
||||
{Credo.Check.Readability.WithSingleClause, []},
|
||||
|
||||
# Test-specific readability checks
|
||||
# Discourage single pipes in tests
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
# Specs not needed in tests
|
||||
{Credo.Check.Readability.Specs, false},
|
||||
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||
|
||||
# Refactoring opportunities - important for test maintainability
|
||||
# Higher limit for complex test setups
|
||||
{Credo.Check.Refactor.ABCSize, max_size: 50},
|
||||
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 10},
|
||||
# Lower for test helpers
|
||||
{Credo.Check.Refactor.FunctionArity, max_arity: 4},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MapInto, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
# Keep tests flat
|
||||
{Credo.Check.Refactor.Nesting, max_nesting: 3},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
{Credo.Check.Refactor.RejectReject, []},
|
||||
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||
|
||||
# Warnings - all should be fixed
|
||||
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||
{Credo.Check.Warning.IExPry, []},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||
{Credo.Check.Warning.UnusedListOperation, []},
|
||||
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||
{Credo.Check.Warning.UnsafeExec, []},
|
||||
|
||||
# Test-specific checks
|
||||
# Important for test isolation
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
|
||||
# Custom checks for test patterns
|
||||
{
|
||||
Credo.Check.Refactor.PipeChainStart,
|
||||
# Factory functions
|
||||
excluded_functions: ["build", "create", "insert"],
|
||||
excluded_argument_types: [:atom, :number]
|
||||
}
|
||||
],
|
||||
|
||||
# Disable these checks for test files
|
||||
disabled: [
|
||||
# Tests don't need module docs
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
# Tests don't need specs
|
||||
{Credo.Check.Readability.Specs, []},
|
||||
# Common in test setup
|
||||
{Credo.Check.Refactor.VariableRebinding, []}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
39
.devcontainer/setup.sh
Executable file
39
.devcontainer/setup.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "→ fetching & compiling deps"
|
||||
mix deps.get
|
||||
mix compile
|
||||
|
||||
# only run Ecto if the project actually has those tasks
|
||||
if mix help | grep -q "ecto.create"; then
|
||||
echo "→ waiting for database to be ready..."
|
||||
|
||||
# Wait for database to be ready
|
||||
DB_HOST=${DB_HOST:-db}
|
||||
timeout=60
|
||||
while ! nc -z $DB_HOST 5432 2>/dev/null; do
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "❌ Database connection timeout"
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for database... ($timeout seconds remaining)"
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
|
||||
# Give the database a bit more time to fully initialize
|
||||
echo "→ giving database 2 more seconds to fully initialize..."
|
||||
sleep 2
|
||||
|
||||
echo "→ database is ready, running ecto.create && ecto.migrate"
|
||||
mix ecto.create --quiet
|
||||
mix ecto.migrate
|
||||
fi
|
||||
|
||||
cd assets
|
||||
echo "→ installing JS & CSS dependencies"
|
||||
yarn install --frozen-lockfile
|
||||
echo "→ building assets"
|
||||
|
||||
echo "✅ setup complete"
|
||||
@@ -8,4 +8,9 @@ export GIT_SHA="1111"
|
||||
export WANDERER_INVITES="false"
|
||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||
export WANDERER_CHARACTER_API_DISABLED="false"
|
||||
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
||||
export WANDERER_KILLS_SERVICE_ENABLED="true"
|
||||
export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
|
||||
export WANDERER_SSE_ENABLED="true"
|
||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||
|
||||
109
.github/workflows/advanced-test.yml
vendored
Normal file
109
.github/workflows/advanced-test.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
name: Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-test:
|
||||
name: 🚀 Deploy to test env (fly.io)
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.base_ref == 'develop' || (github.ref == 'refs/heads/develop' && github.event_name == 'push') }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
|
||||
- name: 👀 Read app name
|
||||
uses: SebRollen/toml-action@v1.0.0
|
||||
id: app_name
|
||||
with:
|
||||
file: "fly.toml"
|
||||
field: "app"
|
||||
|
||||
- name: 🚀 Deploy Test
|
||||
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ (github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
matrix:
|
||||
otp: ["27"]
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Elixir
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{matrix.otp}}
|
||||
elixir-version: ${{matrix.elixir}}
|
||||
# nix build would also work here because `todos` is the default package
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
${{ runner.os }}-build-
|
||||
# Step: Download project dependencies. If unchanged, uses
|
||||
# the cached version.
|
||||
- name: 🌐 Install dependencies
|
||||
run: mix deps.get --only "prod"
|
||||
|
||||
# Step: Compile the project treating any warnings as errors.
|
||||
# Customize this step if a different behavior is desired.
|
||||
- name: 🛠 Compiles without warnings
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
run: mix compile
|
||||
73
.github/workflows/build.yml
vendored
73
.github/workflows/build.yml
vendored
@@ -4,7 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "releases/*"
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
|
||||
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -52,6 +53,7 @@ jobs:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: "${{ secrets.COMMIT_KEY }}"
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
@@ -89,20 +91,26 @@ jobs:
|
||||
|
||||
- name: Generate Changelog & Update Tag Version
|
||||
id: generate-changelog
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
git config --global user.name 'CI'
|
||||
git config --global user.email 'ci@users.noreply.github.com'
|
||||
mix git_ops.release --force-patch --yes
|
||||
git commit --allow-empty -m 'chore: [skip ci]'
|
||||
git push --follow-tags
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set commit hash for develop
|
||||
id: set-commit-develop
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -116,6 +124,7 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -128,17 +137,6 @@ jobs:
|
||||
ref: ${{ needs.build.outputs.commit_hash }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -187,24 +185,6 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer new release available 🎉
|
||||
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
@@ -235,9 +215,8 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
@@ -252,19 +231,25 @@ jobs:
|
||||
create-release:
|
||||
name: 🏷 Create Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
||||
needs: build
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: 🏷 Create Draft Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.docker.outputs.release-tag }}
|
||||
name: Release ${{ needs.docker.outputs.release-tag }}
|
||||
tag_name: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
name: Release ${{ steps.get-latest-tag.outputs.tag }}
|
||||
body: |
|
||||
## Info
|
||||
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
|
||||
@@ -274,9 +259,3 @@ jobs:
|
||||
## How to Promote?
|
||||
In order to promote this to prod, edit the draft and press **"Publish release"**.
|
||||
draft: true
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
|
||||
187
.github/workflows/docker-arm.yml
vendored
Normal file
187
.github/workflows/docker-arm.yml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
name: Build Docker ARM Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition-arm
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer **ARM** release available 🎉
|
||||
|
||||
**Version**: :${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: 🏷 Notify about release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
187
.github/workflows/docker.yml
vendored
Normal file
187
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer new release available 🎉
|
||||
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: 🏷 Notify about release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
300
.github/workflows/flaky-test-detection.yml
vendored
Normal file
300
.github/workflows/flaky-test-detection.yml
vendored
Normal file
@@ -0,0 +1,300 @@
|
||||
name: Flaky Test Detection
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_file:
|
||||
description: 'Specific test file to check (optional)'
|
||||
required: false
|
||||
type: string
|
||||
iterations:
|
||||
description: 'Number of test iterations'
|
||||
required: false
|
||||
default: '10'
|
||||
type: string
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
ELIXIR_VERSION: "1.17"
|
||||
OTP_VERSION: "27"
|
||||
|
||||
jobs:
|
||||
detect-flaky-tests:
|
||||
name: 🔍 Detect Flaky Tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wanderer_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🏗️ Setup Elixir & Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||
otp-version: ${{ env.OTP_VERSION }}
|
||||
|
||||
- name: 📦 Restore dependencies cache
|
||||
uses: actions/cache@v4
|
||||
id: deps-cache
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
if: steps.deps-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
|
||||
- name: 🏗️ Compile project
|
||||
run: mix compile --warnings-as-errors
|
||||
|
||||
- name: 🏗️ Setup test database
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
|
||||
|
||||
- name: 🔍 Run flaky test detection
|
||||
id: flaky-detection
|
||||
run: |
|
||||
# Determine test target
|
||||
TEST_FILE="${{ github.event.inputs.test_file }}"
|
||||
ITERATIONS="${{ github.event.inputs.iterations || '10' }}"
|
||||
|
||||
if [ -n "$TEST_FILE" ]; then
|
||||
echo "Checking specific file: $TEST_FILE"
|
||||
mix test.stability --runs $ITERATIONS --file "$TEST_FILE" --detect --report flaky_report.json
|
||||
else
|
||||
echo "Checking all tests"
|
||||
mix test.stability --runs $ITERATIONS --detect --report flaky_report.json
|
||||
fi
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
|
||||
continue-on-error: true
|
||||
|
||||
- name: 📊 Upload flaky test report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: flaky-test-report
|
||||
path: flaky_report.json
|
||||
retention-days: 30
|
||||
|
||||
- name: 💬 Comment on flaky tests
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Read the report
|
||||
let report;
|
||||
try {
|
||||
const reportContent = fs.readFileSync('flaky_report.json', 'utf8');
|
||||
report = JSON.parse(reportContent);
|
||||
} catch (error) {
|
||||
console.log('No flaky test report found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!report.flaky_tests || report.flaky_tests.length === 0) {
|
||||
console.log('No flaky tests detected!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create issue body
|
||||
const issueBody = `## 🔍 Flaky Tests Detected
|
||||
|
||||
The automated flaky test detection found ${report.flaky_tests.length} potentially flaky test(s).
|
||||
|
||||
### Summary
|
||||
- **Total test runs**: ${report.summary.total_runs}
|
||||
- **Success rate**: ${(report.summary.success_rate * 100).toFixed(1)}%
|
||||
- **Average duration**: ${(report.summary.avg_duration_ms / 1000).toFixed(2)}s
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
| Test | Failure Rate | Details |
|
||||
|------|--------------|---------|
|
||||
${report.flaky_tests.map(test =>
|
||||
`| ${test.test} | ${(test.failure_rate * 100).toFixed(1)}% | Failed ${test.failures}/${report.summary.total_runs} runs |`
|
||||
).join('\n')}
|
||||
|
||||
### Recommended Actions
|
||||
|
||||
1. Review the identified tests for race conditions
|
||||
2. Check for timing dependencies or async issues
|
||||
3. Ensure proper test isolation and cleanup
|
||||
4. Consider adding explicit waits or synchronization
|
||||
5. Use \`async: false\` if tests share resources
|
||||
|
||||
---
|
||||
*This issue was automatically created by the flaky test detection workflow.*
|
||||
*Run time: ${new Date().toISOString()}*
|
||||
`;
|
||||
|
||||
try {
|
||||
// Check if there's already an open issue
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'flaky-test',
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (issues.data.length > 0) {
|
||||
// Update existing issue
|
||||
const issue = issues.data[0];
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: issueBody
|
||||
});
|
||||
console.log(`Updated existing issue #${issue.number}`);
|
||||
} catch (commentError) {
|
||||
console.error('Failed to create comment:', commentError.message);
|
||||
throw commentError;
|
||||
}
|
||||
} else {
|
||||
// Create new issue
|
||||
try {
|
||||
const newIssue = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: '🔍 Flaky Tests Detected',
|
||||
body: issueBody,
|
||||
labels: ['flaky-test', 'test-quality', 'automated']
|
||||
});
|
||||
console.log(`Created new issue #${newIssue.data.number}`);
|
||||
} catch (createError) {
|
||||
console.error('Failed to create issue:', createError.message);
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
} catch (listError) {
|
||||
console.error('Failed to list issues:', listError.message);
|
||||
console.error('API error details:', listError.response?.data || 'No response data');
|
||||
throw listError;
|
||||
}
|
||||
|
||||
- name: 📈 Update metrics
|
||||
if: always()
|
||||
run: |
|
||||
# Parse and store metrics for tracking
|
||||
if [ -f flaky_report.json ]; then
|
||||
FLAKY_COUNT=$(jq '.flaky_tests | length' flaky_report.json)
|
||||
SUCCESS_RATE=$(jq '.summary.success_rate' flaky_report.json)
|
||||
|
||||
echo "FLAKY_TEST_COUNT=$FLAKY_COUNT" >> $GITHUB_ENV
|
||||
echo "TEST_SUCCESS_RATE=$SUCCESS_RATE" >> $GITHUB_ENV
|
||||
|
||||
# Log metrics (could be sent to monitoring service)
|
||||
echo "::notice title=Flaky Test Metrics::Found $FLAKY_COUNT flaky tests with ${SUCCESS_RATE}% success rate"
|
||||
fi
|
||||
|
||||
analyze-test-history:
|
||||
name: 📊 Analyze Test History
|
||||
runs-on: ubuntu-22.04
|
||||
needs: detect-flaky-tests
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📥 Download previous reports
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: flaky-test-detection.yml
|
||||
workflow_conclusion: completed
|
||||
name: flaky-test-report
|
||||
path: historical-reports
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: 📊 Generate trend analysis
|
||||
run: |
|
||||
# Analyze historical trends
|
||||
python3 <<'EOF'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import glob
|
||||
|
||||
reports = []
|
||||
for report_file in glob.glob('historical-reports/*/flaky_report.json'):
|
||||
try:
|
||||
with open(report_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
reports.append(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not reports:
|
||||
print("No historical data found")
|
||||
exit(0)
|
||||
|
||||
# Sort by timestamp
|
||||
reports.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
||||
|
||||
# Analyze trends
|
||||
print("## Test Stability Trend Analysis")
|
||||
print(f"\nAnalyzed {len(reports)} historical reports")
|
||||
print("\n### Flaky Test Counts Over Time")
|
||||
|
||||
for report in reports[:10]: # Last 10 reports
|
||||
timestamp = report.get('timestamp', 'Unknown')
|
||||
flaky_count = len(report.get('flaky_tests', []))
|
||||
success_rate = report.get('summary', {}).get('success_rate', 0) * 100
|
||||
print(f"- {timestamp[:10]}: {flaky_count} flaky tests ({success_rate:.1f}% success rate)")
|
||||
|
||||
# Identify persistently flaky tests
|
||||
all_flaky = {}
|
||||
for report in reports:
|
||||
for test in report.get('flaky_tests', []):
|
||||
test_name = test.get('test', '')
|
||||
if test_name not in all_flaky:
|
||||
all_flaky[test_name] = 0
|
||||
all_flaky[test_name] += 1
|
||||
|
||||
if all_flaky:
|
||||
print("\n### Persistently Flaky Tests")
|
||||
sorted_flaky = sorted(all_flaky.items(), key=lambda x: x[1], reverse=True)
|
||||
for test_name, count in sorted_flaky[:5]:
|
||||
percentage = (count / len(reports)) * 100
|
||||
print(f"- {test_name}: Flaky in {count}/{len(reports)} runs ({percentage:.1f}%)")
|
||||
EOF
|
||||
|
||||
- name: 💾 Save analysis
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-stability-analysis
|
||||
path: |
|
||||
flaky_report.json
|
||||
historical-reports/
|
||||
retention-days: 90
|
||||
333
.github/workflows/test.yml
vendored
Normal file
333
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
name: 🧪 Test Suite
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
push:
|
||||
branches: [main, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
ELIXIR_VERSION: '1.16'
|
||||
OTP_VERSION: '26'
|
||||
NODE_VERSION: '18'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wanderer_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Elixir/OTP
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||
otp-version: ${{ env.OTP_VERSION }}
|
||||
|
||||
- name: Cache Elixir dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
|
||||
- name: Install Elixir dependencies
|
||||
run: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
|
||||
- name: Check code formatting
|
||||
id: format
|
||||
run: |
|
||||
if mix format --check-formatted; then
|
||||
echo "status=✅ Passed" >> $GITHUB_OUTPUT
|
||||
echo "count=0" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||
echo "count=1" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Compile code and capture warnings
|
||||
id: compile
|
||||
run: |
|
||||
# Capture compilation output
|
||||
output=$(mix compile 2>&1 || true)
|
||||
echo "$output" > compile_output.txt
|
||||
|
||||
# Count warnings
|
||||
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
|
||||
# Check if compilation succeeded
|
||||
if mix compile > /dev/null 2>&1; then
|
||||
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
|
||||
echo "output<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup database
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
|
||||
- name: Run tests with coverage
|
||||
id: tests
|
||||
run: |
|
||||
# Run tests with coverage
|
||||
output=$(mix test --cover 2>&1 || true)
|
||||
echo "$output" > test_output.txt
|
||||
|
||||
# Parse test results
|
||||
if echo "$output" | grep -q "0 failures"; then
|
||||
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
|
||||
test_status="success"
|
||||
else
|
||||
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
|
||||
test_status="failed"
|
||||
fi
|
||||
|
||||
# Extract test counts
|
||||
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures")
|
||||
total_tests=$(echo "$test_line" | grep -o '[0-9]\+ tests\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
|
||||
echo "total=$total_tests" >> $GITHUB_OUTPUT
|
||||
echo "failures=$failures" >> $GITHUB_OUTPUT
|
||||
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
|
||||
|
||||
# Calculate success rate
|
||||
if [ "$total_tests" -gt 0 ]; then
|
||||
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
|
||||
else
|
||||
success_rate="0"
|
||||
fi
|
||||
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
|
||||
|
||||
exit_code=$?
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate coverage report
|
||||
id: coverage
|
||||
run: |
|
||||
# Generate coverage report with GitHub format
|
||||
output=$(mix coveralls.github 2>&1 || true)
|
||||
echo "$output" > coverage_output.txt
|
||||
|
||||
# Extract coverage percentage
|
||||
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
|
||||
if [ -z "$coverage" ]; then
|
||||
coverage="0"
|
||||
fi
|
||||
|
||||
echo "percentage=$coverage" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if (( $(echo "$coverage >= 80" | bc -l) )); then
|
||||
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$coverage >= 60" | bc -l) )); then
|
||||
echo "status=⚠️ Good" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Credo analysis
|
||||
id: credo
|
||||
run: |
|
||||
# Run Credo and capture output
|
||||
output=$(mix credo --strict --format=json 2>&1 || true)
|
||||
echo "$output" > credo_output.txt
|
||||
|
||||
# Try to parse JSON output
|
||||
if echo "$output" | jq . > /dev/null 2>&1; then
|
||||
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
|
||||
high_issues=$(echo "$output" | jq '.issues | map(select(.priority == "high")) | length' 2>/dev/null || echo "0")
|
||||
normal_issues=$(echo "$output" | jq '.issues | map(select(.priority == "normal")) | length' 2>/dev/null || echo "0")
|
||||
low_issues=$(echo "$output" | jq '.issues | map(select(.priority == "low")) | length' 2>/dev/null || echo "0")
|
||||
else
|
||||
# Fallback: try to count issues from regular output
|
||||
regular_output=$(mix credo --strict 2>&1 || true)
|
||||
issues=$(echo "$regular_output" | grep -c "┃" || echo "0")
|
||||
high_issues="0"
|
||||
normal_issues="0"
|
||||
low_issues="0"
|
||||
fi
|
||||
|
||||
echo "total_issues=$issues" >> $GITHUB_OUTPUT
|
||||
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
|
||||
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
|
||||
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "$issues" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
elif [ "$issues" -lt 10 ]; then
|
||||
echo "status=⚠️ Minor Issues" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Dialyzer analysis
|
||||
id: dialyzer
|
||||
run: |
|
||||
# Ensure PLT is built
|
||||
mix dialyzer --plt
|
||||
|
||||
# Run Dialyzer and capture output
|
||||
output=$(mix dialyzer --format=github 2>&1 || true)
|
||||
echo "$output" > dialyzer_output.txt
|
||||
|
||||
# Count warnings and errors
|
||||
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
errors=$(echo "$output" | grep -c "error:" || echo "0")
|
||||
|
||||
echo "warnings=$warnings" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
elif [ "$errors" -eq 0 ]; then
|
||||
echo "status=⚠️ Warnings Only" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create test results summary
|
||||
id: summary
|
||||
run: |
|
||||
# Calculate overall score
|
||||
format_score=${{ steps.format.outputs.count == '0' && '100' || '0' }}
|
||||
compile_score=${{ steps.compile.outputs.warnings == '0' && '100' || '80' }}
|
||||
test_score=${{ steps.tests.outputs.success_rate }}
|
||||
coverage_score=${{ steps.coverage.outputs.percentage }}
|
||||
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
|
||||
dialyzer_score=$(echo "scale=0; (100 - ${{ steps.dialyzer.outputs.warnings }} * 2 - ${{ steps.dialyzer.outputs.errors }} * 10)" | bc | sed 's/^-.*$/0/')
|
||||
|
||||
overall_score=$(echo "scale=1; ($format_score + $compile_score + $test_score + $coverage_score + $credo_score + $dialyzer_score) / 6" | bc)
|
||||
|
||||
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine overall status
|
||||
if (( $(echo "$overall_score >= 90" | bc -l) )); then
|
||||
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$overall_score >= 80" | bc -l) )); then
|
||||
echo "overall_status=✅ Good" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$overall_score >= 70" | bc -l) )); then
|
||||
echo "overall_status=⚠️ Needs Improvement" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Find existing PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
id: find_comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '## 🧪 Test Results Summary'
|
||||
|
||||
- name: Create or update PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.find_comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## 🧪 Test Results Summary
|
||||
|
||||
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
|
||||
|
||||
### 📊 Metrics Dashboard
|
||||
|
||||
| Category | Status | Count | Details |
|
||||
|----------|---------|-------|---------|
|
||||
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
|
||||
| 🔨 **Compilation** | ${{ steps.compile.outputs.status }} | ${{ steps.compile.outputs.warnings }} warnings | `mix compile` |
|
||||
| 🧪 **Tests** | ${{ steps.tests.outputs.status }} | ${{ steps.tests.outputs.failures }}/${{ steps.tests.outputs.total }} failed | Success rate: ${{ steps.tests.outputs.success_rate }}% |
|
||||
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` |
|
||||
| 🎯 **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} |
|
||||
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
|
||||
|
||||
### 🎯 Quality Gates
|
||||
|
||||
Based on the project's quality thresholds:
|
||||
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
|
||||
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
|
||||
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
|
||||
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
|
||||
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
|
||||
|
||||
<details>
|
||||
<summary>📈 Progress Toward Goals</summary>
|
||||
|
||||
Target goals for the project:
|
||||
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
|
||||
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
|
||||
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
|
||||
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
|
||||
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🔧 Quick Actions</summary>
|
||||
|
||||
To improve code quality:
|
||||
```bash
|
||||
# Fix formatting issues
|
||||
mix format
|
||||
|
||||
# View detailed Credo analysis
|
||||
mix credo --strict
|
||||
|
||||
# Check Dialyzer warnings
|
||||
mix dialyzer
|
||||
|
||||
# Generate detailed coverage report
|
||||
mix coveralls.html
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,7 +4,8 @@
|
||||
*.iml
|
||||
|
||||
*.key
|
||||
|
||||
.repomixignore
|
||||
repomix*
|
||||
/.idea/
|
||||
/node_modules/
|
||||
/assets/node_modules/
|
||||
@@ -17,6 +18,9 @@
|
||||
/priv/static/*.js
|
||||
/priv/static/*.css
|
||||
|
||||
# Dialyzer PLT files
|
||||
/priv/plts/
|
||||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
|
||||
2691
CHANGELOG.md
2691
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -21,21 +21,17 @@ RUN mkdir config
|
||||
# to ensure any relevant config change will trigger the dependencies
|
||||
# to be re-compiled.
|
||||
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||
|
||||
COPY priv priv
|
||||
|
||||
COPY lib lib
|
||||
|
||||
COPY assets assets
|
||||
|
||||
RUN mix compile
|
||||
|
||||
RUN mix assets.deploy
|
||||
RUN mix compile
|
||||
|
||||
# Changes to config/runtime.exs don't require recompiling the code
|
||||
COPY config/runtime.exs config/
|
||||
|
||||
COPY rel rel
|
||||
|
||||
RUN mix release
|
||||
|
||||
# start a new build stage so that the final image will only contain
|
||||
|
||||
@@ -18,5 +18,28 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
"linebreak-style": "off",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "primereact/button",
|
||||
"importNames": ["Button"],
|
||||
"message": "Use WdButton instead Button"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/forbid-elements": [
|
||||
"error",
|
||||
{
|
||||
"forbid": [
|
||||
{
|
||||
"element": "Button",
|
||||
"message": "Use WdButton instead Button"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
14
assets/jest.config.js
Normal file
14
assets/jest.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['<rootDir>'],
|
||||
moduleDirectories: ['node_modules', 'js'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/js/$1',
|
||||
'\.scss$': 'identity-obj-proxy', // Mock SCSS files
|
||||
},
|
||||
transform: {
|
||||
'^.+\.(ts|tsx)$': 'ts-jest',
|
||||
'^.+\.(js|jsx)$': 'babel-jest', // Add babel-jest for JS/JSX files if needed
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { useMapperHandlers } from './useMapperHandlers';
|
||||
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
|
||||
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
|
||||
import './common-styles/main.scss';
|
||||
import { ToastProvider } from '@/hooks/Mapper/ToastProvider.tsx';
|
||||
|
||||
const ErrorFallback = () => {
|
||||
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
|
||||
@@ -39,13 +40,15 @@ export default function MapRoot({ hooks }) {
|
||||
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
|
||||
<ReactFlowProvider>
|
||||
<MapRootContent />
|
||||
</ReactFlowProvider>
|
||||
</ErrorBoundary>
|
||||
</MapRootProvider>
|
||||
<ToastProvider>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
|
||||
<ReactFlowProvider>
|
||||
<MapRootContent />
|
||||
</ReactFlowProvider>
|
||||
</ErrorBoundary>
|
||||
</MapRootProvider>
|
||||
</ToastProvider>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
}
|
||||
|
||||
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal file
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { createContext, useContext, useRef } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import type { ToastMessage } from 'primereact/toast';
|
||||
|
||||
interface ToastContextValue {
|
||||
toastRef: React.RefObject<Toast>;
|
||||
show: (message: ToastMessage | ToastMessage[]) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null);
|
||||
|
||||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const toastRef = useRef<Toast>(null);
|
||||
|
||||
const show = (message: ToastMessage | ToastMessage[]) => {
|
||||
toastRef.current?.show(message);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toastRef, show }}>
|
||||
<Toast ref={toastRef} position="top-right" />
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useToast = (): ToastContextValue => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) throw new Error('useToast must be used within a ToastProvider');
|
||||
return context;
|
||||
};
|
||||
@@ -212,3 +212,79 @@
|
||||
.p-inputtext:enabled:hover {
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
|
||||
// --------------- TOAST
|
||||
.p-toast .p-toast-message {
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
border-left: 4px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.p-toast .p-toast-message .p-toast-summary {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-toast .p-toast-message .p-toast-detail {
|
||||
color: #c0c0c0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.p-toast .p-toast-icon-close {
|
||||
color: #ffaa00;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.p-toast .p-toast-icon-close:hover {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.p-toast-message-success {
|
||||
border-left-color: #f1c40f;
|
||||
}
|
||||
.p-toast-message-error {
|
||||
border-left-color: #e74c3c;
|
||||
}
|
||||
.p-toast-message-info {
|
||||
border-left-color: #3498db;
|
||||
}
|
||||
.p-toast-message-warn {
|
||||
border-left-color: #e67e22;
|
||||
}
|
||||
|
||||
.p-toast-message-success .p-toast-message-icon {
|
||||
color: #f1c40f;
|
||||
}
|
||||
.p-toast-message-error .p-toast-message-icon {
|
||||
color: #e74c3c;
|
||||
}
|
||||
.p-toast-message-info .p-toast-message-icon {
|
||||
color: #3498db;
|
||||
}
|
||||
.p-toast-message-warn .p-toast-message-icon {
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
.p-toast-message-success .p-toast-message-content {
|
||||
border-left-color: #f1c40f;
|
||||
}
|
||||
|
||||
.p-toast-message-error .p-toast-message-content {
|
||||
border-left-color: #e74c3c;
|
||||
}
|
||||
|
||||
.p-toast-message-info .p-toast-message-content {
|
||||
border-left-color: #3498db;
|
||||
}
|
||||
|
||||
.p-toast-message-warn .p-toast-message-content {
|
||||
border-left-color: #e67e22;
|
||||
}
|
||||
|
||||
.p-dialog-header-icon.p-dialog-header-close.p-link {
|
||||
position: relative;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
// import './tailwind.css';
|
||||
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
|
||||
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
|
||||
//@import "prime-fixes";
|
||||
@import 'primereact/resources/primereact.min.css';
|
||||
//@import 'primeflex/primeflex.css';
|
||||
@import 'primeicons/primeicons.css';
|
||||
//@import 'primereact/resources/primereact.css';
|
||||
@use 'primereact/resources/primereact.min.css';
|
||||
@use 'primeicons/primeicons.css';
|
||||
|
||||
|
||||
@import "fixes";
|
||||
@import "prime-fixes";
|
||||
@import "custom-scrollbar";
|
||||
@import "tooltip";
|
||||
@import "context-menu";
|
||||
@use "fixes";
|
||||
@use "prime-fixes";
|
||||
@use "custom-scrollbar";
|
||||
@use "tooltip";
|
||||
@use "context-menu";
|
||||
|
||||
|
||||
.fixedImportant {
|
||||
|
||||
@@ -64,9 +64,9 @@ body .p-dialog {
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #ddd;
|
||||
background: #f4f4f4;
|
||||
padding: .75rem 1rem;
|
||||
border-top: none !important;
|
||||
//background: #f4f4f4;
|
||||
}
|
||||
|
||||
.p-dialog-header-close {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.vertical-tabs-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
min-height: 200px;
|
||||
|
||||
.p-tabview {
|
||||
width: 100%;
|
||||
@@ -68,6 +68,28 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.color-warn {
|
||||
@apply bg-yellow-600/5 border-r-yellow-600/20;
|
||||
|
||||
&:hover {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600/40;
|
||||
}
|
||||
|
||||
|
||||
&.p-tabview-selected {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600;
|
||||
|
||||
.p-tabview-nav-link {
|
||||
@apply text-yellow-600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
@import "fix-dialog";
|
||||
@import "fix-popup";
|
||||
@import "fix-tabs";
|
||||
//@import "fix-input";
|
||||
|
||||
//@import "theme";
|
||||
@use "fix-dialog";
|
||||
@use "fix-popup";
|
||||
@use "fix-tabs";
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import classes from './Characters.module.scss';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import classes from './Characters.module.scss';
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
@@ -17,13 +16,22 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback((character: CharacterTypeRaw) => {
|
||||
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
|
||||
if (!character) {
|
||||
return;
|
||||
}
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.startTracking,
|
||||
data: { character_eve_id: character.eve_id },
|
||||
});
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: character?.location?.solar_system_id?.toString(),
|
||||
data: character.location?.solar_system_id?.toString(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -37,14 +45,26 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.name}
|
||||
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
|
||||
>
|
||||
{character.tracking_paused && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
|
||||
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
|
||||
'pi',
|
||||
PrimeIcons.PAUSE,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
@@ -55,6 +75,7 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ContextMenuSystemProps {
|
||||
onSystemStatus(val: number): void;
|
||||
onSystemLabels(val: string): void;
|
||||
onCustomLabelDialog(): void;
|
||||
onTogglePing(type: PingType, solar_system_id: string, hasPing: boolean): void;
|
||||
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
|
||||
onWaypointSet: WaypointSetContextHandler;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Button } from 'primereact/button';
|
||||
import { LayoutEventBlocker, WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const AVAILABLE_TAGS = [
|
||||
'A',
|
||||
@@ -61,7 +60,7 @@ export const useTagMenu = (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
|
||||
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
|
||||
{AVAILABLE_TAGS.map(x => (
|
||||
<Button
|
||||
<WdButton
|
||||
outlined={system?.tag !== x}
|
||||
severity="warning"
|
||||
key={x}
|
||||
@@ -71,9 +70,9 @@ export const useTagMenu = (
|
||||
onClick={() => system?.tag !== x && onSystemTag(x)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
</WdButton>
|
||||
))}
|
||||
<Button
|
||||
<WdButton
|
||||
disabled={!isSelectedTag}
|
||||
icon="pi pi-ban"
|
||||
size="small"
|
||||
@@ -81,7 +80,7 @@ export const useTagMenu = (
|
||||
outlined
|
||||
severity="help"
|
||||
onClick={() => onSystemTag()}
|
||||
></Button>
|
||||
></WdButton>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ export const useContextMenuSystemItems = ({
|
||||
|
||||
{ separator: true },
|
||||
{
|
||||
command: () => onTogglePing(PingType.Rally, systemId, hasPing),
|
||||
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing),
|
||||
disabled: !isShowPingBtn,
|
||||
template: () => {
|
||||
const iconClasses = clsx({
|
||||
@@ -118,7 +118,11 @@ export const useContextMenuSystemItems = ({
|
||||
});
|
||||
|
||||
if (isShowPingBtn) {
|
||||
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
|
||||
return (
|
||||
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -126,7 +130,7 @@ export const useContextMenuSystemItems = ({
|
||||
infoTitle="Locked. Ping can be set only for one system."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon={iconClasses}>
|
||||
<WdMenuItem disabled icon={iconClasses} className="!ml-[-2px]">
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
|
||||
@@ -2,25 +2,60 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface ContextMenuSystemMultipleProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
onDeleteSystems(): void;
|
||||
onCopySystems(): void;
|
||||
}
|
||||
|
||||
export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps> = ({
|
||||
contextMenuRef,
|
||||
onDeleteSystems,
|
||||
onCopySystems,
|
||||
}) => {
|
||||
const {
|
||||
data: { options, userPermissions },
|
||||
} = useMapRootState();
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: PrimeIcons.TRASH,
|
||||
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
|
||||
command: onDeleteSystems,
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Copy',
|
||||
icon: PrimeIcons.COPY,
|
||||
command: onCopySystems,
|
||||
disabled: !allowCopy,
|
||||
template: () => {
|
||||
if (allowCopy) {
|
||||
return <WdMenuItem icon="pi pi-copy">Copy</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Copy."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-copy">
|
||||
Copy
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [onDeleteSystems]);
|
||||
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,40 +1,81 @@
|
||||
import { Node } from 'reactflow';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { encodeJsonToUriBase64 } from '@/hooks/Mapper/utils';
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider.tsx';
|
||||
|
||||
export const useContextMenuSystemMultipleHandlers = () => {
|
||||
const {
|
||||
data: { pings, connections },
|
||||
} = useMapRootState();
|
||||
|
||||
const { show } = useToast();
|
||||
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
|
||||
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
|
||||
const refVars = useRef({ systems, ping, connections, deleteSystems });
|
||||
refVars.current = { systems, ping, connections, deleteSystems };
|
||||
|
||||
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
|
||||
const handleSystemMultipleContext = useCallback<NodeSelectionMouseHandler>((ev, systems_) => {
|
||||
setSystems(systems_);
|
||||
ev.preventDefault();
|
||||
ctxManager.next('ctxSysMult', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(ev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDeleteSystems = useCallback(() => {
|
||||
const { systems, ping, deleteSystems } = refVars.current;
|
||||
|
||||
if (!systems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
|
||||
const sysToDel = systems
|
||||
.filter(x => !x.data.locked)
|
||||
.filter(x => x.id !== ping?.solar_system_id)
|
||||
.map(x => x.id);
|
||||
|
||||
if (sysToDel.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteSystems(sysToDel);
|
||||
}, [deleteSystems, systems]);
|
||||
}, []);
|
||||
|
||||
const onCopySystems = useCallback(async () => {
|
||||
const { systems, connections } = refVars.current;
|
||||
if (!systems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionToCopy = connections.filter(
|
||||
c => systems.filter(s => [c.target, c.source].includes(s.id)).length == 2,
|
||||
);
|
||||
|
||||
await navigator.clipboard.writeText(
|
||||
encodeJsonToUriBase64({ systems: systems.map(x => x.data), connections: connectionToCopy }),
|
||||
);
|
||||
|
||||
show({
|
||||
severity: 'success',
|
||||
summary: 'Copied to clipboard',
|
||||
detail: `Successfully copied to clipboard - [${systems.length}] systems and [${connectionToCopy.length}] connections`,
|
||||
life: 3000,
|
||||
});
|
||||
}, [show]);
|
||||
|
||||
return {
|
||||
handleSystemMultipleContext,
|
||||
contextMenuRef,
|
||||
onDeleteSystems,
|
||||
onCopySystems,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
|
||||
export interface FastSystemActionsProps {
|
||||
systemId: string;
|
||||
@@ -27,7 +27,7 @@ export const FastSystemActions = ({
|
||||
ref.current = { systemId, systemName, regionName, isWH };
|
||||
|
||||
const handleOpenZKB = useCallback(
|
||||
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
|
||||
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}/`, '_blank'),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,4 @@ export type WaypointSetContextHandlerProps = {
|
||||
destination: string;
|
||||
};
|
||||
export type WaypointSetContextHandler = (props: WaypointSetContextHandlerProps) => void;
|
||||
export type NodeSelectionMouseHandler =
|
||||
| ((event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void)
|
||||
| undefined;
|
||||
export type NodeSelectionMouseHandler = (event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void;
|
||||
|
||||
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './parseMapUserSettings.ts';
|
||||
@@ -0,0 +1,64 @@
|
||||
import { MapUserSettings, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export const REQUIRED_KEYS = [
|
||||
'widgets',
|
||||
'interface',
|
||||
'onTheMap',
|
||||
'routes',
|
||||
'localWidget',
|
||||
'signaturesWidget',
|
||||
'killsWidget',
|
||||
] as const;
|
||||
|
||||
type RequiredKeys = (typeof REQUIRED_KEYS)[number];
|
||||
|
||||
/** Custom error for any parsing / validation issue */
|
||||
export class MapUserSettingsParseError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(`MapUserSettings parse error: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal check that an object matches SettingsWrapper<*> */
|
||||
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
|
||||
|
||||
/** Ensure every required key is present */
|
||||
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
|
||||
typeof v === 'object' && v !== null && REQUIRED_KEYS.every(k => k in v);
|
||||
|
||||
/* ------------------------------ Main parser ------------------------------- */
|
||||
|
||||
/**
|
||||
* Parses and validates a JSON string as `MapUserSettings`.
|
||||
*
|
||||
* @throws `MapUserSettingsParseError` – если строка не JSON или нарушена структура
|
||||
*/
|
||||
export const parseMapUserSettings = (json: unknown): MapUserSettings => {
|
||||
if (typeof json !== 'string') throw new MapUserSettingsParseError('Input must be a JSON string');
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch (e) {
|
||||
throw new MapUserSettingsParseError(`Invalid JSON: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (!hasAllRequiredKeys(data)) {
|
||||
const missing = REQUIRED_KEYS.filter(k => !(k in (data as any)));
|
||||
throw new MapUserSettingsParseError(`Missing top-level field(s): ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
for (const key of REQUIRED_KEYS) {
|
||||
if (!isSettings((data as any)[key])) {
|
||||
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<T>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Everything passes, so cast is safe
|
||||
return data as MapUserSettings;
|
||||
};
|
||||
|
||||
/* ------------------------------ Usage example ----------------------------- */
|
||||
|
||||
// const raw = fetchFromServer(); // string
|
||||
// const settings = parseMapUserSettings(raw);
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useSystemInfo';
|
||||
export * from './useGetOwnOnlineCharacters';
|
||||
export * from './useElementWidth';
|
||||
export * from './useDetectSettingsChanged';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useDetectSettingsChanged = () => {
|
||||
const {
|
||||
storedSettings: {
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
settingsKills,
|
||||
},
|
||||
} = useMapRootState();
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect(
|
||||
() => setCounter(x => x + 1),
|
||||
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
|
||||
);
|
||||
|
||||
return counter;
|
||||
};
|
||||
26
assets/js/hooks/Mapper/components/hooks/useLocalCounter.ts
Normal file
26
assets/js/hooks/Mapper/components/hooks/useLocalCounter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useMemo } from 'react';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
export type UseLocalCounterProps = {
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
userCharacters: string[];
|
||||
};
|
||||
|
||||
export const getLocalCharacters = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
|
||||
return charactersInSystem
|
||||
.map(char => ({
|
||||
...char,
|
||||
compact: true,
|
||||
isOwn: userCharacters.includes(char.eve_id),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
export const useLocalCounter = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
|
||||
const localCounterCharacters = useMemo(
|
||||
() => getLocalCharacters({ charactersInSystem, userCharacters }),
|
||||
[charactersInSystem, userCharacters],
|
||||
);
|
||||
|
||||
return { localCounterCharacters };
|
||||
};
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import clsx from 'clsx';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Edge,
|
||||
@@ -16,8 +22,6 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
@@ -26,26 +30,11 @@ import {
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
const getViewPortFromStore = () => {
|
||||
const restored = localStorage.getItem(SESSION_KEY.viewPort);
|
||||
|
||||
if (!restored) {
|
||||
return { ...DEFAULT_VIEW_PORT };
|
||||
}
|
||||
|
||||
return JSON.parse(restored);
|
||||
};
|
||||
import { MapViewport, OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import type { Viewport } from '@reactflow/core/dist/esm/types';
|
||||
import { usePrevious } from 'primereact/hooks';
|
||||
|
||||
const initialNodes: Node<SolarSystemRawType>[] = [
|
||||
// {
|
||||
@@ -88,6 +77,7 @@ interface MapCompProps {
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
onChangeViewport?: (viewport: MapViewport) => void;
|
||||
minimapClasses?: string;
|
||||
isShowMinimap?: boolean;
|
||||
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
|
||||
@@ -98,6 +88,8 @@ interface MapCompProps {
|
||||
theme?: string;
|
||||
pings: PingData[];
|
||||
minimapPlacement?: PanelPosition;
|
||||
localShowShipName?: boolean;
|
||||
defaultViewport?: Viewport;
|
||||
}
|
||||
|
||||
const MapComp = ({
|
||||
@@ -117,19 +109,26 @@ const MapComp = ({
|
||||
onAddSystem,
|
||||
pings,
|
||||
minimapPlacement = 'bottom-right',
|
||||
localShowShipName = false,
|
||||
onChangeViewport,
|
||||
defaultViewport,
|
||||
}: MapCompProps) => {
|
||||
const { getNodes } = useReactFlow();
|
||||
const { getNodes, setViewport } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
|
||||
useMapHandlers(refn, onSelectionChange);
|
||||
useUpdateNodes(nodes);
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem, onCommand });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
|
||||
const refVars = useRef({ onChangeViewport });
|
||||
refVars.current = { onChangeViewport };
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
@@ -185,9 +184,10 @@ const MapComp = ({
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const handleMoveEnd: OnMoveEnd = (_, viewport) => {
|
||||
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
|
||||
};
|
||||
const handleMoveEnd: OnMoveEnd = useCallback((_, viewport) => {
|
||||
// @ts-ignore
|
||||
refVars.current.onChangeViewport?.(viewport);
|
||||
}, []);
|
||||
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
@@ -212,8 +212,22 @@ const MapComp = ({
|
||||
showKSpaceBG: showKSpaceBG,
|
||||
isThickConnections: isThickConnections,
|
||||
pings,
|
||||
localShowShipName,
|
||||
}));
|
||||
}, [showKSpaceBG, isThickConnections, pings, update]);
|
||||
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
|
||||
|
||||
const prevViewport = usePrevious(defaultViewport);
|
||||
useEffect(() => {
|
||||
if (defaultViewport == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevViewport == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setViewport(defaultViewport);
|
||||
}, [defaultViewport, prevViewport, setViewport]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -229,7 +243,7 @@ const MapComp = ({
|
||||
onConnect={onConnect}
|
||||
// TODO we need save into session all of this
|
||||
// and on any action do either
|
||||
defaultViewport={getViewPortFromStore()}
|
||||
defaultViewport={defaultViewport}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
|
||||
@@ -10,6 +10,8 @@ export type MapData = MapUnionTypes & {
|
||||
showKSpaceBG: boolean;
|
||||
isThickConnections: boolean;
|
||||
linkedSigEveId: string;
|
||||
localShowShipName: boolean;
|
||||
systemHighlighted: string | undefined;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
@@ -42,6 +44,8 @@ const INITIAL_DATA: MapData = {
|
||||
followingCharacterEveId: null,
|
||||
userHubs: [],
|
||||
pings: [],
|
||||
localShowShipName: false,
|
||||
systemHighlighted: undefined,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.ConnectionTimeEOL {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
|
||||
@@ -8,6 +8,10 @@
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
|
||||
}
|
||||
|
||||
.ConnectionBridge {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
|
||||
}
|
||||
|
||||
.ConnectionSave {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-save));
|
||||
}
|
||||
@@ -15,3 +19,14 @@
|
||||
.SelectedItem {
|
||||
background-color: var(--selected-item-bg);
|
||||
}
|
||||
|
||||
.FastActions {
|
||||
:global {
|
||||
.p-menuitem-content {
|
||||
background-color: initial !important;
|
||||
}
|
||||
.p-menuitem-content:hover {
|
||||
background-color: initial !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
@@ -13,14 +6,25 @@ import {
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { Edge } from 'reactflow';
|
||||
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
|
||||
export interface ContextMenuConnectionProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
onDeleteConnection(): void;
|
||||
onChangeTimeState(): void;
|
||||
onChangeTimeState(lifetime: TimeStatus): void;
|
||||
onChangeMassState(state: MassState): void;
|
||||
onChangeShipSizeStatus(state: ShipSizeStatus): void;
|
||||
onChangeType(type: ConnectionType): void;
|
||||
onToggleMassSave(isLocked: boolean): void;
|
||||
onHide(): void;
|
||||
edge?: Edge<SolarSystemConnection>;
|
||||
@@ -32,6 +36,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
onChangeTimeState,
|
||||
onChangeMassState,
|
||||
onChangeShipSizeStatus,
|
||||
onChangeType,
|
||||
onToggleMassSave,
|
||||
onHide,
|
||||
edge,
|
||||
@@ -41,88 +46,128 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const sourceInfo = getSystemStaticInfo(edge.data?.source);
|
||||
const targetInfo = getSystemStaticInfo(edge.data?.target);
|
||||
|
||||
const bothNullsec =
|
||||
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
|
||||
|
||||
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
|
||||
const isWormhole = edge.data?.type !== ConnectionType.gate;
|
||||
|
||||
if (edge.data?.type === ConnectionType.bridge) {
|
||||
return [
|
||||
{
|
||||
label: `Set as Wormhole`,
|
||||
icon: 'pi hero-arrow-uturn-left',
|
||||
command: () => onChangeType(ConnectionType.wormhole),
|
||||
},
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (edge.data?.type === ConnectionType.gate) {
|
||||
return [
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...(isWormhole
|
||||
{
|
||||
className: clsx(classes.FastActions, '!h-[54px]'),
|
||||
template: () => {
|
||||
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
label: `Set as Bridge`,
|
||||
icon: 'pi hero-forward',
|
||||
command: () => onChangeType(ConnectionType.bridge),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
|
||||
}, [
|
||||
edge,
|
||||
onChangeTimeState,
|
||||
onDeleteConnection,
|
||||
onChangeType,
|
||||
onChangeShipSizeStatus,
|
||||
onToggleMassSave,
|
||||
onChangeMassState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" />
|
||||
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" className="!w-[250px]" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdLifetimeSelector, WdLifetimeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
|
||||
|
||||
export const LifetimeActionsWrapper = (props: WdLifetimeSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Life time:</div>
|
||||
|
||||
<WdLifetimeSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
setEdge(undefined);
|
||||
};
|
||||
|
||||
const onChangeTimeState = () => {
|
||||
const onChangeTimeState = (lifetime: TimeStatus) => {
|
||||
if (!edge || !edge.data) {
|
||||
return;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: edge.data.time_status === TimeStatus.default ? TimeStatus.eol : TimeStatus.default,
|
||||
value: lifetime,
|
||||
},
|
||||
});
|
||||
setEdge(undefined);
|
||||
|
||||
@@ -2,22 +2,70 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface ContextMenuRootProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
|
||||
onAddSystem(): void;
|
||||
onPasteSystemsAnsConnections(): void;
|
||||
}
|
||||
|
||||
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({ contextMenuRef, onAddSystem }) => {
|
||||
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
|
||||
contextMenuRef,
|
||||
onAddSystem,
|
||||
onPasteSystemsAnsConnections,
|
||||
pasteSystemsAndConnections,
|
||||
}) => {
|
||||
const {
|
||||
data: { options, userPermissions },
|
||||
} = useMapState();
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Add System',
|
||||
icon: PrimeIcons.PLUS,
|
||||
command: onAddSystem,
|
||||
},
|
||||
...(pasteSystemsAndConnections != null
|
||||
? [
|
||||
{
|
||||
icon: 'pi pi-clipboard',
|
||||
disabled: !allowPaste,
|
||||
command: onPasteSystemsAnsConnections,
|
||||
template: () => {
|
||||
if (allowPaste) {
|
||||
return (
|
||||
<WdMenuItem icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Paste."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [onAddSystem]);
|
||||
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,36 +1,76 @@
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { recenterSystemsByBounds } from '@/hooks/Mapper/helpers/recenterSystems.ts';
|
||||
import { OutCommand, OutCommandHandler, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { decodeUriBase64ToJson } from '@/hooks/Mapper/utils';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
|
||||
export type PasteSystemsAndConnections = {
|
||||
systems: SolarSystemRawType[];
|
||||
connections: SolarSystemConnection[];
|
||||
};
|
||||
|
||||
type UseContextMenuRootHandlers = {
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onCommand?: OutCommandHandler;
|
||||
};
|
||||
|
||||
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
|
||||
export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContextMenuRootHandlers = {}) => {
|
||||
const rf = useReactFlow();
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
const [position, setPosition] = useState<XYPosition | null>(null);
|
||||
const [pasteSystemsAndConnections, setPasteSystemsAndConnections] = useState<PasteSystemsAndConnections>();
|
||||
|
||||
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleRootContext = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
setPosition(rf.project({ x: e.clientX, y: e.clientY }));
|
||||
e.preventDefault();
|
||||
ctxManager.next('ctxRoot', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(e);
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const result = decodeUriBase64ToJson(text);
|
||||
setPasteSystemsAndConnections(result as PasteSystemsAndConnections);
|
||||
} catch (err) {
|
||||
setPasteSystemsAndConnections(undefined);
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef({ onAddSystem, position });
|
||||
ref.current = { onAddSystem, position };
|
||||
const ref = useRef({ onAddSystem, position, pasteSystemsAndConnections, onCommand });
|
||||
ref.current = { onAddSystem, position, pasteSystemsAndConnections, onCommand };
|
||||
|
||||
const onAddSystemCallback = useCallback(() => {
|
||||
ref.current.onAddSystem?.({ coordinates: position });
|
||||
}, [position]);
|
||||
|
||||
const onPasteSystemsAnsConnections = useCallback(async () => {
|
||||
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
|
||||
if (!position || !onCommand || !pasteSystemsAndConnections) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { systems } = recenterSystemsByBounds(pasteSystemsAndConnections.systems);
|
||||
|
||||
await onCommand({
|
||||
type: OutCommand.manualPasteSystemsAndConnections,
|
||||
data: {
|
||||
systems: systems.map(({ position: srcPos, ...rest }) => ({
|
||||
position: { x: Math.round(srcPos.x + position.x), y: Math.round(srcPos.y + position.y) },
|
||||
...rest,
|
||||
})),
|
||||
connections: pasteSystemsAndConnections.connections,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleRootContext,
|
||||
|
||||
pasteSystemsAndConnections,
|
||||
contextMenuRef,
|
||||
onAddSystem: onAddSystemCallback,
|
||||
onPasteSystemsAnsConnections,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
||||
import { useKillsCounter } from '../../hooks/useKillsCounter.ts';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import {
|
||||
KILLS_ROW_HEIGHT,
|
||||
SystemKillsList,
|
||||
@@ -49,14 +49,15 @@ export const KillsCounter = ({
|
||||
content={
|
||||
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
|
||||
<div className="flex-1 h-full">
|
||||
<SystemKillsList kills={limitedKills} onlyOneSystem />
|
||||
<SystemKillsList kills={limitedKills} onlyOneSystem timeRange={1} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
tooltipClassName="!px-0"
|
||||
size={size}
|
||||
interactive={true}
|
||||
interactive
|
||||
smallPaddings
|
||||
>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './KillsCounter.tsx';
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
.hoverTarget {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -3,21 +3,33 @@ import clsx from 'clsx';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
|
||||
import classes from './SolarSystemLocalCounter.module.scss';
|
||||
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
|
||||
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import classes from './LocalCounter.module.scss';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useLocalCharactersItemTemplate } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
|
||||
|
||||
interface LocalCounterProps {
|
||||
localCounterCharacters: Array<CharItemProps>;
|
||||
hasUserCharacters: boolean;
|
||||
showIcon?: boolean;
|
||||
disableInteractive?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const [settings] = useLocalCharacterWidgetSettings();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
export const LocalCounter = ({
|
||||
className,
|
||||
contentClassName,
|
||||
localCounterCharacters,
|
||||
hasUserCharacters,
|
||||
showIcon = true,
|
||||
disableInteractive,
|
||||
}: LocalCounterProps) => {
|
||||
const {
|
||||
data: { localShowShipName },
|
||||
} = useMapState();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
@@ -40,16 +52,30 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.TooltipActive, {
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
className={clsx(
|
||||
classes.TooltipActive,
|
||||
{
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
|
||||
<WdTooltipWrapper
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={0}
|
||||
interactive={!disableInteractive}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.hoverTarget)}>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
className={clsx(
|
||||
classes.localCounter,
|
||||
{
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
},
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{showIcon && <i className="pi pi-users" />}
|
||||
<span>{localCounterCharacters.length}</span>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCounter';
|
||||
@@ -1,10 +1,20 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.EdgePathBack {
|
||||
fill: none;
|
||||
stroke: #80a5c5;
|
||||
stroke-width: 3px;
|
||||
|
||||
&.time1 {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.time4 {
|
||||
stroke: #a654e3;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.TimeCrit {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
@@ -29,6 +39,13 @@
|
||||
&.Gate {
|
||||
stroke: #9aff40;
|
||||
}
|
||||
|
||||
&.Bridge {
|
||||
stroke: #9aff40;
|
||||
|
||||
stroke-dasharray: 10 5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathFront {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import classes from './SolarSystemEdge.module.scss';
|
||||
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
|
||||
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
|
||||
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
|
||||
import clsx from 'clsx';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
@@ -9,6 +9,7 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const MAP_TRANSLATES: Record<string, string> = {
|
||||
[Position.Top]: 'translate(-48%, 0%)',
|
||||
@@ -42,7 +43,9 @@ export const SHIP_SIZES_COLORS = {
|
||||
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
|
||||
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
|
||||
const isWormhole = data?.type !== ConnectionType.gate;
|
||||
const isWormhole = data?.type === ConnectionType.wormhole;
|
||||
const isGate = data?.type === ConnectionType.gate;
|
||||
const isBridge = data?.type === ConnectionType.bridge;
|
||||
|
||||
const {
|
||||
data: { isThickConnections },
|
||||
@@ -51,13 +54,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
|
||||
|
||||
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
|
||||
|
||||
const method = isWormhole ? getBezierPath : getSmoothStepPath;
|
||||
|
||||
const [edgePath, labelX, labelY] = method({
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx - offset.x,
|
||||
sourceY: sy - offset.y,
|
||||
sourcePosition: sourcePos,
|
||||
@@ -67,7 +68,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
});
|
||||
|
||||
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
|
||||
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
|
||||
}, [isThickConnections, sourceNode, targetNode]);
|
||||
|
||||
if (!sourceNode || !targetNode || !data) {
|
||||
return null;
|
||||
@@ -79,9 +80,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
id={`back_${id}`}
|
||||
className={clsx(classes.EdgePathBack, {
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
|
||||
[classes.time1]: isWormhole && data.time_status === TimeStatus._1h,
|
||||
[classes.time4]: isWormhole && data.time_status === TimeStatus._4h,
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -95,7 +98,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
|
||||
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
|
||||
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -147,6 +151,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isBridge && (
|
||||
<WdTooltipWrapper
|
||||
content="Ansiblex Jump Bridge"
|
||||
position={TooltipPosition.top}
|
||||
className={clsx(
|
||||
classes.LinkLabel,
|
||||
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
|
||||
)}
|
||||
>
|
||||
B
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
|
||||
<WdTooltipWrapper
|
||||
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: rgb(30, 161, 255);
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020;
|
||||
|
||||
$neon-color-1: rgb(27, 132, 236);
|
||||
$neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
@use "sass:color";
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
|
||||
|
||||
@keyframes move-stripes {
|
||||
from {
|
||||
@@ -34,12 +27,13 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Pochven,
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
@@ -95,6 +89,15 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
}
|
||||
}
|
||||
|
||||
&.Pochven {
|
||||
&::after {
|
||||
opacity: 0.8;
|
||||
background-image: url('/images/pochven.webp');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
@@ -12,17 +12,21 @@ import {
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
|
||||
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
|
||||
|
||||
// let render = 0;
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
|
||||
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
|
||||
nodeVars.solarSystemId,
|
||||
);
|
||||
|
||||
// console.log('JOipP', `render ${nodeVars.id}`, render++);
|
||||
|
||||
@@ -38,17 +42,17 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
|
||||
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
killsActivityType={localKillsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{localKillsCount}</span>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
@@ -137,12 +141,26 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
<div className="flex items-center gap-0.5 justify-end">
|
||||
<div className={clsx('flex items-center gap-0.5')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
{nodeVars.description != null && nodeVars.description !== '' && (
|
||||
<WdTooltipWrapper
|
||||
className="h-[15px] transform -translate-y-[6%]"
|
||||
position={TooltipPosition.top}
|
||||
content={`System have description`}
|
||||
>
|
||||
<i
|
||||
className={clsx(
|
||||
'pi hero-chat-bubble-bottom-center-text w-[10px] h-[10px]',
|
||||
'text-[8px] relative top-[1px]',
|
||||
)}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
@@ -175,6 +193,17 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</>
|
||||
)}
|
||||
|
||||
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
|
||||
<div
|
||||
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
|
||||
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
|
||||
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
|
||||
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
@use './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
/* ---------------------------------------------
|
||||
Only override what's different from the base
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
@@ -12,16 +12,19 @@ import {
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
|
||||
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
|
||||
|
||||
// let render = 0;
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
|
||||
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
|
||||
nodeVars.solarSystemId,
|
||||
);
|
||||
|
||||
// console.log('JOipP', `render ${nodeVars.id}`, render++);
|
||||
|
||||
@@ -37,17 +40,17 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
|
||||
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
killsActivityType={localKillsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{localKillsCount}</span>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
@@ -170,6 +173,17 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</>
|
||||
)}
|
||||
|
||||
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
|
||||
<div
|
||||
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
|
||||
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
|
||||
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
|
||||
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.Signature {
|
||||
position: relative;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
@@ -35,7 +36,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.isEOL;
|
||||
return customInfo?.time_status === TimeStatus._1h;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
@@ -58,6 +59,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -716,11 +716,12 @@ export const STATUS_CLASSES: Record<number, string> = {
|
||||
[STATUSES.dangerous]: 'eve-system-status-dangerous',
|
||||
};
|
||||
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
|
||||
|
||||
export const TYPE_NAMES = {
|
||||
[ConnectionType.wormhole]: 'Wormhole',
|
||||
[ConnectionType.gate]: 'Gate',
|
||||
[ConnectionType.bridge]: 'Jumpgate',
|
||||
};
|
||||
|
||||
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
|
||||
return targetPermission != null && permissions[targetPermission];
|
||||
};
|
||||
@@ -4,3 +4,4 @@ export * from './getSystemClassStyles';
|
||||
export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
export * from './checkPermissions';
|
||||
|
||||
@@ -15,3 +15,12 @@ export const isKnownSpace = (wormholeClassID: number) => {
|
||||
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
|
||||
return spaces.includes(wormholeClassID);
|
||||
};
|
||||
|
||||
export const isNullsecSpace = (wormholeClassID: number) => {
|
||||
switch (wormholeClassID) {
|
||||
case SOLAR_SYSTEM_CLASS_IDS.ns:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCenterSystem';
|
||||
export * from './useSelectSystem';
|
||||
export * from './useSelectSystems';
|
||||
export * from './useMapCommands';
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandCenterSystem } from '@/hooks/Mapper/types';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SYSTEM_FOCUSED_LIFETIME } from '@/hooks/Mapper/constants.ts';
|
||||
|
||||
export const useCenterSystem = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
const { update } = useMapState();
|
||||
|
||||
const ref = useRef({ rf, update });
|
||||
ref.current = { rf, update };
|
||||
|
||||
const highlightTimeout = useRef<number>();
|
||||
|
||||
return useCallback((systemId: CommandCenterSystem) => {
|
||||
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId);
|
||||
@@ -14,5 +20,16 @@ export const useCenterSystem = () => {
|
||||
return;
|
||||
}
|
||||
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
|
||||
|
||||
ref.current.update({ systemHighlighted: systemId });
|
||||
|
||||
if (highlightTimeout.current !== undefined) {
|
||||
clearTimeout(highlightTimeout.current);
|
||||
}
|
||||
|
||||
highlightTimeout.current = setTimeout(() => {
|
||||
highlightTimeout.current = undefined;
|
||||
ref.current.update({ systemHighlighted: undefined });
|
||||
}, SYSTEM_FOCUSED_LIFETIME);
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandCharacterAdded,
|
||||
CommandCharacterRemoved,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
CommandCharacterUpdated,
|
||||
CommandPresentCharacters,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useCommandsCharacters = () => {
|
||||
const { update } = useMapState();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const rf = useReactFlow();
|
||||
@@ -11,6 +13,20 @@ export const useMapInit = () => {
|
||||
const ref = useRef({ rf, data, update });
|
||||
ref.current = { update, data, rf };
|
||||
|
||||
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
|
||||
|
||||
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
|
||||
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
@@ -22,9 +38,10 @@ export const useMapInit = () => {
|
||||
user_characters,
|
||||
present_characters,
|
||||
hubs,
|
||||
options,
|
||||
user_permissions,
|
||||
}: CommandInit) => {
|
||||
const { update } = ref.current;
|
||||
const { rf } = ref.current;
|
||||
|
||||
const updateData: Partial<MapData> = {};
|
||||
|
||||
@@ -48,6 +65,14 @@ export const useMapInit = () => {
|
||||
updateData.hubs = hubs;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
updateData.options = options;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
updateData.userPermissions = user_permissions;
|
||||
}
|
||||
|
||||
if (systems) {
|
||||
updateData.systems = systems;
|
||||
}
|
||||
@@ -63,11 +88,13 @@ export const useMapInit = () => {
|
||||
update(updateData);
|
||||
|
||||
if (systems) {
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
handleUpdateSystems(systems);
|
||||
// rf.setNodes(systems.map(convertSystem2Node));
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
handleUpdateConnections(connections);
|
||||
// rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystem } from '@/hooks/Mapper/types';
|
||||
|
||||
export const useSelectSystem = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
|
||||
return useCallback((systemId: CommandSelectSystem) => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: node.id === systemId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf, onSelectionChange });
|
||||
ref.current = { rf, onSelectionChange };
|
||||
|
||||
return useCallback(({ systems, delay }: CommandSelectSystems) => {
|
||||
const run = () => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: systems.includes(node.id),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (delay == null || delay === 0) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(run, delay);
|
||||
}, []);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
||||
systemId: realSystemId,
|
||||
outCommand,
|
||||
showAllVisible: false,
|
||||
sinceHours: 1,
|
||||
});
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -14,12 +13,16 @@ import {
|
||||
CommandRemoveSystems,
|
||||
Commands,
|
||||
CommandSelectSystem,
|
||||
CommandSelectSystems,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
MapHandlers,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import {
|
||||
useCenterSystem,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
useMapAddSystems,
|
||||
@@ -27,10 +30,8 @@ import {
|
||||
useMapInit,
|
||||
useMapRemoveSystems,
|
||||
useMapUpdateSystems,
|
||||
useCenterSystem,
|
||||
useSelectSystem,
|
||||
useSelectSystems,
|
||||
} from './api';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -38,7 +39,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
const mapUpdateSystems = useMapUpdateSystems();
|
||||
const removeSystems = useMapRemoveSystems(onSelectionChange);
|
||||
const centerSystem = useCenterSystem();
|
||||
const selectSystem = useSelectSystem();
|
||||
const selectSystems = useSelectSystems(onSelectionChange);
|
||||
|
||||
const selectRef = useRef({ onSelectionChange });
|
||||
selectRef.current = { onSelectionChange };
|
||||
@@ -105,14 +106,11 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
break;
|
||||
|
||||
case Commands.selectSystem:
|
||||
setTimeout(() => {
|
||||
const systemId = `${data}`;
|
||||
selectRef.current.onSelectionChange({
|
||||
systems: [systemId],
|
||||
connections: [],
|
||||
});
|
||||
selectSystem(systemId as CommandSelectSystem);
|
||||
}, 500);
|
||||
selectSystems({ systems: [data as string], delay: 500 });
|
||||
break;
|
||||
|
||||
case Commands.selectSystems:
|
||||
selectSystems(data as CommandSelectSystems);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
interface Kill {
|
||||
solar_system_id: number | string;
|
||||
@@ -9,32 +10,78 @@ interface Kill {
|
||||
|
||||
interface MapEvent {
|
||||
name: Commands;
|
||||
data?: any;
|
||||
data?: unknown;
|
||||
payload?: Kill[];
|
||||
}
|
||||
|
||||
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
|
||||
function getActivityType(count: number): string {
|
||||
if (count <= 5) return 'activityNormal';
|
||||
if (count <= 30) return 'activityWarn';
|
||||
return 'activityDanger';
|
||||
}
|
||||
|
||||
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null = null): { killsCount: number | null; killsActivityType: string | null } {
|
||||
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
|
||||
const { data: mapData } = useMapRootState();
|
||||
const { detailedKills = {} } = mapData;
|
||||
|
||||
// Calculate 1-hour kill count from detailed kills
|
||||
const oneHourKillCount = useMemo(() => {
|
||||
const systemKills = detailedKills[systemId] || [];
|
||||
|
||||
// If we have detailed kills data (even if empty), use it for counting
|
||||
if (Object.prototype.hasOwnProperty.call(detailedKills, systemId)) {
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
const recentKills = systemKills.filter(kill => {
|
||||
if (!kill.kill_time) return false;
|
||||
const killTime = new Date(kill.kill_time).getTime();
|
||||
if (isNaN(killTime)) return false;
|
||||
return killTime >= oneHourAgo;
|
||||
});
|
||||
|
||||
return recentKills.length; // Return 0 if no recent kills, not null
|
||||
}
|
||||
|
||||
// Return null only if we don't have detailed kills data for this system
|
||||
return null;
|
||||
}, [detailedKills, systemId]);
|
||||
|
||||
useEffect(() => {
|
||||
setKillsCount(initialKillsCount);
|
||||
}, [initialKillsCount]);
|
||||
// Always prefer the calculated 1-hour count over initial count
|
||||
// This ensures we properly expire old kills
|
||||
if (oneHourKillCount !== null) {
|
||||
setKillsCount(oneHourKillCount);
|
||||
} else if (detailedKills[systemId] && detailedKills[systemId].length === 0) {
|
||||
// If we have detailed kills data but it's empty, set to 0
|
||||
setKillsCount(0);
|
||||
} else {
|
||||
// Only fall back to initial count if we have no detailed kills data at all
|
||||
setKillsCount(initialKillsCount);
|
||||
}
|
||||
}, [oneHourKillCount, initialKillsCount, detailedKills, systemId]);
|
||||
|
||||
const handleEvent = useCallback(
|
||||
(event: MapEvent): boolean => {
|
||||
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
|
||||
const killForSystem = event.payload.find(kill => kill.solar_system_id.toString() === systemId.toString());
|
||||
if (killForSystem && typeof killForSystem.kills === 'number') {
|
||||
setKillsCount(killForSystem.kills);
|
||||
// Only update if we don't have detailed kills data
|
||||
if (!detailedKills[systemId] || detailedKills[systemId].length === 0) {
|
||||
setKillsCount(killForSystem.kills);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[systemId],
|
||||
[systemId, detailedKills],
|
||||
);
|
||||
|
||||
useMapEventListener(handleEvent);
|
||||
|
||||
return killsCount;
|
||||
const killsActivityType = useMemo(() => {
|
||||
return killsCount !== null && killsCount > 0 ? getActivityType(killsCount) : null;
|
||||
}, [killsCount]);
|
||||
|
||||
return { killsCount, killsActivityType };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
|
||||
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
|
||||
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
|
||||
import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
@@ -15,20 +15,12 @@ import { useSystemName } from './useSystemName';
|
||||
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
function getActivityType(count: number): string {
|
||||
if (count <= 5) return 'activityNormal';
|
||||
if (count <= 30) return 'activityWarn';
|
||||
return 'activityDanger';
|
||||
}
|
||||
|
||||
export interface SolarSystemNodeVars {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
visible: boolean;
|
||||
isWormhole: boolean;
|
||||
classTitleColor: string | null;
|
||||
killsCount: number | null;
|
||||
killsActivityType: string | null;
|
||||
hasUserCharacters: boolean;
|
||||
showHandlers: boolean;
|
||||
regionClass: string | null;
|
||||
@@ -58,26 +50,9 @@ export interface SolarSystemNodeVars {
|
||||
isRally: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
|
||||
const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Caldari]: 'Caldaria',
|
||||
[Spaces.Matar]: 'Mataria',
|
||||
[Spaces.Amarr]: 'Amarria',
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
};
|
||||
|
||||
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
const localCounterCharacters = useMemo(() => {
|
||||
return nodeVars.charactersInSystem
|
||||
.map(char => ({
|
||||
...char,
|
||||
compact: true,
|
||||
isOwn: nodeVars.userCharacters.includes(char.eve_id),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
|
||||
return { localCounterCharacters };
|
||||
description: string | null;
|
||||
comments_count: number | null;
|
||||
systemHighlighted: string | undefined;
|
||||
}
|
||||
|
||||
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
|
||||
@@ -91,6 +66,8 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
labels,
|
||||
temporary_name,
|
||||
linked_sig_eve_id: linkedSigEveId = '',
|
||||
description,
|
||||
comments_count,
|
||||
} = data;
|
||||
|
||||
const {
|
||||
@@ -112,6 +89,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
constellation_name,
|
||||
} = systemStaticInfo;
|
||||
|
||||
const { isShowUnsplashedSignatures } = interfaceSettings;
|
||||
@@ -124,7 +102,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
characters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
userCharacters,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
@@ -132,6 +109,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
pings,
|
||||
systemHighlighted,
|
||||
},
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
@@ -161,9 +139,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
isShowLinkedSigId,
|
||||
});
|
||||
|
||||
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
|
||||
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
|
||||
|
||||
const hasUserCharacters = useMemo(
|
||||
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
|
||||
[charactersInSystem, userCharacters],
|
||||
@@ -179,7 +154,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
const showHandlers = isConnecting || hoverNodeId === id;
|
||||
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
|
||||
const regionClass = showKSpaceBG ? SPACE_TO_CLASS[space] || null : null;
|
||||
|
||||
const { systemName, computedTemporaryName, customName } = useSystemName({
|
||||
isTempSystemNameEnabled,
|
||||
@@ -195,18 +170,24 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
|
||||
|
||||
const isRally = useMemo(
|
||||
() => pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
|
||||
() => !!pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
|
||||
[pings, solar_system_id],
|
||||
);
|
||||
|
||||
const regionName = useMemo(() => {
|
||||
if (region_id === Regions.Pochven) {
|
||||
return constellation_name;
|
||||
}
|
||||
|
||||
return region_name;
|
||||
}, [constellation_name, region_id, region_name]);
|
||||
|
||||
const nodeVars: SolarSystemNodeVars = {
|
||||
id,
|
||||
selected,
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
hasUserCharacters,
|
||||
userCharacters,
|
||||
showHandlers,
|
||||
@@ -233,9 +214,12 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: computedTemporaryName,
|
||||
regionName: region_name,
|
||||
regionName,
|
||||
solarSystemName: solar_system_name,
|
||||
isRally,
|
||||
description,
|
||||
comments_count,
|
||||
systemHighlighted,
|
||||
};
|
||||
|
||||
return nodeVars;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface UseSystemNameParams {
|
||||
isTempSystemNameEnabled: boolean;
|
||||
@@ -26,7 +26,7 @@ export const useSystemName = ({
|
||||
}
|
||||
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
return temporary_name ? `${linkedSigPrefix}:${temporary_name}` : `${linkedSigPrefix}:${solar_system_name}`;
|
||||
}
|
||||
|
||||
return temporary_name ?? '';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
|
||||
const useThrottle = () => {
|
||||
const throttleSeed = useRef<number | null>(null);
|
||||
|
||||
@@ -10,3 +10,5 @@ export type OnMapSelectionChange = (event: {
|
||||
}) => void;
|
||||
|
||||
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;
|
||||
|
||||
export type MapViewport = { zoom: 1; x: 0; y: 0 };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@use './eve-common-variables';
|
||||
@use './eve-common';
|
||||
|
||||
.default-theme {
|
||||
--rf-bg-color: #0C0A09;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
@use "sass:color";
|
||||
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyDark20: darken($friendlyBase, 20%);
|
||||
$friendlyDark30: darken($friendlyBase, 30%);
|
||||
$friendlyDark5: darken($friendlyBase, 5%);
|
||||
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
|
||||
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
|
||||
$friendlyDark5: color.adjust($friendlyBase, $lightness: -5%);
|
||||
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
|
||||
$lookingForDark15: darken($lookingForBase, 15%);
|
||||
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
|
||||
|
||||
$homeBase: rgb(179, 253, 67);
|
||||
$homeAlpha: rgba(186, 248, 48, 0.32);
|
||||
$homeBackground: #a0fa5636;
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
|
||||
|
||||
:root {
|
||||
--pastel-blue: #5a7d9a;
|
||||
@@ -117,6 +118,7 @@ $homeDark30: darken($homeBase, 30%);
|
||||
|
||||
--conn-time-eol: #7452c3e3;
|
||||
--conn-frigate: #325d88;
|
||||
--conn-bridge: rgba(135, 185, 93, 0.85);
|
||||
--conn-save: rgba(155, 102, 45, 0.85);
|
||||
--selected-item-bg: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './eve-common-variables';
|
||||
@use './eve-common-variables';
|
||||
|
||||
|
||||
.eve-wh-effect-color-pulsar {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
@import './default-theme.scss';
|
||||
@import './pathfinder-theme.scss';
|
||||
@use './default-theme.scss';
|
||||
@use './pathfinder-theme.scss';
|
||||
@@ -1,10 +1,11 @@
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@use "sass:color";
|
||||
@use './eve-common-variables';
|
||||
@use './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: rgb(30, 161, 255);
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020;
|
||||
|
||||
$neon-color-1: rgb(27, 132, 236);
|
||||
$neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
@@ -1,37 +1,48 @@
|
||||
import { Position, internalsSymbol } from 'reactflow';
|
||||
import { Position, internalsSymbol, Node } from 'reactflow';
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to
|
||||
function getParams(nodeA, nodeB) {
|
||||
type Coords = [number, number];
|
||||
type CoordsWithPosition = [number, number, Position];
|
||||
|
||||
function segmentsIntersect(a1: number, a2: number, b1: number, b2: number): boolean {
|
||||
const [minA, maxA] = a1 < a2 ? [a1, a2] : [a2, a1];
|
||||
const [minB, maxB] = b1 < b2 ? [b1, b2] : [b2, b1];
|
||||
|
||||
return maxA >= minB && maxB >= minA;
|
||||
}
|
||||
|
||||
function getParams(nodeA: Node, nodeB: Node): CoordsWithPosition {
|
||||
const centerA = getNodeCenter(nodeA);
|
||||
const centerB = getNodeCenter(nodeB);
|
||||
|
||||
const horizontalDiff = Math.abs(centerA.x - centerB.x);
|
||||
const verticalDiff = Math.abs(centerA.y - centerB.y);
|
||||
|
||||
let position: Position;
|
||||
|
||||
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
|
||||
if (horizontalDiff > verticalDiff) {
|
||||
position = centerA.x > centerB.x ? Position.Left : Position.Right;
|
||||
} else {
|
||||
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
|
||||
if (
|
||||
segmentsIntersect(
|
||||
nodeA.positionAbsolute!.x - 10,
|
||||
nodeA.positionAbsolute!.x - 10 + nodeA.width! + 20,
|
||||
nodeB.positionAbsolute!.x,
|
||||
nodeB.positionAbsolute!.x + nodeB.width!,
|
||||
)
|
||||
) {
|
||||
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
|
||||
} else {
|
||||
position = centerA.x > centerB.x ? Position.Left : Position.Right;
|
||||
}
|
||||
|
||||
const [x, y] = getHandleCoordsByPosition(nodeA, position);
|
||||
return [x, y, position];
|
||||
}
|
||||
|
||||
function getHandleCoordsByPosition(node, handlePosition) {
|
||||
// all handles are from type source, that's why we use handleBounds.source here
|
||||
const handle = node[internalsSymbol].handleBounds.source.find(h => h.position === handlePosition);
|
||||
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords {
|
||||
const handle = node[internalsSymbol]!.handleBounds!.source!.find(h => h.position === handlePosition);
|
||||
|
||||
if (!handle) {
|
||||
throw new Error(`Handle with position ${handlePosition} not found on node ${node.id}`);
|
||||
}
|
||||
|
||||
let offsetX = handle.width / 2;
|
||||
let offsetY = handle.height / 2;
|
||||
|
||||
// this is a tiny detail to make the markerEnd of an edge visible.
|
||||
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
||||
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
||||
switch (handlePosition) {
|
||||
case Position.Left:
|
||||
offsetX = 0;
|
||||
@@ -47,21 +58,20 @@ function getHandleCoordsByPosition(node, handlePosition) {
|
||||
break;
|
||||
}
|
||||
|
||||
const x = node.positionAbsolute.x + handle.x + offsetX;
|
||||
const y = node.positionAbsolute.y + handle.y + offsetY;
|
||||
const x = node.positionAbsolute!.x + handle.x + offsetX;
|
||||
const y = node.positionAbsolute!.y + handle.y + offsetY;
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function getNodeCenter(node) {
|
||||
function getNodeCenter(node: Node): { x: number; y: number } {
|
||||
return {
|
||||
x: node.positionAbsolute.x + node.width / 2,
|
||||
y: node.positionAbsolute.y + node.height / 2,
|
||||
x: node.positionAbsolute!.x + node.width! / 2,
|
||||
y: node.positionAbsolute!.y + node.height! / 2,
|
||||
};
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source, target) {
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
const [sx, sy, sourcePos] = getParams(source, target);
|
||||
const [tx, ty, targetPos] = getParams(target, source);
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
|
||||
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import classes from './AddSystemDialog.module.scss';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
|
||||
|
||||
@@ -34,6 +33,7 @@ export const AddSystemDialog = ({
|
||||
data: { wormholesData },
|
||||
} = useMapRootState();
|
||||
|
||||
// TODO fix it
|
||||
const inputRef = useRef<any>();
|
||||
const onShow = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
@@ -62,6 +62,7 @@ export const AddSystemDialog = ({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO fix it
|
||||
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
|
||||
const amatch = a.label.indexOf(query);
|
||||
const bmatch = b.label.indexOf(query);
|
||||
@@ -114,90 +115,93 @@ export const AddSystemDialog = ({
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3 px-1.5">
|
||||
<div className="flex flex-col gap-2 py-3.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<IconField>
|
||||
<AutoComplete
|
||||
ref={inputRef}
|
||||
multiple
|
||||
showEmptyMessage
|
||||
scrollHeight="300px"
|
||||
value={selectedItem}
|
||||
suggestions={filteredItems}
|
||||
completeMethod={searchItems}
|
||||
onChange={e => {
|
||||
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
|
||||
}}
|
||||
emptyMessage="Not found any system..."
|
||||
placeholder="Type here..."
|
||||
field="label"
|
||||
id="value"
|
||||
className="w-full"
|
||||
itemTemplate={(item: SearchSystemItem) => {
|
||||
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
|
||||
const sortedStatics = sortWHClasses(wormholesData, statics);
|
||||
const isWH = isWormholeSpace(system_class);
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-3 px-1.5">
|
||||
<div className="flex flex-col gap-2 py-3.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<IconField>
|
||||
<AutoComplete
|
||||
ref={inputRef}
|
||||
multiple
|
||||
showEmptyMessage
|
||||
scrollHeight="300px"
|
||||
value={selectedItem}
|
||||
suggestions={filteredItems}
|
||||
completeMethod={searchItems}
|
||||
onChange={e => {
|
||||
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
|
||||
}}
|
||||
emptyMessage="Not found any system..."
|
||||
placeholder="Type here..."
|
||||
field="label"
|
||||
id="value"
|
||||
className="w-full"
|
||||
itemTemplate={(item: SearchSystemItem) => {
|
||||
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
|
||||
const sortedStatics = sortWHClasses(wormholesData, statics);
|
||||
const isWH = isWormholeSpace(system_class);
|
||||
|
||||
return (
|
||||
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
|
||||
<SystemViewStandalone
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
|
||||
{effect_name && isWH && (
|
||||
<WHEffectView
|
||||
effectName={effect_name}
|
||||
effectPower={effect_power}
|
||||
className={classes.SearchItemEffect}
|
||||
return (
|
||||
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
|
||||
<SystemViewStandalone
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWH && (
|
||||
<div className="flex gap-1 grow justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-1">
|
||||
{sortedStatics.map(x => (
|
||||
<WHClassView key={x} whClassName={x} />
|
||||
))}
|
||||
{effect_name && isWH && (
|
||||
<WHEffectView
|
||||
effectName={effect_name}
|
||||
effectPower={effect_power}
|
||||
className={classes.SearchItemEffect}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWH && (
|
||||
<div className="flex gap-1 grow justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-1">
|
||||
{sortedStatics.map(x => (
|
||||
<WHClassView key={x} whClassName={x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selectedItemTemplate={(item: SearchSystemItem) => (
|
||||
<SystemViewStandalone
|
||||
security={item.system_static_info.security}
|
||||
system_class={item.system_static_info.system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</IconField>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selectedItemTemplate={(item: SearchSystemItem) => (
|
||||
<SystemViewStandalone
|
||||
security={item.system_static_info.security}
|
||||
system_class={item.system_static_info.system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</IconField>
|
||||
|
||||
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
|
||||
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<WdButton
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
outlined
|
||||
disabled={!selectedItem || selectedItem.length !== 1}
|
||||
size="small"
|
||||
label="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
outlined
|
||||
disabled={!selectedItem || selectedItem.length !== 1}
|
||||
size="small"
|
||||
label="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
|
||||
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
|
||||
|
||||
@@ -28,8 +29,7 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
const char = useGetCacheCharacter(characterEveId);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const cpRemoveBtnRef = useRef<HTMLElement>();
|
||||
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
const ref = useRef({ outCommand, id });
|
||||
@@ -45,9 +45,6 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
const handleMouseEnter = useCallback(() => setHovered(true), []);
|
||||
const handleMouseLeave = useCallback(() => setHovered(false), []);
|
||||
|
||||
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
|
||||
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoDrawer
|
||||
@@ -68,11 +65,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
{!hovered && <TimeAgo timestamp={time} />}
|
||||
{hovered && (
|
||||
// @ts-ignore
|
||||
<div ref={cpRemoveBtnRef}>
|
||||
<div ref={cfRef}>
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')}
|
||||
tooltip={TOOLTIP_PROPS}
|
||||
onClick={handleShowCP}
|
||||
onClick={cfShow}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -85,9 +82,9 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
</InfoDrawer>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cpRemoveBtnRef.current}
|
||||
visible={cpRemoveVisible}
|
||||
onHide={handleHideCP}
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete?"
|
||||
icon="pi pi-exclamation-triangle"
|
||||
accept={handleDelete}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Button } from 'primereact/button';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import clsx from 'clsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
|
||||
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
|
||||
import {
|
||||
CharacterCardById,
|
||||
SystemView,
|
||||
TimeAgo,
|
||||
TooltipPosition,
|
||||
WdButton,
|
||||
WdImgButton,
|
||||
WdImgButtonTooltip,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import useRefState from 'react-usestateref';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import useRefState from 'react-usestateref';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
|
||||
const PING_PLACEMENT_MAP = {
|
||||
[PingsPlacement.rightTop]: 'top-right',
|
||||
@@ -78,9 +79,7 @@ export interface PingsInterfaceProps {
|
||||
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
const toast = useRef<Toast>(null);
|
||||
const [isShow, setIsShow, isShowRef] = useRefState(false);
|
||||
|
||||
const cpRemoveBtnRef = useRef<HTMLElement>();
|
||||
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
@@ -98,9 +97,6 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
|
||||
|
||||
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
|
||||
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
|
||||
|
||||
const navigateTo = useCallback(() => {
|
||||
if (!ping) {
|
||||
return;
|
||||
@@ -119,7 +115,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.cancelPing,
|
||||
data: { type: ping.type, solar_system_id: ping.solar_system_id },
|
||||
data: { type: ping.type, id: ping.id },
|
||||
});
|
||||
}, [outCommand, ping]);
|
||||
|
||||
@@ -242,11 +238,11 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
/>
|
||||
|
||||
{/*@ts-ignore*/}
|
||||
<div ref={cpRemoveBtnRef}>
|
||||
<div ref={cfRef}>
|
||||
<WdImgButton
|
||||
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
|
||||
tooltip={DELETE_TOOLTIP_PROPS}
|
||||
onClick={handleShowCP}
|
||||
onClick={cfShow}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO ADD solar system menu*/}
|
||||
@@ -260,7 +256,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
)}
|
||||
></Toast>
|
||||
|
||||
<Button
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
@@ -272,9 +268,9 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
/>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cpRemoveBtnRef.current}
|
||||
visible={cpRemoveVisible}
|
||||
onHide={handleHideCP}
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { WdImageSize, WdImgButton, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SystemCustomLabelDialog {
|
||||
systemId: string;
|
||||
@@ -126,7 +125,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton type="submit" onClick={handleSave} outlined size="small" label="Save"></WdButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,17 +7,14 @@ import {
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import {
|
||||
SETTINGS_KEYS,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
|
||||
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData';
|
||||
|
||||
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
|
||||
|
||||
@@ -119,14 +116,14 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (signature: SystemSignature) => {
|
||||
(signature: SystemSignature) => {
|
||||
if (!signature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { outCommand } = ref.current;
|
||||
|
||||
await outCommand({
|
||||
outCommand({
|
||||
type: OutCommand.linkSignatureToSystem,
|
||||
data: {
|
||||
...data,
|
||||
@@ -134,34 +131,16 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
},
|
||||
});
|
||||
|
||||
if (parseSignatureCustomInfo(signature.custom_info).isEOL === true) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionTimeStatus,
|
||||
data: {
|
||||
source: data.solar_system_source,
|
||||
target: data.solar_system_target,
|
||||
value: TimeStatus.eol,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const whShipSize = getWhSize(wormholes, signature.type);
|
||||
if (whShipSize !== undefined && whShipSize !== null) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionShipSizeType,
|
||||
data: {
|
||||
source: data.solar_system_source,
|
||||
target: data.solar_system_target,
|
||||
value: whShipSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
},
|
||||
[data, setVisible, wormholes],
|
||||
[data, setVisible],
|
||||
);
|
||||
|
||||
const { signatures } = useSystemSignaturesData({
|
||||
systemId: `${data.solar_system_source}`,
|
||||
settings: LINK_SIGNTATURE_SETTINGS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetSystemDynamicInfo) {
|
||||
handleHide();
|
||||
@@ -179,10 +158,12 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
>
|
||||
<SystemSignaturesContent
|
||||
systemId={`${data.solar_system_source}`}
|
||||
hideLinkedSignatures
|
||||
signatures={signatures}
|
||||
hasUnsupportedLanguage={false}
|
||||
settings={LINK_SIGNTATURE_SETTINGS}
|
||||
hideLinkedSignatures
|
||||
selectable
|
||||
onSelect={handleSelect}
|
||||
selectable={true}
|
||||
filterSignature={filterSignature}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
const PING_TITLES = {
|
||||
[PingType.Rally]: 'RALLY',
|
||||
@@ -63,7 +62,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
@@ -92,7 +91,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
|
||||
<WdButton type="submit" onClick={handleSave} size="small" severity="danger" label="Ping!" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SystemSettingsDialog {
|
||||
systemId: string;
|
||||
@@ -114,7 +113,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
<Dialog
|
||||
header="System settings"
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
@@ -126,7 +125,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 px-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="username">Custom name</label>
|
||||
@@ -206,7 +205,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
aria-describedby="temporaryName"
|
||||
autoComplete="off"
|
||||
value={temporaryName}
|
||||
maxLength={10}
|
||||
maxLength={12}
|
||||
onChange={e => setTemporaryName(e.target.value)}
|
||||
/>
|
||||
</IconField>
|
||||
@@ -226,7 +225,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Save" type="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootP
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions';
|
||||
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import clsx from 'clsx';
|
||||
@@ -14,9 +13,9 @@ import clsx from 'clsx';
|
||||
export const LocalCharacters = () => {
|
||||
const {
|
||||
data: { characters, userCharacters, selectedSystems },
|
||||
storedSettings: { settingsLocal, settingsLocalUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||
const [systemId] = selectedSystems;
|
||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||
@@ -31,12 +30,12 @@ export const LocalCharacters = () => {
|
||||
.map(x => ({
|
||||
...x,
|
||||
isOwn: userCharacters.includes(x.eve_id),
|
||||
compact: settings.compact,
|
||||
showShipName: settings.showShipName,
|
||||
compact: settingsLocal.compact,
|
||||
showShipName: settingsLocal.showShipName,
|
||||
}))
|
||||
.sort(sortCharacters);
|
||||
|
||||
if (!showOffline || !settings.showOffline) {
|
||||
if (!showOffline || !settingsLocal.showOffline) {
|
||||
return filtered.filter(c => c.online);
|
||||
}
|
||||
return filtered;
|
||||
@@ -44,9 +43,9 @@ export const LocalCharacters = () => {
|
||||
characters,
|
||||
systemId,
|
||||
userCharacters,
|
||||
settings.compact,
|
||||
settings.showOffline,
|
||||
settings.showShipName,
|
||||
settingsLocal.compact,
|
||||
settingsLocal.showOffline,
|
||||
settingsLocal.showShipName,
|
||||
showOffline,
|
||||
]);
|
||||
|
||||
@@ -54,7 +53,7 @@ export const LocalCharacters = () => {
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
const showList = sorted.length > 0 && selectedSystems.length === 1;
|
||||
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settingsLocal.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
@@ -63,8 +62,8 @@ export const LocalCharacters = () => {
|
||||
sortedCount={sorted.length}
|
||||
showList={showList}
|
||||
showOffline={showOffline}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
settings={settingsLocal}
|
||||
setSettings={settingsLocalUpdate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -81,7 +80,7 @@ export const LocalCharacters = () => {
|
||||
{showList && (
|
||||
<LocalCharactersList
|
||||
items={sorted}
|
||||
itemSize={settings.compact ? 26 : 41}
|
||||
itemSize={settingsLocal.compact ? 26 : 41}
|
||||
itemTemplate={itemTemplate}
|
||||
containerClassName={clsx(
|
||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface LocalCharacterWidgetSettings {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
version: number;
|
||||
showShipName: boolean;
|
||||
}
|
||||
|
||||
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
|
||||
compact: true,
|
||||
showOffline: false,
|
||||
version: 0,
|
||||
showShipName: false,
|
||||
};
|
||||
|
||||
export function useLocalCharacterWidgetSettings() {
|
||||
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
|
||||
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import {
|
||||
RoutesType,
|
||||
useRouteProvider,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
|
||||
interface RoutesSettingsDialog {
|
||||
visible: boolean;
|
||||
@@ -83,7 +81,7 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Apply"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Apply"></WdButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
@@ -64,12 +65,8 @@ export const useLoadRoutes = ({
|
||||
systems?.length,
|
||||
connections,
|
||||
hubs,
|
||||
routesSettings,
|
||||
...Object.keys(routesSettings)
|
||||
.sort()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map(x => routesSettings[x]),
|
||||
// we need make it flat recursively
|
||||
...flattenValues(routesSettings),
|
||||
...deps,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
|
||||
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemInfoContent } from './SystemInfoContent';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { SystemInfoContent } from './SystemInfoContent';
|
||||
|
||||
export const SystemInfo = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -48,7 +48,7 @@ export const SystemInfo = () => {
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-1 items-center">
|
||||
<a href={`https://zkillboard.com/system/${systemId}`} rel="noreferrer" target="_blank">
|
||||
<a href={`https://zkillboard.com/system/${systemId}/`} rel="noreferrer" target="_blank">
|
||||
<img src={ZKB_ICON} width="14" height="14" className="external-icon" />
|
||||
</a>
|
||||
<a href={`http://anoik.is/systems/${solarSystemName}`} rel="noreferrer" target="_blank">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
@@ -8,8 +7,9 @@ import {
|
||||
Setting,
|
||||
SettingsTypes,
|
||||
SIGNATURE_SETTINGS,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface SystemSignatureSettingsDialogProps {
|
||||
settings: SignatureSettingsType;
|
||||
@@ -92,7 +92,7 @@ export const SystemSignatureSettingsDialog = ({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Save" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,187 +1,80 @@
|
||||
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSignatureUndo } from './hooks/useSignatureUndo';
|
||||
import { useSystemSignaturesData } from './hooks/useSystemSignaturesData';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
||||
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import {
|
||||
SETTINGS_KEYS,
|
||||
SETTINGS_VALUES,
|
||||
SIGNATURE_SETTING_STORE_KEY,
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SignatureSettingsType,
|
||||
getDeletionTimeoutMs,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
/**
|
||||
* Custom hook for managing pending signature deletions and undo countdown.
|
||||
*/
|
||||
function useSignatureUndo(
|
||||
systemId: string | undefined,
|
||||
settings: SignatureSettingsType,
|
||||
outCommand: OutCommandHandler,
|
||||
) {
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
|
||||
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
|
||||
const newIds = signatures.map(sig => sig.eve_id);
|
||||
setPendingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
newIds.forEach(id => next.add(id));
|
||||
return next;
|
||||
});
|
||||
setDeletedSignatures(prev => [...prev, ...signatures]);
|
||||
}, []);
|
||||
|
||||
// Clear deleted signatures when system changes
|
||||
useEffect(() => {
|
||||
if (systemId) {
|
||||
setDeletedSignatures([]);
|
||||
setPendingIds(new Set());
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [systemId]);
|
||||
|
||||
// kick off or clear countdown whenever pendingIds changes
|
||||
useEffect(() => {
|
||||
// clear any existing timer
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
if (pendingIds.size === 0) {
|
||||
setCountdown(0);
|
||||
setDeletedSignatures([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// determine timeout from settings
|
||||
const timeoutMs = getDeletionTimeoutMs(settings);
|
||||
|
||||
setCountdown(Math.ceil(timeoutMs / 1000));
|
||||
|
||||
// start new interval
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalRef.current!);
|
||||
intervalRef.current = null;
|
||||
setPendingIds(new Set());
|
||||
setDeletedSignatures([]);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pendingIds, settings]);
|
||||
|
||||
// undo handler
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!systemId || pendingIds.size === 0) return;
|
||||
await outCommand({
|
||||
type: OutCommand.undoDeleteSignatures,
|
||||
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
|
||||
});
|
||||
setPendingIds(new Set());
|
||||
setDeletedSignatures([]);
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [systemId, pendingIds, outCommand]);
|
||||
|
||||
return {
|
||||
pendingIds,
|
||||
countdown,
|
||||
deletedSignatures,
|
||||
addDeleted,
|
||||
handleUndo,
|
||||
};
|
||||
}
|
||||
|
||||
export const SystemSignatures = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sigCount, setSigCount] = useState(0);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
outCommand,
|
||||
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
|
||||
SIGNATURE_SETTING_STORE_KEY,
|
||||
{
|
||||
defaultValue: SETTINGS_VALUES,
|
||||
},
|
||||
);
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
|
||||
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
|
||||
systemId,
|
||||
currentSettings,
|
||||
outCommand,
|
||||
|
||||
const handleLazyDeleteToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
settingsSignaturesUpdate(prev => ({
|
||||
...prev,
|
||||
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
|
||||
}));
|
||||
},
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings: settingsSignatures,
|
||||
onLazyDeleteChange: handleLazyDeleteToggle,
|
||||
});
|
||||
|
||||
const sigCount = useMemo(() => signatures.length, [signatures]);
|
||||
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
|
||||
|
||||
const { countdown, handleUndo } = useSignatureUndo(systemId, settingsSignatures, deletedSignatures, outCommand);
|
||||
|
||||
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
|
||||
if (pendingIds.size > 0 && countdown > 0) {
|
||||
if (deletedSignatures.length > 0 && countdown > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUndo();
|
||||
}
|
||||
});
|
||||
|
||||
const handleCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
}, []);
|
||||
|
||||
const handleSettingsSave = useCallback(
|
||||
(newSettings: SignatureSettingsType) => {
|
||||
setCurrentSettings(newSettings);
|
||||
setVisible(false);
|
||||
settingsSignaturesUpdate(newSettings);
|
||||
setShowSettings(false);
|
||||
},
|
||||
[setCurrentSettings],
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const handleLazyDeleteToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
setCurrentSettings(prev => ({
|
||||
...prev,
|
||||
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
|
||||
}));
|
||||
},
|
||||
[setCurrentSettings],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(() => setVisible(true), []);
|
||||
const openSettings = useCallback(() => setShowSettings(true), []);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<SystemSignaturesHeader
|
||||
sigCount={sigCount}
|
||||
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={pendingIds.size}
|
||||
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={deletedSignatures.length}
|
||||
undoCountdown={countdown}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onUndoClick={handleUndo}
|
||||
@@ -197,18 +90,21 @@ export const SystemSignatures = () => {
|
||||
) : (
|
||||
<SystemSignaturesContent
|
||||
systemId={systemId}
|
||||
settings={currentSettings}
|
||||
deletedSignatures={deletedSignatures}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onCountChange={handleCountChange}
|
||||
onSignatureDeleted={addDeleted}
|
||||
signatures={signatures}
|
||||
selectedSignatures={selectedSignatures}
|
||||
onSelectSignatures={setSelectedSignatures}
|
||||
onDeleteSelected={handleDeleteSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
onPaste={handlePaste}
|
||||
hasUnsupportedLanguage={hasUnsupportedLanguage}
|
||||
settings={settingsSignatures}
|
||||
/>
|
||||
)}
|
||||
|
||||
{visible && (
|
||||
{showSettings && (
|
||||
<SystemSignatureSettingsDialog
|
||||
settings={currentSettings}
|
||||
onCancel={() => setVisible(false)}
|
||||
settings={settingsSignatures}
|
||||
onCancel={() => setShowSettings(false)}
|
||||
onSave={handleSettingsSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
SortOrder,
|
||||
} from 'primereact/datatable';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
|
||||
import {
|
||||
@@ -17,9 +16,6 @@ import {
|
||||
GROUPS_LIST,
|
||||
MEDIUM_MAX_WIDTH,
|
||||
OTHER_COLUMNS_WIDTH,
|
||||
SETTINGS_KEYS,
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
|
||||
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
|
||||
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
@@ -32,53 +28,54 @@ import {
|
||||
renderInfoColumn,
|
||||
renderUpdatedTimeLeft,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
|
||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
|
||||
|
||||
type SystemSignaturesSortSettings = {
|
||||
sortField: string;
|
||||
sortOrder: SortOrder;
|
||||
};
|
||||
|
||||
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
|
||||
sortField: 'inserted_at',
|
||||
sortOrder: -1,
|
||||
};
|
||||
|
||||
interface SystemSignaturesContentProps {
|
||||
systemId: string;
|
||||
signatures: ExtendedSystemSignature[];
|
||||
selectedSignatures?: ExtendedSystemSignature[];
|
||||
onSelectSignatures?: (s: ExtendedSystemSignature[]) => void;
|
||||
onDeleteSelected?: () => Promise<void>;
|
||||
onSelectAll?: () => void;
|
||||
onPaste?: (clipboardString: string) => void;
|
||||
settings: SignatureSettingsType;
|
||||
hideLinkedSignatures?: boolean;
|
||||
hasUnsupportedLanguage?: boolean;
|
||||
selectable?: boolean;
|
||||
onSelect?: (signature: SystemSignature) => void;
|
||||
onLazyDeleteChange?: (value: boolean) => void;
|
||||
onCountChange?: (count: number) => void;
|
||||
filterSignature?: (signature: SystemSignature) => boolean;
|
||||
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
|
||||
deletedSignatures?: ExtendedSystemSignature[];
|
||||
}
|
||||
|
||||
export const SystemSignaturesContent = ({
|
||||
systemId,
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
onSelectSignatures,
|
||||
onDeleteSelected,
|
||||
onSelectAll,
|
||||
onPaste,
|
||||
settings,
|
||||
hideLinkedSignatures,
|
||||
hasUnsupportedLanguage,
|
||||
selectable,
|
||||
onSelect,
|
||||
onLazyDeleteChange,
|
||||
onCountChange,
|
||||
filterSignature,
|
||||
onSignatureDeleted,
|
||||
deletedSignatures = [],
|
||||
}: SystemSignaturesContentProps) => {
|
||||
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
|
||||
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
|
||||
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
|
||||
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
|
||||
|
||||
const {
|
||||
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<WdTooltipHandlers>(null);
|
||||
|
||||
@@ -87,37 +84,18 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
const { clipboardContent, setClipboardContent } = useClipboard();
|
||||
|
||||
const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
|
||||
'window:signatures:sort',
|
||||
{ defaultValue: SORT_DEFAULT_VALUES },
|
||||
);
|
||||
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onLazyDeleteChange,
|
||||
onSignatureDeleted,
|
||||
});
|
||||
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectable) return;
|
||||
if (!clipboardContent?.text) return;
|
||||
|
||||
handlePaste(clipboardContent.text);
|
||||
onPaste?.(clipboardContent.text);
|
||||
|
||||
setClipboardContent(null);
|
||||
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
|
||||
}, [selectable, clipboardContent, onPaste, setClipboardContent]);
|
||||
|
||||
useHotkey(true, ['a'], handleSelectAll);
|
||||
useHotkey(true, ['a'], () => onSelectAll?.());
|
||||
|
||||
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
|
||||
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${SIGNATURE_WINDOW_ID}"]`);
|
||||
@@ -130,7 +108,7 @@ export const SystemSignaturesContent = ({
|
||||
event.stopPropagation();
|
||||
|
||||
// Delete key should always immediately delete, never show pending deletions
|
||||
handleDeleteSelected();
|
||||
onDeleteSelected?.();
|
||||
});
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
@@ -165,14 +143,23 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
selectable
|
||||
? onSelect?.(selectableSignatures[0])
|
||||
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
|
||||
: onSelectSignatures?.(selectableSignatures as ExtendedSystemSignature[]);
|
||||
},
|
||||
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
|
||||
[onSelect, selectable, onSelectSignatures, deletedSignatures],
|
||||
);
|
||||
|
||||
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
||||
const {
|
||||
showGroupColumn,
|
||||
showDescriptionColumn,
|
||||
showAddedColumn,
|
||||
showUpdatedColumn,
|
||||
showCharacterColumn,
|
||||
showCharacterPortrait,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
showGroupColumn: settings[SETTINGS_KEYS.SHOW_GROUP_COLUMN] as boolean,
|
||||
showDescriptionColumn: settings[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN] as boolean,
|
||||
showAddedColumn: settings[SETTINGS_KEYS.SHOW_ADDED_COLUMN] as boolean,
|
||||
showUpdatedColumn: settings[SETTINGS_KEYS.SHOW_UPDATED_COLUMN] as boolean,
|
||||
showCharacterColumn: settings[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN] as boolean,
|
||||
showCharacterPortrait: settings[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT] as boolean,
|
||||
@@ -181,9 +168,6 @@ export const SystemSignaturesContent = ({
|
||||
);
|
||||
|
||||
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
|
||||
// Get the set of deleted signature IDs for quick lookup
|
||||
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
|
||||
|
||||
// Common filter function
|
||||
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
|
||||
if (filterSignature && !filterSignature(sig)) {
|
||||
@@ -217,24 +201,8 @@ export const SystemSignaturesContent = ({
|
||||
return settings[sig.kind] as boolean;
|
||||
};
|
||||
|
||||
// Filter active signatures, excluding any that are in the deleted list
|
||||
const activeSignatures = signatures.filter(sig => {
|
||||
// Skip if this signature is in the deleted list
|
||||
if (deletedIds.has(sig.eve_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shouldShowSignature(sig);
|
||||
});
|
||||
|
||||
// Add deleted signatures with pending deletion flag, applying the same filters
|
||||
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
|
||||
...sig,
|
||||
pendingDeletion: true,
|
||||
}));
|
||||
|
||||
return [...activeSignatures, ...deletedWithPendingFlag];
|
||||
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
|
||||
return signatures.filter(sig => shouldShowSignature(sig));
|
||||
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
|
||||
|
||||
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
|
||||
setHoveredSignature(e.data as SystemSignature);
|
||||
@@ -246,8 +214,8 @@ export const SystemSignaturesContent = ({
|
||||
tooltipRef.current?.hide();
|
||||
}, []);
|
||||
|
||||
const refVars = useRef({ settings, selectedSignatures, setSortSettings });
|
||||
refVars.current = { settings, selectedSignatures, setSortSettings };
|
||||
const refVars = useRef({ settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate });
|
||||
refVars.current = { settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate };
|
||||
|
||||
// @ts-ignore
|
||||
const getRowClassName = useCallback(rowData => {
|
||||
@@ -257,15 +225,18 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
return getSignatureRowClass(
|
||||
rowData as ExtendedSystemSignature,
|
||||
refVars.current.selectedSignatures,
|
||||
refVars.current.selectedSignatures || [],
|
||||
refVars.current.settings[SETTINGS_KEYS.COLOR_BY_TYPE] as boolean,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSortSettings = useCallback(
|
||||
(e: DataTableStateEvent) => refVars.current.setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder }),
|
||||
[],
|
||||
);
|
||||
const handleSortSettings = useCallback((e: DataTableStateEvent) => {
|
||||
refVars.current.settingsSignaturesUpdate({
|
||||
...refVars.current.settingsSignatures,
|
||||
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
|
||||
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={tableRef} className="h-full">
|
||||
@@ -286,7 +257,7 @@ export const SystemSignaturesContent = ({
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
selection={selectedSignatures || []}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
@@ -295,8 +266,8 @@ export const SystemSignaturesContent = ({
|
||||
rowHover
|
||||
selectAll
|
||||
onRowDoubleClick={handleRowClick}
|
||||
sortField={sortSettings.sortField}
|
||||
sortOrder={sortSettings.sortOrder}
|
||||
sortField={settingsSignatures[SETTINGS_KEYS.SORT_FIELD] as string}
|
||||
sortOrder={settingsSignatures[SETTINGS_KEYS.SORT_ORDER] as SortOrder}
|
||||
onSort={handleSortSettings}
|
||||
onRowMouseEnter={onRowMouseEnter}
|
||||
onRowMouseLeave={onRowMouseLeave}
|
||||
@@ -317,15 +288,17 @@ export const SystemSignaturesContent = ({
|
||||
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
{showGroupColumn && (
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
@@ -333,6 +306,8 @@ export const SystemSignaturesContent = ({
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
sortable
|
||||
sortField="name"
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
@@ -344,15 +319,17 @@ export const SystemSignaturesContent = ({
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
{showAddedColumn && (
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
{showUpdatedColumn && (
|
||||
<Column
|
||||
field="updated_at"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import {
|
||||
GroupType,
|
||||
SignatureGroup,
|
||||
@@ -96,44 +97,11 @@ export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined =>
|
||||
return MAPPING_GROUP_TO_ENG[val] || undefined;
|
||||
};
|
||||
|
||||
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
|
||||
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
|
||||
|
||||
export enum SETTINGS_KEYS {
|
||||
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
|
||||
SHOW_UPDATED_COLUMN = 'show_updated_column',
|
||||
SHOW_CHARACTER_COLUMN = 'show_character_column',
|
||||
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
|
||||
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
|
||||
DELETION_TIMING = 'deletion_timing',
|
||||
COLOR_BY_TYPE = 'color_by_type',
|
||||
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
|
||||
|
||||
// From SignatureKind
|
||||
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
|
||||
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
|
||||
DEPLOYABLE = SignatureKind.Deployable,
|
||||
STRUCTURE = SignatureKind.Structure,
|
||||
STARBASE = SignatureKind.Starbase,
|
||||
SHIP = SignatureKind.Ship,
|
||||
DRONE = SignatureKind.Drone,
|
||||
|
||||
// From SignatureGroup
|
||||
WORMHOLE = SignatureGroup.Wormhole,
|
||||
RELIC_SITE = SignatureGroup.RelicSite,
|
||||
DATA_SITE = SignatureGroup.DataSite,
|
||||
ORE_SITE = SignatureGroup.OreSite,
|
||||
GAS_SITE = SignatureGroup.GasSite,
|
||||
COMBAT_SITE = SignatureGroup.CombatSite,
|
||||
}
|
||||
|
||||
export enum SettingsTypes {
|
||||
flag,
|
||||
dropdown,
|
||||
}
|
||||
|
||||
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
|
||||
|
||||
export type Setting = {
|
||||
key: SETTINGS_KEYS;
|
||||
name: string;
|
||||
@@ -142,12 +110,6 @@ export type Setting = {
|
||||
options?: { label: string; value: number | string | boolean }[];
|
||||
};
|
||||
|
||||
export enum SIGNATURES_DELETION_TIMING {
|
||||
IMMEDIATE,
|
||||
DEFAULT,
|
||||
EXTENDED,
|
||||
}
|
||||
|
||||
// Now use a stricter type: every timing key maps to a number
|
||||
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
|
||||
|
||||
@@ -168,6 +130,8 @@ export const SIGNATURE_SETTINGS = {
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.COMBAT_SITE, name: 'Show Combat Sites' },
|
||||
],
|
||||
uiFlags: [
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_GROUP_COLUMN, name: 'Show Group Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_ADDED_COLUMN, name: 'Show Added Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_UPDATED_COLUMN, name: 'Show Updated Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN, name: 'Show Description Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_CHARACTER_COLUMN, name: 'Show Character Column' },
|
||||
@@ -194,32 +158,6 @@ export const SIGNATURE_SETTINGS = {
|
||||
],
|
||||
};
|
||||
|
||||
export const SETTINGS_VALUES: SignatureSettingsType = {
|
||||
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
|
||||
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
|
||||
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
|
||||
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
|
||||
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
|
||||
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
|
||||
|
||||
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
|
||||
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
|
||||
[SETTINGS_KEYS.DEPLOYABLE]: true,
|
||||
[SETTINGS_KEYS.STRUCTURE]: true,
|
||||
[SETTINGS_KEYS.STARBASE]: true,
|
||||
[SETTINGS_KEYS.SHIP]: true,
|
||||
[SETTINGS_KEYS.DRONE]: true,
|
||||
|
||||
[SETTINGS_KEYS.WORMHOLE]: true,
|
||||
[SETTINGS_KEYS.RELIC_SITE]: true,
|
||||
[SETTINGS_KEYS.DATA_SITE]: true,
|
||||
[SETTINGS_KEYS.ORE_SITE]: true,
|
||||
[SETTINGS_KEYS.GAS_SITE]: true,
|
||||
[SETTINGS_KEYS.COMBAT_SITE]: true,
|
||||
};
|
||||
|
||||
// Now this map is strongly typed as “number” for each timing enum
|
||||
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
|
||||
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getState } from './getState';
|
||||
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
describe('getState', () => {
|
||||
const mockSignaturesMatch: string[] = []; // This parameter is not used in the function
|
||||
|
||||
it('should return 0 if group is undefined', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: undefined } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 if group is CosmicSignature', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: SignatureGroup.CosmicSignature } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 1 if group is not CosmicSignature and name is undefined', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: undefined, group: SignatureGroup.Wormhole } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 1 if group is not CosmicSignature and name is empty', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.Wormhole } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 1 if group is not CosmicSignature and name is UNKNOWN_SIGNATURE_NAME', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: UNKNOWN_SIGNATURE_NAME, group: SignatureGroup.Wormhole } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 2 if group is not CosmicSignature and name is a non-empty string', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: 'Custom Name', group: SignatureGroup.Wormhole } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
|
||||
});
|
||||
|
||||
// According to the current implementation, state = -1 is unreachable
|
||||
// because the conditions for 0, 1, and 2 cover all possibilities for the given inputs.
|
||||
// If the logic of getState were to change to make -1 possible, a test case should be added here.
|
||||
// For now, we can test a scenario that should lead to one of the valid states,
|
||||
// for example, if group is something other than CosmicSignature and name is valid.
|
||||
it('should handle other valid signature groups correctly, leading to state 2 with a valid name', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: 'Combat Site', group: SignatureGroup.CombatSite } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle other valid signature groups correctly, leading to state 1 with an empty name', () => {
|
||||
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.DataSite } as SystemSignature;
|
||||
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { getRowBackgroundColor } from './getRowBackgroundColor';
|
||||
import classes from './rowStyles.module.scss';
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getSignatureRowClass(
|
||||
return clsx([...baseCls, 'bg-violet-400/40 hover:bg-violet-300/40']);
|
||||
}
|
||||
|
||||
if (row.pendingDeletion) {
|
||||
if (row.deleted) {
|
||||
return clsx([...baseCls, 'bg-red-400/40 hover:bg-red-400/50']);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user