mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-02 07:27:04 +00:00
Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
9d3db19dc1 | ||
|
|
3953e33f37 | ||
|
|
611fdd56d0 | ||
|
|
36a4fd0f35 | ||
|
|
488984a988 | ||
|
|
1b183d6e58 | ||
|
|
3dcb6d30b5 | ||
|
|
1d11788f89 | ||
|
|
d3822128ab | ||
|
|
6ac7836505 | ||
|
|
5796fccae4 | ||
|
|
22ab6e544c | ||
|
|
5e735ab8bd | ||
|
|
450139402d | ||
|
|
1e0d4f1fde | ||
|
|
7e9b1af3a3 | ||
|
|
30b90cd4be | ||
|
|
f28affa222 | ||
|
|
2ae7b187b8 | ||
|
|
ad037be1f4 | ||
|
|
9e31542c5f | ||
|
|
aae6ed0cb3 | ||
|
|
7ea2ecb8e9 | ||
|
|
852fc28896 | ||
|
|
fcdab79802 | ||
|
|
bb0e06589f | ||
|
|
d65b72c4dd | ||
|
|
24a3d5b3de | ||
|
|
2814b46941 | ||
|
|
6ffc25448d | ||
|
|
d9a82f7c9f | ||
|
|
9a8106947e | ||
|
|
e21ca79ea1 | ||
|
|
dd579caeac | ||
|
|
ddf8a4b9fb | ||
|
|
3c04f4194e | ||
|
|
1e0de841eb | ||
|
|
c9c88cf0a6 | ||
|
|
fd1e124166 | ||
|
|
aa2bee258a | ||
|
|
12d696dc07 | ||
|
|
b33772a1d6 | ||
|
|
b41f89d7de | ||
|
|
e8ca213b74 | ||
|
|
7620b8d493 | ||
|
|
23306fd9e3 | ||
|
|
a68ccdca50 | ||
|
|
771e432546 | ||
|
|
20bdbfb81a | ||
|
|
90729b436c | ||
|
|
8ea4e97bbf | ||
|
|
17e12e9263 | ||
|
|
4cb3d021f4 | ||
|
|
97cec2e127 | ||
|
|
9c4294659c | ||
|
|
5b8526db96 | ||
|
|
54cce9e9fb | ||
|
|
54d1691d19 | ||
|
|
4d4431868e | ||
|
|
cdae69d346 | ||
|
|
3e86baabd8 | ||
|
|
6ec92b19fb | ||
|
|
630caa8686 | ||
|
|
497c3b2165 | ||
|
|
8823d06893 | ||
|
|
72a2bf50df | ||
|
|
15c1ed2011 | ||
|
|
3fb19663cc | ||
|
|
00cdbbc89c | ||
|
|
07f3cec16d | ||
|
|
e893e25ff4 | ||
|
|
1ad9a1eb13 | ||
|
|
d9288596e8 | ||
|
|
a2a642d9ce | ||
|
|
4f00007108 | ||
|
|
816ee77b6f | ||
|
|
26d470ecda | ||
|
|
3babd9d95e | ||
|
|
802e81b1cd | ||
|
|
41f0834c51 | ||
|
|
880de0b047 | ||
|
|
bbe7fda4e0 | ||
|
|
4fd214e328 | ||
|
|
92a9274dce | ||
|
|
8765d83083 | ||
|
|
a298152bc8 | ||
|
|
2b7abe5774 | ||
|
|
3e9241892e | ||
|
|
a8dcdcf339 | ||
|
|
6ea79a7960 | ||
|
|
2af562e313 | ||
|
|
064a36fcbb | ||
|
|
40672f6a47 | ||
|
|
6d66ae3f50 | ||
|
|
94c89e0325 | ||
|
|
3670ef40a3 | ||
|
|
16d464fba5 | ||
|
|
0b7e0b9cd0 | ||
|
|
dd5fd114d2 | ||
|
|
6e53879344 | ||
|
|
af2bfd4d59 | ||
|
|
a4a34c8ba7 | ||
|
|
8c609f4fdf | ||
|
|
197f5b583f | ||
|
|
4eb4a03e59 | ||
|
|
5719469452 | ||
|
|
6e9d525890 | ||
|
|
e82805dd48 | ||
|
|
3d4e66d438 | ||
|
|
ffbc9f169a | ||
|
|
99650187e9 | ||
|
|
92699317cd | ||
|
|
0e48315803 | ||
|
|
868ec246bd | ||
|
|
0030a688c6 | ||
|
|
3ba8f51a2f | ||
|
|
04576b335c | ||
|
|
ea29aa176f | ||
|
|
9a9b7289ba | ||
|
|
5edcd5572a | ||
|
|
d601790864 | ||
|
|
3cd9b819f5 | ||
|
|
bf58d3ae93 | ||
|
|
d6c32e2d39 | ||
|
|
6914f75bb7 | ||
|
|
3adf3946b5 | ||
|
|
bdc4948afb | ||
|
|
331db10029 | ||
|
|
c036a157c8 | ||
|
|
2daf9e34d2 | ||
|
|
acf35f8c51 | ||
|
|
9155515082 | ||
|
|
558cd9b8b3 | ||
|
|
a0f02d0d2f | ||
|
|
9feb8492aa | ||
|
|
e5aa726899 | ||
|
|
93d1c28ccd | ||
|
|
b5ba9200bc | ||
|
|
699d866670 | ||
|
|
c3071344cb | ||
|
|
9e998dd2b6 | ||
|
|
c9accf6079 | ||
|
|
3338dce900 | ||
|
|
1364779f81 | ||
|
|
cb318aa6c6 | ||
|
|
61235828ce | ||
|
|
1a27b21efe | ||
|
|
e57f565812 | ||
|
|
da2605ee03 | ||
|
|
927c07bfd5 | ||
|
|
d00caf1f4c | ||
|
|
a5e9d72bc5 | ||
|
|
8ac9047831 | ||
|
|
65509ace59 | ||
|
|
ea173f971a | ||
|
|
c45c97c5d8 |
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
|
||||
111
.github/workflows/build.yml
vendored
111
.github/workflows/build.yml
vendored
@@ -4,7 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "releases/*"
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -18,51 +19,10 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-test:
|
||||
name: 🚀 Deploy to test env (fly.io)
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && 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 }}
|
||||
|
||||
manual-approval:
|
||||
name: Manual Approval
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-test
|
||||
if: success()
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Await Manual Approval
|
||||
uses: trstringer/manual-approval@v1
|
||||
with:
|
||||
secret: ${{ github.TOKEN }}
|
||||
approvers: DmitryPopov
|
||||
minimum-approvals: 1
|
||||
issue-title: "Manual Approval Required for Release"
|
||||
issue-body: "Please approve or deny the deployment."
|
||||
|
||||
build:
|
||||
name: 🛠 Build
|
||||
needs: manual-approval
|
||||
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
|
||||
@@ -77,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: |
|
||||
@@ -130,6 +90,7 @@ 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'
|
||||
@@ -137,13 +98,17 @@ jobs:
|
||||
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
|
||||
@@ -170,17 +135,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
|
||||
@@ -229,24 +183,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:
|
||||
@@ -277,9 +213,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
|
||||
@@ -294,19 +229,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 }}).
|
||||
@@ -316,9 +257,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 }}
|
||||
|
||||
184
.github/workflows/docker-arm.yml
vendored
Normal file
184
.github/workflows/docker-arm.yml
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
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: 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 🎉
|
||||
|
||||
[wandererltd/community-edition-arm:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition-arm/tags)
|
||||
|
||||
**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 }}
|
||||
184
.github/workflows/docker.yml
vendored
Normal file
184
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
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: 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 🎉
|
||||
|
||||
[wandererltd/community-edition:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition/tags)
|
||||
|
||||
**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
|
||||
|
||||
|
||||
940
CHANGELOG.md
940
CHANGELOG.md
@@ -2,6 +2,946 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.75.6](https://github.com/wanderer-industries/wanderer/compare/v1.75.5...v1.75.6) (2025-08-11)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.75.5](https://github.com/wanderer-industries/wanderer/compare/v1.75.4...v1.75.5) (2025-08-11)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.75.4](https://github.com/wanderer-industries/wanderer/compare/v1.75.3...v1.75.4) (2025-08-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restore security audit
|
||||
|
||||
## [v1.75.3](https://github.com/wanderer-industries/wanderer/compare/v1.75.2...v1.75.3) (2025-08-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed character tracking issues
|
||||
|
||||
## [v1.75.2](https://github.com/wanderer-industries/wanderer/compare/v1.75.1...v1.75.2) (2025-08-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix indents for ally logos in list "On the map"
|
||||
|
||||
* Map: Fix cancelling ping from system context menu
|
||||
|
||||
* Map: Hide admin settings tab
|
||||
|
||||
* Map: Remote map setting refactoring
|
||||
|
||||
## [v1.75.1](https://github.com/wanderer-industries/wanderer/compare/v1.75.0...v1.75.1) (2025-07-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* unable to cancel ping from right click context menu
|
||||
|
||||
## [v1.75.0](https://github.com/wanderer-industries/wanderer/compare/v1.74.13...v1.75.0) (2025-07-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* autoset connection size for c4->null and c13
|
||||
|
||||
* apiv1 and tests
|
||||
|
||||
* support webhook and sse
|
||||
|
||||
* disable webhook/websocket by default
|
||||
|
||||
* add websocket and webhooks for events
|
||||
|
||||
* Add Jest testing for getState util
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove bug with lazy delete
|
||||
|
||||
* update broken length and remove verbose logging
|
||||
|
||||
* removed old documents
|
||||
|
||||
* removed unneeded api, and fixed data comparision bug
|
||||
|
||||
* ci comments
|
||||
|
||||
* test updates
|
||||
|
||||
* properly send sse events
|
||||
|
||||
* add test coverage for api
|
||||
|
||||
* add more logging around character online and tracking
|
||||
|
||||
* clean up SSE warnings
|
||||
|
||||
* update env variable usage for sse
|
||||
|
||||
* sse cleanup
|
||||
|
||||
* remove misleading error
|
||||
|
||||
* update killactivity color on nodes
|
||||
|
||||
## [v1.74.13](https://github.com/wanderer-industries/wanderer/compare/v1.74.12...v1.74.13) (2025-07-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with callback url
|
||||
|
||||
## [v1.74.12](https://github.com/wanderer-industries/wanderer/compare/v1.74.11...v1.74.12) (2025-07-22)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.74.11](https://github.com/wanderer-industries/wanderer/compare/v1.74.10...v1.74.11) (2025-07-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed remove pings for removed systems
|
||||
|
||||
## [v1.74.10](https://github.com/wanderer-industries/wanderer/compare/v1.74.9...v1.74.10) (2025-07-15)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.74.9](https://github.com/wanderer-industries/wanderer/compare/v1.74.8...v1.74.9) (2025-07-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Trying to fix problem with fast forwarding after page are inactive some time.
|
||||
|
||||
## [v1.74.8](https://github.com/wanderer-industries/wanderer/compare/v1.74.7...v1.74.8) (2025-07-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: removed comments
|
||||
|
||||
* Map: Fixed conflict
|
||||
|
||||
* Map: Unified settings. Second part: Import/Export
|
||||
|
||||
* Map: Unified settings. First part: add one place for storing settings
|
||||
|
||||
## [v1.74.7](https://github.com/wanderer-industries/wanderer/compare/v1.74.6...v1.74.7) (2025-07-09)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.74.6](https://github.com/wanderer-industries/wanderer/compare/v1.74.5...v1.74.6) (2025-07-09)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.74.5](https://github.com/wanderer-industries/wanderer/compare/v1.74.4...v1.74.5) (2025-07-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add background for Pochven's systems. Changed from Region name to constellation name for pochven systems. Changed connection style for gates (display like common connection). Changed behaviour of connections.
|
||||
|
||||
## [v1.74.4](https://github.com/wanderer-industries/wanderer/compare/v1.74.3...v1.74.4) (2025-07-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with update system positions
|
||||
|
||||
## [v1.74.3](https://github.com/wanderer-industries/wanderer/compare/v1.74.2...v1.74.3) (2025-07-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issues with map subscription component
|
||||
|
||||
## [v1.74.2](https://github.com/wanderer-industries/wanderer/compare/v1.74.1...v1.74.2) (2025-06-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map loading for not existing maps
|
||||
|
||||
## [v1.74.1](https://github.com/wanderer-industries/wanderer/compare/v1.74.0...v1.74.1) (2025-06-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Mark connections between Pochven systems as known.
|
||||
|
||||
## [v1.74.0](https://github.com/wanderer-industries/wanderer/compare/v1.73.0...v1.74.0) (2025-06-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Reverted showing linked signature ID as part of temporary names
|
||||
|
||||
## [v1.73.0](https://github.com/wanderer-industries/wanderer/compare/v1.72.1...v1.73.0) (2025-06-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Allowed system temp names up to 12 characters. Deprecated showing linked signature ID as part of temporary name.
|
||||
|
||||
## [v1.72.1](https://github.com/wanderer-industries/wanderer/compare/v1.72.0...v1.72.1) (2025-06-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* issue with tracking signature activity
|
||||
|
||||
## [v1.72.0](https://github.com/wanderer-industries/wanderer/compare/v1.71.3...v1.72.0) (2025-06-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added an ability to see & topup map balance and map subscription info (on public)
|
||||
|
||||
## [v1.71.3](https://github.com/wanderer-industries/wanderer/compare/v1.71.2...v1.71.3) (2025-06-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix incorrect placing of labels
|
||||
|
||||
## [v1.71.2](https://github.com/wanderer-industries/wanderer/compare/v1.71.1...v1.71.2) (2025-06-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fix issue with kill service disconnect
|
||||
|
||||
## [v1.71.1](https://github.com/wanderer-industries/wanderer/compare/v1.71.0...v1.71.1) (2025-06-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* update system kills widget timing
|
||||
|
||||
## [v1.71.0](https://github.com/wanderer-industries/wanderer/compare/v1.70.7...v1.71.0) (2025-06-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* use external services for kill data
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove duplicate kills connections
|
||||
|
||||
* Fixed kills clinet init & map start/update logic
|
||||
|
||||
* avoid duplicate subs, and remove subs on inactive maps
|
||||
|
||||
## [v1.70.7](https://github.com/wanderer-industries/wanderer/compare/v1.70.6...v1.70.7) (2025-06-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Subscriptions: Added option to topup using ALL user balance available
|
||||
|
||||
## [v1.70.6](https://github.com/wanderer-industries/wanderer/compare/v1.70.5...v1.70.6) (2025-06-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.70.5](https://github.com/wanderer-industries/wanderer/compare/v1.70.4...v1.70.5) (2025-06-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed character caching issues
|
||||
|
||||
## [v1.70.4](https://github.com/wanderer-industries/wanderer/compare/v1.70.3...v1.70.4) (2025-06-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Distribute tracking to minimal pool first
|
||||
|
||||
## [v1.70.3](https://github.com/wanderer-industries/wanderer/compare/v1.70.2...v1.70.3) (2025-06-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Don't pause tracking for new pools
|
||||
|
||||
## [v1.70.2](https://github.com/wanderer-industries/wanderer/compare/v1.70.1...v1.70.2) (2025-06-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Invalidate character copr and ally data on map server start
|
||||
|
||||
## [v1.70.1](https://github.com/wanderer-industries/wanderer/compare/v1.70.0...v1.70.1) (2025-06-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* resolve api issue with custom name
|
||||
|
||||
## [v1.70.0](https://github.com/wanderer-industries/wanderer/compare/v1.69.1...v1.70.0) (2025-06-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Fix admin page error
|
||||
|
||||
## [v1.69.1](https://github.com/wanderer-industries/wanderer/compare/v1.69.0...v1.69.1) (2025-06-11)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.69.0](https://github.com/wanderer-industries/wanderer/compare/v1.68.6...v1.69.0) (2025-06-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added multiple tracking pools support
|
||||
|
||||
## [v1.68.6](https://github.com/wanderer-industries/wanderer/compare/v1.68.5...v1.68.6) (2025-06-10)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.68.5](https://github.com/wanderer-industries/wanderer/compare/v1.68.4...v1.68.5) (2025-06-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed updating map options
|
||||
|
||||
## [v1.68.4](https://github.com/wanderer-industries/wanderer/compare/v1.68.3...v1.68.4) (2025-06-10)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.68.3](https://github.com/wanderer-industries/wanderer/compare/v1.68.2...v1.68.3) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.68.2](https://github.com/wanderer-industries/wanderer/compare/v1.68.1...v1.68.2) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed character auth with wallet (on characters page)
|
||||
|
||||
## [v1.68.1](https://github.com/wanderer-industries/wanderer/compare/v1.68.0...v1.68.1) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed auth from welcome page if invites disabled
|
||||
|
||||
## [v1.68.0](https://github.com/wanderer-industries/wanderer/compare/v1.67.5...v1.68.0) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added invites store support
|
||||
|
||||
## [v1.67.5](https://github.com/wanderer-industries/wanderer/compare/v1.67.4...v1.67.5) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added back ARM docker image build
|
||||
|
||||
## [v1.67.4](https://github.com/wanderer-industries/wanderer/compare/v1.67.3...v1.67.4) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with system splash updates
|
||||
|
||||
## [v1.67.3](https://github.com/wanderer-industries/wanderer/compare/v1.67.2...v1.67.3) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed issue with system splash updates
|
||||
|
||||
## [v1.67.2](https://github.com/wanderer-industries/wanderer/compare/v1.67.1...v1.67.2) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.67.1](https://github.com/wanderer-industries/wanderer/compare/v1.67.0...v1.67.1) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.67.0](https://github.com/wanderer-industries/wanderer/compare/v1.66.25...v1.67.0) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers
|
||||
|
||||
## [v1.66.25](https://github.com/wanderer-industries/wanderer/compare/v1.66.24...v1.66.25) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Disabled kills fetching based on env settings
|
||||
|
||||
## [v1.66.24](https://github.com/wanderer-industries/wanderer/compare/v1.66.23...v1.66.24) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.23](https://github.com/wanderer-industries/wanderer/compare/v1.66.22...v1.66.23) (2025-06-08)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.22](https://github.com/wanderer-industries/wanderer/compare/v1.66.21...v1.66.22) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.21](https://github.com/wanderer-industries/wanderer/compare/v1.66.20...v1.66.21) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed kills fetching based on env settings
|
||||
|
||||
## [v1.66.20](https://github.com/wanderer-industries/wanderer/compare/v1.66.19...v1.66.20) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.19](https://github.com/wanderer-industries/wanderer/compare/v1.66.18...v1.66.19) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added check for offline characters timeouts
|
||||
|
||||
## [v1.66.18](https://github.com/wanderer-industries/wanderer/compare/v1.66.17...v1.66.18) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.17](https://github.com/wanderer-industries/wanderer/compare/v1.66.16...v1.66.17) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
|
||||
|
||||
## [v1.66.16](https://github.com/wanderer-industries/wanderer/compare/v1.66.15...v1.66.16) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
|
||||
|
||||
## [v1.66.15](https://github.com/wanderer-industries/wanderer/compare/v1.66.14...v1.66.15) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added back arm docker image build
|
||||
|
||||
## [v1.66.14](https://github.com/wanderer-industries/wanderer/compare/v1.66.13...v1.66.14) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed online updates
|
||||
|
||||
## [v1.66.13](https://github.com/wanderer-industries/wanderer/compare/v1.66.12...v1.66.13) (2025-06-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed location tracking issues
|
||||
|
||||
## [v1.66.12](https://github.com/wanderer-industries/wanderer/compare/v1.66.11...v1.66.12) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.11](https://github.com/wanderer-industries/wanderer/compare/v1.66.10...v1.66.11) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed refresh character tokens
|
||||
|
||||
## [v1.66.10](https://github.com/wanderer-industries/wanderer/compare/v1.66.9...v1.66.10) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.9](https://github.com/wanderer-industries/wanderer/compare/v1.66.8...v1.66.9) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.8](https://github.com/wanderer-industries/wanderer/compare/v1.66.7...v1.66.8) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fixed disable detailed kills env check
|
||||
|
||||
## [v1.66.7](https://github.com/wanderer-industries/wanderer/compare/v1.66.6...v1.66.7) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fixed disable detailed kills env check
|
||||
|
||||
## [v1.66.6](https://github.com/wanderer-industries/wanderer/compare/v1.66.5...v1.66.6) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.5](https://github.com/wanderer-industries/wanderer/compare/v1.66.4...v1.66.5) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.4](https://github.com/wanderer-industries/wanderer/compare/v1.66.3...v1.66.4) (2025-06-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* respect error limits for ESI APIs
|
||||
|
||||
## [v1.66.3](https://github.com/wanderer-industries/wanderer/compare/v1.66.2...v1.66.3) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.2](https://github.com/wanderer-industries/wanderer/compare/v1.66.1...v1.66.2) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.66.1](https://github.com/wanderer-industries/wanderer/compare/v1.66.0...v1.66.1) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove bugs with signature deletion
|
||||
|
||||
## [v1.66.0](https://github.com/wanderer-industries/wanderer/compare/v1.65.24...v1.66.0) (2025-06-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* show deleted signatures during undo timer
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove callbacks from effect dependencies
|
||||
|
||||
## [v1.65.24](https://github.com/wanderer-industries/wanderer/compare/v1.65.23...v1.65.24) (2025-06-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed errors duration check to reduce requests amount to ESI
|
||||
|
||||
## [v1.65.23](https://github.com/wanderer-industries/wanderer/compare/v1.65.22...v1.65.23) (2025-06-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added back arm docker image build
|
||||
|
||||
## [v1.65.22](https://github.com/wanderer-industries/wanderer/compare/v1.65.21...v1.65.22) (2025-06-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fix character tracking issues
|
||||
|
||||
## [v1.65.21](https://github.com/wanderer-industries/wanderer/compare/v1.65.20...v1.65.21) (2025-06-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fix connection pool errors
|
||||
|
||||
## [v1.65.20](https://github.com/wanderer-industries/wanderer/compare/v1.65.19...v1.65.20) (2025-06-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fix waypoint set timeout errors
|
||||
|
||||
## [v1.65.19](https://github.com/wanderer-industries/wanderer/compare/v1.65.18...v1.65.19) (2025-06-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fix updating character online
|
||||
|
||||
## [v1.65.18](https://github.com/wanderer-industries/wanderer/compare/v1.65.17...v1.65.18) (2025-05-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fix updating systems and connections
|
||||
|
||||
## [v1.65.17](https://github.com/wanderer-industries/wanderer/compare/v1.65.16...v1.65.17) (2025-05-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fix updating systems and connections
|
||||
|
||||
* Comments: fix error loading comments
|
||||
|
||||
## [v1.65.16](https://github.com/wanderer-industries/wanderer/compare/v1.65.15...v1.65.16) (2025-05-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Allow lock systems for members
|
||||
|
||||
## [v1.65.15](https://github.com/wanderer-industries/wanderer/compare/v1.65.14...v1.65.15) (2025-05-28)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.65.14](https://github.com/wanderer-industries/wanderer/compare/v1.65.13...v1.65.14) (2025-05-28)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.65.13](https://github.com/wanderer-industries/wanderer/compare/v1.65.12...v1.65.13) (2025-05-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signatures: small wh size is now passed from signature to connection
|
||||
|
||||
## [v1.65.12](https://github.com/wanderer-industries/wanderer/compare/v1.65.11...v1.65.12) (2025-05-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed showing character ship
|
||||
|
||||
## [v1.65.11](https://github.com/wanderer-industries/wanderer/compare/v1.65.10...v1.65.11) (2025-05-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed showing character ship
|
||||
|
||||
## [v1.65.10](https://github.com/wanderer-industries/wanderer/compare/v1.65.9...v1.65.10) (2025-05-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed sorting for characters in Local
|
||||
|
||||
* Map: Rally: fixed conflict style of status and rally
|
||||
|
||||
* Core: Fixed character token refresh
|
||||
|
||||
* Map: Add Rally point. Change placement of settings in Map User Settings. Add ability to placement minimap.
|
||||
|
||||
* Map: Routes - hide user routes btn from context if subscriptions is not active or widget is closed. Also now hidden widget will show again in place where it was on moment of hide (except cases when screen size has changed.
|
||||
|
||||
* Map: PINGS - Rally point first prototype
|
||||
|
||||
## [v1.65.9](https://github.com/wanderer-industries/wanderer/compare/v1.65.8...v1.65.9) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.65.8](https://github.com/wanderer-industries/wanderer/compare/v1.65.7...v1.65.8) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.65.7](https://github.com/wanderer-industries/wanderer/compare/v1.65.6...v1.65.7) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map character tracking issues
|
||||
|
||||
## [v1.65.6](https://github.com/wanderer-industries/wanderer/compare/v1.65.5...v1.65.6) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map character tracking issues
|
||||
|
||||
## [v1.65.5](https://github.com/wanderer-industries/wanderer/compare/v1.65.4...v1.65.5) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map character tracking issues
|
||||
|
||||
* Signature: Update restored signature character
|
||||
|
||||
## [v1.65.4](https://github.com/wanderer-industries/wanderer/compare/v1.65.3...v1.65.4) (2025-05-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Force signature update even if there are no any changes
|
||||
|
||||
## [v1.65.3](https://github.com/wanderer-industries/wanderer/compare/v1.65.2...v1.65.3) (2025-05-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Fixed signature clenup
|
||||
|
||||
## [v1.65.2](https://github.com/wanderer-industries/wanderer/compare/v1.65.1...v1.65.2) (2025-05-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signature: Fixed signature updates
|
||||
|
||||
## [v1.65.1](https://github.com/wanderer-industries/wanderer/compare/v1.65.0...v1.65.1) (2025-05-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
|
||||
|
||||
## [v1.65.0](https://github.com/wanderer-industries/wanderer/compare/v1.64.8...v1.65.0) (2025-05-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* default connections from c1 holes to medium size
|
||||
|
||||
* support german and french signatures
|
||||
|
||||
* improve signature undo process
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove required id field from character schema
|
||||
|
||||
* update openapi spec response types
|
||||
|
||||
* fix issue with connection generation between k-space
|
||||
|
||||
* Signature: Fixed signatures updates
|
||||
|
||||
* update openapi spec for other apis
|
||||
|
||||
## [v1.64.8](https://github.com/wanderer-industries/wanderer/compare/v1.64.7...v1.64.8) (2025-05-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
|
||||
|
||||
## [v1.64.7](https://github.com/wanderer-industries/wanderer/compare/v1.64.6...v1.64.7) (2025-05-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed connection EOL time refreshed every 2 minutes
|
||||
|
||||
## [v1.64.6](https://github.com/wanderer-industries/wanderer/compare/v1.64.5...v1.64.6) (2025-05-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added map hubs limits checking & a proper warning message shown
|
||||
|
||||
## [v1.64.5](https://github.com/wanderer-industries/wanderer/compare/v1.64.4...v1.64.5) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Added character name update on re-auth
|
||||
|
||||
## [v1.64.4](https://github.com/wanderer-industries/wanderer/compare/v1.64.3...v1.64.4) (2025-05-14)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,5 +17,6 @@ module.exports = {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
"linebreak-style": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf"
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
};
|
||||
@@ -198,3 +198,89 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.p-autocomplete .p-autocomplete-multiple-container:not(.p-disabled).p-focus {
|
||||
box-shadow: 0 0 0 1px #335c7e;
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
.p-inputtext:enabled:focus {
|
||||
box-shadow: 0 0 0 1px #335c7e;
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: 400px;
|
||||
|
||||
.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,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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { PingType, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useContextMenuSystemItems } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ContextMenuSystemProps {
|
||||
onSystemStatus(val: number): void;
|
||||
onSystemLabels(val: string): void;
|
||||
onCustomLabelDialog(): void;
|
||||
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
|
||||
onWaypointSet: WaypointSetContextHandler;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useTagMenu';
|
||||
export * from './useStatusMenu';
|
||||
export * from './useLabelsMenu';
|
||||
export * from './useUserRoute';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { MapUserAddIcon, MapUserDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
|
||||
interface UseUserRouteProps {
|
||||
systemId: string | undefined;
|
||||
userHubs: string[];
|
||||
onUserHubToggle(): void;
|
||||
}
|
||||
|
||||
export const useUserRoute = ({ userHubs, systemId, onUserHubToggle }: UseUserRouteProps) => {
|
||||
const {
|
||||
data: { isSubscriptionActive },
|
||||
windowsSettings,
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef({ userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings });
|
||||
ref.current = { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings };
|
||||
|
||||
return useCallback(() => {
|
||||
const { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings } = ref.current;
|
||||
|
||||
const isVisibleUserRoutes = windowsSettings.visible.some(x => x === WidgetsIds.userRoutes);
|
||||
|
||||
if (!isSubscriptionActive || !isVisibleUserRoutes || !systemId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: !userHubs.includes(systemId) ? 'Add User Route' : 'Remove User Route',
|
||||
icon: !userHubs.includes(systemId) ? (
|
||||
<MapUserAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapUserDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onUserHubToggle,
|
||||
},
|
||||
];
|
||||
}, [windowsSettings]);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
|
||||
interface UseContextMenuSystemHandlersProps {
|
||||
hubs: string[];
|
||||
@@ -93,6 +94,22 @@ export const useContextMenuSystemHandlers = ({
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
// const onTogglePingRally = useCallback(() => {
|
||||
// const { userHubs, system, outCommand } = ref.current;
|
||||
// if (!system) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// outCommand({
|
||||
// type: OutCommand.openPing,
|
||||
// data: {
|
||||
// solar_system_id: system,
|
||||
// type: PingType.Rally,
|
||||
// },
|
||||
// });
|
||||
// setSystem(undefined);
|
||||
// }, []);
|
||||
|
||||
const onSystemTag = useCallback((tag?: string) => {
|
||||
const { system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
@@ -198,6 +215,7 @@ export const useContextMenuSystemHandlers = ({
|
||||
onLockToggle,
|
||||
onHubToggle,
|
||||
onUserHubToggle,
|
||||
// onTogglePingRally,
|
||||
onSystemTag,
|
||||
onSystemTemporaryName,
|
||||
onSystemStatus,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { useLabelsMenu, useStatusMenu, useTagMenu } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
|
||||
import {
|
||||
useLabelsMenu,
|
||||
useStatusMenu,
|
||||
useTagMenu,
|
||||
useUserRoute,
|
||||
} from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import classes from './ContextMenuSystem.module.scss';
|
||||
@@ -10,13 +15,19 @@ import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { MapAddIcon, MapDeleteIcon, MapUserAddIcon, MapUserDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { PingType } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import clsx from 'clsx';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const useContextMenuSystemItems = ({
|
||||
onDeleteSystem,
|
||||
onLockToggle,
|
||||
onHubToggle,
|
||||
onUserHubToggle,
|
||||
onTogglePing,
|
||||
onSystemTag,
|
||||
onSystemStatus,
|
||||
onSystemLabels,
|
||||
@@ -33,11 +44,33 @@ export const useContextMenuSystemItems = ({
|
||||
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
|
||||
const canManageSystem = useMapCheckPermissions([UserPermission.UPDATE_SYSTEM]);
|
||||
const canDeleteSystem = useMapCheckPermissions([UserPermission.DELETE_SYSTEM]);
|
||||
const getUserRoutes = useUserRoute({ userHubs, systemId, onUserHubToggle });
|
||||
|
||||
return useMemo(() => {
|
||||
const {
|
||||
data: { pings, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
|
||||
const isShowPingBtn = useMemo(() => {
|
||||
if (!isSubscriptionActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pings.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pings[0].solar_system_id === systemId;
|
||||
}, [isSubscriptionActive, pings, systemId]);
|
||||
|
||||
return useMemo((): MenuItem[] => {
|
||||
const system = systemId ? getSystemById(systems, systemId) : undefined;
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
|
||||
const hasPing = ping?.solar_system_id === systemId;
|
||||
|
||||
if (!system || !systemId) {
|
||||
return [];
|
||||
}
|
||||
@@ -72,55 +105,96 @@ export const useContextMenuSystemItems = ({
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
...getUserRoutes(),
|
||||
|
||||
{ separator: true },
|
||||
{
|
||||
label: !userHubs.includes(systemId) ? 'Add User Route' : 'Remove User Route',
|
||||
icon: !userHubs.includes(systemId) ? (
|
||||
<MapUserAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapUserDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onUserHubToggle,
|
||||
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing),
|
||||
disabled: !isShowPingBtn,
|
||||
template: () => {
|
||||
const iconClasses = clsx({
|
||||
'pi text-cyan-400 hero-signal': !hasPing,
|
||||
'pi text-red-400 hero-signal-slash': hasPing,
|
||||
});
|
||||
|
||||
if (isShowPingBtn) {
|
||||
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Locked. Ping can be set only for one system."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon={iconClasses}>
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(system.locked
|
||||
? canLockSystem
|
||||
? [
|
||||
{
|
||||
label: 'Unlock',
|
||||
icon: PrimeIcons.LOCK_OPEN,
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []
|
||||
: [
|
||||
...(canLockSystem
|
||||
? [
|
||||
{
|
||||
label: 'Lock',
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(system.locked && canLockSystem
|
||||
? [
|
||||
{
|
||||
label: 'Unlock',
|
||||
icon: PrimeIcons.LOCK_OPEN,
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!system.locked && canManageSystem
|
||||
? [
|
||||
{
|
||||
label: 'Lock',
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(canDeleteSystem && !system.locked
|
||||
? [
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteSystem,
|
||||
disabled: hasPing,
|
||||
template: () => {
|
||||
if (!hasPing) {
|
||||
return <WdMenuItem icon="text-red-400 pi pi-trash">Delete</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Locked. System can not be deleted until ping set."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon="text-red-400 pi pi-trash">
|
||||
Delete
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [
|
||||
canLockSystem,
|
||||
systems,
|
||||
systemId,
|
||||
systems,
|
||||
getTags,
|
||||
getStatus,
|
||||
getLabels,
|
||||
getWaypointMenu,
|
||||
getUserRoutes,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onOpenSettings,
|
||||
canLockSystem,
|
||||
onLockToggle,
|
||||
canDeleteSystem,
|
||||
onDeleteSystem,
|
||||
onOpenSettings,
|
||||
onTogglePing,
|
||||
ping,
|
||||
isShowPingBtn,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
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';
|
||||
|
||||
export const useContextMenuSystemMultipleHandlers = () => {
|
||||
const {
|
||||
data: { pings },
|
||||
} = useMapRootState();
|
||||
|
||||
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 handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
|
||||
setSystems(systems_);
|
||||
ev.preventDefault();
|
||||
@@ -24,13 +31,17 @@ export const useContextMenuSystemMultipleHandlers = () => {
|
||||
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]);
|
||||
}, [deleteSystems, systems, ping]);
|
||||
|
||||
return {
|
||||
handleSystemMultipleContext,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
@@ -59,9 +59,21 @@ export const FastSystemActions = ({
|
||||
return (
|
||||
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}>
|
||||
<div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
|
||||
<WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} />
|
||||
<WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} />
|
||||
<WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} />
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
|
||||
source={ZKB_ICON}
|
||||
onClick={handleOpenZKB}
|
||||
/>
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open Anoikis' }}
|
||||
source={ANOIK_ICON}
|
||||
onClick={handleOpenAnoikis}
|
||||
/>
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open Dotlan' }}
|
||||
source={DOTLAN_ICON}
|
||||
onClick={handleOpenDotlan}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center pl-1">
|
||||
@@ -69,14 +81,14 @@ export const FastSystemActions = ({
|
||||
textSize={WdImageSize.off}
|
||||
className={PrimeIcons.COPY}
|
||||
onClick={copySystemNameToClipboard}
|
||||
tooltip={{ content: 'Copy system name' }}
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }}
|
||||
/>
|
||||
{showEdit && (
|
||||
<WdImgButton
|
||||
textSize={WdImageSize.off}
|
||||
className="pi pi-pen-to-square text-base"
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{ content: 'Edit system name and description' }}
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
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,67 @@
|
||||
import { MapUserSettings, SettingsWithVersion } 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
|
||||
|
||||
/** Minimal check that an object matches SettingsWithVersion<*> */
|
||||
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
|
||||
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
|
||||
|
||||
/** 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 (!isSettingsWithVersion((data as any)[key])) {
|
||||
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<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;
|
||||
};
|
||||
@@ -28,11 +28,12 @@ import {
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
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 { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
@@ -95,6 +96,9 @@ interface MapCompProps {
|
||||
isShowBackgroundPattern?: boolean;
|
||||
isSoftBackground?: boolean;
|
||||
theme?: string;
|
||||
pings: PingData[];
|
||||
minimapPlacement?: PanelPosition;
|
||||
localShowShipName?: boolean;
|
||||
}
|
||||
|
||||
const MapComp = ({
|
||||
@@ -112,6 +116,9 @@ const MapComp = ({
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
pings,
|
||||
minimapPlacement = 'bottom-right',
|
||||
localShowShipName = false,
|
||||
}: MapCompProps) => {
|
||||
const { getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
@@ -206,8 +213,10 @@ const MapComp = ({
|
||||
...x,
|
||||
showKSpaceBG: showKSpaceBG,
|
||||
isThickConnections: isThickConnections,
|
||||
pings,
|
||||
localShowShipName,
|
||||
}));
|
||||
}, [showKSpaceBG, isThickConnections, update]);
|
||||
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -270,7 +279,9 @@ const MapComp = ({
|
||||
// onlyRenderVisibleElements
|
||||
selectionMode={SelectionMode.Partial}
|
||||
>
|
||||
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
|
||||
{isShowMinimap && (
|
||||
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
|
||||
)}
|
||||
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
|
||||
</ReactFlow>
|
||||
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>
|
||||
|
||||
@@ -10,6 +10,7 @@ export type MapData = MapUnionTypes & {
|
||||
showKSpaceBG: boolean;
|
||||
isThickConnections: boolean;
|
||||
linkedSigEveId: string;
|
||||
localShowShipName: boolean;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
@@ -40,6 +41,9 @@ const INITIAL_DATA: MapData = {
|
||||
isSubscriptionActive: false,
|
||||
mainCharacterEveId: null,
|
||||
followingCharacterEveId: null,
|
||||
userHubs: [],
|
||||
pings: [],
|
||||
localShowShipName: false,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
|
||||
@@ -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,7 +49,7 @@ 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>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './KillsCounter.tsx';
|
||||
@@ -3,11 +3,11 @@ 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>;
|
||||
@@ -16,8 +16,10 @@ interface LocalCounterProps {
|
||||
}
|
||||
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const [settings] = useLocalCharacterWidgetSettings();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
const {
|
||||
data: { localShowShipName },
|
||||
} = useMapState();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCounter';
|
||||
@@ -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';
|
||||
@@ -51,11 +51,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 method = isWormhole ? getBezierPath : getBezierPath;
|
||||
|
||||
const [edgePath, labelX, labelY] = method({
|
||||
sourceX: sx - offset.x,
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: #d291bc;
|
||||
$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);
|
||||
|
||||
@keyframes move-stripes {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
@@ -28,11 +40,12 @@ $tooltip-bg: #202020;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Pochven,
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -48,7 +61,7 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::before {
|
||||
&::after {
|
||||
background-image: url('/images/mataria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
@@ -57,7 +70,7 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
&::after {
|
||||
background-image: url('/images/caldaria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
@@ -66,7 +79,7 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::before {
|
||||
&::after {
|
||||
opacity: 0.45;
|
||||
background-image: url('/images/amarr-180.png');
|
||||
background-position-x: 0;
|
||||
@@ -75,7 +88,7 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::before {
|
||||
&::after {
|
||||
opacity: 0.5;
|
||||
background-image: url('/images/gallente-180.png');
|
||||
background-position-x: 1px;
|
||||
@@ -83,11 +96,43 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
&.rally {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
|
||||
border-color: $neon-color-1;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$neon-color-3 0px,
|
||||
$neon-color-3 8px,
|
||||
transparent 8px,
|
||||
transparent 21px
|
||||
);
|
||||
background-size: 30px 30px;
|
||||
animation: move-stripes 3s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
|
||||
|
||||
@@ -12,17 +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 { 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';
|
||||
|
||||
// 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 +40,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>
|
||||
)}
|
||||
@@ -71,8 +73,11 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.rally]: nodeVars.isRally,
|
||||
},
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
|
||||
@@ -12,16 +12,18 @@ 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';
|
||||
|
||||
// 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 +39,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>
|
||||
)}
|
||||
@@ -71,7 +73,10 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.rally]: nodeVars.isRally,
|
||||
},
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
@@ -116,23 +121,13 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{nodeVars.customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CustomName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RegionName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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,8 @@
|
||||
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 { 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();
|
||||
|
||||
@@ -22,6 +22,7 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
||||
systemId: realSystemId,
|
||||
outCommand,
|
||||
showAllVisible: false,
|
||||
sinceHours: 1,
|
||||
});
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
|
||||
@@ -115,35 +115,18 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
}, 500);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
case Commands.pingCancelled:
|
||||
case Commands.routes:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
// do nothing
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -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,20 +5,51 @@ 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, Spaces } 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';
|
||||
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
|
||||
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;
|
||||
hasUserCharacters: boolean;
|
||||
showHandlers: boolean;
|
||||
regionClass: string | null;
|
||||
systemName: string;
|
||||
customName?: string | null;
|
||||
labelCustom: string | null;
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
labelsInfo: LabelInfo[];
|
||||
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: string;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[];
|
||||
name: string | null;
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
isThickConnections: boolean;
|
||||
isRally: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
|
||||
const SpaceToClass: Record<string, string> = {
|
||||
@@ -26,6 +57,7 @@ const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Matar]: 'Mataria',
|
||||
[Spaces.Amarr]: 'Amarria',
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
[Spaces.Pochven]: 'Pochven',
|
||||
};
|
||||
|
||||
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
@@ -41,7 +73,7 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
return { localCounterCharacters };
|
||||
}
|
||||
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
|
||||
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
|
||||
const { id, data, selected } = props;
|
||||
const {
|
||||
id: solar_system_id,
|
||||
@@ -73,6 +105,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
constellation_name,
|
||||
} = systemStaticInfo;
|
||||
|
||||
const { isShowUnsplashedSignatures } = interfaceSettings;
|
||||
@@ -85,13 +118,13 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
characters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
userCharacters,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
pings,
|
||||
},
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
@@ -121,9 +154,6 @@ export function 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],
|
||||
@@ -154,14 +184,25 @@ export function 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, 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,
|
||||
@@ -188,47 +229,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: computedTemporaryName,
|
||||
regionName: region_name,
|
||||
regionName,
|
||||
solarSystemName: solar_system_name,
|
||||
isRally,
|
||||
};
|
||||
|
||||
return nodeVars;
|
||||
}
|
||||
|
||||
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;
|
||||
systemName: string;
|
||||
customName?: string | null;
|
||||
labelCustom: string | null;
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
labelsInfo: LabelInfo[];
|
||||
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: string;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[];
|
||||
name: string | null;
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
isThickConnections: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,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,9 +1,12 @@
|
||||
import classes from './MarkdownComment.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { InfoDrawer, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import {
|
||||
InfoDrawer,
|
||||
MarkdownTextViewer,
|
||||
TimeAgo,
|
||||
TooltipPosition,
|
||||
WdImgButton,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useGetCacheCharacter } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { WdTransition } from '@/hooks/Mapper/components/ui-kit/WdTransition/WdTransition.tsx';
|
||||
@@ -11,9 +14,9 @@ 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 };
|
||||
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
|
||||
|
||||
export interface MarkdownCommentProps {
|
||||
text: string;
|
||||
@@ -26,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 });
|
||||
@@ -43,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
|
||||
@@ -66,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>
|
||||
)}
|
||||
@@ -79,13 +78,13 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Markdown remarkPlugins={REMARK_PLUGINS}>{text}</Markdown>
|
||||
<MarkdownTextViewer>{text}</MarkdownTextViewer>
|
||||
</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}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export interface CommentsEditorProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const CommentsEditor = ({}: CommentsEditorProps) => {
|
||||
const [textVal, setTextVal] = useState('');
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import { RoutesList } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesList';
|
||||
|
||||
export const PingRoute = () => {
|
||||
const {
|
||||
data: { routes, pings, loadingPublicRoutes },
|
||||
} = useMapRootState();
|
||||
|
||||
const route = useMemo(() => {
|
||||
const [ping] = pings;
|
||||
if (!ping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return routes?.routes.find(x => ping.solar_system_id === x.destination.toString()) ?? null;
|
||||
}, [routes, pings]);
|
||||
|
||||
const preparedRoute = useMemo(() => {
|
||||
if (!route) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...route,
|
||||
mapped_systems:
|
||||
route.systems?.map(solar_system_id =>
|
||||
routes?.systems_static_data.find(
|
||||
system_static_data => system_static_data.solar_system_id === solar_system_id,
|
||||
),
|
||||
) ?? [],
|
||||
};
|
||||
}, [route, routes?.systems_static_data]);
|
||||
|
||||
if (loadingPublicRoutes) {
|
||||
return <span className="m-0 text-[12px]">Loading...</span>;
|
||||
}
|
||||
|
||||
if (!preparedRoute || preparedRoute.origin === preparedRoute.destination) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="m-0 flex gap-2 items-center text-[12px]">
|
||||
{preparedRoute.has_connection && <div className="text-[12px]">{preparedRoute.systems?.length ?? 2}</div>}
|
||||
<RoutesList data={preparedRoute} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
|
||||
import {
|
||||
CharacterCardById,
|
||||
SystemView,
|
||||
TimeAgo,
|
||||
TooltipPosition,
|
||||
WdImgButton,
|
||||
WdImgButtonTooltip,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
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 { Button } from 'primereact/button';
|
||||
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',
|
||||
[PingsPlacement.leftTop]: 'top-left',
|
||||
[PingsPlacement.rightBottom]: 'bottom-right',
|
||||
[PingsPlacement.leftBottom]: 'bottom-left',
|
||||
};
|
||||
|
||||
const PING_PLACEMENT_MAP_OFFSETS = {
|
||||
[PingsPlacement.rightTop]: { default: '!top-[56px]', withLeftMenu: '!top-[56px] !right-[64px]' },
|
||||
[PingsPlacement.rightBottom]: { default: '!bottom-[15px]', withLeftMenu: '!bottom-[15px] !right-[64px]' },
|
||||
[PingsPlacement.leftTop]: { default: '!top-[56px] !left-[64px]', withLeftMenu: '!top-[56px] !left-[64px]' },
|
||||
[PingsPlacement.leftBottom]: { default: '!left-[64px] !bottom-[15px]', withLeftMenu: '!bottom-[15px]' },
|
||||
};
|
||||
|
||||
const CLOSE_TOOLTIP_PROPS: WdImgButtonTooltip = {
|
||||
content: 'Hide',
|
||||
position: TooltipPosition.top,
|
||||
className: '!leading-[0]',
|
||||
};
|
||||
|
||||
const NAVIGATE_TOOLTIP_PROPS: WdImgButtonTooltip = {
|
||||
content: 'Navigate To',
|
||||
position: TooltipPosition.top,
|
||||
className: '!leading-[0]',
|
||||
};
|
||||
|
||||
const DELETE_TOOLTIP_PROPS: WdImgButtonTooltip = {
|
||||
content: 'Remove',
|
||||
position: TooltipPosition.top,
|
||||
className: '!leading-[0]',
|
||||
};
|
||||
|
||||
// const TOOLTIP_WAYPOINT_PROPS: WdImgButtonTooltip = {
|
||||
// content: 'Waypoint',
|
||||
// position: TooltipPosition.bottom,
|
||||
// className: '!leading-[0]',
|
||||
// };
|
||||
|
||||
const TITLES = {
|
||||
[PingType.Alert]: 'Alert',
|
||||
[PingType.Rally]: 'Rally Point',
|
||||
};
|
||||
|
||||
const ICONS = {
|
||||
[PingType.Alert]: 'pi-bell',
|
||||
[PingType.Rally]: 'pi-bell',
|
||||
};
|
||||
|
||||
export interface PingsInterfaceProps {
|
||||
hasLeftOffset?: boolean;
|
||||
}
|
||||
|
||||
// TODO: right now can be one ping. But in future will be multiple pings then:
|
||||
// 1. we will use this as container
|
||||
// 2. we will create PingInstance (which will contains ping Button and Toast
|
||||
// 3. ADD Context menu
|
||||
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
const toast = useRef<Toast>(null);
|
||||
const [isShow, setIsShow, isShowRef] = useRefState(false);
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
data: { pings, selectedSystems },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const selectedSystem = useMemo(() => {
|
||||
if (selectedSystems.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return selectedSystems[0];
|
||||
}, [selectedSystems]);
|
||||
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
|
||||
|
||||
const navigateTo = useCallback(() => {
|
||||
if (!ping) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: ping.solar_system_id?.toString(),
|
||||
});
|
||||
}, [ping]);
|
||||
|
||||
const removePing = useCallback(async () => {
|
||||
if (!ping) {
|
||||
return;
|
||||
}
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.cancelPing,
|
||||
data: { type: ping.type, id: ping.id },
|
||||
});
|
||||
}, [outCommand, ping]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ping) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tid = setTimeout(() => {
|
||||
toast.current?.replace({ severity: 'warn', detail: ping.message });
|
||||
setIsShow(true);
|
||||
}, 200);
|
||||
|
||||
return () => clearTimeout(tid);
|
||||
}, [ping]);
|
||||
|
||||
const handleClickShow = useCallback(() => {
|
||||
if (!ping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isShowRef.current) {
|
||||
toast.current?.show({ severity: 'warn', detail: ping.message });
|
||||
setIsShow(true);
|
||||
return;
|
||||
}
|
||||
toast.current?.clear();
|
||||
setIsShow(false);
|
||||
}, [ping]);
|
||||
|
||||
const handleClickHide = useCallback(() => {
|
||||
toast.current?.clear();
|
||||
setIsShow(false);
|
||||
}, []);
|
||||
|
||||
const { placement, offsets } = useMemo(() => {
|
||||
const rawPlacement =
|
||||
interfaceSettings.pingsPlacement == null ? PingsPlacement.rightTop : interfaceSettings.pingsPlacement;
|
||||
|
||||
return {
|
||||
placement: PING_PLACEMENT_MAP[rawPlacement],
|
||||
offsets: PING_PLACEMENT_MAP_OFFSETS[rawPlacement],
|
||||
};
|
||||
}, [interfaceSettings]);
|
||||
|
||||
if (!ping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<div className="m-0 font-semibold text-base text-white">{TITLES[ping.type]}</div>
|
||||
|
||||
<div className="flex gap-1 items-center">
|
||||
{isShowSelectedSystem && (
|
||||
<>
|
||||
<SystemView systemId={selectedSystem} />
|
||||
<span className="pi pi-angle-double-right text-[10px] relative top-[1px] text-stone-400" />
|
||||
</>
|
||||
)}
|
||||
<SystemView systemId={ping.solar_system_id} />
|
||||
{isShowSelectedSystem && (
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.QUESTION_CIRCLE, 'ml-[2px] relative top-[-2px] !text-[10px]')}
|
||||
tooltip={{
|
||||
position: TooltipPosition.top,
|
||||
content: (
|
||||
<div className="flex flex-col gap-1">
|
||||
The settings for the route are taken from the Routes settings and can be configured
|
||||
through them.
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<CharacterCardById className="" characterId={ping.character_eve_id} simpleMode />
|
||||
<TimeAgo timestamp={ping.inserted_at.toString()} className="text-stone-400 text-[11px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSystem != null && <PingRoute />}
|
||||
|
||||
<p className="m-0 text-[13px] text-stone-200 min-h-[20px] pr-[16px]">{message.detail}</p>
|
||||
</div>
|
||||
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.TIMES, 'hover:text-red-400 mt-[3px]')}
|
||||
tooltip={CLOSE_TOOLTIP_PROPS}
|
||||
onClick={handleClickHide}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*Button bar*/}
|
||||
<div className="flex justify-end items-center gap-2 h-0 relative top-[-8px]">
|
||||
<WdImgButton
|
||||
className={clsx('pi-compass', 'hover:text-red-400 mt-[3px]')}
|
||||
tooltip={NAVIGATE_TOOLTIP_PROPS}
|
||||
onClick={navigateTo}
|
||||
/>
|
||||
|
||||
{/*@ts-ignore*/}
|
||||
<div ref={cfRef}>
|
||||
<WdImgButton
|
||||
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
|
||||
tooltip={DELETE_TOOLTIP_PROPS}
|
||||
onClick={cfShow}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO ADD solar system menu*/}
|
||||
{/*<WdImgButton*/}
|
||||
{/* className={clsx('pi-map-marker', 'hover:text-red-400 mt-[3px]')}*/}
|
||||
{/* tooltip={TOOLTIP_WAYPOINT_PROPS}*/}
|
||||
{/* onClick={handleClickHide}*/}
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
></Toast>
|
||||
|
||||
<Button
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
|
||||
<ConfirmPopup
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PingsInterface.tsx';
|
||||
@@ -7,10 +7,6 @@ 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';
|
||||
@@ -18,6 +14,7 @@ import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureC
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
|
||||
|
||||
@@ -146,7 +143,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
}
|
||||
|
||||
const whShipSize = getWhSize(wormholes, signature.type);
|
||||
if (whShipSize) {
|
||||
if (whShipSize !== undefined && whShipSize !== null) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionShipSizeType,
|
||||
data: {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
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';
|
||||
|
||||
const PING_TITLES = {
|
||||
[PingType.Rally]: 'RALLY',
|
||||
[PingType.Alert]: 'ALERT',
|
||||
};
|
||||
|
||||
interface SystemPingDialogProps {
|
||||
systemId: string;
|
||||
type: PingType;
|
||||
visible: boolean;
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const SystemPingDialog = ({ systemId, type, visible, setVisible }: SystemPingDialogProps) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
const inputRef = useRef<HTMLTextAreaElement>();
|
||||
|
||||
const ref = useRef({ message, outCommand, systemId, type });
|
||||
ref.current = { message, outCommand, systemId, type };
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const { message, outCommand, systemId, type } = ref.current;
|
||||
|
||||
outCommand({
|
||||
type: OutCommand.addPing,
|
||||
data: {
|
||||
solar_system_id: systemId,
|
||||
type,
|
||||
message,
|
||||
},
|
||||
});
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
const onShow = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={
|
||||
<div className="flex gap-1 text-[13px] items-center text-stone-300">
|
||||
<div>Ping:{` `}</div>
|
||||
<div
|
||||
className={clsx({
|
||||
['text-cyan-400']: type === PingType.Rally,
|
||||
})}
|
||||
>
|
||||
{PING_TITLES[type]}
|
||||
</div>
|
||||
<div className="text-[11px]">in</div> <SystemView systemId={systemId} className="relative top-[1px]" />
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="flex flex-col gap-3 px-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px]" htmlFor="username">
|
||||
Message
|
||||
</label>
|
||||
<InputTextarea
|
||||
// @ts-ignore
|
||||
ref={inputRef}
|
||||
autoResize
|
||||
rows={3}
|
||||
cols={30}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SystemPingDialog';
|
||||
@@ -206,7 +206,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>
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './Widget';
|
||||
export * from './SystemSettingsDialog';
|
||||
export * from './SystemCustomLabelDialog';
|
||||
export * from './SystemLinkSignatureDialog';
|
||||
export * from './PingsInterface';
|
||||
|
||||
@@ -62,7 +62,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
position: { x: 10, y: 530 },
|
||||
position: { x: 10, y: 10 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesUser />,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
|
||||
export const sortCharacters = (a: CharacterTypeRaw & WithIsOwnCharacter, b: CharacterTypeRaw & WithIsOwnCharacter) => {
|
||||
if (a.online === b.online) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
if (a.online !== b.online) {
|
||||
return a.online && !b.online ? -1 : 1;
|
||||
}
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
import classes from './LocalCharactersItemTemplate.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import classes from './LocalCharactersItemTemplate.module.scss';
|
||||
|
||||
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
|
||||
VirtualScrollerTemplateOptions;
|
||||
@@ -22,7 +22,7 @@ export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalC
|
||||
)}
|
||||
style={{ height: `${options.props.itemSize}px` }}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} showTicker {...options} />
|
||||
<CharacterCard showShipName={showShipName} showTicker showShip {...options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import React, { createContext, forwardRef, useContext, useImperativeHandle, useState } from 'react';
|
||||
import React, { createContext, forwardRef, useContext } from 'react';
|
||||
import {
|
||||
RoutesImperativeHandle,
|
||||
RoutesProviderInnerProps,
|
||||
@@ -15,17 +15,14 @@ const RoutesContext = createContext<RoutesProviderInnerProps>({
|
||||
data: {},
|
||||
});
|
||||
|
||||
export const RoutesProvider = forwardRef<RoutesImperativeHandle, MapProviderProps>(({ children, ...props }, ref) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
// INFO: this component have imperative handler but now it not using.
|
||||
export const RoutesProvider = forwardRef<RoutesImperativeHandle, MapProviderProps>(
|
||||
({ children, ...props } /*, ref*/) => {
|
||||
// useImperativeHandle(ref, () => ({}));
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
stopLoading() {
|
||||
setLoading(false);
|
||||
},
|
||||
}));
|
||||
|
||||
return <RoutesContext.Provider value={{ ...props, loading, setLoading }}>{children}</RoutesContext.Provider>;
|
||||
});
|
||||
return <RoutesContext.Provider value={{ ...props /*, loading, setLoading*/ }}>{children}</RoutesContext.Provider>;
|
||||
},
|
||||
);
|
||||
RoutesProvider.displayName = 'RoutesProvider';
|
||||
|
||||
export const useRouteProvider = () => {
|
||||
|
||||
@@ -3,16 +3,15 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
LoadingWrapper,
|
||||
SystemViewStandalone,
|
||||
SystemView,
|
||||
TooltipPosition,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
|
||||
import { forwardRef, MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers/getSystemById.ts';
|
||||
import classes from './RoutesWidget.module.scss';
|
||||
import { useLoadRoutes } from './hooks';
|
||||
import { RoutesList } from './RoutesList';
|
||||
import clsx from 'clsx';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
@@ -42,24 +41,13 @@ export const RoutesWidgetContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, systems, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
const { hubs = [], routesList, isRestricted } = useRouteProvider();
|
||||
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const { loading } = useLoadRoutes();
|
||||
|
||||
const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
|
||||
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] });
|
||||
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers();
|
||||
|
||||
const preparedHubs = useMemo(() => {
|
||||
return hubs.map(x => {
|
||||
const sys = getSystemById(systems, x.toString());
|
||||
|
||||
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hubs, systems, systemStatics, lastUpdateKey]);
|
||||
|
||||
const preparedRoutes: Route[] = useMemo(() => {
|
||||
return (
|
||||
routesList?.routes
|
||||
@@ -125,9 +113,7 @@ export const RoutesWidgetContent = () => {
|
||||
<LoadingWrapper loading={loading}>
|
||||
<div className={clsx(classes.RoutesGrid, 'px-2 py-2')}>
|
||||
{preparedRoutes.map(route => {
|
||||
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
|
||||
|
||||
// TODO do not delte this console log
|
||||
// TODO do not delete this console log
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('JOipP', `Check sys [${route.destination}]:`, sys);
|
||||
|
||||
@@ -144,12 +130,12 @@ export const RoutesWidgetContent = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<SystemViewStandalone
|
||||
key={route.destination}
|
||||
<SystemView
|
||||
systemId={route.destination.toString()}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
hideRegion
|
||||
compact
|
||||
{...sys}
|
||||
showCustomName
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
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';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
@@ -13,8 +14,22 @@ function usePrevious<T>(value: T): T | undefined {
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export const useLoadRoutes = () => {
|
||||
const { data: routesSettings, loadRoutesCommand, hubs, routesList, loading, setLoading } = useRouteProvider();
|
||||
type UseLoadRoutesProps = {
|
||||
loadRoutesCommand: LoadRoutesCommand;
|
||||
hubs: string[];
|
||||
routesList: RoutesList | undefined;
|
||||
data: RoutesType;
|
||||
deps?: unknown[];
|
||||
};
|
||||
|
||||
export const useLoadRoutes = ({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
hubs,
|
||||
routesList,
|
||||
deps = [],
|
||||
}: UseLoadRoutesProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems, systems, connections },
|
||||
@@ -55,7 +70,8 @@ export const useLoadRoutes = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map(x => routesSettings[x]),
|
||||
...deps,
|
||||
]);
|
||||
|
||||
return { loading, loadRoutes };
|
||||
return { loading, loadRoutes, setLoading };
|
||||
};
|
||||
|
||||
@@ -10,17 +10,14 @@ export type RoutesWidgetProps = {
|
||||
update: (d: RoutesType) => void;
|
||||
hubs: string[];
|
||||
routesList: RoutesList | undefined;
|
||||
loading: boolean;
|
||||
|
||||
loadRoutesCommand: LoadRoutesCommand;
|
||||
addHubCommand: AddHubCommand;
|
||||
toggleHubCommand: ToggleHubCommand;
|
||||
isRestricted?: boolean;
|
||||
};
|
||||
|
||||
export type RoutesProviderInnerProps = RoutesWidgetProps & {
|
||||
loading: boolean;
|
||||
setLoading(loading: boolean): void;
|
||||
};
|
||||
export type RoutesProviderInnerProps = RoutesWidgetProps;
|
||||
|
||||
export type RoutesImperativeHandle = {
|
||||
stopLoading: () => void;
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface SignatureViewProps {
|
||||
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
|
||||
const isWormhole = signature?.group === SignatureGroup.Wormhole;
|
||||
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
|
||||
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
|
||||
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
|
||||
const characterName = signature.character_name || 'Unknown character';
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ export type HeaderProps = {
|
||||
lazyDeleteValue: boolean;
|
||||
onLazyDeleteChange: (checked: boolean) => void;
|
||||
pendingCount: number;
|
||||
pendingTimeRemaining?: number; // Time remaining in ms
|
||||
undoCountdown?: number;
|
||||
onUndoClick: () => void;
|
||||
onSettingsClick: () => void;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
|
||||
lazyDeleteValue,
|
||||
onLazyDeleteChange,
|
||||
pendingCount,
|
||||
pendingTimeRemaining,
|
||||
undoCountdown,
|
||||
onUndoClick,
|
||||
onSettingsClick,
|
||||
}: HeaderProps) => {
|
||||
@@ -43,13 +43,6 @@ export const SystemSignaturesHeader = ({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
|
||||
|
||||
// Format time remaining as seconds
|
||||
const formatTimeRemaining = () => {
|
||||
if (!pendingTimeRemaining) return '';
|
||||
const seconds = Math.ceil(pendingTimeRemaining / 1000);
|
||||
return ` (${seconds}s remaining)`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full">
|
||||
<div className="flex justify-between items-center text-xs w-full h-full">
|
||||
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({
|
||||
<WdImgButton
|
||||
className={PrimeIcons.UNDO}
|
||||
style={{ color: 'red' }}
|
||||
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
|
||||
tooltip={{
|
||||
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? ` — ${undoCountdown}s left` : ''}`,
|
||||
}}
|
||||
onClick={onUndoClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
Setting,
|
||||
SettingsTypes,
|
||||
SIGNATURE_SETTINGS,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
|
||||
interface SystemSignatureSettingsDialogProps {
|
||||
settings: SignatureSettingsType;
|
||||
|
||||
@@ -1,139 +1,205 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
||||
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import {
|
||||
SETTINGS_KEYS,
|
||||
SETTINGS_VALUES,
|
||||
SIGNATURE_DELETION_TIMEOUTS,
|
||||
SIGNATURE_SETTING_STORE_KEY,
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SIGNATURES_DELETION_TIMING,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { calculateTimeRemaining } from './helpers';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
export const SystemSignatures = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sigCount, setSigCount] = useState<number>(0);
|
||||
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
|
||||
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
|
||||
const undoPendingFnRef = useRef<() => void>(() => {});
|
||||
/**
|
||||
* 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 {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
|
||||
defaultValue: SETTINGS_VALUES,
|
||||
});
|
||||
|
||||
const handleSigCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
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]);
|
||||
}, []);
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
|
||||
setCurrentSettings(newSettings);
|
||||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleLazyDeleteChange = useCallback((value: boolean) => {
|
||||
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
|
||||
}, []);
|
||||
|
||||
useHotkey(true, ['z'], event => {
|
||||
if (pendingSigs.length > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
undoPendingFnRef.current();
|
||||
setPendingSigs([]);
|
||||
setPendingTimeRemaining(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const handleUndoClick = useCallback(() => {
|
||||
undoPendingFnRef.current();
|
||||
setPendingSigs([]);
|
||||
setPendingTimeRemaining(undefined);
|
||||
}, []);
|
||||
|
||||
const handleSettingsButtonClick = useCallback(() => {
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
const handlePendingChange = useCallback(
|
||||
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
|
||||
setPendingSigs(() => {
|
||||
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
|
||||
});
|
||||
undoPendingFnRef.current = newUndo;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Calculate the minimum time remaining for any pending signature
|
||||
// Clear deleted signatures when system changes
|
||||
useEffect(() => {
|
||||
if (pendingSigs.length === 0) {
|
||||
setPendingTimeRemaining(undefined);
|
||||
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;
|
||||
}
|
||||
|
||||
const calculate = () => {
|
||||
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
|
||||
};
|
||||
// determine timeout from settings
|
||||
const timeoutMs = getDeletionTimeoutMs(settings);
|
||||
|
||||
// Ensure a minimum of 1 second for immediate deletion so the UI shows
|
||||
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
|
||||
|
||||
calculate();
|
||||
const interval = setInterval(calculate, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [pendingSigs]);
|
||||
setCountdown(Math.ceil(effectiveTimeoutMs / 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 {
|
||||
data: { selectedSystems },
|
||||
outCommand,
|
||||
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
|
||||
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
|
||||
systemId,
|
||||
settingsSignatures,
|
||||
outCommand,
|
||||
);
|
||||
|
||||
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
|
||||
if (pendingIds.size > 0 && countdown > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUndo();
|
||||
}
|
||||
});
|
||||
|
||||
const handleCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
}, []);
|
||||
|
||||
const handleSettingsSave = useCallback(
|
||||
(newSettings: SignatureSettingsType) => {
|
||||
settingsSignaturesUpdate(newSettings);
|
||||
setVisible(false);
|
||||
},
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const handleLazyDeleteToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
settingsSignaturesUpdate(prev => ({
|
||||
...prev,
|
||||
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
|
||||
}));
|
||||
},
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(() => setVisible(true), []);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<SystemSignaturesHeader
|
||||
sigCount={sigCount}
|
||||
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={pendingSigs.length}
|
||||
pendingTimeRemaining={pendingTimeRemaining}
|
||||
onLazyDeleteChange={handleLazyDeleteChange}
|
||||
onUndoClick={handleUndoClick}
|
||||
onSettingsClick={handleSettingsButtonClick}
|
||||
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={pendingIds.size}
|
||||
undoCountdown={countdown}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onUndoClick={handleUndo}
|
||||
onSettingsClick={openSettings}
|
||||
/>
|
||||
}
|
||||
windowId={SIGNATURE_WINDOW_ID}
|
||||
>
|
||||
{isNotSelectedSystem ? (
|
||||
{!isSystemSelected ? (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
System is not selected
|
||||
</div>
|
||||
) : (
|
||||
<SystemSignaturesContent
|
||||
systemId={systemId}
|
||||
settings={currentSettings}
|
||||
deletionTiming={
|
||||
SIGNATURE_DELETION_TIMEOUTS[
|
||||
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
|
||||
SIGNATURES_DELETION_TIMING.DEFAULT
|
||||
] as number
|
||||
}
|
||||
onLazyDeleteChange={handleLazyDeleteChange}
|
||||
onCountChange={handleSigCountChange}
|
||||
onPendingChange={handlePendingChange}
|
||||
settings={settingsSignatures}
|
||||
deletedSignatures={deletedSignatures}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onCountChange={handleCountChange}
|
||||
onSignatureDeleted={addDeleted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{visible && (
|
||||
<SystemSignatureSettingsDialog
|
||||
settings={currentSettings}
|
||||
settings={settingsSignatures}
|
||||
onCancel={() => setVisible(false)}
|
||||
onSave={handleSettingsChange}
|
||||
onSave={handleSettingsSave}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -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';
|
||||
@@ -36,19 +32,11 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
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;
|
||||
settings: SignatureSettingsType;
|
||||
@@ -57,12 +45,9 @@ interface SystemSignaturesContentProps {
|
||||
onSelect?: (signature: SystemSignature) => void;
|
||||
onLazyDeleteChange?: (value: boolean) => void;
|
||||
onCountChange?: (count: number) => void;
|
||||
onPendingChange?: (
|
||||
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
|
||||
undo: () => void,
|
||||
) => void;
|
||||
deletionTiming?: number;
|
||||
filterSignature?: (signature: SystemSignature) => boolean;
|
||||
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
|
||||
deletedSignatures?: ExtendedSystemSignature[];
|
||||
}
|
||||
|
||||
export const SystemSignaturesContent = ({
|
||||
@@ -73,15 +58,19 @@ export const SystemSignaturesContent = ({
|
||||
onSelect,
|
||||
onLazyDeleteChange,
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
deletionTiming,
|
||||
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);
|
||||
|
||||
@@ -90,20 +79,21 @@ 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 } =
|
||||
useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
onLazyDeleteChange,
|
||||
deletionTiming,
|
||||
});
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onLazyDeleteChange,
|
||||
onSignatureDeleted,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectable) return;
|
||||
@@ -125,6 +115,8 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Delete key should always immediately delete, never show pending deletions
|
||||
handleDeleteSelected();
|
||||
});
|
||||
|
||||
@@ -153,9 +145,16 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
const handleSelectSignatures = useCallback(
|
||||
(e: { value: SystemSignature[] }) => {
|
||||
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
|
||||
// Filter out deleted signatures from selection
|
||||
const selectableSignatures = e.value.filter(
|
||||
sig => !deletedSignatures.some(deleted => deleted.eve_id === sig.eve_id),
|
||||
);
|
||||
|
||||
selectable
|
||||
? onSelect?.(selectableSignatures[0])
|
||||
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
|
||||
},
|
||||
[selectable],
|
||||
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
|
||||
);
|
||||
|
||||
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
||||
@@ -169,7 +168,11 @@ export const SystemSignaturesContent = ({
|
||||
);
|
||||
|
||||
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
|
||||
return signatures.filter(sig => {
|
||||
// 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)) {
|
||||
return false;
|
||||
}
|
||||
@@ -188,15 +191,37 @@ export const SystemSignaturesContent = ({
|
||||
x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS],
|
||||
);
|
||||
|
||||
return enabledGroups.includes(getGroupIdByRawGroup(sig.group));
|
||||
const mappedGroup = getGroupIdByRawGroup(sig.group);
|
||||
if (!mappedGroup) {
|
||||
return true; // If we can't determine the group, still show it
|
||||
}
|
||||
return enabledGroups.includes(mappedGroup);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return settings[sig.kind];
|
||||
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);
|
||||
});
|
||||
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
|
||||
|
||||
// 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]);
|
||||
|
||||
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
|
||||
setHoveredSignature(e.data as SystemSignature);
|
||||
@@ -208,8 +233,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 => {
|
||||
@@ -225,7 +250,12 @@ export const SystemSignaturesContent = ({
|
||||
}, []);
|
||||
|
||||
const handleSortSettings = useCallback(
|
||||
(e: DataTableStateEvent) => refVars.current.setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder }),
|
||||
(e: DataTableStateEvent) =>
|
||||
refVars.current.settingsSignaturesUpdate({
|
||||
...refVars.current.settingsSignatures,
|
||||
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
|
||||
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -236,113 +266,122 @@ export const SystemSignaturesContent = ({
|
||||
No signatures
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
className="w-full select-none"
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
onRowDoubleClick={handleRowClick}
|
||||
sortField={sortSettings.sortField}
|
||||
sortOrder={sortSettings.sortOrder}
|
||||
onSort={handleSortSettings}
|
||||
onRowMouseEnter={onRowMouseEnter}
|
||||
onRowMouseLeave={onRowMouseLeave}
|
||||
// @ts-ignore
|
||||
rowClassName={getRowClassName}
|
||||
>
|
||||
<Column
|
||||
field="icon"
|
||||
header=""
|
||||
body={renderColIcon}
|
||||
bodyClassName="p-0 px-1"
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
/>
|
||||
<Column
|
||||
field="eve_id"
|
||||
header="Id"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
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
|
||||
/>
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
field="description"
|
||||
header="Description"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
hidden={isCompact}
|
||||
body={renderDescription}
|
||||
sortable
|
||||
/>
|
||||
<>
|
||||
{hasUnsupportedLanguage && (
|
||||
<div className="w-full flex justify-center items-center text-amber-500 text-xs p-1 bg-amber-950/20 border-b border-amber-800/30">
|
||||
<i className={PrimeIcons.EXCLAMATION_TRIANGLE + ' mr-1'} />
|
||||
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature
|
||||
details.
|
||||
</div>
|
||||
)}
|
||||
<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"
|
||||
header="Updated"
|
||||
dataType="date"
|
||||
body={renderUpdatedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCharacterColumn && (
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
></Column>
|
||||
)}
|
||||
|
||||
{!selectable && (
|
||||
<DataTable
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
className="w-full select-none"
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
onRowDoubleClick={handleRowClick}
|
||||
sortField={settingsSignatures[SETTINGS_KEYS.SORT_FIELD] as string}
|
||||
sortOrder={settingsSignatures[SETTINGS_KEYS.SORT_ORDER] as SortOrder}
|
||||
onSort={handleSortSettings}
|
||||
onRowMouseEnter={onRowMouseEnter}
|
||||
onRowMouseLeave={onRowMouseLeave}
|
||||
// @ts-ignore
|
||||
rowClassName={getRowClassName}
|
||||
>
|
||||
<Column
|
||||
field="icon"
|
||||
header=""
|
||||
body={() => (
|
||||
<div className="flex justify-end items-center gap-2 mr-[4px]">
|
||||
<WdTooltipWrapper content="Double-click a row to edit signature">
|
||||
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
body={renderColIcon}
|
||||
bodyClassName="p-0 px-1"
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
bodyClassName="p-0 pl-1 pr-2"
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
<Column
|
||||
field="eve_id"
|
||||
header="Id"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
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
|
||||
/>
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
field="description"
|
||||
header="Description"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
hidden={isCompact}
|
||||
body={renderDescription}
|
||||
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
|
||||
/>
|
||||
{showUpdatedColumn && (
|
||||
<Column
|
||||
field="updated_at"
|
||||
header="Updated"
|
||||
dataType="date"
|
||||
body={renderUpdatedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCharacterColumn && (
|
||||
<Column
|
||||
field="character_name"
|
||||
header="Character"
|
||||
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
></Column>
|
||||
)}
|
||||
|
||||
{!selectable && (
|
||||
<Column
|
||||
header=""
|
||||
body={() => (
|
||||
<div className="flex justify-end items-center gap-2 mr-[4px]">
|
||||
<WdTooltipWrapper content="Double-click a row to edit signature">
|
||||
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
bodyClassName="p-0 pl-1 pr-2"
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
</>
|
||||
)}
|
||||
|
||||
<WdTooltip
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
GroupType,
|
||||
SignatureGroup,
|
||||
SignatureGroupDE,
|
||||
SignatureGroupENG,
|
||||
SignatureGroupFR,
|
||||
SignatureGroupRU,
|
||||
SignatureKind,
|
||||
SignatureKindDE,
|
||||
SignatureKindENG,
|
||||
SignatureKindFR,
|
||||
SignatureKindRU,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
export const TIME_ONE_MINUTE = 1000 * 60;
|
||||
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
|
||||
@@ -40,99 +45,73 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
|
||||
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
|
||||
};
|
||||
|
||||
export const MAPPING_GROUP_TO_ENG = {
|
||||
// ENGLISH
|
||||
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
|
||||
// RUSSIAN
|
||||
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
export const LANGUAGE_GROUP_MAPPINGS = {
|
||||
EN: {
|
||||
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
RU: {
|
||||
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
FR: {
|
||||
[SignatureGroupFR.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupFR.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupFR.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupFR.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupFR.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupFR.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupFR.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
DE: {
|
||||
[SignatureGroupDE.GasSite]: SignatureGroup.GasSite,
|
||||
[SignatureGroupDE.RelicSite]: SignatureGroup.RelicSite,
|
||||
[SignatureGroupDE.DataSite]: SignatureGroup.DataSite,
|
||||
[SignatureGroupDE.OreSite]: SignatureGroup.OreSite,
|
||||
[SignatureGroupDE.CombatSite]: SignatureGroup.CombatSite,
|
||||
[SignatureGroupDE.Wormhole]: SignatureGroup.Wormhole,
|
||||
[SignatureGroupDE.CosmicSignature]: SignatureGroup.CosmicSignature,
|
||||
},
|
||||
};
|
||||
|
||||
export const MAPPING_TYPE_TO_ENG = {
|
||||
// ENGLISH
|
||||
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindENG.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindENG.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindENG.Drone]: SignatureKind.Drone,
|
||||
// Flatten the structure for backward compatibility
|
||||
export const MAPPING_GROUP_TO_ENG: Record<string, SignatureGroup> = (() => {
|
||||
const flattened: Record<string, SignatureGroup> = {};
|
||||
for (const [, mappings] of Object.entries(LANGUAGE_GROUP_MAPPINGS)) {
|
||||
Object.assign(flattened, mappings);
|
||||
}
|
||||
return flattened;
|
||||
})();
|
||||
|
||||
// RUSSIAN
|
||||
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindRU.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindRU.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindRU.Drone]: SignatureKind.Drone,
|
||||
export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined => {
|
||||
return MAPPING_GROUP_TO_ENG[val] || undefined;
|
||||
};
|
||||
|
||||
export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup];
|
||||
|
||||
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;
|
||||
type: SettingsTypes;
|
||||
isSeparator?: boolean;
|
||||
options?: { label: string; value: any }[];
|
||||
options?: { label: string; value: number | string | boolean }[];
|
||||
};
|
||||
|
||||
export enum SIGNATURES_DELETION_TIMING {
|
||||
IMMEDIATE,
|
||||
DEFAULT,
|
||||
EXTENDED,
|
||||
}
|
||||
|
||||
export type SignatureDeletionTimingType = { [key in SIGNATURES_DELETION_TIMING]?: unknown };
|
||||
// Now use a stricter type: every timing key maps to a number
|
||||
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
|
||||
|
||||
export const SIGNATURE_SETTINGS = {
|
||||
filterFlags: [
|
||||
@@ -177,34 +156,73 @@ 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.DEFAULT]: 10_000,
|
||||
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
|
||||
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
|
||||
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to extract the deletion timeout in milliseconds from settings
|
||||
*/
|
||||
export function getDeletionTimeoutMs(settings: SignatureSettingsType): number {
|
||||
const raw = settings[SETTINGS_KEYS.DELETION_TIMING];
|
||||
const timing =
|
||||
raw && typeof raw === 'object' && 'value' in raw
|
||||
? (raw as { value: SIGNATURES_DELETION_TIMING }).value
|
||||
: (raw as SIGNATURES_DELETION_TIMING | undefined);
|
||||
|
||||
const validTiming = typeof timing === 'number' ? timing : SIGNATURES_DELETION_TIMING.DEFAULT;
|
||||
|
||||
return SIGNATURE_DELETION_TIMEOUTS[validTiming];
|
||||
}
|
||||
|
||||
// Replace the flat structure with a nested structure by language
|
||||
export const LANGUAGE_TYPE_MAPPINGS = {
|
||||
EN: {
|
||||
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindENG.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindENG.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindENG.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindENG.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
RU: {
|
||||
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindRU.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindRU.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindRU.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindRU.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
FR: {
|
||||
[SignatureKindFR.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindFR.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindFR.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindFR.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindFR.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindFR.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindFR.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
DE: {
|
||||
[SignatureKindDE.CosmicSignature]: SignatureKind.CosmicSignature,
|
||||
[SignatureKindDE.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
|
||||
[SignatureKindDE.Structure]: SignatureKind.Structure,
|
||||
[SignatureKindDE.Ship]: SignatureKind.Ship,
|
||||
[SignatureKindDE.Deployable]: SignatureKind.Deployable,
|
||||
[SignatureKindDE.Drone]: SignatureKind.Drone,
|
||||
[SignatureKindDE.Starbase]: SignatureKind.Starbase,
|
||||
},
|
||||
};
|
||||
|
||||
// Flatten the structure for backward compatibility
|
||||
export const MAPPING_TYPE_TO_ENG: Record<string, SignatureKind> = (() => {
|
||||
const flattened: Record<string, SignatureKind> = {};
|
||||
for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) {
|
||||
Object.assign(flattened, mappings);
|
||||
}
|
||||
return flattened;
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { getState } from './getState';
|
||||
|
||||
/**
|
||||
@@ -22,6 +22,7 @@ export const getActualSigs = (
|
||||
|
||||
oldSignatures.forEach(oldSig => {
|
||||
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
|
||||
|
||||
if (newSig) {
|
||||
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
|
||||
const mergedSig = { ...oldSig };
|
||||
|
||||
@@ -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,13 +1,15 @@
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export const getState = (_: string[], newSig: SystemSignature) => {
|
||||
let state = -1;
|
||||
if (!newSig.group) {
|
||||
if (!newSig.group || newSig.group === SignatureGroup.CosmicSignature) {
|
||||
state = 0;
|
||||
} else if (!newSig.name || newSig.name === '') {
|
||||
} else if (!newSig.name || newSig.name === '' || newSig.name === UNKNOWN_SIGNATURE_NAME) {
|
||||
state = 1;
|
||||
} else if (newSig.name !== '') {
|
||||
state = 2;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
|
||||
export interface UseSystemSignaturesDataProps {
|
||||
systemId: string;
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
|
||||
import { prepareUpdatePayload } from '../helpers';
|
||||
import { UsePendingDeletionParams } from './types';
|
||||
import { FINAL_DURATION_MS } from '../constants';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export function usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
deletionTiming,
|
||||
onPendingChange,
|
||||
}: UsePendingDeletionParams) {
|
||||
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
|
||||
const { outCommand } = useMapRootState();
|
||||
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
|
||||
|
||||
// Use the provided deletion timing or fall back to the default
|
||||
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
|
||||
|
||||
const processRemovedSignatures = useCallback(
|
||||
async (
|
||||
removed: ExtendedSystemSignature[],
|
||||
@@ -25,63 +20,15 @@ export function usePendingDeletions({
|
||||
updated: ExtendedSystemSignature[],
|
||||
) => {
|
||||
if (!removed.length) return;
|
||||
|
||||
// If deletion timing is 0, immediately delete without pending state
|
||||
if (finalDuration === 0) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const processedRemoved = removed.map(r => ({
|
||||
...r,
|
||||
pendingDeletion: true,
|
||||
pendingUntil: now + finalDuration,
|
||||
}));
|
||||
pendingDeletionMapRef.current = {
|
||||
...pendingDeletionMapRef.current,
|
||||
...processedRemoved.reduce((acc: any, sig) => {
|
||||
acc[sig.eve_id] = sig;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
|
||||
setSignatures(prev =>
|
||||
prev.map(sig => {
|
||||
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
|
||||
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
|
||||
}
|
||||
return sig;
|
||||
}),
|
||||
);
|
||||
|
||||
scheduleLazyTimers(
|
||||
processedRemoved,
|
||||
pendingDeletionMapRef,
|
||||
async sig => {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, [], [], [sig]),
|
||||
});
|
||||
delete pendingDeletionMapRef.current[sig.eve_id];
|
||||
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
},
|
||||
finalDuration,
|
||||
);
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
},
|
||||
[systemId, outCommand, finalDuration],
|
||||
[systemId, outCommand],
|
||||
);
|
||||
|
||||
const clearPendingDeletions = useCallback(() => {
|
||||
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
|
||||
clearTimeout(finalTimeoutId);
|
||||
});
|
||||
pendingDeletionMapRef.current = {};
|
||||
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
|
||||
@@ -5,12 +5,13 @@ import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useRefState from 'react-usestateref';
|
||||
|
||||
import { SETTINGS_KEYS } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getActualSigs } from '../helpers';
|
||||
import { UseSystemSignaturesDataProps } from './types';
|
||||
import { usePendingDeletions } from './usePendingDeletions';
|
||||
import { useSignatureFetching } from './useSignatureFetching';
|
||||
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
|
||||
export const useSystemSignaturesData = ({
|
||||
systemId,
|
||||
@@ -18,16 +19,18 @@ export const useSystemSignaturesData = ({
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
onLazyDeleteChange,
|
||||
deletionTiming,
|
||||
}: UseSystemSignaturesDataProps) => {
|
||||
onSignatureDeleted,
|
||||
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
|
||||
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
|
||||
}) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
|
||||
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
|
||||
|
||||
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
deletionTiming,
|
||||
onPendingChange,
|
||||
});
|
||||
|
||||
@@ -42,6 +45,7 @@ export const useSystemSignaturesData = ({
|
||||
async (clipboardString: string) => {
|
||||
const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
|
||||
|
||||
// Parse the incoming signatures
|
||||
const incomingSignatures = parseSignatures(
|
||||
clipboardString,
|
||||
Object.keys(settings).filter(skey => skey in SignatureKind),
|
||||
@@ -51,14 +55,32 @@ export const useSystemSignaturesData = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any signatures might be using unsupported languages
|
||||
// This is a basic heuristic: if we have signatures where the original group wasn't mapped
|
||||
const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== '');
|
||||
const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length;
|
||||
|
||||
// If we detected valid IDs but got fewer parsed signatures, we might have language issues
|
||||
if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) {
|
||||
setHasUnsupportedLanguage(true);
|
||||
} else {
|
||||
setHasUnsupportedLanguage(false);
|
||||
}
|
||||
|
||||
const currentNonPending = lazyDeleteValue
|
||||
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
|
||||
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
|
||||
|
||||
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
|
||||
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
|
||||
|
||||
if (removed.length > 0) {
|
||||
await processRemovedSignatures(removed, added, updated);
|
||||
|
||||
// Show pending deletions if lazy deletion is enabled
|
||||
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
|
||||
if (onSignatureDeleted && lazyDeleteValue) {
|
||||
onSignatureDeleted(removed);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.length !== 0 || added.length !== 0) {
|
||||
@@ -78,17 +100,23 @@ export const useSystemSignaturesData = ({
|
||||
onLazyDeleteChange?.(false);
|
||||
}
|
||||
},
|
||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
|
||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
|
||||
);
|
||||
|
||||
const handleDeleteSelected = useCallback(async () => {
|
||||
if (!selectedSignatures.length) return;
|
||||
|
||||
const selectedIds = selectedSignatures.map(s => s.eve_id);
|
||||
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
|
||||
|
||||
// IMPORTANT: Send deletion to server BEFORE updating local state
|
||||
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
|
||||
await handleUpdateSignatures(finalList, false, true);
|
||||
|
||||
// Update local state after server call
|
||||
setSignatures(finalList);
|
||||
setSelectedSignatures([]);
|
||||
}, [selectedSignatures, signatures]);
|
||||
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedSignatures(signatures);
|
||||
@@ -119,11 +147,12 @@ export const useSystemSignaturesData = ({
|
||||
}, [signatures]);
|
||||
|
||||
return {
|
||||
signatures,
|
||||
signatures: signatures.filter(sig => !sig.deleted),
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Commands, OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
AddHubCommand,
|
||||
LoadRoutesCommand,
|
||||
RoutesImperativeHandle,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
@@ -13,24 +12,11 @@ export const WRoutesPublic = () => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { settingsRoutes, settingsRoutesUpdate },
|
||||
data: { hubs, routes },
|
||||
data: { hubs, routes, loadingPublicRoutes },
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef<RoutesImperativeHandle>(null);
|
||||
|
||||
const loadRoutesCommand: LoadRoutesCommand = useCallback(
|
||||
async (systemId, routesSettings) => {
|
||||
outCommand({
|
||||
type: OutCommand.getRoutes,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
routes_settings: routesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const addHubCommand: AddHubCommand = useCallback(
|
||||
async systemId => {
|
||||
if (hubs.includes(systemId)) {
|
||||
@@ -72,10 +58,10 @@ export const WRoutesPublic = () => {
|
||||
ref={ref}
|
||||
title="Routes"
|
||||
data={settingsRoutes}
|
||||
loading={loadingPublicRoutes}
|
||||
update={settingsRoutesUpdate}
|
||||
hubs={hubs}
|
||||
routesList={routes}
|
||||
loadRoutesCommand={loadRoutesCommand}
|
||||
addHubCommand={addHubCommand}
|
||||
toggleHubCommand={toggleHubCommand}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { useLoadRoutes } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
|
||||
|
||||
export const WRoutesUser = () => {
|
||||
const {
|
||||
@@ -61,9 +62,17 @@ export const WRoutesUser = () => {
|
||||
[userHubs, outCommand],
|
||||
);
|
||||
|
||||
// INFO: User routes loading only if open widget with user routes
|
||||
const { loading, setLoading } = useLoadRoutes({
|
||||
data: settingsRoutes,
|
||||
hubs: userHubs,
|
||||
loadRoutesCommand,
|
||||
routesList: userRoutes,
|
||||
});
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.userRoutes) {
|
||||
ref.current?.stopLoading();
|
||||
setLoading(false);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -76,7 +85,7 @@ export const WRoutesUser = () => {
|
||||
update={settingsRoutesUpdate}
|
||||
hubs={userHubs}
|
||||
routesList={userRoutes}
|
||||
loadRoutesCommand={loadRoutesCommand}
|
||||
loading={loading}
|
||||
addHubCommand={addHubCommand}
|
||||
toggleHubCommand={toggleHubCommand}
|
||||
isRestricted
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemKillsList } from './SystemKillsList';
|
||||
import { KillsHeader } from './components/SystemKillsHeader';
|
||||
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
|
||||
import { useSystemKills } from './hooks/useSystemKills';
|
||||
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
@@ -13,27 +12,25 @@ const SystemKillsContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, isSubscriptionActive },
|
||||
outCommand,
|
||||
storedSettings: { settingsKills },
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const visible = settings.showAll;
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
outCommand,
|
||||
showAllVisible: visible,
|
||||
sinceHours: settings.timeRange,
|
||||
showAllVisible: settingsKills.showAll,
|
||||
sinceHours: settingsKills.timeRange,
|
||||
});
|
||||
|
||||
const isNothingSelected = !systemId && !visible;
|
||||
const isNothingSelected = !systemId && !settingsKills.showAll;
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
|
||||
return kills.filter(kill => {
|
||||
if (!systemStaticInfo) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
@@ -41,7 +38,7 @@ const SystemKillsContent = () => {
|
||||
}
|
||||
return isWormholeSpace(systemStaticInfo.system_class);
|
||||
});
|
||||
}, [kills, settings.whOnly, systemStaticInfo, visible]);
|
||||
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
|
||||
|
||||
if (!isSubscriptionActive) {
|
||||
return (
|
||||
@@ -87,7 +84,9 @@ const SystemKillsContent = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
|
||||
return (
|
||||
<SystemKillsList kills={filteredKills} onlyOneSystem={!settingsKills.showAll} timeRange={settingsKills.timeRange} />
|
||||
);
|
||||
};
|
||||
|
||||
export const WSystemKills = () => {
|
||||
|
||||
@@ -17,67 +17,91 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
export type CompactKillRowProps = {
|
||||
killDetails: DetailedKill;
|
||||
killDetails?: DetailedKill | null;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
} & WithClassName;
|
||||
|
||||
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
killmail_id,
|
||||
// Victim data
|
||||
victim_char_name = 'Unknown Pilot',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
victim_ship_name = 'Unknown Ship',
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
victim_char_id = 0,
|
||||
victim_corp_id = 0,
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
// Attacker data
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
final_blow_alliance_name = '',
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_corp_ticker = '',
|
||||
final_blow_corp_id = 0,
|
||||
final_blow_corp_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
kill_time = '',
|
||||
total_value = 0,
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
|
||||
// Define victim affiliation ticker.
|
||||
const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
|
||||
|
||||
const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
|
||||
victim_char_name,
|
||||
victim_alliance_ticker,
|
||||
victim_corp_ticker,
|
||||
victim_ship_name,
|
||||
victim_corp_name,
|
||||
victim_alliance_name,
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
victim_ship_type_id,
|
||||
// Attacker data
|
||||
final_blow_char_id,
|
||||
final_blow_char_name,
|
||||
final_blow_alliance_ticker,
|
||||
final_blow_alliance_name,
|
||||
final_blow_alliance_id,
|
||||
final_blow_corp_ticker,
|
||||
final_blow_corp_id,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id,
|
||||
kill_time,
|
||||
total_value,
|
||||
} = killDetails || {};
|
||||
|
||||
// Apply fallback values using nullish coalescing to handle both null and undefined
|
||||
const safeKillmailId = killmail_id ?? 0;
|
||||
const safeVictimCharName = victim_char_name ?? 'Unknown Pilot';
|
||||
const safeVictimAllianceTicker = victim_alliance_ticker ?? '';
|
||||
const safeVictimCorpTicker = victim_corp_ticker ?? '';
|
||||
const safeVictimShipName = victim_ship_name ?? 'Unknown Ship';
|
||||
const safeVictimCorpName = victim_corp_name ?? '';
|
||||
const safeVictimAllianceName = victim_alliance_name ?? '';
|
||||
const safeVictimCharId = victim_char_id ?? 0;
|
||||
const safeVictimCorpId = victim_corp_id ?? 0;
|
||||
const safeVictimAllianceId = victim_alliance_id ?? 0;
|
||||
const safeVictimShipTypeId = victim_ship_type_id ?? 0;
|
||||
const safeFinalBlowCharId = final_blow_char_id ?? 0;
|
||||
const safeFinalBlowCharName = final_blow_char_name ?? '';
|
||||
const safeFinalBlowAllianceTicker = final_blow_alliance_ticker ?? '';
|
||||
const safeFinalBlowAllianceName = final_blow_alliance_name ?? '';
|
||||
const safeFinalBlowAllianceId = final_blow_alliance_id ?? 0;
|
||||
const safeFinalBlowCorpTicker = final_blow_corp_ticker ?? '';
|
||||
const safeFinalBlowCorpId = final_blow_corp_id ?? 0;
|
||||
const safeFinalBlowCorpName = final_blow_corp_name ?? '';
|
||||
const safeFinalBlowShipTypeId = final_blow_ship_type_id ?? 0;
|
||||
const safeKillTime = kill_time ?? '';
|
||||
const safeTotalValue = total_value ?? 0;
|
||||
|
||||
const attackerIsNpc = safeFinalBlowCharId === 0;
|
||||
|
||||
// Define victim affiliation ticker.
|
||||
const victimAffiliationTicker = safeVictimAllianceTicker || safeVictimCorpTicker || 'No Ticker';
|
||||
|
||||
const killValueFormatted = safeTotalValue != null && safeTotalValue > 0 ? `${formatISK(safeTotalValue)} ISK` : null;
|
||||
const killTimeAgo = safeKillTime ? formatTimeMixed(safeKillTime) : '0h ago';
|
||||
|
||||
const attackerSubscript = killDetails ? getAttackerSubscript(killDetails) : undefined;
|
||||
|
||||
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
|
||||
victim_char_id: safeVictimCharId,
|
||||
victim_ship_type_id: safeVictimShipTypeId,
|
||||
victim_corp_id: safeVictimCorpId,
|
||||
victim_alliance_id: safeVictimAllianceId,
|
||||
});
|
||||
|
||||
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
final_blow_char_id: safeFinalBlowCharId,
|
||||
final_blow_corp_id: safeFinalBlowCorpId,
|
||||
final_blow_alliance_id: safeFinalBlowAllianceId,
|
||||
});
|
||||
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
safeVictimAllianceName,
|
||||
safeVictimCorpName,
|
||||
'Victim',
|
||||
);
|
||||
|
||||
@@ -87,25 +111,25 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id,
|
||||
safeFinalBlowAllianceName,
|
||||
safeFinalBlowCorpName,
|
||||
safeFinalBlowShipTypeId,
|
||||
),
|
||||
[
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerIsNpc,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id,
|
||||
safeFinalBlowAllianceName,
|
||||
safeFinalBlowCorpName,
|
||||
safeFinalBlowShipTypeId,
|
||||
],
|
||||
);
|
||||
|
||||
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
|
||||
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const attackerTicker = attackerIsNpc ? '' : safeFinalBlowAllianceTicker || safeFinalBlowCorpTicker || '';
|
||||
|
||||
// For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page.
|
||||
const attackerLink = attackerIsNpc ? zkillLink('kill', killmail_id) : zkillLink('character', final_blow_char_id);
|
||||
const attackerLink = attackerIsNpc ? zkillLink('kill', safeKillmailId) : zkillLink('character', safeFinalBlowCharId);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -121,7 +145,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
href={zkillLink('kill', safeKillmailId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
@@ -137,7 +161,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
href={zkillLink('kill', safeKillmailId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
@@ -153,12 +177,12 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
|
||||
<div className="truncate text-stone-200">
|
||||
{victim_char_name}
|
||||
{safeVictimCharName}
|
||||
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
|
||||
</div>
|
||||
<div className="truncate text-stone-300 flex items-center gap-1">
|
||||
<span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
|
||||
{victim_ship_name}
|
||||
{safeVictimShipName}
|
||||
</span>
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
@@ -170,9 +194,9 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
|
||||
</div>
|
||||
<div className="flex items-center ml-auto gap-2">
|
||||
<div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]">
|
||||
{!attackerIsNpc && (final_blow_char_name || attackerTicker) && (
|
||||
{!attackerIsNpc && (safeFinalBlowCharName || attackerTicker) && (
|
||||
<div className="truncate text-stone-200">
|
||||
{final_blow_char_name}
|
||||
{safeFinalBlowCharName}
|
||||
{!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
WdImgButton,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
interface KillsHeaderProps {
|
||||
systemId?: string;
|
||||
@@ -17,11 +17,14 @@ interface KillsHeaderProps {
|
||||
}
|
||||
|
||||
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
const [settings, setSettings] = useKillsWidgetSettings();
|
||||
const { showAll } = settings;
|
||||
const {
|
||||
storedSettings: { settingsKills, settingsKillsUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const { showAll } = settingsKills;
|
||||
|
||||
const onToggleShowAllVisible = () => {
|
||||
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
settingsKillsUpdate(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
};
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import {
|
||||
AddSystemDialog,
|
||||
SearchOnSubmitCallback,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
|
||||
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
interface KillsSettingsDialogProps {
|
||||
visible: boolean;
|
||||
@@ -16,12 +16,15 @@ interface KillsSettingsDialogProps {
|
||||
}
|
||||
|
||||
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
|
||||
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
|
||||
const {
|
||||
storedSettings: { settingsKills, settingsKillsUpdate },
|
||||
} = useMapRootState();
|
||||
|
||||
const localRef = useRef({
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
timeRange: globalSettings.timeRange,
|
||||
showAll: settingsKills.showAll,
|
||||
whOnly: settingsKills.whOnly,
|
||||
excludedSystems: settingsKills.excludedSystems || [],
|
||||
timeRange: settingsKills.timeRange,
|
||||
});
|
||||
|
||||
const [, forceRender] = useState(0);
|
||||
@@ -30,14 +33,14 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
localRef.current = {
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
timeRange: globalSettings.timeRange,
|
||||
showAll: settingsKills.showAll,
|
||||
whOnly: settingsKills.whOnly,
|
||||
excludedSystems: settingsKills.excludedSystems || [],
|
||||
timeRange: settingsKills.timeRange,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}
|
||||
}, [visible, globalSettings]);
|
||||
}, [visible, settingsKills]);
|
||||
|
||||
const handleWHChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
@@ -75,12 +78,12 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
setGlobalSettings(prev => ({
|
||||
settingsKillsUpdate(prev => ({
|
||||
...prev,
|
||||
...localRef.current,
|
||||
}));
|
||||
setVisible(false);
|
||||
}, [setGlobalSettings, setVisible]);
|
||||
}, [settingsKillsUpdate, setVisible]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setVisible(false);
|
||||
|
||||
@@ -33,7 +33,10 @@ export function formatISK(value: number): string {
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
|
||||
export function getAttackerSubscript(kill: DetailedKill) {
|
||||
export function getAttackerSubscript(kill: DetailedKill | undefined) {
|
||||
if (!kill) {
|
||||
return null;
|
||||
}
|
||||
if (kill.npc) {
|
||||
return { label: 'npc', cssClass: 'text-purple-400' };
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface KillsWidgetSettings {
|
||||
showAll: boolean;
|
||||
whOnly: boolean;
|
||||
excludedSystems: number[];
|
||||
version: number;
|
||||
timeRange: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||
showAll: false,
|
||||
whOnly: true,
|
||||
excludedSystems: [],
|
||||
version: 2,
|
||||
timeRange: 4,
|
||||
};
|
||||
|
||||
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {
|
||||
if (!settings) {
|
||||
return DEFAULT_KILLS_WIDGET_SETTINGS;
|
||||
}
|
||||
|
||||
return {
|
||||
...DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
...settings,
|
||||
excludedSystems: Array.isArray(settings.excludedSystems) ? settings.excludedSystems : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function useKillsWidgetSettings() {
|
||||
const [rawValue, setRawValue] = useLocalStorageState<KillsWidgetSettings | undefined>('kills:widget:settings');
|
||||
|
||||
const value = useMemo<KillsWidgetSettings>(() => {
|
||||
return mergeWithDefaults(rawValue);
|
||||
}, [rawValue]);
|
||||
|
||||
const setValue = useCallback(
|
||||
(newVal: KillsWidgetSettings | ((prev: KillsWidgetSettings) => KillsWidgetSettings)) => {
|
||||
setRawValue(prev => {
|
||||
const mergedPrev = mergeWithDefaults(prev);
|
||||
|
||||
const nextUnmerged = typeof newVal === 'function' ? newVal(mergedPrev) : newVal;
|
||||
|
||||
return mergeWithDefaults(nextUnmerged);
|
||||
});
|
||||
},
|
||||
[setRawValue],
|
||||
);
|
||||
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import debounce from 'lodash.debounce';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
|
||||
|
||||
interface UseSystemKillsProps {
|
||||
systemId?: string;
|
||||
@@ -13,26 +12,25 @@ interface UseSystemKillsProps {
|
||||
sinceHours?: number;
|
||||
}
|
||||
|
||||
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
|
||||
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
||||
function combineKills(existing: DetailedKill[], incoming: DetailedKill[]): DetailedKill[] {
|
||||
// Don't filter by time when storing - let components filter when displaying
|
||||
const byId: Record<string, DetailedKill> = {};
|
||||
|
||||
for (const kill of [...existing, ...incoming]) {
|
||||
if (!kill.kill_time) continue;
|
||||
const killTimeMs = new Date(kill.kill_time).valueOf();
|
||||
if (killTimeMs >= cutoff) {
|
||||
byId[kill.killmail_id] = kill;
|
||||
}
|
||||
byId[kill.killmail_id] = kill;
|
||||
}
|
||||
|
||||
return Object.values(byId);
|
||||
}
|
||||
|
||||
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||
const { data, update } = useMapRootState();
|
||||
const { detailedKills = {}, systems = [] } = data;
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const excludedSystems = settings.excludedSystems;
|
||||
const {
|
||||
data: { detailedKills = {}, systems = [] },
|
||||
update,
|
||||
storedSettings: { settingsKills },
|
||||
} = useMapRootState();
|
||||
const { excludedSystems } = settingsKills;
|
||||
|
||||
const effectiveSinceHours = sinceHours;
|
||||
|
||||
@@ -55,14 +53,14 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
|
||||
for (const [sid, newKills] of Object.entries(killsMap)) {
|
||||
const existing = updated[sid] ?? [];
|
||||
const combined = combineKills(existing, newKills, effectiveSinceHours);
|
||||
const combined = combineKills(existing, newKills);
|
||||
updated[sid] = combined;
|
||||
}
|
||||
|
||||
return { ...prev, detailedKills: updated };
|
||||
});
|
||||
},
|
||||
[update, effectiveSinceHours],
|
||||
[update],
|
||||
);
|
||||
|
||||
const fetchKills = useCallback(
|
||||
|
||||
@@ -13,13 +13,15 @@ import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandle
|
||||
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
|
||||
|
||||
export interface MapRootContentProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
storedSettings: { interfaceSettings, isReady, hasOldSettings },
|
||||
data,
|
||||
} = useMapRootState();
|
||||
const { isShowMenu } = interfaceSettings;
|
||||
@@ -33,7 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
|
||||
|
||||
/* Important Notice - this solution needs for use one instance of MapInterface */
|
||||
const mapInterface = <MapInterface />;
|
||||
const mapInterface = isReady ? <MapInterface /> : null;
|
||||
|
||||
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
|
||||
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
|
||||
@@ -62,17 +64,21 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
additionalContent={<PingsInterface hasLeftOffset />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
|
||||
<Topbar>
|
||||
<MapContextMenu
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
/>
|
||||
<div className="flex items-center ml-1">
|
||||
<PingsInterface />
|
||||
<MapContextMenu
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
/>
|
||||
</div>
|
||||
</Topbar>
|
||||
{mapInterface}
|
||||
</div>
|
||||
@@ -85,6 +91,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
{showTrackingDialog && (
|
||||
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
|
||||
)}
|
||||
|
||||
{hasOldSettings && <OldSettingsDialog />}
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,10 @@ export interface MapContextMenuProps {
|
||||
}
|
||||
|
||||
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
|
||||
const { outCommand, setInterfaceSettings } = useMapRootState();
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { setInterfaceSettings },
|
||||
} = useMapRootState();
|
||||
|
||||
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
|
||||
|
||||
|
||||
@@ -3,14 +3,8 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import {
|
||||
CONNECTIONS_CHECKBOXES_PROPS,
|
||||
SIGNATURES_CHECKBOXES_PROPS,
|
||||
SYSTEMS_CHECKBOXES_PROPS,
|
||||
THEME_SETTING,
|
||||
UI_CHECKBOXES_PROPS,
|
||||
} from './constants.ts';
|
||||
import { OutCommand, UserPermission } from '@/hooks/Mapper/types';
|
||||
import { CONNECTIONS_CHECKBOXES_PROPS, SIGNATURES_CHECKBOXES_PROPS, SYSTEMS_CHECKBOXES_PROPS } from './constants.ts';
|
||||
import {
|
||||
MapSettingsProvider,
|
||||
useMapSettings,
|
||||
@@ -18,6 +12,10 @@ import {
|
||||
import { WidgetsSettings } from './components/WidgetsSettings';
|
||||
import { CommonSettings } from './components/CommonSettings';
|
||||
import { SettingsListItem } from './types.ts';
|
||||
import { ImportExport } from './components/ImportExport.tsx';
|
||||
import { ServerSettings } from './components/ServerSettings.tsx';
|
||||
import { AdminSettings } from './components/AdminSettings.tsx';
|
||||
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
|
||||
export interface MapSettingsProps {
|
||||
visible: boolean;
|
||||
@@ -29,11 +27,14 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const { renderSettingItem, setUserRemoteSettings } = useMapSettings();
|
||||
const isAdmin = useMapCheckPermissions([UserPermission.ADMIN_MAP]);
|
||||
|
||||
const refVars = useRef({ outCommand, onHide, visible });
|
||||
refVars.current = { outCommand, onHide, visible };
|
||||
|
||||
const handleShow = useCallback(async () => {
|
||||
// TODO: need fix it - add type?
|
||||
// @ts-ignore
|
||||
const { user_settings } = await refVars.current.outCommand({
|
||||
type: OutCommand.getUserSettings,
|
||||
data: null,
|
||||
@@ -61,7 +62,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
|
||||
header="Map user settings"
|
||||
visible
|
||||
draggable={false}
|
||||
style={{ width: '550px' }}
|
||||
style={{ width: '600px' }}
|
||||
onShow={handleShow}
|
||||
onHide={handleHide}
|
||||
>
|
||||
@@ -88,17 +89,23 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
|
||||
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(UI_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
|
||||
<WidgetsSettings />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingItem(THEME_SETTING)}
|
||||
<TabPanel header="Import/Export" className="h-full" headerClassName={styles.verticalTabHeader}>
|
||||
<ImportExport />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Server Settings" className="h-full" headerClassName="color-warn">
|
||||
<ServerSettings />
|
||||
</TabPanel>
|
||||
|
||||
{isAdmin && (
|
||||
<TabPanel header="Admin Settings" className="h-full" headerClassName="color-warn">
|
||||
<AdminSettings />
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabView>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
type MapSettingsContextType = {
|
||||
renderSettingItem: (item: SettingsListItem) => ReactNode;
|
||||
@@ -30,7 +31,7 @@ type MapSettingsContextType = {
|
||||
|
||||
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
|
||||
|
||||
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
export const MapSettingsProvider = ({ children }: WithChildren) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { interfaceSettings, setInterfaceSettings },
|
||||
@@ -88,8 +89,9 @@ export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
if (item.type === 'dropdown' && item.options) {
|
||||
return (
|
||||
<div key={item.prop.toString()} className="flex items-center gap-2 mt-2">
|
||||
<label className="text-sm">{item.label}:</label>
|
||||
<div key={item.prop.toString()} className="grid grid-cols-[auto_1fr_auto] items-center">
|
||||
<label className="text-[var(--gray-200)] text-[13px] select-none">{item.label}:</label>
|
||||
<div className="border-b-2 border-dotted border-[#3f3f3f] h-px mx-3" />
|
||||
<Dropdown
|
||||
className="text-sm"
|
||||
value={currentValue}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Button } from 'primereact/button';
|
||||
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
import { MapUserSettings, RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
|
||||
import fastDeepEqual from 'fast-deep-equal';
|
||||
import { useDetectSettingsChanged } from '@/hooks/Mapper/components/hooks';
|
||||
|
||||
export const AdminSettings = () => {
|
||||
const {
|
||||
storedSettings: { getSettingsForExport },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const settingsChanged = useDetectSettingsChanged();
|
||||
|
||||
const [currentRemoteSettings, setCurrentRemoteSettings] = useState<MapUserSettings | null>(null);
|
||||
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
const toast = useRef<Toast | null>(null);
|
||||
|
||||
const hasSettingsForExport = useMemo(() => !!getSettingsForExport(), [getSettingsForExport]);
|
||||
|
||||
const refVars = useRef({ currentRemoteSettings, getSettingsForExport });
|
||||
refVars.current = { currentRemoteSettings, getSettingsForExport };
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
let res: RemoteAdminSettingsResponse | undefined;
|
||||
try {
|
||||
res = await outCommand({ type: OutCommand.getDefaultSettings, data: null });
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (!res || res.default_settings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentRemoteSettings(parseMapUserSettings(res.default_settings));
|
||||
};
|
||||
|
||||
load();
|
||||
}, [outCommand]);
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
const { currentRemoteSettings, getSettingsForExport } = refVars.current;
|
||||
const localCurrent = parseMapUserSettings(getSettingsForExport());
|
||||
|
||||
return !fastDeepEqual(currentRemoteSettings, localCurrent);
|
||||
// eslint-disable-next-line
|
||||
}, [settingsChanged, currentRemoteSettings]);
|
||||
|
||||
const handleSync = useCallback(async () => {
|
||||
const settings = getSettingsForExport();
|
||||
|
||||
if (!settings) {
|
||||
callToastWarn(toast.current, 'No settings to save');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let response: { success: boolean } | undefined;
|
||||
|
||||
try {
|
||||
response = await outCommand({
|
||||
type: OutCommand.saveDefaultSettings,
|
||||
data: { settings },
|
||||
});
|
||||
} catch (err) {
|
||||
callToastError(toast.current, 'Something went wrong while saving settings');
|
||||
console.error('ERROR: ', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || !response.success) {
|
||||
callToastError(toast.current, 'Settings not saved - dont not why it');
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentRemoteSettings(parseMapUserSettings(settings));
|
||||
|
||||
callToastSuccess(toast.current, 'Settings saved successfully');
|
||||
}, [getSettingsForExport, outCommand]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Button
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
onClick={cfShow}
|
||||
icon="pi pi-save"
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Save as Map Default"
|
||||
className="py-[4px]"
|
||||
disabled={!hasSettingsForExport || !isDirty}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isDirty && <span className="text-red-500/70 text-[12px]">*Local and remote are identical.</span>}
|
||||
|
||||
<span className="text-stone-500 text-[12px]">
|
||||
*Will save your current settings as the default for all new users of this map. This action will overwrite any
|
||||
existing default settings.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Toast ref={toast} />
|
||||
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Your settings will overwrite default. Sure?."
|
||||
icon="pi pi-exclamation-triangle"
|
||||
accept={handleSync}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,68 @@
|
||||
import { COMMON_CHECKBOXES_PROPS } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
|
||||
import {
|
||||
MINI_MAP_PLACEMENT,
|
||||
PINGS_PLACEMENT,
|
||||
THEME_SETTING,
|
||||
UI_CHECKBOXES_PROPS,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
|
||||
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
|
||||
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
|
||||
export const CommonSettings = () => {
|
||||
const { renderSettingItem } = useMapSettings();
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
|
||||
const renderSettingsList = (list: SettingsListItem[]) => {
|
||||
return list.map(renderSettingItem);
|
||||
};
|
||||
const renderSettingsList = useCallback(
|
||||
(list: SettingsListItem[]) => {
|
||||
return list.map(renderSettingItem);
|
||||
},
|
||||
[renderSettingItem],
|
||||
);
|
||||
|
||||
return <div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>;
|
||||
const handleResetSettings = () => {};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-1">
|
||||
<div>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(UI_CHECKBOXES_PROPS)}</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b-2 border-dotted border-stone-700/50 h-px my-3" />
|
||||
|
||||
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(MINI_MAP_PLACEMENT)}</div>
|
||||
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(PINGS_PLACEMENT)}</div>
|
||||
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(THEME_SETTING)}</div>
|
||||
|
||||
<div className="border-b-2 border-dotted border-stone-700/50 h-px my-3" />
|
||||
|
||||
<div className="grid grid-cols-[1fr_auto]">
|
||||
<div />
|
||||
<WdTooltipWrapper content="This dangerous action. And can not be undone" position={TooltipPosition.top}>
|
||||
<Button
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
className="py-[4px]"
|
||||
onClick={cfShow}
|
||||
outlined
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Reset Settings"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="All settings for this map will be reset to default."
|
||||
icon="pi pi-exclamation-triangle"
|
||||
accept={handleResetSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
|
||||
|
||||
type SaveDefaultSettingsReturn = { success: boolean; error: string };
|
||||
|
||||
export const DefaultSettings = () => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { getSettingsForExport },
|
||||
data: { userPermissions },
|
||||
} = useMapRootState();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toast = useRef<Toast | null>(null);
|
||||
|
||||
const refVars = useRef({ getSettingsForExport, outCommand });
|
||||
refVars.current = { getSettingsForExport, outCommand };
|
||||
|
||||
const handleSaveAsDefault = useCallback(async () => {
|
||||
const settings = refVars.current.getSettingsForExport();
|
||||
if (!settings) {
|
||||
callToastWarn(toast.current, 'No settings to save');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
let response: SaveDefaultSettingsReturn;
|
||||
try {
|
||||
response = await refVars.current.outCommand({
|
||||
type: OutCommand.saveDefaultSettings,
|
||||
data: { settings },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Save default settings error:', error);
|
||||
callToastError(toast.current, 'Failed to save default settings');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
callToastSuccess(toast.current, 'Default settings saved successfully');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
callToastError(toast.current, response.error || 'Failed to save default settings');
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
if (!userPermissions?.admin_map) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<div className="w-full h-full flex flex-col gap-5">
|
||||
<h3 className="text-lg font-semibold">Default Settings (Admin Only)</h3>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleSaveAsDefault}
|
||||
icon="pi pi-save"
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Save as Map Default"
|
||||
className="py-[4px]"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-stone-500 text-[12px]">
|
||||
*Will save your current settings as the default for all new users of this map. This action will overwrite
|
||||
any existing default settings.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Toast ref={toast} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
|
||||
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
|
||||
import { SplitButton } from 'primereact/splitbutton';
|
||||
import { loadTextFile } from '@/hooks/Mapper/utils';
|
||||
|
||||
export const ImportExport = () => {
|
||||
const {
|
||||
storedSettings: { getSettingsForExport, applySettings },
|
||||
data: { map_slug },
|
||||
} = useMapRootState();
|
||||
|
||||
const toast = useRef<Toast | null>(null);
|
||||
|
||||
const handleImportFromClipboard = useCallback(async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
if (text == null || text == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parseMapUserSettings(text);
|
||||
if (applySettings(parsed)) {
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Import',
|
||||
detail: 'Map settings was imported successfully.',
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Warning',
|
||||
detail: 'Settings already imported. Or something went wrong.',
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Import from clipboard Error: `, error);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Some error occurred on import from Clipboard, check console log.',
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}, [applySettings]);
|
||||
|
||||
const handleImportFromFile = useCallback(async () => {
|
||||
try {
|
||||
const text = await loadTextFile();
|
||||
|
||||
const parsed = parseMapUserSettings(text);
|
||||
if (applySettings(parsed)) {
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Import',
|
||||
detail: 'Map settings was imported successfully.',
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Warning',
|
||||
detail: 'Settings already imported. Or something went wrong.',
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Import from file Error: `, error);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Some error occurred on import from File, check console log.',
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}, [applySettings]);
|
||||
|
||||
const handleExportToClipboard = useCallback(async () => {
|
||||
const settings = getSettingsForExport();
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(settings);
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Export',
|
||||
detail: 'Map settings copied into clipboard',
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Export to clipboard Error: `, error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Some error occurred on copying to clipboard, check console log.',
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}, [getSettingsForExport]);
|
||||
|
||||
const handleExportToFile = useCallback(async () => {
|
||||
const settings = getSettingsForExport();
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
saveTextFile(`map_settings_${map_slug}.json`, settings);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Export to File',
|
||||
detail: 'Map settings successfully saved to file',
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Export to cliboard Error: `, error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Some error occurred on saving to file, check console log.',
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}, [getSettingsForExport, map_slug]);
|
||||
|
||||
const importItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Import from File',
|
||||
icon: 'pi pi-file-import',
|
||||
command: handleImportFromFile,
|
||||
},
|
||||
],
|
||||
[handleImportFromFile],
|
||||
);
|
||||
|
||||
const exportItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Export as File',
|
||||
icon: 'pi pi-file-export',
|
||||
command: handleExportToFile,
|
||||
},
|
||||
],
|
||||
[handleExportToFile],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<SplitButton
|
||||
onClick={handleImportFromClipboard}
|
||||
icon="pi pi-download"
|
||||
size="small"
|
||||
severity="warning"
|
||||
label="Import from Clipboard"
|
||||
className="py-[4px]"
|
||||
model={importItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-stone-500 text-[12px]">
|
||||
*Will read map settings from clipboard. Be careful it could overwrite current settings.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<SplitButton
|
||||
onClick={handleExportToClipboard}
|
||||
icon="pi pi-upload"
|
||||
size="small"
|
||||
label="Export to Clipboard"
|
||||
className="py-[4px]"
|
||||
model={exportItems}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-stone-500 text-[12px]">*Will save map settings to clipboard.</span>
|
||||
</div>
|
||||
|
||||
<Toast ref={toast} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user