mirror of
https://gitlab.nic.cz/turris/reforis/foris-js.git
synced 2025-04-20 08:16:38 +02:00
Compare commits
720 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
602e3f58dd | ||
|
4b58e96f71 | ||
|
a174d6a612 | ||
|
5d0276a80f | ||
|
e01295504b | ||
|
af49bc7a24 | ||
|
4a60fb23cc | ||
|
c7282261ef | ||
|
928758f5c6 | ||
|
030a563c77 | ||
|
336fb666cc | ||
|
debd00d519 | ||
|
cef75e5748 | ||
|
027cd6eefb | ||
|
227a975e5f | ||
|
819e5a1dd2 | ||
|
6432073d62 | ||
|
94f436008d | ||
|
6f9e44a7b1 | ||
|
13ca745412 | ||
|
a25133d786 | ||
|
0a839bf369 | ||
|
54a801a580 | ||
|
377b4279fd | ||
|
317966e1c9 | ||
|
326790d80d | ||
|
700b28c463 | ||
|
3d725e7e1b | ||
|
ede4fb0212 | ||
|
33add77704 | ||
|
456cbcfeec | ||
|
bf0b2ce70c | ||
|
1441f6ff5a | ||
|
c7d0655771 | ||
|
7197813cc9 | ||
|
31cb8e2ae0 | ||
|
0a75f24a04 | ||
|
230ae8e35b | ||
|
eb4ffb0651 | ||
|
2f249ce3dc | ||
|
74722b8ff3 | ||
|
a2f9b3bfab | ||
|
2ab65be0bf | ||
|
36a7b4dfda | ||
|
9fb0871cfc | ||
|
c7087eabf2 | ||
|
b315ea2fd0 | ||
|
d46629a1bd | ||
|
8ddb590ba8 | ||
|
1c2a4518d3 | ||
|
b6312075d2 | ||
|
2feedec8d1 | ||
|
dff5f87e91 | ||
|
38de792390 | ||
|
c23616811a | ||
|
f1132c6b22 | ||
|
e8e81b36dc | ||
|
53c7bb1a10 | ||
|
fb1f79c6c1 | ||
|
eafbc01c73 | ||
|
73819809f4 | ||
|
ffa1121d39 | ||
|
ee6865e3bb | ||
|
6352060da3 | ||
|
a63b5bfa4e | ||
|
4b2e47f8f9 | ||
|
66f83b24bd | ||
|
30fd6f91b4 | ||
|
5a53eca138 | ||
|
8d2a4dc108 | ||
|
2481a0c025 | ||
|
71697424c5 | ||
|
07f8e3b9de | ||
|
c9f2b24095 | ||
|
087ecfa670 | ||
|
e6365ecac4 | ||
|
e57722caa0 | ||
|
babdf92ddd | ||
|
42294316d9 | ||
|
b65e034b04 | ||
|
14b90bbbd4 | ||
|
85b207b1dd | ||
|
79e61d9507 | ||
|
6795c3941b | ||
|
969e8e6411 | ||
|
0099759279 | ||
|
87c81a2a2d | ||
|
81b71f8153 | ||
|
c0fd0adbc9 | ||
|
1ec0a26199 | ||
|
76e37b738a | ||
|
8a69d14429 | ||
|
4d5395c826 | ||
|
1fb83e08ea | ||
|
e6cfc6dbb0 | ||
|
a93a64bf96 | ||
|
1ab77decfd | ||
|
b6e1e0adae | ||
|
a7a4e76cd1 | ||
|
e7758cab9a | ||
|
bf88b76613 | ||
|
3cf85a9516 | ||
|
7c8442300a | ||
|
e849397aa2 | ||
|
c1b44d498c | ||
|
1b5a5da953 | ||
|
7f45201f05 | ||
|
f34d9bbdbd | ||
|
c7ff3f42f6 | ||
|
a1036514dd | ||
|
a352f12279 | ||
|
acfbeb2c43 | ||
|
3bef624ce4 | ||
|
7d0d52666d | ||
|
52fe5d65a6 | ||
|
b99add91cf | ||
|
b7a4613cf4 | ||
|
9e2278e016 | ||
|
83a6ff75f6 | ||
|
02f3803265 | ||
|
bb559cbe53 | ||
|
c86e2c8944 | ||
|
b96ccde81c | ||
|
cfa6eade17 | ||
|
380a388a38 | ||
|
cc19b4b293 | ||
|
e7ec494bb2 | ||
|
ea590e443c | ||
|
b127bf5edf | ||
|
40e4a9a4e3 | ||
|
bcb7c43863 | ||
|
c809817283 | ||
|
a429b7c1bf | ||
|
4e6f6e7413 | ||
|
e6356de57f | ||
|
d4a71c346c | ||
|
eb9582db96 | ||
|
036f191949 | ||
|
2f73516384 | ||
|
b97ba379ec | ||
|
5f1372bb37 | ||
|
7c9cd9451b | ||
|
7e0752fc17 | ||
|
4c5aeed26e | ||
|
d90e39a570 | ||
|
2e2f326ade | ||
|
541ca7a784 | ||
|
8e0c60a576 | ||
|
928cf716d6 | ||
|
bd8d5bc8cb | ||
|
804e0022eb | ||
|
d69398ac06 | ||
|
e297410f16 | ||
|
17e5a959f7 | ||
|
3b48510246 | ||
|
8bac4f18f4 | ||
|
6cb2a5388e | ||
|
0b02bead71 | ||
|
12c6d05ca6 | ||
|
923bbab6d5 | ||
|
0ea5d43c75 | ||
|
c209796118 | ||
|
bf7e5303e9 | ||
|
59b3130277 | ||
|
4ca07dceb0 | ||
|
912f8facdb | ||
|
42fb16d066 | ||
|
cd9eb80e9c | ||
|
5a77a22755 | ||
|
3fa5ab7c07 | ||
|
ff48d6ca36 | ||
|
39257567d4 | ||
|
5ed48bf2a3 | ||
|
c8fbdc5bba | ||
|
46bd8edcea | ||
|
42fcc02d83 | ||
|
96785f0774 | ||
|
7823bff6d9 | ||
|
bee4bee300 | ||
|
0fd7c08188 | ||
|
32630601c2 | ||
|
03aad5498a | ||
|
2f2b812ddb | ||
|
c0bcd46b2b | ||
|
68ea8cf460 | ||
|
79fe68dba3 | ||
|
683a8736a6 | ||
|
6631d4847b | ||
|
8887d0d68e | ||
|
390e4bdce8 | ||
|
5232b55cf6 | ||
|
5823012c6e | ||
|
e907a3a21f | ||
|
9e4cb4b417 | ||
|
55f4d2ff15 | ||
|
6b464783ed | ||
|
85ba270135 | ||
|
80619fab3c | ||
|
a1a47e0d0f | ||
|
d49ff0150c | ||
|
a0f42906f5 | ||
|
bc1b6e7877 | ||
|
efb3fa80ce | ||
|
9c3dcaf6b5 | ||
|
fb41c9629e | ||
|
620eee3f53 | ||
|
f6ec82609c | ||
|
0b47c38f21 | ||
|
6183669c9b | ||
|
f3694bb62c | ||
|
0f2344a005 | ||
|
f2ae6c4d0a | ||
|
f382e743aa | ||
|
d71f4a7967 | ||
|
aeabc0bf06 | ||
|
46fe75d3cf | ||
|
c469d8dfde | ||
|
f327428765 | ||
|
5a359661da | ||
|
3d30e2720e | ||
|
badb043554 | ||
|
ab692e5c4d | ||
|
2e398388b5 | ||
|
5e539de03f | ||
|
87f5557ef6 | ||
|
f128c5c7d6 | ||
|
7a633574da | ||
|
ee40697e2b | ||
|
4525c6bc66 | ||
|
1d6987b21b | ||
|
67fc2d32ce | ||
|
ab13b7aa08 | ||
|
2a73502710 | ||
|
003606cb8c | ||
|
aeddd9ce74 | ||
|
a4fd74bf38 | ||
|
b0e2f62a41 | ||
|
caf8af44d1 | ||
|
fd7cd49790 | ||
|
d95fdf8517 | ||
|
68c560078b | ||
|
83caf505d9 | ||
|
f3a1090dbd | ||
|
d588291f1c | ||
|
bc044df7a8 | ||
|
b4c6a7fb70 | ||
|
d6563d2ffd | ||
|
af90d8d09d | ||
|
006d6ce8d9 | ||
|
cee08f48ce | ||
|
2d0ca58057 | ||
|
db4a6fb763 | ||
|
caaa154d21 | ||
|
518fa30306 | ||
|
fb3562373a | ||
|
5afbbac74c | ||
|
f25832432b | ||
|
926cb2505f | ||
|
985fd08b46 | ||
|
da019b6d86 | ||
|
514f02a5f6 | ||
|
c6557aae5e | ||
|
92ed7f1ee7 | ||
|
46ce6ebbb9 | ||
|
1a4ba03ff5 | ||
|
e24cd76009 | ||
|
ae6b495683 | ||
|
272c97dc8a | ||
|
fd25ae04a8 | ||
|
cc1b0b3f81 | ||
|
3ba279f42c | ||
|
0167b7ce66 | ||
|
9db509ace3 | ||
|
d42347f169 | ||
|
82c34e5d42 | ||
|
7867a1a494 | ||
|
1bb8abd633 | ||
|
536ccc0f03 | ||
|
671c711a33 | ||
|
e2f3e6857e | ||
|
fe4ab298d8 | ||
|
0f7f89dfc0 | ||
|
be2e3fe3f0 | ||
|
577ad70c06 | ||
|
d17638eb6e | ||
|
13869336db | ||
|
7c46abcd5d | ||
|
894d92b683 | ||
|
ca335ab3a5 | ||
|
2161fc0b32 | ||
|
0a23506a38 | ||
|
d23c7cb790 | ||
|
129327cfcf | ||
|
0a143e7de6 | ||
|
7ec1c46a63 | ||
|
7ceccd5222 | ||
|
f868881b3d | ||
|
188ed87fc0 | ||
|
c0f64e8c6c | ||
|
b95cfb664d | ||
|
52cdaf5bc5 | ||
|
175a07a865 | ||
|
f952e25205 | ||
|
01eeb06f9e | ||
|
839e227feb | ||
|
4b239ed195 | ||
|
2bcbe0ae59 | ||
|
b1ff608337 | ||
|
b662ba5b52 | ||
|
a4115245fe | ||
|
e1a893874a | ||
|
dd383ef1b2 | ||
|
b6e2cb71bb | ||
048e686185 | |||
eacb2f66a3 | |||
|
2433641f56 | ||
|
185d2e6436 | ||
|
7f262d4941 | ||
|
72356bb6c1 | ||
|
13c94caeb5 | ||
|
c24e58fae8 | ||
|
6329b5a104 | ||
|
fbac503586 | ||
|
550af8967c | ||
|
3640d6a90a | ||
|
7b2bc43f3f | ||
|
1e693b0963 | ||
|
afde04c662 | ||
|
22fb7dcf58 | ||
|
b557b67308 | ||
|
cc6e5e2933 | ||
|
60f850a095 | ||
|
a1e9f23620 | ||
|
579ed5ea8c | ||
|
c2eda33998 | ||
|
f49529018c | ||
|
a66a2f4708 | ||
|
2e473003bd | ||
|
43cb5bff50 | ||
|
c67ea089fd | ||
|
4b25f6eafc | ||
|
c1e807bc74 | ||
|
69da5afffe | ||
|
1669ac8576 | ||
|
6e6c349866 | ||
|
5207029462 | ||
|
53aec6372d | ||
|
a7d7e59028 | ||
|
0beb1f0418 | ||
|
2644f6fd70 | ||
|
585fec4e3e | ||
|
682abc126a | ||
|
a9f3f77bd5 | ||
|
4703721c5c | ||
|
aff1ba7b6d | ||
|
9eb7197035 | ||
|
462a86b31d | ||
|
cbce4c1ec1 | ||
|
aee19694b5 | ||
|
f3b1ef741a | ||
|
c35a4a8236 | ||
|
67b8386cd0 | ||
|
f67edc39e1 | ||
|
6f0f344eb4 | ||
|
3a39e44c34 | ||
|
cff5f1e5e1 | ||
|
b7bab92d5d | ||
|
75dd0fec92 | ||
|
3619532124 | ||
|
ce62fd1043 | ||
|
c5bac99d8e | ||
|
f7146e3b14 | ||
|
18ba90567c | ||
|
2e9da55df7 | ||
|
da10a34d64 | ||
|
764a6c86cd | ||
|
6059ce9e7b | ||
|
4368bea2c2 | ||
|
9dd6bbca90 | ||
|
d5bb99570c | ||
|
e1260a5ea1 | ||
|
02f2c5be4f | ||
|
ce04f6c27e | ||
|
80d4dd914d | ||
|
7f82b2e73c | ||
|
ac8646a4e7 | ||
|
7505302875 | ||
|
adc6bbca14 | ||
|
86f98148c6 | ||
|
f623b98acc | ||
|
3be1213b3b | ||
|
09007b922e | ||
|
f6231370b9 | ||
|
449b93ce41 | ||
|
764c8dedd8 | ||
|
9bfd20ef0c | ||
|
0289c5010f | ||
|
1733b8609b | ||
|
d5c3365fdb | ||
|
0ba4814275 | ||
|
fca410ec82 | ||
|
4f09c2da9a | ||
|
57ef9c4ea0 | ||
|
b7695cc854 | ||
|
fd8b8b926a | ||
|
b91ec527d1 | ||
|
7369d906b5 | ||
|
45fee77426 | ||
|
b12cba893e | ||
|
09d1698647 | ||
|
83c05c6c89 | ||
|
a08de54ca1 | ||
|
cb5fa4ce34 | ||
|
fb32c84dc2 | ||
|
4060b3c916 | ||
|
7abfd627e4 | ||
|
0fbc3df247 | ||
|
bc9c00d3a1 | ||
|
8d75b5ec6e | ||
|
c1aa1948b4 | ||
|
8c110ebf52 | ||
|
abb5be53aa | ||
|
af0fb80e45 | ||
|
688192504f | ||
|
b8e5dbec8d | ||
|
bcb5365d08 | ||
|
037d1993c8 | ||
|
2287ddc420 | ||
|
fde751a25f | ||
|
79006cfb99 | ||
|
de398901f3 | ||
|
bea429d6ac | ||
|
e818120986 | ||
|
56173d4959 | ||
|
7c837d041e | ||
|
473c81f9a4 | ||
|
ba9abca5cf | ||
|
15567a7dde | ||
|
e2695d49a1 | ||
|
a87e6858bf | ||
|
e864de5a24 | ||
|
5469e6ec80 | ||
|
4898016388 | ||
|
e0fab75c69 | ||
|
6480a39cdb | ||
|
6f05d5d136 | ||
|
96150fe230 | ||
|
0892a1534a | ||
|
1bac60e054 | ||
|
328e568ab3 | ||
|
c68389359e | ||
|
e03e0f44cc | ||
|
1e04d34645 | ||
|
187ecc54e5 | ||
|
ed7cf34e76 | ||
|
aaf4087c96 | ||
|
240db88661 | ||
|
913a7d7b75 | ||
|
bdc8726791 | ||
|
1c986519f6 | ||
|
defc363f01 | ||
|
ef66fb43cc | ||
|
69723f6b0b | ||
|
c32137e29a | ||
|
03cf73be6e | ||
|
be7349661f | ||
|
5186385b9f | ||
|
002786d073 | ||
|
4d246540c1 | ||
|
35b97ec0fe | ||
|
d2688532af | ||
|
e1d75d8328 | ||
|
0f85713483 | ||
|
c3cdafce13 | ||
|
b96b434a3e | ||
|
0ea5f7de84 | ||
|
0c7997f6c0 | ||
|
90ce866869 | ||
|
ad99a2034d | ||
|
4ff814f0fd | ||
|
896277b62a | ||
|
b0365e3b06 | ||
|
8bd71a08af | ||
|
1903016f13 | ||
|
443f14d26c | ||
|
f1feffb4bb | ||
|
61b349c6cc | ||
|
7a98ab0c2d | ||
|
5de05fe4eb | ||
|
50943e0b11 | ||
|
f64419c643 | ||
|
a0f7a312e5 | ||
|
f8726e6012 | ||
|
e41da48b1a | ||
|
a434ecac18 | ||
|
5ae129b0f5 | ||
|
a2acac255d | ||
|
c1b1d8c079 | ||
|
e422acc92f | ||
|
705ed5ac80 | ||
|
1dd1805ae0 | ||
|
e858b30994 | ||
|
8a56d71c51 | ||
|
d34c465787 | ||
|
cbf37dd747 | ||
|
f9cfb248d3 | ||
|
9be880aeaa | ||
|
a4bb41d585 | ||
|
c3b09b01e5 | ||
|
12b862c568 | ||
|
54f9f984f1 | ||
|
5dbc58d44b | ||
|
e7f9fbca96 | ||
|
8d40dbb841 | ||
|
cea8aa0c12 | ||
|
16a7a6c52d | ||
|
597b6fcf4c | ||
|
5eb6b90ed4 | ||
|
48c323c1a1 | ||
|
3d57b38808 | ||
|
ae8baddbdd | ||
|
67e4abe4d1 | ||
|
57f1ccced8 | ||
|
1e95bff7ff | ||
|
0f253ecc19 | ||
|
a5e096dc00 | ||
|
074ddf8a8b | ||
|
182cbe698f | ||
|
982eb371ad | ||
|
2786f856f7 | ||
|
48b080dc26 | ||
|
71beeb46f1 | ||
|
060a0489e1 | ||
|
ae49b246cd | ||
|
27c37eb74b | ||
|
cd708fa294 | ||
|
8ec0392852 | ||
|
27a5e62d9a | ||
|
aeaad4aa72 | ||
|
256a000d61 | ||
|
c78ed9a5d0 | ||
|
bded10211a | ||
|
25ac6cf1e9 | ||
|
9a2547a6c2 | ||
|
7968c7af4a | ||
|
4b94c470c3 | ||
|
e1b5a25ddd | ||
|
95af86c776 | ||
|
02b5583712 | ||
|
2f4d757a1a | ||
|
3c7a67783f | ||
|
4500e85a40 | ||
|
ce955095fd | ||
|
00b861531e | ||
|
fad5b97a2e | ||
|
aa639596d4 | ||
|
1ee41f4f14 | ||
|
bf8c2d28bf | ||
|
dbb840d51c | ||
|
ba772be869 | ||
|
70da1c3c00 | ||
|
8e68bbc91f | ||
|
0af8c4aa28 | ||
|
a9114caf9e | ||
|
3c81264024 | ||
|
0330b39f2e | ||
|
a7dcced08b | ||
|
c453a35763 | ||
|
d97248c6ec | ||
|
9fbc4e8383 | ||
|
57bebc92c7 | ||
|
5939e9dd0e | ||
|
0665869c30 | ||
|
199b27d63a | ||
|
2b28434712 | ||
|
388860d51e | ||
|
8b7c459855 | ||
|
83409b0118 | ||
|
c1cd90dff6 | ||
|
01fb897180 | ||
|
716c323b28 | ||
|
55dbf8f8bb | ||
|
85e42980ec | ||
|
3dee532ea2 | ||
|
3aac48d2bf | ||
|
ee33d33738 | ||
|
605f682356 | ||
|
a0a775996e | ||
|
532acf9d86 | ||
|
cbc3c2f3e7 | ||
|
556e12c964 | ||
|
813a865f62 | ||
|
c495aa97ac | ||
|
2d375b1690 | ||
|
e7e389e843 | ||
|
8679749e0f | ||
|
6d8e0cec70 | ||
|
5091eecedf | ||
|
75bfbb88ae | ||
|
e5cbbc9019 | ||
|
7ab1d2aaa4 | ||
|
e62accc4b3 | ||
|
03e071d5ee | ||
|
d83ba3bfd3 | ||
|
3e2c89cac7 | ||
|
e3d159d6a3 | ||
|
afa9b5a402 | ||
|
b8555247f2 | ||
|
de8462429b | ||
|
5fd0d3626a | ||
|
9dcc689491 | ||
|
35f307200d | ||
|
afb5366dd7 | ||
|
1e6278abdf | ||
|
6769e84e62 | ||
|
71b0a9a5fa | ||
|
418e38de31 | ||
|
56a4c47948 | ||
|
c67ad164ce | ||
|
6374fd5adf | ||
|
cc13e9c164 | ||
|
bb90800945 | ||
|
6d4bff2b4f | ||
|
92f560b69f | ||
|
a318f12352 | ||
|
32e3a57bd7 | ||
|
dd27802056 | ||
|
bd4e1953e3 | ||
|
68e4368ae3 | ||
|
51ba380cf0 | ||
|
4eae1ed8d2 | ||
|
3d290114fa | ||
|
3f87e9e4b4 | ||
|
6d5cb6a951 | ||
|
8d3be8df67 | ||
|
90509f2a23 | ||
|
73f84a2d81 | ||
|
cea7325427 | ||
|
a8d8c872f9 | ||
|
cda7898a96 | ||
|
26bea9c7c4 | ||
|
fd1518265f | ||
|
61d10e91e0 | ||
|
aac6c6bf2a | ||
|
d55615abcc | ||
|
9d322811c3 | ||
|
f30685d9c2 | ||
|
5bb298270b | ||
|
8d0c640994 | ||
|
25ddb5949c | ||
|
7b739f55a0 | ||
|
03a020f87c | ||
|
c6fd9bbadb | ||
|
acaaab0654 | ||
|
04a667eb6f | ||
|
d71f638bd5 | ||
|
1064277cd9 | ||
|
cffa0a2b80 | ||
|
8d1d5b57fd | ||
|
7579fc3b8c | ||
|
6601cd55e0 | ||
|
fd01bc6f56 | ||
|
e27a23600f | ||
|
daf787e2df | ||
|
512c65c213 | ||
|
3925fb6439 | ||
|
21bbfb6d2e | ||
|
1dcba1dfa8 | ||
|
fd8acd1ceb | ||
|
48d5cf0119 | ||
|
1a82f8e225 | ||
|
638821d025 | ||
|
fbaa73e378 | ||
|
e54db2c577 | ||
|
a6866a0673 | ||
|
429814ebb5 | ||
|
73e213c467 | ||
|
2e68e56e44 | ||
|
b3bb4de646 | ||
|
b30f9f59b4 | ||
|
158bd1bb24 | ||
|
d935f78d6e | ||
|
73f4ab48c3 | ||
|
7075592f24 | ||
|
23029470b9 | ||
|
7e6e6f8c87 | ||
|
b831d664a9 | ||
|
0f5b35a3ba | ||
|
6e02f1d194 | ||
|
13ff8221ca | ||
|
a51ba0630d | ||
|
ee5cf07614 | ||
|
8b39bf4193 | ||
|
644726a0fc | ||
|
654ae6914a | ||
|
81dedf59cd | ||
|
7f8aaea7b8 | ||
|
dfa80e64ec | ||
|
031b53f03c | ||
|
ca23b2d335 | ||
|
0984c45161 | ||
|
6835bc7a28 | ||
|
0915d477fe | ||
|
c114579b7f | ||
|
22b2bc9c09 | ||
|
cab2cfa068 | ||
|
a137a0d4cf | ||
|
54cf7e3c06 | ||
|
18eb28f368 | ||
|
88f3836485 | ||
|
8f88b09e66 | ||
|
f2aa28f172 | ||
|
a88fbf63e9 | ||
|
ff9e8fdeb1 | ||
|
8cd4ac8b44 | ||
|
760e6d9243 | ||
|
2429f4662c | ||
|
b320e6a860 | ||
|
1e3e9433ec | ||
|
e3a795e40d |
65
.eslintrc.js
65
.eslintrc.js
@ -1,66 +1,3 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"env": {
|
extends: "eslint-config-reforis",
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"es6": true,
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"airbnb",
|
|
||||||
"airbnb/hooks"
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"_": "readonly",
|
|
||||||
"babel": "readonly",
|
|
||||||
"ForisTranslations": "readonly",
|
|
||||||
"ngettext": "readonly",
|
|
||||||
"ForisPlugins": "readonly"
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"rules": {
|
|
||||||
"quotes": ["error", "double"],
|
|
||||||
"indent": ["error", 4],
|
|
||||||
"react/jsx-indent": ["error", 4],
|
|
||||||
"react/jsx-indent-props": ["error", 4],
|
|
||||||
"react/prop-types": "warn",
|
|
||||||
"react/no-array-index-key": "warn",
|
|
||||||
"react/button-has-type": "warn",
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"import/no-unresolved": [
|
|
||||||
"error",
|
|
||||||
// Ignore imports used only in tests
|
|
||||||
{ ignore: ["customTestRender"] }
|
|
||||||
],
|
|
||||||
"import/no-cycle": "warn",
|
|
||||||
"no-console": "error",
|
|
||||||
"no-use-before-define": ["error", {
|
|
||||||
functions: false,
|
|
||||||
classes: true,
|
|
||||||
variables: true
|
|
||||||
}],
|
|
||||||
"no-restricted-syntax": "warn",
|
|
||||||
// Should be enabled in the future
|
|
||||||
"camelcase": "off",
|
|
||||||
"no-param-reassign": "off",
|
|
||||||
"react/jsx-props-no-spreading": "off",
|
|
||||||
"react/require-default-props": "off",
|
|
||||||
"react/default-props-match-prop-types": "off",
|
|
||||||
"react/forbid-prop-types": "off",
|
|
||||||
// Permanently disabled
|
|
||||||
"react/jsx-filename-extension": "off",
|
|
||||||
"no-plusplus": "off",
|
|
||||||
"consistent-return": "off",
|
|
||||||
"radix": "off",
|
|
||||||
"no-continue": "off",
|
|
||||||
"react/no-danger": "off",
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"node": {
|
|
||||||
"paths": ["src"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,6 +4,9 @@
|
|||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Python
|
||||||
|
venv/
|
||||||
|
|
||||||
# NodeJS
|
# NodeJS
|
||||||
## Logs
|
## Logs
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
@ -1,43 +1,44 @@
|
|||||||
image: node:8-alpine
|
image: registry.nic.cz/turris/reforis/reforis/reforis-image
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
- publish
|
- publish
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- apt-get update && apt-get install -y make
|
||||||
|
- npm install
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- npm test
|
- make test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- npm run lint
|
- make lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- npm pack
|
- make pack
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- foris-*.tgz
|
- dist/foris-*.tgz
|
||||||
|
|
||||||
publish_beta:
|
publish_beta:
|
||||||
stage: publish
|
stage: publish
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
- dev
|
- dev
|
||||||
script:
|
script:
|
||||||
- sh scripts/publish.sh beta
|
- make publish-beta
|
||||||
|
|
||||||
publish_latest:
|
publish_latest:
|
||||||
stage: publish
|
stage: publish
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
- master
|
- master
|
||||||
script:
|
script:
|
||||||
- sh scripts/publish.sh latest
|
- make publish-latest
|
||||||
|
3
.weblate
Normal file
3
.weblate
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[weblate]
|
||||||
|
url = https://hosted.weblate.org/api/
|
||||||
|
translation = turris/foris-js
|
547
CHANGELOG.md
Normal file
547
CHANGELOG.md
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to
|
||||||
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [6.7.1] - 2025-04-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
|
||||||
|
## [6.7.0] - 2025-03-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added encryption property to guest WiFi settings in tests
|
||||||
|
- Added global fuzzy search and columns visibility to RichTable
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Made thead of RichTable lighter
|
||||||
|
- Updated dependencies in package.json to latest versions
|
||||||
|
- Enhanced ActionButtonWithModal to support dynamic methods
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [6.6.2] - 2025-02-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Enhanced SubmitButton component to accept a custom label prop
|
||||||
|
- Refactored RichTable component to remove forwardRef and simplify data handling
|
||||||
|
|
||||||
|
## [6.6.1] - 2025-02-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored RichTable component to use forwardRef
|
||||||
|
|
||||||
|
## [6.6.0] - 2025-02-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
- Added Wi-Fi and LAN settings URLs to ForisURLs
|
||||||
|
- Added Wi-Fi modes VHT/HE 80+80
|
||||||
|
- Added encryption selection to WiFiGuestForm
|
||||||
|
- Added optional close button to ModalHeader component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated Wi-Fi API
|
||||||
|
- Enhanced NumberInput component with keyboard & touch accessibility
|
||||||
|
- Refactored pagination condition in RichTable component
|
||||||
|
|
||||||
|
## [6.5.0] - 2024-11-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
- Added RichTable component with pagination and sorting
|
||||||
|
- Added @tanstack/react-table v8.20.5 to dependencies
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated documentation
|
||||||
|
- Replaced RebootButton with ActionButtonWithModal component
|
||||||
|
- Fixed import path for CustomizationContextMock in customTestRender.js
|
||||||
|
|
||||||
|
## [6.4.0] - 2024-10-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored Alert component to include dismiss animation and timeout
|
||||||
|
- Refactored ThreeDotsMenu component to include additional props
|
||||||
|
|
||||||
|
## [6.3.0] - 2024-09-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added ThreeDotsMenu component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored EmailInput description
|
||||||
|
- Refactored RadioSet & ignore Radio component
|
||||||
|
- Refactored npm package badge in introduction.md
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [6.2.1] - 2024-09-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored CopyInput component
|
||||||
|
- Refactored ForisURLs to include new URLs for Overview page
|
||||||
|
|
||||||
|
## [6.2.0] - 2024-09-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added useFocusTrap hook
|
||||||
|
- Added extendSession endpoint
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored Spinner.css to use CSS variable for color
|
||||||
|
- Refactored Modal component to use useFocusTrap hook
|
||||||
|
- Refactored Alert component to use useFocusTrap hook
|
||||||
|
|
||||||
|
## [6.1.1] - 2024-08-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated icon color classes to use "text-secondary" instead of "text-dark"
|
||||||
|
- Updated Wi-Fi QRCodeModal component to use new styles & added close button
|
||||||
|
- Refactored WiFiGuestForm component to get rid of obsolete div element
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [6.1.0] - 2024-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrated to Font Awesome v6
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [6.0.3] - 2024-07-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated WiFiQRCode component
|
||||||
|
|
||||||
|
## [6.0.2] - 2024-06-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added className prop to CheckBox and Radio components
|
||||||
|
|
||||||
|
## [6.0.1] - 2024-06-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added className prop to Switch component
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated dependencies in package.json
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [6.0.0] - 2024-06-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added CHANGELOG.md
|
||||||
|
- Added JS_DIR variable to Makefile
|
||||||
|
- Added support for shared reForis ESLint configuration
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated dependencies in package.json
|
||||||
|
- Updated Spinner.css styles for better positioning and responsiveness
|
||||||
|
- Migrated to Bootstrap 5
|
||||||
|
- NPM audit fix
|
||||||
|
- Other small improvements
|
||||||
|
|
||||||
|
## [5.6.1] - 2024-01-19
|
||||||
|
|
||||||
|
- Added & updated Weblate translations
|
||||||
|
- Fixed loading state & button's layout
|
||||||
|
- Updated bootstrap library to version 4.6.2
|
||||||
|
- Used custom reforis-image in GitLab CI/CD
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.6.0] - 2022-12-29
|
||||||
|
|
||||||
|
- Add & update Weblate translations
|
||||||
|
- Add CustomizationContext and custom hook
|
||||||
|
- Update caniuse-lite
|
||||||
|
- Remove testUtils from .gitignore
|
||||||
|
- Make ieee80211w_disabled as optional in WiFiForm
|
||||||
|
- Move contexts in a context folder
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.5.0] - 2022-12-02
|
||||||
|
|
||||||
|
- Add & update translations
|
||||||
|
- Add a switch to disable Management Frame Protection (802.11w)
|
||||||
|
- Improved Foris JS documentation
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.4.1] - 2022-06-03
|
||||||
|
|
||||||
|
- Add Weblate translations
|
||||||
|
- Update PropType peer dependency
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.4.0] - 2022-05-20
|
||||||
|
|
||||||
|
- Add & update translations
|
||||||
|
- Add CopyInput bootstrap component
|
||||||
|
- Update WiFiForm labels and description for wifi ax
|
||||||
|
- Make WS path in lighttpd mode configurable
|
||||||
|
- Fix Wi-Fi password helptext string
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.3.0] - 2022-02-21
|
||||||
|
|
||||||
|
- Added & update translations
|
||||||
|
- Added rest of the props to DownloadButton component
|
||||||
|
- Added hostname validation
|
||||||
|
- Added wifi 802.11ax HE modes
|
||||||
|
- Set best Wi-Fi HT mode depending on the checked frequency
|
||||||
|
- Improved domain name RegEx pattern
|
||||||
|
- Removed customOrder prop in Select component
|
||||||
|
- Fixed Wi-Fi translation strings
|
||||||
|
- Fixed autocomplete attribute in PasswordInput
|
||||||
|
- Fixed WiFi password max length check
|
||||||
|
- Fixed documentation build
|
||||||
|
- Fixed access token in publish script
|
||||||
|
- Refined & restructure Makefile
|
||||||
|
- Updated GitLab CI image to Node.js v16
|
||||||
|
- NPM update (several dependencies)
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.2.0] - 2021-12-15
|
||||||
|
|
||||||
|
- Remove login page
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.16] - 2021-11-18
|
||||||
|
|
||||||
|
- Revert bad NPM audit fix
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.15] - 2021-11-03
|
||||||
|
|
||||||
|
- Add WPA3 option
|
||||||
|
- Add custom order ability of Select options
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.14] - 2021-07-30
|
||||||
|
|
||||||
|
- Add & update translations
|
||||||
|
- Fix infinity redirect loop when WS error occurs
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.13] - 2021-06-30
|
||||||
|
|
||||||
|
- Add sentinelAgreement endpoint to forisUrls
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.12] - 2021-05-14
|
||||||
|
|
||||||
|
- Add & update translations
|
||||||
|
- Add & fix obsolete links
|
||||||
|
- Expend library with the ResetWifiSettings function
|
||||||
|
- Fix switching Wi-Fi modes depending on bands in WiFiForm
|
||||||
|
- Fix translation sources in WiFiForm
|
||||||
|
- NPM audit fix
|
||||||
|
- Other small improvements
|
||||||
|
|
||||||
|
## [5.1.11] - 2021-01-04
|
||||||
|
|
||||||
|
- Remove duplicated file for Norwegian language
|
||||||
|
- Fix translations inconsistency
|
||||||
|
|
||||||
|
## [5.1.10] - 2021-12-29
|
||||||
|
|
||||||
|
- Add and update translations
|
||||||
|
|
||||||
|
## [5.1.9] - 2021-12-20
|
||||||
|
|
||||||
|
- Increase bottom margin of formFieldsSize
|
||||||
|
- Change formFieldsSize of ResetWiFiSettings card
|
||||||
|
- Fix trailing space in Modal classes
|
||||||
|
|
||||||
|
## [5.1.8] - 2020-12-19
|
||||||
|
|
||||||
|
- Add isPluginInstalled function
|
||||||
|
|
||||||
|
## [5.1.7] - 2020-11-27
|
||||||
|
|
||||||
|
## [5.1.6] - 2020-11-25
|
||||||
|
|
||||||
|
- NPM audit fix
|
||||||
|
- Add displayCard function to utils
|
||||||
|
- Add optional sizes to Modal
|
||||||
|
- Add information about optional sizes to docs
|
||||||
|
- Remove redundant merge.py
|
||||||
|
|
||||||
|
## [5.1.5] - 2020-09-25
|
||||||
|
|
||||||
|
- Fix DateTime import
|
||||||
|
- Fix extra empty space in Switch's classes
|
||||||
|
|
||||||
|
## [5.1.4] - 2020-09-25
|
||||||
|
|
||||||
|
- Add inline option to Wi-Fi's RadioSet
|
||||||
|
- Fix Alert's dismissible class condition
|
||||||
|
- Add closing bootstrap modal using ESC
|
||||||
|
- Change reboot modal's heading to "Warning!"
|
||||||
|
|
||||||
|
## [5.1.3] - 2020-09-11
|
||||||
|
|
||||||
|
- Add SSID validation for 32 bytes length
|
||||||
|
- Add helpText for SSID input
|
||||||
|
|
||||||
|
## [5.1.2] - 2020-09-08
|
||||||
|
|
||||||
|
- Fix infinity loop caused by WebSockets
|
||||||
|
- Resolve small issues
|
||||||
|
|
||||||
|
## [5.1.1] - 2020-08-31
|
||||||
|
|
||||||
|
- Add "inline" option to RadioSet
|
||||||
|
- NPM audit fix
|
||||||
|
|
||||||
|
## [5.1.0] - 2020-08-25
|
||||||
|
|
||||||
|
- Add new Switch component
|
||||||
|
- Swap checkboxes for switches on Wi-Fi page
|
||||||
|
- Decrease button width on different breakpoints
|
||||||
|
- Add integration of Prettier + ESLint + reForis Style Guide
|
||||||
|
- Add appropriate links to dropdown headers
|
||||||
|
- Add semantic & accessibility structure for headings
|
||||||
|
- NPM audit & Update packages
|
||||||
|
- GitLab CI: image update to node 10
|
||||||
|
|
||||||
|
## [5.0.3] - 2020-09-23
|
||||||
|
|
||||||
|
- Fixes issue with WebSockets
|
||||||
|
|
||||||
|
## [5.0.2] - 2020-09-22
|
||||||
|
|
||||||
|
- Fix infinity loop caused by WebSockets
|
||||||
|
|
||||||
|
## [5.0.1] - 2020-07-21
|
||||||
|
|
||||||
|
- Fix Wi-Fi Form
|
||||||
|
- NPM audit fix & update of packages
|
||||||
|
|
||||||
|
## [5.0.0] - 2020-05-07
|
||||||
|
|
||||||
|
- I've realized that it should be major update due to broken API.
|
||||||
|
|
||||||
|
## [4.5.1] - 2020-05-07
|
||||||
|
|
||||||
|
- Add initialData to ForisForm children.
|
||||||
|
- Update translations .pot file.
|
||||||
|
|
||||||
|
## [4.5.0] - 2020-03-25
|
||||||
|
|
||||||
|
- Use exposed pdfmake.
|
||||||
|
- NPM audit fix & update of packages.
|
||||||
|
|
||||||
|
## [4.4.0] - 2020-03-13
|
||||||
|
|
||||||
|
- Update domain validation.
|
||||||
|
|
||||||
|
## [4.3.1] - 2020-03-06
|
||||||
|
|
||||||
|
- Add logout link.
|
||||||
|
|
||||||
|
## [4.3.0] - 2020-02-26
|
||||||
|
|
||||||
|
- Allow RadioSet accept elements as children.
|
||||||
|
- Add option to make modal scrollable.
|
||||||
|
|
||||||
|
## [4.2.0] - 2020-02-21
|
||||||
|
|
||||||
|
- Add translations.
|
||||||
|
- Improve datatime localization.
|
||||||
|
|
||||||
|
## [4.1.0] - 2020-02-20
|
||||||
|
|
||||||
|
- Added date and time utilities.
|
||||||
|
|
||||||
|
## [4.0.0] - 2020-02-20
|
||||||
|
|
||||||
|
- Throw an error if unhandled exception happens during API request.
|
||||||
|
|
||||||
|
## [3.4.0] - 2020-02-17
|
||||||
|
|
||||||
|
- Display actual GET error response within the form.
|
||||||
|
- Added styles extracted from reForis.
|
||||||
|
- Added reference to form element (for programmatically submitting it).
|
||||||
|
|
||||||
|
## [3.2.0] - 2020-01-17
|
||||||
|
|
||||||
|
- Swapped react-router with react-router-dom. Prepared Foris JS for using
|
||||||
|
react-router-dom exposed by reForis.
|
||||||
|
- Added controller ID filter to WebSocket hook.
|
||||||
|
- Updated translation messages after moving WiFi form.
|
||||||
|
- Increased request timeout to 30.5 sec.
|
||||||
|
|
||||||
|
## [3.1.1] - 2020-01-10
|
||||||
|
|
||||||
|
- Fixed package dependencies related to exposing libraries via reForis
|
||||||
|
|
||||||
|
## [3.1.0] - 2020-01-09
|
||||||
|
|
||||||
|
- Added Wi-Fi settings form
|
||||||
|
- Fixed path to index.js file in package.json
|
||||||
|
|
||||||
|
## [3.0.0] - 2020-01-07
|
||||||
|
|
||||||
|
- Removal of Babel compiler
|
||||||
|
- Fixed width of ForisForm, removed default sizing for form widgets (like
|
||||||
|
buttons)
|
||||||
|
|
||||||
|
## [2.1.1] - 2020-01-06
|
||||||
|
|
||||||
|
- Display date and time picker above input element
|
||||||
|
|
||||||
|
## [2.1.0] - 2019-12-19
|
||||||
|
|
||||||
|
- Set WebSocket logging to debug level
|
||||||
|
- Added hook that detects clicking outside of component
|
||||||
|
- Added Radio to list of publicly available components
|
||||||
|
- Fixed link to git repository in package.json
|
||||||
|
|
||||||
|
## [2.0.0] - 2019-12-09
|
||||||
|
|
||||||
|
- Added dynamic suffix for API URLs (allowing to use one hook for different
|
||||||
|
resources with e.g. PUT)
|
||||||
|
- Added unsubscribe method to WebSocket client
|
||||||
|
- Added custom class to SpinnerElement
|
||||||
|
- Improved documentation
|
||||||
|
- Published README.md
|
||||||
|
|
||||||
|
## [1.4.0] - 2019-11-29
|
||||||
|
|
||||||
|
- Add reboot button.
|
||||||
|
- Fix Foris URLs prefixes
|
||||||
|
|
||||||
|
## [1.3.3] - 2019-11-22
|
||||||
|
|
||||||
|
- Add translations from Weblate.
|
||||||
|
|
||||||
|
## [1.3.2] - 2019-11-20
|
||||||
|
|
||||||
|
- Expose only AlertContext.
|
||||||
|
- Add hook for API pooling.
|
||||||
|
|
||||||
|
## [1.3.1] - 2019-11-14
|
||||||
|
|
||||||
|
## [1.2.0] - 2019-10-24
|
||||||
|
|
||||||
|
## [1.1.0] - 2019-10-22
|
||||||
|
|
||||||
|
## [1.0.0] - 2019-10-07
|
||||||
|
|
||||||
|
## [0.0.7] - 2019-09-02
|
||||||
|
|
||||||
|
[unreleased]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.7.1...dev
|
||||||
|
[6.7.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.7.0...v6.7.1
|
||||||
|
[6.7.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.2...v6.7.0
|
||||||
|
[6.6.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.1...v6.6.2
|
||||||
|
[6.6.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.6.0...v6.6.1
|
||||||
|
[6.6.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.5.0...v6.6.0
|
||||||
|
[6.5.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.4.0...v6.5.0
|
||||||
|
[6.4.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.3.0...v6.4.0
|
||||||
|
[6.3.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.2.1...v6.3.0
|
||||||
|
[6.2.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.2.0...v6.2.1
|
||||||
|
[6.2.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.1.1...v6.2.0
|
||||||
|
[6.1.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.1.0...v6.1.1
|
||||||
|
[6.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.0.3...v6.1.0
|
||||||
|
[6.0.3]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.0.2...v6.0.3
|
||||||
|
[6.0.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.0.1...v6.0.2
|
||||||
|
[6.0.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v6.0.0...v6.0.1
|
||||||
|
[6.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.6.1...v6.0.0
|
||||||
|
[5.6.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.6.0...v5.6.1
|
||||||
|
[5.6.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.5.0...v5.6.0
|
||||||
|
[5.5.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.4.1...v5.5.0
|
||||||
|
[5.4.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.4.0...v5.4.1
|
||||||
|
[5.4.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.3.0...v5.4.0
|
||||||
|
[5.3.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.2.0...v5.3.0
|
||||||
|
[5.2.0]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.16...v5.2.0
|
||||||
|
[5.1.16]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.15...v5.1.16
|
||||||
|
[5.1.15]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.14...v5.1.15
|
||||||
|
[5.1.14]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.13...v5.1.14
|
||||||
|
[5.1.13]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.12...v5.1.13
|
||||||
|
[5.1.12]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.11...v5.1.12
|
||||||
|
[5.1.11]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.10...v5.1.11
|
||||||
|
[5.1.10]:
|
||||||
|
https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.9...v5.1.10
|
||||||
|
[5.1.9]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.8...v5.1.9
|
||||||
|
[5.1.8]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.7...v5.1.8
|
||||||
|
[5.1.7]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.6...v5.1.7
|
||||||
|
[5.1.6]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.5...v5.1.6
|
||||||
|
[5.1.5]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.4...v5.1.5
|
||||||
|
[5.1.4]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.3...v5.1.4
|
||||||
|
[5.1.3]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.2...v5.1.3
|
||||||
|
[5.1.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.1...v5.1.2
|
||||||
|
[5.1.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.1.0...v5.1.1
|
||||||
|
[5.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.0.3...v5.1.0
|
||||||
|
[5.0.3]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.0.2...v5.0.3
|
||||||
|
[5.0.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.0.1...v5.0.2
|
||||||
|
[5.0.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v5.5.0...v5.0.1
|
||||||
|
[5.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.5.1...v5.0.0
|
||||||
|
[4.5.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.5.0...v4.5.1
|
||||||
|
[4.5.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.4.0...v4.5.0
|
||||||
|
[4.4.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.3.1...v4.4.0
|
||||||
|
[4.3.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.3.0...v4.3.1
|
||||||
|
[4.3.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.2.0...v4.3.0
|
||||||
|
[4.2.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.1.0...v4.2.0
|
||||||
|
[4.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v4.0.0...v4.1.0
|
||||||
|
[4.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v3.4.0...v4.0.0
|
||||||
|
[3.4.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v3.2.0...v3.4.0
|
||||||
|
[3.2.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v3.1.1...v3.2.0
|
||||||
|
[3.1.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v3.1.0...v3.1.1
|
||||||
|
[3.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v3.0.0...v3.1.0
|
||||||
|
[3.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v2.1.1...v3.0.0
|
||||||
|
[2.1.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v2.1.0...v2.1.1
|
||||||
|
[2.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v2.0.0...v2.1.0
|
||||||
|
[2.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.4.0...v2.0.0
|
||||||
|
[1.4.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.3.3...v1.4.0
|
||||||
|
[1.3.3]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.3.2...v1.3.3
|
||||||
|
[1.3.2]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.3.1...v1.3.2
|
||||||
|
[1.3.1]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.2.0...v1.3.1
|
||||||
|
[1.2.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.1.0...v1.2.0
|
||||||
|
[1.1.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v1.0.0...v1.1.0
|
||||||
|
[1.0.0]: https://gitlab.nic.cz/turris/reforis/foris-js/-/compare/v0.0.7...v1.0.0
|
||||||
|
[0.0.7]: https://gitlab.nic.cz/turris/reforis/foris-js/-/tags/v0.0.7
|
115
Makefile
115
Makefile
@ -1,16 +1,31 @@
|
|||||||
.PHONY: all install-js watch-js build-js lint-js test-js create-messages update-messages docs clean
|
# Copyright (C) 2019-2022 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
#
|
||||||
|
# This is free software, licensed under the GNU General Public License v3.
|
||||||
|
# See /LICENSE for more information.
|
||||||
|
|
||||||
|
PROJECT="Foris JS"
|
||||||
|
# Retrieve Foris JS version from package.json
|
||||||
|
VERSION= $(shell sed -En "s/.*version['\"]: ['\"](.+)['\"].*/\1/p" package.json)
|
||||||
|
COPYRIGHT_HOLDER="CZ.NIC, z.s.p.o. (https://www.nic.cz/)"
|
||||||
|
MSGID_BUGS_ADDRESS="tech.support@turris.cz"
|
||||||
|
|
||||||
|
DEV_PYTHON=python3
|
||||||
|
VENV_NAME?=venv
|
||||||
|
JS_DIR=js
|
||||||
|
VENV_BIN=$(shell pwd)/$(VENV_NAME)/bin
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
all:
|
all:
|
||||||
@echo "make install-js"
|
@echo "make install-js"
|
||||||
@echo " Install dependencies"
|
@echo " Install npm dependencies."
|
||||||
@echo "make watch-js"
|
@echo "make lint"
|
||||||
@echo " Compile JS in watch mode."
|
@echo " Run linter on the project."
|
||||||
@echo "make build-js"
|
@echo "make test"
|
||||||
@echo " Compile JS."
|
@echo " Run tests on the project."
|
||||||
@echo "make lint-js"
|
@echo "make test-js-watch"
|
||||||
@echo " Run linter"
|
@echo " Run tests on the project in watch mode."
|
||||||
@echo "make test-js"
|
@echo "make test-js-update-snapshots"
|
||||||
@echo " Run tests"
|
@echo " Update snapshots."
|
||||||
@echo "make create-messages"
|
@echo "make create-messages"
|
||||||
@echo " Create locale messages (.pot)."
|
@echo " Create locale messages (.pot)."
|
||||||
@echo "make update-messages"
|
@echo "make update-messages"
|
||||||
@ -22,29 +37,93 @@ all:
|
|||||||
@echo "make clean"
|
@echo "make clean"
|
||||||
@echo " Remove python artifacts and virtualenv."
|
@echo " Remove python artifacts and virtualenv."
|
||||||
|
|
||||||
|
|
||||||
|
# Preparation
|
||||||
|
|
||||||
|
.PHONY: venv
|
||||||
|
venv: $(VENV_NAME)/bin/activate
|
||||||
|
$(VENV_NAME)/bin/activate:
|
||||||
|
test -d $(VENV_NAME) || $(DEV_PYTHON) -m virtualenv -p $(DEV_PYTHON) $(VENV_NAME)
|
||||||
|
$(VENV_BIN)/$(DEV_PYTHON) -m pip install -r requirements.txt
|
||||||
|
touch $(VENV_NAME)/bin/activate
|
||||||
|
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
.PHONY: install-js
|
||||||
install-js: package.json
|
install-js: package.json
|
||||||
npm install --save-dev
|
npm install --save-dev
|
||||||
|
|
||||||
watch-js:
|
|
||||||
npm run build:watch
|
|
||||||
build-js:
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
|
# Publishing
|
||||||
|
|
||||||
|
.PHONY: collect-files
|
||||||
|
collect-files:
|
||||||
|
sh scripts/collect_files.sh
|
||||||
|
|
||||||
|
.PHONY: pack
|
||||||
|
pack: collect-files
|
||||||
|
cd dist && npm pack
|
||||||
|
|
||||||
|
.PHONY: publish-beta
|
||||||
|
publish-beta: collect-files
|
||||||
|
sh scripts/publish.sh beta
|
||||||
|
|
||||||
|
.PHONY: publish-latest
|
||||||
|
publish-latest: collect-files
|
||||||
|
sh scripts/publish.sh latest
|
||||||
|
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
npm run lint
|
npm run lint
|
||||||
|
|
||||||
|
.PHONY: lint-js-fix
|
||||||
|
lint-js-fix:
|
||||||
|
npm run lint:fix
|
||||||
|
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
create-messages:
|
.PHONY: test-js-watch
|
||||||
pybabel extract -F babel.cfg -o ./translations/forisjs.pot .
|
test-js-watch:
|
||||||
update-messages:
|
cd $(JS_DIR); npm test -- --watch
|
||||||
pybabel update -i translations/forisjs.pot -d translations
|
|
||||||
|
|
||||||
|
.PHONY: test-js-update-snapshots
|
||||||
|
test-js-update-snapshots:
|
||||||
|
npm test -- -u
|
||||||
|
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
.PHONY: create-messages
|
||||||
|
create-messages: venv
|
||||||
|
$(VENV_BIN)/pybabel extract -F babel.cfg -o ./translations/forisjs.pot . --project=$(PROJECT) --version=$(VERSION) --copyright-holder=$(COPYRIGHT_HOLDER) --msgid-bugs-address=$(MSGID_BUGS_ADDRESS)
|
||||||
|
|
||||||
|
.PHONY: update-messages
|
||||||
|
update-messages: venv
|
||||||
|
$(VENV_BIN)/pybabel update -i ./translations/forisjs.pot -d ./translations -D forisjs --update-header-comment
|
||||||
|
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
|
||||||
|
.PHONY: docs
|
||||||
docs:
|
docs:
|
||||||
npm run-script docs
|
npm run-script docs
|
||||||
|
|
||||||
|
.PHONY: docs-watch
|
||||||
docs-watch:
|
docs-watch:
|
||||||
npm run-script docs:watch
|
npm run-script docs:watch
|
||||||
|
|
||||||
|
|
||||||
|
# Other
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf node_modules dist
|
rm -rf node_modules dist
|
||||||
|
41
README.md
41
README.md
@ -1,17 +1,52 @@
|
|||||||
# foris-js
|
# foris-js
|
||||||
|
|
||||||
|
Set of utils and common React elements for reForis.
|
||||||
|
|
||||||
## Publishing package
|
## Publishing package
|
||||||
|
|
||||||
### Beta versions
|
### Beta versions
|
||||||
|
|
||||||
Each commit to `dev` branch will result in publishing a new version of library
|
Each commit to `dev` branch will result in publishing a new version of library
|
||||||
tagged `beta`. Versions names are based on commit SHA, e.g.
|
tagged `beta`. Versions names are based on commit SHA, e.g.
|
||||||
`foris@0.1.0-d9073aa4.0`.
|
`foris@0.1.0-beta.d9073aa4`.
|
||||||
|
|
||||||
### Preparing a release
|
### Preparing a release
|
||||||
|
|
||||||
1. Crete a merge request to `dev` branch with version bumped
|
1. Crete a merge request to `dev` branch with version bumped
|
||||||
2. When merging add `[skip ci]` to commit message to prevent publishing
|
2. When merging add `[skip ci]` to commit message to prevent publishing
|
||||||
unnecessary version
|
unnecessary version
|
||||||
3. Create a merge request from `dev` to `master` branch
|
3. Create a merge request from `dev` to `master` branch
|
||||||
4. New version should be published automatically
|
4. New version should be published automatically
|
||||||
|
|
||||||
|
## Manually managed dependencies
|
||||||
|
|
||||||
|
Because of `<ForisForm />` component it's required to use exposed
|
||||||
|
`ReactRouterDOM` object from `react-router-dom` library. `ReactRouterDOM` is
|
||||||
|
exposed by
|
||||||
|
[reForis](https://gitlab.labs.nic.cz/turris/reforis/reforis/blob/master/js/webpack.config.js).
|
||||||
|
It can be done by following steps:
|
||||||
|
|
||||||
|
1. Setting `react-router-dom` as `peerDependencies` and `devDependencies` in
|
||||||
|
`package.json`.
|
||||||
|
2. Adding the following rules to `externals` in `webpack.conf.js` of the plugin:
|
||||||
|
|
||||||
|
```js
|
||||||
|
externals: {
|
||||||
|
...
|
||||||
|
"react-router-dom": "ReactRouterDOM",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
Build or watch docs to get more info about library:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docs
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docs-watch
|
||||||
|
```
|
||||||
|
@ -1,17 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ["@babel/preset-env", "@babel/preset-react"],
|
||||||
"@babel/preset-env",
|
plugins: ["@babel/plugin-transform-runtime"],
|
||||||
"@babel/preset-react",
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
"@babel/plugin-syntax-export-default-from",
|
|
||||||
["module-resolver", {
|
|
||||||
root: ["./src"],
|
|
||||||
alias: {
|
|
||||||
test: "./test",
|
|
||||||
underscore: "lodash",
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
36
docs/components/Logo.js
Normal file
36
docs/components/Logo.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import Styled from "rsg-components/Styled";
|
||||||
|
import logo from "./logo.svg";
|
||||||
|
|
||||||
|
const styles = ({ fontFamily }) => ({
|
||||||
|
logo: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: fontFamily.base,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "normal",
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
height: "1.3em",
|
||||||
|
marginLeft: "-0.2em",
|
||||||
|
marginRight: "0.2em",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function LogoRenderer({ classes, children }) {
|
||||||
|
return (
|
||||||
|
<h1 className={classes.logo}>
|
||||||
|
<img className={classes.image} src={logo} alt="React logo" />
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoRenderer.propTypes = {
|
||||||
|
classes: PropTypes.object.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Styled(styles)(LogoRenderer);
|
3
docs/components/logo.svg
Normal file
3
docs/components/logo.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000">
|
||||||
|
<path d="M288.258 240.0394L717.5586-.44c.803 62.277-1.8207 124.502-1.4996 186.7266 1.8208 7.6343-7.2288 10.1966-12.102 13.4908L286.4375 432.1518l1.8206-192.1124zm2.284 277.645L711 278.3176l-.8416 192.7742L457.357 614.514l-1.842 289.03-167.7097 95.8926 2.7365-481.753z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 349 B |
25
docs/development.md
Normal file
25
docs/development.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
Sooner or later, you will face with situation when you want/need to make some
|
||||||
|
changes in the library. Then the most important tool for you it's the
|
||||||
|
[`npm link`](https://docs.npmjs.com/cli/link).
|
||||||
|
|
||||||
|
Please, notice that it will not work if you link the library just from the root
|
||||||
|
of the repo. It happens due to the location of sources `./src`. You need to pack
|
||||||
|
the library first, `make pack` and then link it from the `./dist` directory.
|
||||||
|
|
||||||
|
Yeah, it's not such a comfortable solution for development. But it can be fixed
|
||||||
|
by writing a small script similar to making a pack but by linking every file and
|
||||||
|
directory from `./src` to the same directory and linking then from it. Notice
|
||||||
|
that you need to link a `package.json` and a `package-lock.json` as well.
|
||||||
|
|
||||||
|
So step by step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make pack;
|
||||||
|
cd dist;
|
||||||
|
npm link;
|
||||||
|
|
||||||
|
cd $project_dir/js # Navigate to JS directory of the project where you want to link the library
|
||||||
|
npm link foris
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it ;)
|
4
docs/forisjs-npm.svg
Normal file
4
docs/forisjs-npm.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="white">
|
||||||
|
<path d="M49.5 171.722222222222h400v133.333333333333h-200v22.222222222223h-88.888888888889v-22.222222222223H49.5V171.722222222222zm22.222222222222 111.111111111111h44.444444444445v-66.666666666667h22.222222222222v66.666666666667h22.222222222222v-88.888888888889H71.722222222222v88.888888888889zm111.111111111111-88.888888888889v111.111111111111h44.444444444445v-22.222222222222h44.444444444444v-88.888888888889h-88.888888888889zm44.444444444445 22.222222222222H249.5v44.444444444445h-22.222222222222v-44.444444444445zm66.666666666666-22.222222222222v88.888888888889h44.444444444445v-66.666666666667h22.222222222222v66.666666666667h22.222222222222v-66.666666666667h22.222222222222v66.666666666667h22.222222222223v-88.888888888889H293.944444444444z" fill="#cb3837" />
|
||||||
|
<path d="M71.722222222222 282.833333333333h44.444444444444v-66.666666666667h22.222222222223v66.666666666667h22.222222222222v-88.888888888889H71.722222222222zm111.111111111111-88.888888888889v111.111111111111h44.444444444444v-22.222222222222h44.444444444445v-88.888888888889h-88.888888888889zM249.5 260.611111111111h-22.222222222223v-44.444444444445H249.5v44.444444444445zm44.444444444444-66.666666666667v88.888888888889h44.444444444444v-66.666666666667h22.222222222223v66.666666666667h22.222222222222v-66.666666666667h22.222222222222v66.666666666667h22.222222222222v-88.888888888889z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -1 +0,0 @@
|
|||||||
Foris JS library is set of componets and utils for Foris JS application and plugins.
|
|
36
docs/introduction.md
Normal file
36
docs/introduction.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
Welcome! This is the official documentation for Foris JS.
|
||||||
|
|
||||||
|
## What Foris JS is
|
||||||
|
|
||||||
|
Foris JS library is a set of components and utils for reForis application and
|
||||||
|
plugins.
|
||||||
|
|
||||||
|
Please notice that all of these components or utils are used in reForis and
|
||||||
|
plugins. If you want to study them by example, I recommend you to full-text
|
||||||
|
search those repositories.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Please make sure that [Node.js](https://nodejs.org/en/) is installed on your
|
||||||
|
system.
|
||||||
|
|
||||||
|
The current Long Term Support (LTS) release is an ideal starting point, see
|
||||||
|
[here](https://github.com/nodejs/Release#release-schedule).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install the latest release:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
npm install foris
|
||||||
|
```
|
||||||
|
|
||||||
|
To install a specific version:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
npm install foris@version
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://badge.fury.io/js/foris)
|
@ -12,20 +12,18 @@ module.exports = {
|
|||||||
"<rootDir>/src/testUtils",
|
"<rootDir>/src/testUtils",
|
||||||
"<rootDir>/src/",
|
"<rootDir>/src/",
|
||||||
],
|
],
|
||||||
|
moduleNameMapper: {
|
||||||
|
"\\.(css|less)$": "<rootDir>/src/__mocks__/styleMock.js",
|
||||||
|
},
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
collectCoverageFrom: ["src/**/*.{js,jsx}"],
|
collectCoverageFrom: ["src/**/*.{js,jsx}"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
testPathIgnorePatterns: ["/node_modules/", "/__fixtures__/"],
|
testPathIgnorePatterns: ["/node_modules/", "/__fixtures__/", "/dist/"],
|
||||||
|
testEnvironment: "jsdom",
|
||||||
verbose: false,
|
verbose: false,
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: ["<rootDir>/src/testUtils/setup"],
|
||||||
"@testing-library/react/cleanup-after-each",
|
|
||||||
"<rootDir>/src/testUtils/setup",
|
|
||||||
],
|
|
||||||
globals: {
|
globals: {
|
||||||
TZ: "utc",
|
TZ: "utc",
|
||||||
},
|
},
|
||||||
transform: {
|
transformIgnorePatterns: ["node_modules/(?!(react-datetime)/)"],
|
||||||
"^.+\\.js$": "babel-jest",
|
|
||||||
"^.+\\.css$": "jest-transform-css",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
44958
package-lock.json
generated
44958
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
156
package.json
156
package.json
@ -1,84 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "foris",
|
"name": "foris",
|
||||||
"version": "1.0.0",
|
"version": "6.7.1",
|
||||||
"description": "Set of components and utils for Foris and its plugins.",
|
"description": "Foris JS library is a set of components and utils for reForis application and plugins.",
|
||||||
"author": "CZ.NIC, z.s.p.o.",
|
"author": "CZ.NIC, z.s.p.o.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitlab.labs.nic.cz/turris/reforis/forisjs.git"
|
"url": "https://gitlab.nic.cz/turris/reforis/foris-js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"foris",
|
"foris",
|
||||||
"reforis"
|
"reforis"
|
||||||
],
|
],
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"main": "./dist/index.js",
|
"main": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"immutability-helper": "^3.0.0",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"jest-transform-css": "^2.0.0",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"moment": "^2.24.0",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"moment-timezone": "^0.5.25",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"prop-types": "^15.7.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"react-datetime": "^2.16.3",
|
"axios": "^1.7.9",
|
||||||
"react-router": "^5.0.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"react-uid": "^2.2.0"
|
"moment": "^2.30.1",
|
||||||
},
|
"qrcode.react": "^4.2.0",
|
||||||
"peerDependencies": {
|
"react-datetime": "^3.3.1",
|
||||||
"react": "^16.9.0",
|
"react-uid": "^2.4.0"
|
||||||
"react-dom": "^16.9.0"
|
},
|
||||||
},
|
"peerDependencies": {
|
||||||
"devDependencies": {
|
"bootstrap": "^5.3.3",
|
||||||
"@babel/cli": "^7.4.4",
|
"prop-types": "15.8.1",
|
||||||
"@babel/core": "^7.4.5",
|
"react": "16.9.0",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
"react-dom": "16.9.0",
|
||||||
"@babel/plugin-syntax-export-default-from": "^7.2.0",
|
"react-router-dom": "^5.1.2"
|
||||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
},
|
||||||
"@babel/preset-env": "^7.4.5",
|
"devDependencies": {
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/cli": "^7.26.4",
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@babel/core": "^7.26.9",
|
||||||
"@testing-library/react": "^8.0.9",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
"babel-eslint": "^9.0.0",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"babel-jest": "^24.8.0",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"babel-loader": "^8.0.6",
|
"@testing-library/react": "^12.1.5",
|
||||||
"babel-plugin-module-resolver": "^3.2.0",
|
"babel-loader": "^9.2.1",
|
||||||
"babel-plugin-react-transform": "^3.0.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap": "^4.3.1",
|
"css-loader": "^7.1.2",
|
||||||
"copy-webpack-plugin": "^5.0.4",
|
"eslint": "^8.57.0",
|
||||||
"css-loader": "^3.2.0",
|
"eslint-config-reforis": "^2.2.1",
|
||||||
"eslint": "^6.1.0",
|
"file-loader": "^6.0.0",
|
||||||
"eslint-config-airbnb": "^18.0.1",
|
"jest": "^29.7.0",
|
||||||
"eslint-plugin-import": "^2.18.2",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"jest-mock-axios": "^4.8.0",
|
||||||
"eslint-plugin-react": "^7.14.3",
|
"moment-timezone": "^0.5.47",
|
||||||
"eslint-plugin-react-hooks": "^1.7.0",
|
"prettier": "^3.5.3",
|
||||||
"file-loader": "^4.2.0",
|
"prop-types": "15.8.1",
|
||||||
"jest": "^24.8.0",
|
"react": "16.9.0",
|
||||||
"jest-mock-axios": "^3.0.0",
|
"react-dom": "16.9.0",
|
||||||
"moment": "^2.24.0",
|
"react-router-dom": "^5.1.2",
|
||||||
"moment-timezone": "^0.5.25",
|
"react-styleguidist": "^12.0.1",
|
||||||
"react": "^16.9.0",
|
"snapshot-diff": "^0.10.0",
|
||||||
"react-dom": "^16.9.0",
|
"style-loader": "^4.0.0",
|
||||||
"react-styleguidist": "^9.1.16",
|
"webpack": "^5.98.0"
|
||||||
"snapshot-diff": "^0.5.1",
|
},
|
||||||
"style-loader": "^1.0.0",
|
"scripts": {
|
||||||
"webpack": "^4.41.0"
|
"lint": "eslint src",
|
||||||
},
|
"lint:fix": "eslint --fix src",
|
||||||
"scripts": {
|
"test": "jest",
|
||||||
"build": "rm -rf dist; babel src --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files",
|
"test:watch": "jest --watch",
|
||||||
"build:watch": "babel src --verbose --watch --out-dir dist --ignore '**/__tests__' --source-maps inline --copy-files",
|
"test:coverage": "jest --coverage --colors",
|
||||||
"prepare": "rm -rf ./dist && npm run build",
|
"docs": "npx styleguidist build ",
|
||||||
"lint": "eslint src",
|
"docs:watch": "styleguidist server"
|
||||||
"test": "jest",
|
}
|
||||||
"test:watch": "jest --watch",
|
}
|
||||||
"test:coverage": "jest --coverage --colors",
|
|
||||||
"test:update-snapshots": "jest -u",
|
|
||||||
"docs": "npx styleguidist build ",
|
|
||||||
"docs:watch": "styleguidist server"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**",
|
|
||||||
"translations"
|
|
||||||
]
|
|
||||||
}
|
|
1
prettier.config.js
Normal file
1
prettier.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("eslint-config-reforis/prettier.config");
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Babel
|
13
scripts/collect_files.sh
Normal file
13
scripts/collect_files.sh
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Collect files
|
||||||
|
mkdir -p dist
|
||||||
|
cp -rf ./src/* dist
|
||||||
|
cp package.json package-lock.json README.md dist
|
||||||
|
sed -i 's/\/src//g' dist/package.json # remove ./src from main js file path
|
||||||
|
|
||||||
|
cp -rf translations dist
|
||||||
|
|
||||||
|
# Remove unwanted files
|
||||||
|
find dist -type d -name __tests__ -exec rm -r {} +
|
||||||
|
rm -rf dist/__mocks__
|
@ -5,12 +5,13 @@ then
|
|||||||
echo "\$NPM_TOKEN is not set"
|
echo "\$NPM_TOKEN is not set"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
# Need to replace "_" with "_" as GitLab CI won't accept secret vars with "-"
|
cd dist
|
||||||
echo "//registry.npmjs.org/:_authToken=$(echo $NPM_TOKEN | tr _ -)" > .npmrc
|
echo "//registry.npmjs.org/:_authToken=$(echo "$NPM_TOKEN")" > .npmrc
|
||||||
echo "unsafe-perm = true" >> ~/.npmrc
|
echo "unsafe-perm = true" >> ~/.npmrc
|
||||||
if test "$1" = "beta"
|
if test "$1" = "beta"
|
||||||
then
|
then
|
||||||
npm version prerelease --preid=$CI_COMMIT_SHORT_SHA --git-tag-version false
|
BETA_VERSION=$(npx -c 'echo "$npm_package_version"')-beta.$CI_COMMIT_SHORT_SHA
|
||||||
|
npm version "$BETA_VERSION" --git-tag-version false
|
||||||
npm publish --tag beta
|
npm publish --tag beta
|
||||||
elif test "$1" = "latest"
|
elif test "$1" = "latest"
|
||||||
then
|
then
|
||||||
|
8
src/__mocks__/styleMock.js
Normal file
8
src/__mocks__/styleMock.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {};
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
|
||||||
*
|
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
|
||||||
* See /LICENSE for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { Alert } from "bootstrap/Alert";
|
|
||||||
|
|
||||||
const AlertContext = React.createContext();
|
|
||||||
|
|
||||||
AlertContextProvider.propTypes = {
|
|
||||||
children: PropTypes.oneOfType([
|
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
|
||||||
PropTypes.node,
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
function AlertContextProvider({ children }) {
|
|
||||||
const [alert, setAlert] = useState(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{alert && <Alert type="danger" message={alert} onDismiss={() => setAlert(null)} />}
|
|
||||||
<AlertContext.Provider value={setAlert}>
|
|
||||||
{ children }
|
|
||||||
</AlertContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AlertContext, AlertContextProvider };
|
|
@ -1,28 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AlertContext should render alert 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="alert alert-dismissible alert-danger"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="close"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
Alert content
|
|
||||||
</div>
|
|
||||||
<button>
|
|
||||||
Set alert
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AlertContext should render component without alert 1`] = `
|
|
||||||
<div>
|
|
||||||
<button>
|
|
||||||
Set alert
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
|
||||||
*
|
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
|
||||||
* See /LICENSE for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useReducer, useCallback } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import {
|
|
||||||
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
export function useAPIDelete(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIReducer, {
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestDelete = useCallback(async () => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
await axios.delete(url, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
headers: HEADERS,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: getErrorMessage(error),
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return [state, requestDelete];
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
|
||||||
*
|
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
|
||||||
* See /LICENSE for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useReducer, useCallback } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { ForisURLs } from "forisUrls";
|
|
||||||
import { API_ACTIONS, TIMEOUT } from "./utils";
|
|
||||||
|
|
||||||
const APIGetReducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case API_ACTIONS.INIT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: true,
|
|
||||||
isError: false,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.FAILURE:
|
|
||||||
if (action.status === 403) window.location.assign(ForisURLs.login);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: false,
|
|
||||||
isError: true,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useAPIGet(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIGetReducer, {
|
|
||||||
isLoading: false,
|
|
||||||
isError: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
const get = useCallback(async () => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
const result = await axios.get(url, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: error.response.data,
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return [state, get];
|
|
||||||
}
|
|
143
src/api/hooks.js
Normal file
143
src/api/hooks.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2021 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
API_ACTIONS,
|
||||||
|
API_METHODS,
|
||||||
|
API_STATE,
|
||||||
|
getErrorPayload,
|
||||||
|
HEADERS,
|
||||||
|
TIMEOUT,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
const DATA_METHODS = ["POST", "PATCH", "PUT"];
|
||||||
|
|
||||||
|
function createAPIHook(method) {
|
||||||
|
return (urlRoot, contentType) => {
|
||||||
|
const [state, dispatch] = useReducer(APIReducer, {
|
||||||
|
state: API_STATE.INIT,
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendRequest = useCallback(
|
||||||
|
async ({ data, suffix } = {}) => {
|
||||||
|
const headers = { ...HEADERS };
|
||||||
|
if (contentType) {
|
||||||
|
headers["Content-Type"] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: API_ACTIONS.INIT });
|
||||||
|
try {
|
||||||
|
// Prepare request
|
||||||
|
const request = API_METHODS[method];
|
||||||
|
const config = {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
const url = suffix ? `${urlRoot}/${suffix}` : urlRoot;
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
let result;
|
||||||
|
if (DATA_METHODS.includes(method)) {
|
||||||
|
result = await request(url, data, config);
|
||||||
|
} else {
|
||||||
|
result = await request(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process request result
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.SUCCESS,
|
||||||
|
payload: result.data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorPayload = getErrorPayload(error);
|
||||||
|
dispatch({
|
||||||
|
type: API_ACTIONS.FAILURE,
|
||||||
|
status: error.response && error.response.status,
|
||||||
|
payload: errorPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[urlRoot, contentType]
|
||||||
|
);
|
||||||
|
return [state, sendRequest];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function APIReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case API_ACTIONS.INIT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
state: API_STATE.SENDING,
|
||||||
|
};
|
||||||
|
case API_ACTIONS.SUCCESS:
|
||||||
|
return {
|
||||||
|
state: API_STATE.SUCCESS,
|
||||||
|
data: action.payload,
|
||||||
|
};
|
||||||
|
case API_ACTIONS.FAILURE:
|
||||||
|
if (action.status === 401) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not an API error - should be rethrown.
|
||||||
|
if (
|
||||||
|
action.payload &&
|
||||||
|
action.payload.stack &&
|
||||||
|
action.payload.message
|
||||||
|
) {
|
||||||
|
throw action.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: API_STATE.ERROR,
|
||||||
|
data: action.payload,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAPIGet = createAPIHook("GET");
|
||||||
|
const useAPIPost = createAPIHook("POST");
|
||||||
|
const useAPIPatch = createAPIHook("PATCH");
|
||||||
|
const useAPIPut = createAPIHook("PUT");
|
||||||
|
const useAPIDelete = createAPIHook("DELETE");
|
||||||
|
|
||||||
|
/* eslint-disable default-param-last */
|
||||||
|
function useAPIPolling(endpoint, delay = 1000, until) {
|
||||||
|
// delay ms
|
||||||
|
const [state, setState] = useState({ state: API_STATE.INIT });
|
||||||
|
const [getResponse, get] = useAPIGet(endpoint);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getResponse.state !== API_STATE.INIT) {
|
||||||
|
setState(getResponse);
|
||||||
|
}
|
||||||
|
}, [getResponse]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (until) {
|
||||||
|
const interval = setInterval(get, delay);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [until, delay, get]);
|
||||||
|
|
||||||
|
return [state];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useAPIGet,
|
||||||
|
useAPIPost,
|
||||||
|
useAPIPatch,
|
||||||
|
useAPIPut,
|
||||||
|
useAPIDelete,
|
||||||
|
useAPIPolling,
|
||||||
|
};
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
|
||||||
*
|
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
|
||||||
* See /LICENSE for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useReducer } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import {
|
|
||||||
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
export function useAPIPatch(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIReducer, {
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const patch = async (data) => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
const result = await axios.patch(url, data, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
headers: HEADERS,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: getErrorMessage(error),
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [state, patch];
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
|
||||||
*
|
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
|
||||||
* See /LICENSE for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useReducer } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import {
|
|
||||||
API_ACTIONS, TIMEOUT, HEADERS, APIReducer, getErrorMessage,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
export function useAPIPost(url) {
|
|
||||||
const [state, dispatch] = useReducer(APIReducer, {
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const post = async (data) => {
|
|
||||||
dispatch({ type: API_ACTIONS.INIT });
|
|
||||||
try {
|
|
||||||
const result = await axios.post(url, data, {
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
headers: HEADERS,
|
|
||||||
});
|
|
||||||
dispatch({ type: API_ACTIONS.SUCCESS, payload: result.data });
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: API_ACTIONS.FAILURE,
|
|
||||||
payload: getErrorMessage(error),
|
|
||||||
status: error.response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [state, post];
|
|
||||||
}
|
|
103
src/api/utils.js
103
src/api/utils.js
@ -1,11 +1,41 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2021 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ForisURLs } from "forisUrls";
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const HEADERS = {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRFToken": getCookie("_csrf_token"),
|
||||||
|
"X-Requested-With": "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIMEOUT = 30500;
|
||||||
|
|
||||||
|
export const API_ACTIONS = {
|
||||||
|
INIT: 1,
|
||||||
|
SUCCESS: 2,
|
||||||
|
FAILURE: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_STATE = {
|
||||||
|
INIT: "init",
|
||||||
|
SENDING: "sending",
|
||||||
|
SUCCESS: "success",
|
||||||
|
ERROR: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_METHODS = {
|
||||||
|
GET: axios.get,
|
||||||
|
POST: axios.post,
|
||||||
|
PATCH: axios.patch,
|
||||||
|
PUT: axios.put,
|
||||||
|
DELETE: axios.delete,
|
||||||
|
};
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@ -14,8 +44,10 @@ function getCookie(name) {
|
|||||||
for (let i = 0; i < cookies.length; i++) {
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
const cookie = cookies[i].trim();
|
const cookie = cookies[i].trim();
|
||||||
// Does this cookie string begin with the name we want?
|
// Does this cookie string begin with the name we want?
|
||||||
if (cookie.substring(0, name.length + 1) === (`${name}=`)) {
|
if (cookie.substring(0, name.length + 1) === `${name}=`) {
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
cookieValue = decodeURIComponent(
|
||||||
|
cookie.substring(name.length + 1)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,55 +55,26 @@ function getCookie(name) {
|
|||||||
return cookieValue;
|
return cookieValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HEADERS = {
|
export function getErrorPayload(error) {
|
||||||
Accept: "application/json",
|
if (error.response) {
|
||||||
"Content-Type": "application/json",
|
if (error.response.status === 401) {
|
||||||
"X-CSRFToken": getCookie("_csrf_token"),
|
return _("The session is expired. Please log in again.");
|
||||||
};
|
}
|
||||||
|
return getJSONErrorMessage(error);
|
||||||
export const TIMEOUT = 5000;
|
|
||||||
|
|
||||||
export const API_ACTIONS = {
|
|
||||||
INIT: 1,
|
|
||||||
SUCCESS: 2,
|
|
||||||
FAILURE: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function APIReducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case API_ACTIONS.INIT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: true,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: false,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: false,
|
|
||||||
isError: false,
|
|
||||||
isSuccess: true,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
case API_ACTIONS.FAILURE:
|
|
||||||
if (action.status === 403) window.location.assign(ForisURLs.login);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSending: false,
|
|
||||||
isError: true,
|
|
||||||
isSuccess: false,
|
|
||||||
data: action.payload,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
}
|
||||||
|
if (error.code === "ECONNABORTED") {
|
||||||
|
return _("Timeout error occurred.");
|
||||||
|
}
|
||||||
|
if (error.request) {
|
||||||
|
return _("No response received.");
|
||||||
|
}
|
||||||
|
// Return original error because it's not directly related to API request/response.
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getErrorMessage(error) {
|
export function getJSONErrorMessage(error) {
|
||||||
let payload = "An unknown error occurred";
|
|
||||||
if (error.response.headers["content-type"] === "application/json") {
|
if (error.response.headers["content-type"] === "application/json") {
|
||||||
payload = error.response.data;
|
return error.response.data;
|
||||||
}
|
}
|
||||||
return payload;
|
return _("An unknown API error occurred.");
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { useFocusTrap } from "../utils/hooks";
|
||||||
|
|
||||||
|
export const ALERT_TYPES = Object.freeze({
|
||||||
|
PRIMARY: "primary",
|
||||||
|
SECONDARY: "secondary",
|
||||||
|
SUCCESS: "success",
|
||||||
|
DANGER: "danger",
|
||||||
|
WARNING: "warning",
|
||||||
|
INFO: "info",
|
||||||
|
LIGHT: "light",
|
||||||
|
DARK: "dark",
|
||||||
|
});
|
||||||
|
|
||||||
Alert.propTypes = {
|
Alert.propTypes = {
|
||||||
/** Type of the alert it adds as `alert-${type}` class. */
|
/** Type of the alert it adds as `alert-${type}` class. */
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.oneOf(Object.values(ALERT_TYPES)),
|
||||||
/** Alert message. */
|
|
||||||
message: PropTypes.string,
|
|
||||||
/** Alert content. */
|
/** Alert content. */
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
@ -22,14 +34,48 @@ Alert.propTypes = {
|
|||||||
onDismiss: PropTypes.func,
|
onDismiss: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Alert({
|
Alert.defaultProps = {
|
||||||
type, message, onDismiss, children,
|
type: ALERT_TYPES.DANGER,
|
||||||
}) {
|
};
|
||||||
|
|
||||||
|
function Alert({ type, onDismiss, children }) {
|
||||||
|
const alertRef = useRef();
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
useFocusTrap(alertRef, !!onDismiss);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onDismiss) {
|
||||||
|
const timeout = setTimeout(() => setIsVisible(false), 7000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [onDismiss]);
|
||||||
|
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
if (!isVisible && onDismiss) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`alert alert-dismissible alert-${type}`}>
|
<div
|
||||||
{onDismiss ? <button type="button" className="close" onClick={onDismiss}>×</button> : false}
|
ref={alertRef}
|
||||||
{message}
|
className={`alert alert-${type} ${isVisible ? "alert-fade-in" : "alert-slide-out-top"} ${
|
||||||
|
onDismiss ? "alert-dismissible" : ""
|
||||||
|
}`.trim()}
|
||||||
|
role="alert"
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
>
|
||||||
|
{onDismiss && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={() => setIsVisible(false)}
|
||||||
|
aria-label={_("Close")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Alert;
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
Bootstrap alert component.
|
Bootstrap alert component.
|
||||||
```jsx
|
|
||||||
import {useState} from 'react';
|
|
||||||
|
|
||||||
function AlertExample(){
|
```jsx
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function AlertExample() {
|
||||||
const [alert, setAlert] = useState(true);
|
const [alert, setAlert] = useState(true);
|
||||||
if (alert)
|
if (alert)
|
||||||
return <Alert
|
return (
|
||||||
type='warning'
|
<Alert type="warning" onDismiss={() => setAlert(false)}>
|
||||||
message='Some warning out there!'
|
Some warning out there!
|
||||||
onDismiss={()=>setAlert(false)}
|
</Alert>
|
||||||
/>;
|
);
|
||||||
return <button
|
return (
|
||||||
className='btn btn-secondary'
|
<button className="btn btn-secondary" onClick={() => setAlert(true)}>
|
||||||
onClick={()=>setAlert(true)}
|
Show alert again
|
||||||
>Show alert again</button>;
|
</button>
|
||||||
};
|
);
|
||||||
<AlertExample/>
|
}
|
||||||
|
<AlertExample />;
|
||||||
```
|
```
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const OFFSET = 8;
|
import PropTypes from "prop-types";
|
||||||
const SIZE = 3;
|
|
||||||
const SIZE_CLASS = ` offset-lg-${OFFSET} col-lg-${SIZE}`;
|
|
||||||
const SIZE_CLASS_SM = " col-sm-12";
|
|
||||||
|
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
/** Additional class name. */
|
/** Additional class name. */
|
||||||
@ -29,22 +25,28 @@ Button.propTypes = {
|
|||||||
]).isRequired,
|
]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Button({
|
function Button({ className, loading, forisFormSize, children, ...props }) {
|
||||||
className, loading, forisFormSize, children, ...props
|
let buttonClass = className ? `btn ${className}` : "btn btn-primary";
|
||||||
}) {
|
if (forisFormSize) {
|
||||||
className = className ? `btn ${className}` : "btn btn-primary ";
|
buttonClass = `${buttonClass} col-12 col-md-3 col-lg-2`;
|
||||||
if (forisFormSize) className += SIZE_CLASS + SIZE_CLASS_SM;
|
}
|
||||||
|
|
||||||
const span = loading
|
|
||||||
? <span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" /> : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" className={className} {...props}>
|
<button
|
||||||
{span}
|
type="button"
|
||||||
{" "}
|
className={`${buttonClass} d-inline-flex justify-content-center align-items-center`}
|
||||||
{span ? " " : null}
|
{...props}
|
||||||
{" "}
|
>
|
||||||
{children}
|
{loading && (
|
||||||
|
<span
|
||||||
|
className="spinner-border spinner-border-sm me-1"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{children}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
@ -11,5 +11,7 @@ Can be used without parameters:
|
|||||||
Using loading spinner:
|
Using loading spinner:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<Button loading disabled>Loading...</Button>
|
<Button loading disabled>
|
||||||
|
Loading...
|
||||||
|
</Button>
|
||||||
```
|
```
|
||||||
|
@ -1,50 +1,52 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
import PropTypes from "prop-types";
|
||||||
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
CheckBox.propTypes = {
|
CheckBox.propTypes = {
|
||||||
/** Label message */
|
/** Label message */
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
/** Help text message */
|
/** Help text message */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
/** Apply default size (full-width) */
|
|
||||||
useDefaultSize: PropTypes.bool,
|
|
||||||
/** Control if checkbox is clickable */
|
/** Control if checkbox is clickable */
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
/** Additional class name */
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
CheckBox.defaultProps = {
|
CheckBox.defaultProps = {
|
||||||
useDefaultSize: true,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CheckBox({
|
function CheckBox({ label, helpText, disabled, className, ...props }) {
|
||||||
label, helpText, useDefaultSize, disabled, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
return (
|
|
||||||
<div className={useDefaultSize ? formFieldsSize : ""} style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="custom-control custom-checkbox" style={{ marginBottom: "0" }}>
|
|
||||||
<input
|
|
||||||
className="custom-control-input"
|
|
||||||
type="checkbox"
|
|
||||||
id={uid}
|
|
||||||
disabled={disabled}
|
|
||||||
|
|
||||||
{...props}
|
return (
|
||||||
/>
|
<div className={`${className || "mb-3"} form-check`.trim()}>
|
||||||
<label className="custom-control-label" htmlFor={uid} style={helpText ? { marginBottom: "0" } : null}>{label}</label>
|
<input
|
||||||
</div>
|
className="form-check-input"
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
type="checkbox"
|
||||||
|
id={uid}
|
||||||
|
disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={uid}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CheckBox;
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
Checkbox with label Bootstrap component with predefined sizes and structure for using in foris forms.
|
Checkbox with label Bootstrap component with predefined sizes and structure for
|
||||||
|
using in foris forms.
|
||||||
All additional `props` are passed to the `<input type="checkbox">` HTML component.
|
|
||||||
|
|
||||||
|
All additional `props` are passed to the `<input type="checkbox">` HTML
|
||||||
|
component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState(false);
|
const [value, setValue] = useState(false);
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
value={value}
|
value={value}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
62
src/bootstrap/CopyInput.js
Normal file
62
src/bootstrap/CopyInput.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useRef } from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import Input from "./Input";
|
||||||
|
|
||||||
|
CopyInput.propTypes = {
|
||||||
|
/** Field label. */
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/** Field value. */
|
||||||
|
value: PropTypes.string,
|
||||||
|
/** Help text message. */
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
/** Disable input field */
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
/** Readonly input field */
|
||||||
|
readOnly: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
function CopyInput({ value, ...props }) {
|
||||||
|
const inputTextRef = useRef();
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopyClick = async () => {
|
||||||
|
// Clipboard API works only in a secure (HTTPS) context.
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
} else {
|
||||||
|
// Fallback to the "classic" copy to clipboard implementation.
|
||||||
|
inputTextRef.current.focus();
|
||||||
|
inputTextRef.current.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
inputTextRef.current.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input type="text" value={value} ref={inputTextRef} {...props}>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCopyClick}
|
||||||
|
>
|
||||||
|
<span>{isCopied ? _("Copied!") : _("Copy")}</span>
|
||||||
|
</button>
|
||||||
|
</Input>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyInput;
|
17
src/bootstrap/CopyInput.md
Normal file
17
src/bootstrap/CopyInput.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CopyInput Bootstrap component contains input with a label, predefined sizes, and
|
||||||
|
structure for use in ForisForm and the "Copy" button (copy to clipboard). It can
|
||||||
|
be used with `readOnly` and `disabled` parameters, please see an example.
|
||||||
|
|
||||||
|
All additional `props` are passed to the `<input type="text">` HTML component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import React, { useState } from "react";
|
||||||
|
const [value, setValue] = useState("Text to appear in clipboard.");
|
||||||
|
|
||||||
|
<CopyInput
|
||||||
|
label="Copy me"
|
||||||
|
value={value}
|
||||||
|
helpText="Read the small text!"
|
||||||
|
readOnly
|
||||||
|
/>;
|
||||||
|
```
|
4
src/bootstrap/DataTimeInput.css
Normal file
4
src/bootstrap/DataTimeInput.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* Override defaults from "react-datetime" - display picker above input */
|
||||||
|
.rdtPicker {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
@ -1,17 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import Datetime from "react-datetime/DateTime";
|
|
||||||
import moment from "moment/moment";
|
|
||||||
import "react-datetime/css/react-datetime.css";
|
|
||||||
|
|
||||||
import { Input } from "./Input";
|
import moment from "moment/moment";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import Datetime from "react-datetime";
|
||||||
|
import "react-datetime/css/react-datetime.css";
|
||||||
|
import "./DataTimeInput.css";
|
||||||
|
|
||||||
|
import Input from "./Input";
|
||||||
|
|
||||||
DataTimeInput.propTypes = {
|
DataTimeInput.propTypes = {
|
||||||
/** Field label. */
|
/** Field label. */
|
||||||
@ -36,25 +38,32 @@ DataTimeInput.propTypes = {
|
|||||||
const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
|
const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
|
||||||
const DEFAULT_TIME_FORMAT = "HH:mm:ss";
|
const DEFAULT_TIME_FORMAT = "HH:mm:ss";
|
||||||
|
|
||||||
export function DataTimeInput({
|
function DataTimeInput({
|
||||||
value, onChange, isValidDate, dateFormat, timeFormat, children, ...props
|
value,
|
||||||
|
onChange,
|
||||||
|
isValidDate,
|
||||||
|
dateFormat,
|
||||||
|
timeFormat,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
function renderInput(datetimeProps) {
|
const renderInput = (datetimeProps) => {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input {...props} {...datetimeProps}>
|
||||||
{...props}
|
|
||||||
{...datetimeProps}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Input>
|
</Input>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datetime
|
<Datetime
|
||||||
locale={ForisTranslations.locale}
|
locale={ForisTranslations.locale}
|
||||||
dateFormat={dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT}
|
dateFormat={
|
||||||
timeFormat={timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT}
|
dateFormat !== undefined ? dateFormat : DEFAULT_DATE_FORMAT
|
||||||
|
}
|
||||||
|
timeFormat={
|
||||||
|
timeFormat !== undefined ? timeFormat : DEFAULT_TIME_FORMAT
|
||||||
|
}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
isValidDate={isValidDate}
|
isValidDate={isValidDate}
|
||||||
@ -62,3 +71,5 @@ export function DataTimeInput({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default DataTimeInput;
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
Adopted from `react-datetime/DateTime` datatime picker component.
|
Adopted from `react-datetime/DateTime` datatime picker component. It uses
|
||||||
It uses `momentjs` see example.
|
`momentjs` see example.
|
||||||
|
|
||||||
It requires `ForisTranslations.locale` to be defined in order to use right locale.
|
It requires `ForisTranslations.locale` to be defined in order to use right
|
||||||
|
locale.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
ForisTranslations={locale:'en'};
|
ForisTranslations = { locale: "en" };
|
||||||
|
|
||||||
import {useState, useEffect} from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import moment from 'moment/moment';
|
import moment from "moment/moment";
|
||||||
|
|
||||||
const [dataTime, setDataTime] = useState(moment());
|
const [dataTime, setDataTime] = useState(moment());
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
dataTime.isValid() ? setError(null) : setError('Invalid value!');
|
dataTime.isValid() ? setError(null) : setError("Invalid value!");
|
||||||
},[dataTime]);
|
}, [dataTime]);
|
||||||
|
|
||||||
<DataTimeInput
|
<DataTimeInput
|
||||||
label='Time to sleep'
|
label="Time to sleep"
|
||||||
value={dataTime}
|
value={dataTime}
|
||||||
error={error}
|
error={error}
|
||||||
helpText='Example helptext...'
|
helpText="Example helptext..."
|
||||||
onChange={value => setDataTime(value)}
|
onChange={(value) => setDataTime(value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
38
src/bootstrap/DownloadButton.js
Normal file
38
src/bootstrap/DownloadButton.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
DownloadButton.propTypes = {
|
||||||
|
href: PropTypes.string.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadButton.defaultProps = {
|
||||||
|
className: "btn-primary",
|
||||||
|
};
|
||||||
|
|
||||||
|
function DownloadButton({ href, className, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={`btn ${className}`.trim()}
|
||||||
|
{...props}
|
||||||
|
download
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadButton;
|
10
src/bootstrap/DownloadButton.md
Normal file
10
src/bootstrap/DownloadButton.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Hyperlink with apperance of a button.
|
||||||
|
|
||||||
|
It has `download` attribute, which prevents closing WebSocket connection on
|
||||||
|
Firefox. See
|
||||||
|
[related issue](https://bugzilla.mozilla.org/show_bug.cgi?id=858538) for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<DownloadButton href="example.zip">Download</DownloadButton>
|
||||||
|
```
|
@ -8,10 +8,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Input } from "./Input";
|
|
||||||
|
|
||||||
export const EmailInput = ({ ...props }) => <Input type="email" {...props} />;
|
import Input from "./Input";
|
||||||
|
|
||||||
|
function EmailInput({ ...props }) {
|
||||||
|
return <Input type="email" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
EmailInput.propTypes = {
|
EmailInput.propTypes = {
|
||||||
/** Field label. */
|
/** Field label. */
|
||||||
@ -23,3 +25,5 @@ EmailInput.propTypes = {
|
|||||||
/** Email value. */
|
/** Email value. */
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default EmailInput;
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
Bootstrap component of email input with label with predefined sizes and structure for using in foris forms.
|
Bootstrap component of email input with label with predefined sizes and
|
||||||
It use built-in browser email address checking. It's only meaningful using inside `<form>`.
|
structure for using in foris forms. It use built-in browser email address
|
||||||
|
checking. It's only meaningful using inside `<form>`.
|
||||||
|
|
||||||
All additional `props` are passed to the `<input type="email">` HTML component.
|
All additional `props` are passed to the `<input type="email">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [email, setEmail] = useState('Wrong email');
|
import Button from "./Button";
|
||||||
<form onSubmit={e=>e.preventDefault()}>
|
const [email, setEmail] = useState("Wrong email");
|
||||||
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<EmailInput
|
<EmailInput
|
||||||
value={email}
|
value={email}
|
||||||
label="Some label"
|
label="Some label"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setEmail(event.target.value)}
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Try to submit</button>
|
<Button type="submit">Try to submit</Button>
|
||||||
</form>
|
</form>;
|
||||||
```
|
```
|
||||||
|
39
src/bootstrap/FileInput.js
Normal file
39
src/bootstrap/FileInput.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import Input from "./Input";
|
||||||
|
|
||||||
|
FileInput.propTypes = {
|
||||||
|
/** Field label. */
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
/** Error message. */
|
||||||
|
error: PropTypes.string,
|
||||||
|
/** Help text message. */
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
/** Email value. */
|
||||||
|
value: PropTypes.string,
|
||||||
|
/** Allow selecting multiple files. */
|
||||||
|
multiple: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileInput({ ...props }) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
className="custom-file-input"
|
||||||
|
labelClassName="custom-file-label"
|
||||||
|
groupClassName="custom-file"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileInput;
|
48
src/bootstrap/FileInput.md
Normal file
48
src/bootstrap/FileInput.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
Bootstrap component for file input. Includes label and has predefined sizes and
|
||||||
|
structure for using in foris forms.
|
||||||
|
|
||||||
|
All additional `props` are passed to the `<input type="file">` HTML component.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
// Note that files is not an array but FileList.
|
||||||
|
const label = files.length === 1 ? files[0].name : "Choose file";
|
||||||
|
|
||||||
|
<form className="col">
|
||||||
|
<FileInput
|
||||||
|
files={files}
|
||||||
|
label={label}
|
||||||
|
helpText="Will be uploaded"
|
||||||
|
onChange={(event) => setFiles(event.target.files)}
|
||||||
|
/>
|
||||||
|
</form>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### FileInput with multiple files
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
// Note that files is not an array but FileList.
|
||||||
|
const label =
|
||||||
|
files.length > 0
|
||||||
|
? Array.from(files)
|
||||||
|
.map((file) => file.name)
|
||||||
|
.join(", ")
|
||||||
|
: "Choose files";
|
||||||
|
|
||||||
|
<form className="col">
|
||||||
|
<FileInput
|
||||||
|
files={files}
|
||||||
|
label={label}
|
||||||
|
helpText="Will be uploaded"
|
||||||
|
onChange={(event) => setFiles(event.target.files)}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</form>;
|
||||||
|
```
|
@ -1,19 +1,73 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2022 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
import PropTypes from "prop-types";
|
||||||
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
|
/** Base bootstrap input component. */
|
||||||
|
const Input = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
error,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
labelClassName,
|
||||||
|
groupClassName,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const uid = useUID();
|
||||||
|
|
||||||
|
const inputClassName = `${className || ""} ${
|
||||||
|
error ? "is-invalid" : ""
|
||||||
|
}`.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
className={`form-label ${labelClassName || ""}`.trim()}
|
||||||
|
htmlFor={uid}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className={`input-group ${groupClassName || ""}`.trim()}>
|
||||||
|
<input
|
||||||
|
className={`form-control ${inputClassName}`.trim()}
|
||||||
|
type={type}
|
||||||
|
id={uid}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{error && <div className="invalid-feedback">{error}</div>}
|
||||||
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
Input.propTypes = {
|
Input.propTypes = {
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string,
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@ -21,31 +75,8 @@ Input.propTypes = {
|
|||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
]),
|
]),
|
||||||
|
labelClassName: PropTypes.string,
|
||||||
|
groupClassName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Base bootstrap input component. */
|
export default Input;
|
||||||
export function Input({
|
|
||||||
type, label, helpText, error, className, children, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
|
||||||
const inputClassName = `form-control ${className || ""} ${(error ? "is-invalid" : "")}`.trim();
|
|
||||||
return (
|
|
||||||
<div className={formFieldsSize}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor={uid}>{label}</label>
|
|
||||||
<div className="input-group">
|
|
||||||
<input
|
|
||||||
className={inputClassName}
|
|
||||||
type={type}
|
|
||||||
id={uid}
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{error ? <div className="invalid-feedback">{error}</div> : null}
|
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
15
src/bootstrap/Modal.css
Normal file
15
src/bootstrap/Modal.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@keyframes modalFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: block;
|
||||||
|
animation-name: modalFade;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
@ -1,20 +1,25 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2020-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useRef, useEffect } from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Portal } from "utils/Portal";
|
import { useClickOutside, useFocusTrap } from "../utils/hooks";
|
||||||
|
import Portal from "../utils/Portal";
|
||||||
|
import "./Modal.css";
|
||||||
|
|
||||||
Modal.propTypes = {
|
Modal.propTypes = {
|
||||||
/** Is modal shown value */
|
/** Is modal shown value */
|
||||||
shown: PropTypes.bool.isRequired,
|
shown: PropTypes.bool.isRequired,
|
||||||
/** Callback to manage modal visibility */
|
/** Callback to manage modal visibility */
|
||||||
setShown: PropTypes.func.isRequired,
|
setShown: PropTypes.func.isRequired,
|
||||||
|
scrollable: PropTypes.bool,
|
||||||
|
size: PropTypes.string,
|
||||||
|
|
||||||
/** Modal content use following: `ModalHeader`, `ModalBody`, `ModalFooter` */
|
/** Modal content use following: `ModalHeader`, `ModalBody`, `ModalFooter` */
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
@ -23,28 +28,57 @@ Modal.propTypes = {
|
|||||||
]).isRequired,
|
]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Modal({ shown, setShown, children }) {
|
export function Modal({ shown, setShown, scrollable, size, children }) {
|
||||||
const dialogRef = useRef();
|
const modalRef = useRef();
|
||||||
|
let modalSize = "modal-";
|
||||||
|
|
||||||
|
useClickOutside(modalRef, () => setShown(false));
|
||||||
|
useFocusTrap(modalRef, shown);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutsideDialog(e) {
|
const handleEsc = (event) => {
|
||||||
if (!dialogRef.current.contains(e.target)) setShown(false);
|
if (event.keyCode === 27) {
|
||||||
}
|
setShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleEsc);
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutsideDialog);
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutsideDialog);
|
window.removeEventListener("keydown", handleEsc);
|
||||||
};
|
};
|
||||||
}, [setShown]);
|
}, [setShown]);
|
||||||
|
|
||||||
|
switch (size) {
|
||||||
|
case "sm":
|
||||||
|
modalSize += "sm";
|
||||||
|
break;
|
||||||
|
case "lg":
|
||||||
|
modalSize += "lg";
|
||||||
|
break;
|
||||||
|
case "xl":
|
||||||
|
modalSize += "xl";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
modalSize = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal containerId="modal-container">
|
<Portal containerId="modal-container">
|
||||||
<div className={`modal fade ${shown ? "show" : ""}`} role="dialog">
|
<div
|
||||||
<div ref={dialogRef} className="modal-dialog" role="document">
|
ref={modalRef}
|
||||||
<div className="modal-content">
|
className={`modal fade ${shown ? "show" : ""}`.trim()}
|
||||||
{children}
|
role="dialog"
|
||||||
</div>
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${modalSize.trim()} modal-dialog modal-dialog-centered ${
|
||||||
|
scrollable ? "modal-dialog-scrollable" : ""
|
||||||
|
}`.trim()}
|
||||||
|
role="document"
|
||||||
|
>
|
||||||
|
<div className="modal-content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
@ -54,15 +88,21 @@ export function Modal({ shown, setShown, children }) {
|
|||||||
ModalHeader.propTypes = {
|
ModalHeader.propTypes = {
|
||||||
setShown: PropTypes.func.isRequired,
|
setShown: PropTypes.func.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
showCloseButton: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ModalHeader({ setShown, title }) {
|
export function ModalHeader({ setShown, title, showCloseButton = true }) {
|
||||||
return (
|
return (
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h5 className="modal-title">{title}</h5>
|
<h1 className="modal-title fs-5">{title}</h1>
|
||||||
<button type="button" className="close" onClick={() => setShown(false)}>
|
{showCloseButton && (
|
||||||
<span aria-hidden="true">×</span>
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={() => setShown(false)}
|
||||||
|
aria-label={_("Close")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -86,9 +126,5 @@ ModalFooter.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ModalFooter({ children }) {
|
export function ModalFooter({ children }) {
|
||||||
return (
|
return <div className="modal-footer">{children}</div>;
|
||||||
<div className="modal-footer">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,47 @@
|
|||||||
Bootstrap modal component.
|
Bootstrap modal component.
|
||||||
|
|
||||||
it's required to have an element `<div id={"modal-container"}/>` somewhere on the page since modals are rendered in portals.
|
It's required to have an element `<div id={"modal-container"}/>` somewhere on
|
||||||
|
the page since modals are rendered in portals.
|
||||||
|
|
||||||
|
Modals also have three optional sizes, which can be defined through the `size`
|
||||||
|
prop:
|
||||||
|
|
||||||
|
- small - `sm`
|
||||||
|
- large - `lg`
|
||||||
|
- extra-large - `xl`
|
||||||
|
|
||||||
|
For more details please visit Bootstrap
|
||||||
|
<a href="https://getbootstrap.com/docs/4.5/components/modal/#optional-sizes" target="_blank">
|
||||||
|
documentation</a>.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
<div id="modal-container"/>
|
<div id="modal-container" />
|
||||||
```
|
```
|
||||||
|
|
||||||
I have no idea why example doesn't work here but you can investigate HTML code and Foris project.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {ModalHeader, ModalBody, ModalFooter} from './Modal';
|
import { ModalHeader, ModalBody, ModalFooter } from "./Modal";
|
||||||
|
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [shown, setShown] = useState(false);
|
const [shown, setShown] = useState(false);
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Modal setShown={setShown} shown={shown}>
|
<Modal setShown={setShown} shown={shown} size="sm">
|
||||||
<ModalHeader setShown={setShown} title='Warning!'/>
|
<ModalHeader setShown={setShown} title="Warning!" />
|
||||||
<ModalBody><p>Bla bla bla...</p></ModalBody>
|
<ModalBody>
|
||||||
|
<p>Bla bla bla...</p>
|
||||||
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button
|
<button
|
||||||
className='btn btn-secondary'
|
className="btn btn-secondary"
|
||||||
onClick={() => setShown(false)}
|
onClick={() => setShown(false)}
|
||||||
>Skip it</button>
|
>
|
||||||
|
Skip it
|
||||||
|
</button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<button className='btn btn-secondary' onClick={()=>setShown(true)}>
|
<button className="btn btn-secondary" onClick={() => setShown(true)}>
|
||||||
Show modal
|
Show modal
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
@ -4,7 +4,7 @@ input[type="number"] {
|
|||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number]::-webkit-inner-spin-button,
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
input[type=number]::-webkit-outer-spin-button {
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { useConditionalTimeout } from "utils/hooks";
|
import Input from "./Input";
|
||||||
import { Input } from "./Input";
|
import { useConditionalTimeout } from "../utils/hooks";
|
||||||
import "./NumberInput.css";
|
import "./NumberInput.css";
|
||||||
|
|
||||||
NumberInput.propTypes = {
|
NumberInput.propTypes = {
|
||||||
@ -20,13 +23,10 @@ NumberInput.propTypes = {
|
|||||||
/** Help text message. */
|
/** Help text message. */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
/** Number value. */
|
/** Number value. */
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]),
|
|
||||||
/** Function called when value changes. */
|
/** Function called when value changes. */
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
/** Additional description dispaled to the right of input value. */
|
/** Additional description displayed to the right of input value. */
|
||||||
inlineText: PropTypes.string,
|
inlineText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,39 +34,77 @@ NumberInput.defaultProps = {
|
|||||||
value: 0,
|
value: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NumberInput({
|
function NumberInput({ onChange, inlineText, value, ...props }) {
|
||||||
onChange, inlineText, value, ...props
|
|
||||||
}) {
|
|
||||||
function updateValue(initialValue, difference) {
|
function updateValue(initialValue, difference) {
|
||||||
onChange({ target: { value: initialValue + difference } });
|
onChange({ target: { value: initialValue + difference } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableIncrease = useConditionalTimeout({ callback: updateValue }, value, 1);
|
const enableIncrease = useConditionalTimeout(
|
||||||
const enableDecrease = useConditionalTimeout({ callback: updateValue }, value, -1);
|
{ callback: updateValue },
|
||||||
|
value,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const enableDecrease = useConditionalTimeout(
|
||||||
|
{ callback: updateValue },
|
||||||
|
value,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleKeyDown(event, enableFunction) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
enableFunction(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event, enableFunction) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
enableFunction(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input type="number" onChange={onChange} value={value} {...props}>
|
<Input type="number" onChange={onChange} value={value} {...props}>
|
||||||
<div className="input-group-append">
|
{inlineText && (
|
||||||
{inlineText && <p className="input-group-text">{inlineText}</p>}
|
<span className="input-group-text">{inlineText}</span>
|
||||||
<button
|
)}
|
||||||
type="button"
|
<button
|
||||||
className="btn btn-outline-secondary"
|
type="button"
|
||||||
onMouseDown={() => enableIncrease(true)}
|
className="btn btn-outline-secondary"
|
||||||
onMouseUp={() => enableIncrease(false)}
|
onMouseDown={() => enableIncrease(true)}
|
||||||
aria-label="Increase"
|
onMouseUp={() => enableIncrease(false)}
|
||||||
>
|
onMouseLeave={() => enableIncrease(false)}
|
||||||
<i className="fas fa-plus" />
|
onTouchStart={() => enableIncrease(true)}
|
||||||
</button>
|
onTouchEnd={() => enableIncrease(false)}
|
||||||
<button
|
onTouchCancel={() => enableIncrease(false)}
|
||||||
type="button"
|
onKeyDown={(event) => handleKeyDown(event, enableIncrease)}
|
||||||
className="btn btn-outline-secondary"
|
onKeyUp={(event) => handleKeyUp(event, enableIncrease)}
|
||||||
onMouseDown={() => enableDecrease(true)}
|
onBlur={() => enableIncrease(false)}
|
||||||
onMouseUp={() => enableDecrease(false)}
|
title={_("Increase value. Hint: Hold to increase faster.")}
|
||||||
aria-label="Decrease"
|
aria-label={_("Increase value. Hint: Hold to increase faster.")}
|
||||||
>
|
>
|
||||||
<i className="fas fa-minus" />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onMouseDown={() => enableDecrease(true)}
|
||||||
|
onMouseUp={() => enableDecrease(false)}
|
||||||
|
onMouseLeave={() => enableDecrease(false)}
|
||||||
|
onTouchStart={() => enableDecrease(true)}
|
||||||
|
onTouchEnd={() => enableDecrease(false)}
|
||||||
|
onTouchCancel={() => enableDecrease(false)}
|
||||||
|
onKeyDown={(event) => handleKeyDown(event, enableDecrease)}
|
||||||
|
onKeyUp={(event) => handleKeyUp(event, enableDecrease)}
|
||||||
|
onBlur={() => enableDecrease(false)}
|
||||||
|
title={_("Decrease value. Hint: Hold to decrease faster.")}
|
||||||
|
aria-label={_("Decrease value. Hint: Hold to decrease faster.")}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faMinus} />
|
||||||
|
</button>
|
||||||
</Input>
|
</Input>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default NumberInput;
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
Bootstrap component of number input with label with predefined sizes and structure for using in foris forms.
|
Bootstrap component of number input with label with predefined sizes and
|
||||||
|
structure for using in foris forms.
|
||||||
|
|
||||||
All additional `props` are passed to the `<input type="number">` HTML component.
|
All additional `props` are passed to the `<input type="number">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState(42);
|
const [value, setValue] = useState(42);
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={value}
|
value={value}
|
||||||
label="Some number"
|
label="Some number"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
min='33'
|
min="33"
|
||||||
max='54'
|
max="54"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Input } from "./Input";
|
import Input from "./Input";
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
/** Field label. */
|
/** Field label. */
|
||||||
@ -21,32 +24,37 @@ PasswordInput.propTypes = {
|
|||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
/** Use show/hide password button. */
|
/** Use show/hide password button. */
|
||||||
withEye: PropTypes.bool,
|
withEye: PropTypes.bool,
|
||||||
|
/** Use new-password in autocomplete attribute. */
|
||||||
|
newPass: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PasswordInput({ withEye, ...props }) {
|
function PasswordInput({ withEye, newPass, ...props }) {
|
||||||
const [isHidden, setHidden] = useState(true);
|
const [isHidden, setHidden] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type={withEye && !isHidden ? "text" : "password"}
|
type={withEye && !isHidden ? "text" : "password"}
|
||||||
autoComplete={isHidden ? "new-password" : null}
|
autoComplete={newPass ? "new-password" : "current-password"}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{withEye
|
{withEye && (
|
||||||
? (
|
<button
|
||||||
<div className="input-group-append">
|
type="button"
|
||||||
<button
|
className="input-group-text"
|
||||||
type="button"
|
onClick={(e) => {
|
||||||
className="input-group-text"
|
e.preventDefault();
|
||||||
onClick={(e) => {
|
setHidden((shouldBeHidden) => !shouldBeHidden);
|
||||||
e.preventDefault();
|
}}
|
||||||
setHidden((shouldBeHidden) => !shouldBeHidden);
|
>
|
||||||
}}
|
<FontAwesomeIcon
|
||||||
>
|
icon={isHidden ? faEye : faEyeSlash}
|
||||||
<i className={`fa ${isHidden ? "fa-eye" : "fa-eye-slash"}`} />
|
style={{ width: "1.25rem" }}
|
||||||
</button>
|
className="text-secondary"
|
||||||
</div>
|
/>
|
||||||
)
|
</button>
|
||||||
: null}
|
)}
|
||||||
</Input>
|
</Input>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PasswordInput;
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
Password Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
Password Bootstrap component input with label and predefined sizes and structure
|
||||||
Can be used with "eye" button, see example.
|
for using in foris forms. Can be used with "eye" button, see example.
|
||||||
|
|
||||||
All additional `props` are passed to the `<input type="password">` HTML component.
|
All additional `props` are passed to the `<input type="password">` HTML
|
||||||
|
component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState('secret');
|
const [value, setValue] = useState("secret");
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
withEye
|
withEye
|
||||||
value={value}
|
value={value}
|
||||||
label="Some password"
|
label="Some password"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
48
src/bootstrap/Radio.js
Normal file
48
src/bootstrap/Radio.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
Radio.propTypes = {
|
||||||
|
label: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]).isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
inline: PropTypes.bool,
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Radio({ label, id, helpText, inline, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${className || "mb-3"} ${inline ? "form-check form-check-inline" : ""}`.trim()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className="form-check-input me-2"
|
||||||
|
type="radio"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Radio;
|
@ -1,16 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2020-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
import { formFieldsSize } from "./constants";
|
|
||||||
|
|
||||||
|
import Radio from "./Radio";
|
||||||
|
|
||||||
RadioSet.propTypes = {
|
RadioSet.propTypes = {
|
||||||
/** Name attribute of the input HTML tag. */
|
/** Name attribute of the input HTML tag. */
|
||||||
@ -18,21 +18,28 @@ RadioSet.propTypes = {
|
|||||||
/** RadioSet label . */
|
/** RadioSet label . */
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
/** Choices . */
|
/** Choices . */
|
||||||
choices: PropTypes.arrayOf(PropTypes.shape({
|
choices: PropTypes.arrayOf(
|
||||||
/** Choice lable . */
|
PropTypes.shape({
|
||||||
label: PropTypes.string.isRequired,
|
/** Choice label . */
|
||||||
/** Choice value . */
|
label: PropTypes.oneOfType([
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
PropTypes.string,
|
||||||
})).isRequired,
|
PropTypes.element,
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]).isRequired,
|
||||||
|
/** Choice value . */
|
||||||
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
/** Initial value . */
|
/** Initial value . */
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
/** Help text message . */
|
/** Help text message . */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
|
inline: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RadioSet({
|
function RadioSet({ name, label, choices, value, helpText, inline, ...props }) {
|
||||||
name, label, choices, value, helpText, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
const radios = choices.map((choice, key) => {
|
const radios = choices.map((choice, key) => {
|
||||||
const id = `${name}-${key}`;
|
const id = `${name}-${key}`;
|
||||||
@ -45,49 +52,27 @@ export function RadioSet({
|
|||||||
value={choice.value}
|
value={choice.value}
|
||||||
helpText={choice.helpText}
|
helpText={choice.helpText}
|
||||||
checked={choice.value === value}
|
checked={choice.value === value}
|
||||||
|
inline={inline}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`form-group ${formFieldsSize}`} style={{ marginBottom: "1rem" }}>
|
<div className="mb-3">
|
||||||
{label
|
{label && (
|
||||||
? (
|
<label htmlFor={uid} className="d-block">
|
||||||
<label className="col-12" htmlFor={uid} style={{ paddingLeft: "0" }}>
|
{label}
|
||||||
{label}
|
</label>
|
||||||
</label>
|
)}
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{radios}
|
{radios}
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Radio.propTypes = {
|
export default RadioSet;
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
helpText: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Radio({
|
|
||||||
label, id, helpText, ...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="custom-control custom-radio custom-control-inline">
|
|
||||||
<input
|
|
||||||
id={id}
|
|
||||||
className="custom-control-input"
|
|
||||||
type="radio"
|
|
||||||
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<label className="custom-control-label" htmlFor={id}>{label}</label>
|
|
||||||
</div>
|
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
Set of radio Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
Set of radio Bootstrap component input with label and predefined sizes and
|
||||||
|
structure for using in foris forms.
|
||||||
|
|
||||||
All additional `props` are passed to the `<input type="number">` HTML component.
|
All additional `props` are passed to the `<input type="number">` HTML component.
|
||||||
|
|
||||||
|
Unless `helpText` is set for one of the options they are displayed inline.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const CHOICES=[
|
const CHOICES = [
|
||||||
{value:'one',label:'1'},
|
{ value: "one", label: "1" },
|
||||||
{value:'two',label:'2'},
|
{ value: "two", label: "2" },
|
||||||
{value:'three',label:'3'},
|
{ value: "three", label: "3" },
|
||||||
];
|
];
|
||||||
const [value, setValue] = useState(CHOICES[0].value);
|
const [value, setValue] = useState(CHOICES[0].value);
|
||||||
|
|
||||||
@ -15,10 +18,10 @@ const [value, setValue] = useState(CHOICES[0].value);
|
|||||||
{/*Yeah, it gets event, not value!*/}
|
{/*Yeah, it gets event, not value!*/}
|
||||||
<RadioSet
|
<RadioSet
|
||||||
value={value}
|
value={value}
|
||||||
name='some-radio'
|
name="some-radio"
|
||||||
choices={CHOICES}
|
choices={CHOICES}
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<p>Selected value: {value}</p>
|
<p>Selected value: {value}</p>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useUID } from "react-uid/dist/es5/index";
|
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
Select.propTypes = {
|
Select.propTypes = {
|
||||||
/** Select field Label. */
|
/** Select field Label. */
|
||||||
@ -16,34 +16,35 @@ Select.propTypes = {
|
|||||||
/** Choices if form of {value : "Label",...}. */
|
/** Choices if form of {value : "Label",...}. */
|
||||||
choices: PropTypes.object.isRequired,
|
choices: PropTypes.object.isRequired,
|
||||||
/** Current value. */
|
/** Current value. */
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]).isRequired,
|
|
||||||
/** Help text message. */
|
/** Help text message. */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Select({
|
function Select({ label, choices, helpText, ...props }) {
|
||||||
label, choices, helpText, ...props
|
|
||||||
}) {
|
|
||||||
const uid = useUID();
|
const uid = useUID();
|
||||||
|
|
||||||
const options = Object.keys(choices).map(
|
const options = Object.keys(choices).map((choice) => (
|
||||||
(key) => <option key={key} value={key}>{choices[key]}</option>,
|
<option key={choice} value={choice}>
|
||||||
);
|
{choices[choice]}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-group col-sm-12 offset-lg-1 col-lg-10">
|
<div className="mb-3">
|
||||||
<label htmlFor={uid}>{label}</label>
|
<label className="form-label" htmlFor={uid}>
|
||||||
<select
|
{label}
|
||||||
className="custom-select"
|
</label>
|
||||||
id={uid}
|
<select className="form-select" id={uid} {...props}>
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{options}
|
{options}
|
||||||
</select>
|
</select>
|
||||||
{helpText ? <small className="form-text text-muted">{helpText}</small> : null}
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Select;
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
Select with options Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
Select with options Bootstrap component input with label and predefined sizes
|
||||||
|
and structure for using in foris forms.
|
||||||
|
|
||||||
All additional `props` are passed to the `<select>` HTML component.
|
All additional `props` are passed to the `<select>` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const CHOICES={
|
const CHOICES = {
|
||||||
apple:'Apple',
|
apple: "Apple",
|
||||||
banana:'Banana',
|
banana: "Banana",
|
||||||
peach:'Peach',
|
peach: "Peach",
|
||||||
};
|
};
|
||||||
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
||||||
|
|
||||||
@ -17,9 +18,9 @@ const [value, setValue] = useState(Object.keys(CHOICES)[0]);
|
|||||||
label="Fruit"
|
label="Fruit"
|
||||||
value={value}
|
value={value}
|
||||||
choices={CHOICES}
|
choices={CHOICES}
|
||||||
onChange={event=>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<p>Selected choice label: {CHOICES[value]}</p>
|
<p>Selected choice label: {CHOICES[value]}</p>
|
||||||
<p>Selected choice value: {value}</p>
|
<p>Selected choice value: {value}</p>
|
||||||
</>
|
</>;
|
||||||
```
|
```
|
||||||
|
43
src/bootstrap/Spinner.css
Normal file
43
src/bootstrap/Spinner.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.spinner-fs-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1101; /* increase z-index by 1 to ensure it's on top of spinner-fs-background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-wrapper .spinner-border {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-fs-background {
|
||||||
|
background-color: rgba(2, 2, 2, 0.5);
|
||||||
|
color: rgb(230, 230, 230);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
/*
|
||||||
|
* Set to high value to me sure that it always overlaps all components
|
||||||
|
* https://getbootstrap.com/docs/4.3/layout/overview/#z-index
|
||||||
|
*/
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-fs-wrapper .spinner-border {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-fs-wrapper .spinner-text {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
@ -6,15 +6,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import "./Spinner.css";
|
||||||
|
|
||||||
Spinner.propTypes = {
|
Spinner.propTypes = {
|
||||||
/** Children components put into `div` with "spinner-text" class. */
|
/** Children components put into `div` with "spinner-text" class. */
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node,
|
PropTypes.node,
|
||||||
]),
|
]),
|
||||||
/** Render component with full-screen mode (using apropriate `.css` styles) */
|
/** Render component with full-screen mode (using appropriate `.css` styles) */
|
||||||
fullScreen: PropTypes.bool.isRequired,
|
fullScreen: PropTypes.bool.isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
@ -23,19 +26,19 @@ Spinner.defaultProps = {
|
|||||||
fullScreen: false,
|
fullScreen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Spinner({
|
export function Spinner({ fullScreen, children, className }) {
|
||||||
fullScreen, children, className, ...props
|
|
||||||
}) {
|
|
||||||
if (!fullScreen) {
|
if (!fullScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={`spinner-wrapper ${className || ""}`} {...props}>
|
<div
|
||||||
|
className={`spinner-wrapper ${className || "my-3 text-center"}`}
|
||||||
|
>
|
||||||
<SpinnerElement>{children}</SpinnerElement>
|
<SpinnerElement>{children}</SpinnerElement>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="spinner-fs-wrapper" {...props}>
|
<div className="spinner-fs-wrapper">
|
||||||
<div className="spinner-fs-background">
|
<div className="spinner-fs-background">
|
||||||
<SpinnerElement>{children}</SpinnerElement>
|
<SpinnerElement>{children}</SpinnerElement>
|
||||||
</div>
|
</div>
|
||||||
@ -46,6 +49,8 @@ export function Spinner({
|
|||||||
SpinnerElement.propTypes = {
|
SpinnerElement.propTypes = {
|
||||||
/** Spinner's size */
|
/** Spinner's size */
|
||||||
small: PropTypes.bool,
|
small: PropTypes.bool,
|
||||||
|
/** Additional className */
|
||||||
|
className: PropTypes.string,
|
||||||
/** Children components */
|
/** Children components */
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
@ -53,13 +58,18 @@ SpinnerElement.propTypes = {
|
|||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SpinnerElement({ small, children }) {
|
export function SpinnerElement({ small, className, children }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`spinner-border ${small ? "spinner-border-sm" : ""}`} role="status">
|
<div
|
||||||
|
className={`spinner-border ${
|
||||||
|
small ? "spinner-border-sm" : ""
|
||||||
|
} ${className || ""}`.trim()}
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
<span className="sr-only" />
|
<span className="sr-only" />
|
||||||
</div>
|
</div>
|
||||||
<div className="spinner-text">{children}</div>
|
{children && <div className="spinner-text">{children}</div>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
53
src/bootstrap/Switch.js
Normal file
53
src/bootstrap/Switch.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { useUID } from "react-uid";
|
||||||
|
|
||||||
|
Switch.propTypes = {
|
||||||
|
label: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]).isRequired,
|
||||||
|
helpText: PropTypes.string,
|
||||||
|
switchHeading: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Switch({ label, helpText, switchHeading, className, ...props }) {
|
||||||
|
const uid = useUID();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`form-check form-switch ${className || "mb-3"} ${
|
||||||
|
switchHeading ? "d-flex align-items-center" : ""
|
||||||
|
}`.trim()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className={`form-check-input ${switchHeading ? "me-2" : ""}`.trim()}
|
||||||
|
role="switch"
|
||||||
|
id={uid}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={uid}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{helpText && (
|
||||||
|
<div className="form-text">
|
||||||
|
<small>{helpText}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Switch;
|
5
src/bootstrap/Switch.md
Normal file
5
src/bootstrap/Switch.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Switch example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
<Switch label="Enable Switch" helpText="Toggle that switch!" />
|
||||||
|
```
|
@ -1,18 +1,19 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { Input } from "./Input";
|
import Input from "./Input";
|
||||||
|
|
||||||
|
|
||||||
export const TextInput = ({ ...props }) => <Input type="text" {...props} />;
|
|
||||||
|
|
||||||
|
function TextInput({ ...props }) {
|
||||||
|
return <Input type="text" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
TextInput.propTypes = {
|
TextInput.propTypes = {
|
||||||
/** Field label. */
|
/** Field label. */
|
||||||
@ -22,3 +23,5 @@ TextInput.propTypes = {
|
|||||||
/** Help text message. */
|
/** Help text message. */
|
||||||
helpText: PropTypes.string,
|
helpText: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default TextInput;
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
Text Bootstrap component input with label and predefined sizes and structure for using in foris forms.
|
Text Bootstrap component input with label and predefined sizes and structure for
|
||||||
|
using in foris forms.
|
||||||
|
|
||||||
All additional `props` are passed to the `<input type="text">` HTML component.
|
All additional `props` are passed to the `<input type="text">` HTML component.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {useState} from 'react';
|
import { useState } from "react";
|
||||||
const [value, setValue] = useState('Bla bla');
|
const [value, setValue] = useState("Bla bla");
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
label="Some text"
|
label="Some text"
|
||||||
helpText="Read the small text!"
|
helpText="Read the small text!"
|
||||||
onChange={event =>setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
42
src/bootstrap/ThreeDotsMenu.js
Normal file
42
src/bootstrap/ThreeDotsMenu.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { faEllipsisVertical } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import Button from "./Button";
|
||||||
|
|
||||||
|
ThreeDotsMenu.propTypes = {
|
||||||
|
/** Menu items. */
|
||||||
|
children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ThreeDotsMenu({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<div className="dropdown position-static" {...props}>
|
||||||
|
<Button
|
||||||
|
className="btn-sm btn-link text-body"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</Button>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
{children.map((child) => (
|
||||||
|
<li key={child.key || child.props.id || Math.random()}>
|
||||||
|
{child}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThreeDotsMenu;
|
40
src/bootstrap/ThreeDotsMenu.md
Normal file
40
src/bootstrap/ThreeDotsMenu.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
ThreeDotsMenu Bootstrap component is a dropdown menu that appears when the user
|
||||||
|
clicks on three dots. It is used to display a list of actions that can be
|
||||||
|
performed on a particular item.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
const threeDotsMenuItems = [
|
||||||
|
{
|
||||||
|
text: "Edit",
|
||||||
|
icon: faEdit,
|
||||||
|
onClick: () => {
|
||||||
|
alert("Edit clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Delete",
|
||||||
|
icon: faTrash,
|
||||||
|
onClick: () => {
|
||||||
|
alert("Delete clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<ThreeDotsMenu>
|
||||||
|
{threeDotsMenuItems.map((item, index) => (
|
||||||
|
<button key={index} onClick={item.onClick} className="dropdown-item">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={item.icon}
|
||||||
|
className="me-1"
|
||||||
|
width="1rem"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{item.text}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</ThreeDotsMenu>;
|
||||||
|
```
|
@ -9,24 +9,23 @@ import React from "react";
|
|||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
import { Button } from "../Button";
|
import Button from "../Button";
|
||||||
|
|
||||||
describe("<Button />", () => {
|
describe("<Button />", () => {
|
||||||
it("Render button correctly", () => {
|
it("Render button correctly", () => {
|
||||||
const { container } = render(<Button>Test Button</Button>);
|
const { container } = render(<Button>Test Button</Button>);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render button with custom classes", () => {
|
it("Render button with custom classes", () => {
|
||||||
const { container } = render(<Button className="one two three">Test Button</Button>);
|
const { container } = render(
|
||||||
expect(container.firstChild)
|
<Button className="one two three">Test Button</Button>
|
||||||
.toMatchSnapshot();
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render button with spinner", () => {
|
it("Render button with spinner", () => {
|
||||||
const { container } = render(<Button loading={true}>Test Button</Button>);
|
const { container } = render(<Button loading>Test Button</Button>);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import React from "react";
|
|||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
import { CheckBox } from "../CheckBox";
|
import CheckBox from "../CheckBox";
|
||||||
|
|
||||||
describe("<Checkbox/>", () => {
|
describe("<Checkbox/>", () => {
|
||||||
it("Render checkbox", () => {
|
it("Render checkbox", () => {
|
||||||
@ -18,22 +18,16 @@ describe("<Checkbox/>", () => {
|
|||||||
label="Test label"
|
label="Test label"
|
||||||
checked
|
checked
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Render uncheked checkbox", () => {
|
it("Render uncheked checkbox", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CheckBox
|
<CheckBox label="Test label" helpText="Some help text" />
|
||||||
label="Test label"
|
|
||||||
helpText="Some help text"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
23
src/bootstrap/__tests__/DownloadButton.test.js
Normal file
23
src/bootstrap/__tests__/DownloadButton.test.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
|
import DownloadButton from "../DownloadButton";
|
||||||
|
|
||||||
|
describe("<DownloadButton />", () => {
|
||||||
|
it("should have download attribute", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<DownloadButton href="http://example.com">
|
||||||
|
Test Button
|
||||||
|
</DownloadButton>
|
||||||
|
);
|
||||||
|
expect(container.firstChild.getAttribute("download")).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { render, fireEvent, getByLabelText, wait } from "customTestRender";
|
import { render, fireEvent, getByLabelText, waitFor } from "customTestRender";
|
||||||
|
|
||||||
import { NumberInput } from "../NumberInput";
|
import NumberInput from "../NumberInput";
|
||||||
|
|
||||||
describe("<NumberInput/>", () => {
|
describe("<NumberInput/>", () => {
|
||||||
const onChangeMock = jest.fn();
|
const onChangeMock = jest.fn();
|
||||||
@ -32,14 +32,18 @@ describe("<NumberInput/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Increase number with button", async () => {
|
it("Increase number with button", async () => {
|
||||||
const increaseButton = getByLabelText(componentContainer, "Increase");
|
const increaseButton = getByLabelText(componentContainer, /Increase/);
|
||||||
fireEvent.mouseDown(increaseButton);
|
fireEvent.mouseDown(increaseButton);
|
||||||
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({"target": {"value": 2}}));
|
await waitFor(() =>
|
||||||
|
expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 2 } })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Decrease number with button", async () => {
|
it("Decrease number with button", async () => {
|
||||||
const decreaseButton = getByLabelText(componentContainer, "Decrease");
|
const decreaseButton = getByLabelText(componentContainer, /Decrease/);
|
||||||
fireEvent.mouseDown(decreaseButton);
|
fireEvent.mouseDown(decreaseButton);
|
||||||
await wait(() => expect(onChangeMock).toHaveBeenCalledWith({"target": {"value": 0}}));
|
await waitFor(() =>
|
||||||
|
expect(onChangeMock).toHaveBeenCalledWith({ target: { value: 0 } })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import React from "react";
|
|||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
import { PasswordInput } from "../PasswordInput";
|
import PasswordInput from "../PasswordInput";
|
||||||
|
|
||||||
describe("<PasswordInput/>", () => {
|
describe("<PasswordInput/>", () => {
|
||||||
it("Render password input", () => {
|
it("Render password input", () => {
|
||||||
@ -18,11 +18,9 @@ describe("<PasswordInput/>", () => {
|
|||||||
label="Test label"
|
label="Test label"
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value="Some password"
|
value="Some password"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,37 +9,35 @@ import React from "react";
|
|||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
import { RadioSet } from "../RadioSet";
|
import RadioSet from "../RadioSet";
|
||||||
|
|
||||||
const TEST_CHOICES = [
|
const TEST_CHOICES = [
|
||||||
{
|
{
|
||||||
label: "label",
|
label: "label",
|
||||||
value: "value"
|
value: "value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "another label",
|
label: "another label",
|
||||||
value: "another value"
|
value: "another value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "another one label",
|
label: "another one label",
|
||||||
value: "another on value"
|
value: "another on value",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("<RadioSet/>", () => {
|
describe("<RadioSet/>", () => {
|
||||||
it("Render radio set", () => {
|
it("Render radio set", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<RadioSet
|
<RadioSet
|
||||||
name={"test_name"}
|
name="test_name"
|
||||||
label='Radios set label'
|
label="Radios set label"
|
||||||
value='value'
|
value="value"
|
||||||
choices={TEST_CHOICES}
|
choices={TEST_CHOICES}
|
||||||
helpText={"Some help text"}
|
helpText="Some help text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,27 +7,31 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { fireEvent, getByDisplayValue, getByText, render } from "customTestRender";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
getByDisplayValue,
|
||||||
|
getByText,
|
||||||
|
render,
|
||||||
|
} from "customTestRender";
|
||||||
|
|
||||||
import { Select } from "../Select";
|
import Select from "../Select";
|
||||||
|
|
||||||
const TEST_CHOICES = {
|
const TEST_CHOICES = {
|
||||||
"1": "one",
|
1: "one",
|
||||||
"2": "two",
|
2: "two",
|
||||||
"3": "three",
|
3: "three",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<Select/>", () => {
|
describe("<Select/>", () => {
|
||||||
var selectContainer;
|
let selectContainer;
|
||||||
const onChangeHandler = jest.fn();
|
const onChangeHandler = jest.fn();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<Select
|
<Select
|
||||||
label='Test label'
|
label="Test label"
|
||||||
value='1'
|
value="1"
|
||||||
choices={TEST_CHOICES}
|
choices={TEST_CHOICES}
|
||||||
helpText='Help text'
|
helpText="Help text"
|
||||||
|
|
||||||
onChange={onChangeHandler}
|
onChange={onChangeHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -35,21 +39,17 @@ describe("<Select/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Test with snapshot.", () => {
|
it("Test with snapshot.", () => {
|
||||||
expect(selectContainer)
|
expect(selectContainer).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test onChange handling.", () => {
|
it("Test onChange handling.", () => {
|
||||||
const select = getByDisplayValue(selectContainer, "one");
|
const select = getByDisplayValue(selectContainer, "one");
|
||||||
expect(select.value)
|
expect(select.value).toBe("1");
|
||||||
.toBe("1");
|
|
||||||
fireEvent.change(select, { target: { value: "2" } });
|
fireEvent.change(select, { target: { value: "2" } });
|
||||||
|
|
||||||
const option = getByText(selectContainer, "two");
|
const option = getByText(selectContainer, "two");
|
||||||
expect(onChangeHandler)
|
expect(onChangeHandler).toBeCalled();
|
||||||
.toBeCalled();
|
|
||||||
|
|
||||||
expect(option.value)
|
expect(option.value).toBe("2");
|
||||||
.toBe("2");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
33
src/bootstrap/__tests__/Switch.test.js
Normal file
33
src/bootstrap/__tests__/Switch.test.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
|
import Switch from "../Switch";
|
||||||
|
|
||||||
|
describe("<Switch/>", () => {
|
||||||
|
it("Render switch", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Switch
|
||||||
|
label="Test label"
|
||||||
|
checked
|
||||||
|
helpText="Some help text"
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Render uncheked switch", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Switch label="Test label" helpText="Some help text" />
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -9,7 +9,7 @@ import React from "react";
|
|||||||
|
|
||||||
import { render } from "customTestRender";
|
import { render } from "customTestRender";
|
||||||
|
|
||||||
import { TextInput } from "../TextInput";
|
import TextInput from "../TextInput";
|
||||||
|
|
||||||
describe("<TextInput/>", () => {
|
describe("<TextInput/>", () => {
|
||||||
it("Render text input", () => {
|
it("Render text input", () => {
|
||||||
@ -18,11 +18,9 @@ describe("<TextInput/>", () => {
|
|||||||
label="Test label"
|
label="Test label"
|
||||||
helpText="Some help text"
|
helpText="Some help text"
|
||||||
value="Some text"
|
value="Some text"
|
||||||
onChange={() => {
|
onChange={() => {}}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(container.firstChild)
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,39 +2,38 @@
|
|||||||
|
|
||||||
exports[`<Button /> Render button correctly 1`] = `
|
exports[`<Button /> Render button correctly 1`] = `
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary "
|
class="btn btn-primary d-inline-flex justify-content-center align-items-center"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
<span>
|
||||||
|
Test Button
|
||||||
Test Button
|
</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> Render button with custom classes 1`] = `
|
exports[`<Button /> Render button with custom classes 1`] = `
|
||||||
<button
|
<button
|
||||||
class="btn one two three"
|
class="btn one two three d-inline-flex justify-content-center align-items-center"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
<span>
|
||||||
|
Test Button
|
||||||
Test Button
|
</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> Render button with spinner 1`] = `
|
exports[`<Button /> Render button with spinner 1`] = `
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary "
|
class="btn btn-primary d-inline-flex justify-content-center align-items-center"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm me-1"
|
||||||
role="status"
|
role="status"
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
Test Button
|
||||||
|
</span>
|
||||||
Test Button
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
@ -2,61 +2,51 @@
|
|||||||
|
|
||||||
exports[`<Checkbox/> Render checkbox 1`] = `
|
exports[`<Checkbox/> Render checkbox 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3 form-check"
|
||||||
style="margin-bottom: 1rem;"
|
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="form-check-input"
|
||||||
|
id="1"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-checkbox"
|
class="form-text"
|
||||||
style="margin-bottom: 0px;"
|
|
||||||
>
|
>
|
||||||
<input
|
<small>
|
||||||
checked=""
|
Some help text
|
||||||
class="custom-control-input"
|
</small>
|
||||||
id="1"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="custom-control-label"
|
|
||||||
for="1"
|
|
||||||
style="margin-bottom: 0px;"
|
|
||||||
>
|
|
||||||
Test label
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<small
|
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
|
||||||
Some help text
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Checkbox/> Render uncheked checkbox 1`] = `
|
exports[`<Checkbox/> Render uncheked checkbox 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3 form-check"
|
||||||
style="margin-bottom: 1rem;"
|
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
id="1"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-checkbox"
|
class="form-text"
|
||||||
style="margin-bottom: 0px;"
|
|
||||||
>
|
>
|
||||||
<input
|
<small>
|
||||||
class="custom-control-input"
|
Some help text
|
||||||
id="1"
|
</small>
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="custom-control-label"
|
|
||||||
for="1"
|
|
||||||
style="margin-bottom: 0px;"
|
|
||||||
>
|
|
||||||
Test label
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<small
|
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
|
||||||
Some help text
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -2,51 +2,48 @@
|
|||||||
|
|
||||||
exports[`<NumberInput/> Render number input 1`] = `
|
exports[`<NumberInput/> Render number input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<div
|
<label
|
||||||
class="form-group"
|
class="form-label"
|
||||||
|
for="1"
|
||||||
>
|
>
|
||||||
<label
|
Test label
|
||||||
for="1"
|
</label>
|
||||||
|
<div
|
||||||
|
class="input-group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="1"
|
||||||
|
type="number"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Increase value. Hint: Hold to increase faster."
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
title="Increase value. Hint: Hold to increase faster."
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Test label
|
<i
|
||||||
</label>
|
class="fa"
|
||||||
<div
|
|
||||||
class="input-group"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
id="1"
|
|
||||||
type="number"
|
|
||||||
value="1"
|
|
||||||
/>
|
/>
|
||||||
<div
|
</button>
|
||||||
class="input-group-append"
|
<button
|
||||||
>
|
aria-label="Decrease value. Hint: Hold to decrease faster."
|
||||||
<button
|
class="btn btn-outline-secondary"
|
||||||
aria-label="Increase"
|
title="Decrease value. Hint: Hold to decrease faster."
|
||||||
class="btn btn-outline-secondary"
|
type="button"
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fas fa-plus"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="Decrease"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fas fa-minus"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small
|
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
>
|
||||||
|
<i
|
||||||
|
class="fa"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="form-text"
|
||||||
|
>
|
||||||
|
<small>
|
||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,30 +2,29 @@
|
|||||||
|
|
||||||
exports[`<PasswordInput/> Render password input 1`] = `
|
exports[`<PasswordInput/> Render password input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<div
|
<label
|
||||||
class="form-group"
|
class="form-label"
|
||||||
|
for="1"
|
||||||
>
|
>
|
||||||
<label
|
Test label
|
||||||
for="1"
|
</label>
|
||||||
>
|
<div
|
||||||
Test label
|
class="input-group"
|
||||||
</label>
|
>
|
||||||
<div
|
<input
|
||||||
class="input-group"
|
autocomplete="current-password"
|
||||||
>
|
class="form-control"
|
||||||
<input
|
id="1"
|
||||||
autocomplete="new-password"
|
type="password"
|
||||||
class="form-control"
|
value="Some password"
|
||||||
id="1"
|
/>
|
||||||
type="password"
|
</div>
|
||||||
value="Some password"
|
<div
|
||||||
/>
|
class="form-text"
|
||||||
</div>
|
>
|
||||||
<small
|
<small>
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
|
||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,72 +2,72 @@
|
|||||||
|
|
||||||
exports[`<RadioSet/> Render radio set 1`] = `
|
exports[`<RadioSet/> Render radio set 1`] = `
|
||||||
<div
|
<div
|
||||||
class="form-group col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3"
|
||||||
style="margin-bottom: 1rem;"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="col-12"
|
class="d-block"
|
||||||
for="1"
|
for="1"
|
||||||
style="padding-left: 0px;"
|
|
||||||
>
|
>
|
||||||
Radios set label
|
Radios set label
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-radio custom-control-inline"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked=""
|
checked=""
|
||||||
class="custom-control-input"
|
class="form-check-input me-2"
|
||||||
id="test_name-0"
|
id="test_name-0"
|
||||||
name="test_name"
|
name="test_name"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="value"
|
value="value"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="custom-control-label"
|
class="form-check-label"
|
||||||
for="test_name-0"
|
for="test_name-0"
|
||||||
>
|
>
|
||||||
label
|
label
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-radio custom-control-inline"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="custom-control-input"
|
class="form-check-input me-2"
|
||||||
id="test_name-1"
|
id="test_name-1"
|
||||||
name="test_name"
|
name="test_name"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="another value"
|
value="another value"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="custom-control-label"
|
class="form-check-label"
|
||||||
for="test_name-1"
|
for="test_name-1"
|
||||||
>
|
>
|
||||||
another label
|
another label
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="custom-control custom-radio custom-control-inline"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="custom-control-input"
|
class="form-check-input me-2"
|
||||||
id="test_name-2"
|
id="test_name-2"
|
||||||
name="test_name"
|
name="test_name"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="another on value"
|
value="another on value"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="custom-control-label"
|
class="form-check-label"
|
||||||
for="test_name-2"
|
for="test_name-2"
|
||||||
>
|
>
|
||||||
another one label
|
another one label
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small
|
<div
|
||||||
class="form-text text-muted"
|
class="form-text"
|
||||||
>
|
>
|
||||||
Some help text
|
<small>
|
||||||
</small>
|
Some help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
exports[`<Select/> Test with snapshot. 1`] = `
|
exports[`<Select/> Test with snapshot. 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="form-group col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
class="form-label"
|
||||||
for="1"
|
for="1"
|
||||||
>
|
>
|
||||||
Test label
|
Test label
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
class="custom-select"
|
class="form-select"
|
||||||
id="1"
|
id="1"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@ -30,11 +31,13 @@ exports[`<Select/> Test with snapshot. 1`] = `
|
|||||||
three
|
three
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small
|
<div
|
||||||
class="form-text text-muted"
|
class="form-text"
|
||||||
>
|
>
|
||||||
Help text
|
<small>
|
||||||
</small>
|
Help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
54
src/bootstrap/__tests__/__snapshots__/Switch.test.js.snap
Normal file
54
src/bootstrap/__tests__/__snapshots__/Switch.test.js.snap
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Switch/> Render switch 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-check form-switch mb-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="form-check-input"
|
||||||
|
id="1"
|
||||||
|
role="switch"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="form-text"
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
Some help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<Switch/> Render uncheked switch 1`] = `
|
||||||
|
<div
|
||||||
|
class="form-check form-switch mb-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
id="1"
|
||||||
|
role="switch"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="1"
|
||||||
|
>
|
||||||
|
Test label
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="form-text"
|
||||||
|
>
|
||||||
|
<small>
|
||||||
|
Some help text
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -2,29 +2,28 @@
|
|||||||
|
|
||||||
exports[`<TextInput/> Render text input 1`] = `
|
exports[`<TextInput/> Render text input 1`] = `
|
||||||
<div
|
<div
|
||||||
class="col-sm-12 offset-lg-1 col-lg-10"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<div
|
<label
|
||||||
class="form-group"
|
class="form-label"
|
||||||
|
for="1"
|
||||||
>
|
>
|
||||||
<label
|
Test label
|
||||||
for="1"
|
</label>
|
||||||
>
|
<div
|
||||||
Test label
|
class="input-group"
|
||||||
</label>
|
>
|
||||||
<div
|
<input
|
||||||
class="input-group"
|
class="form-control"
|
||||||
>
|
id="1"
|
||||||
<input
|
type="text"
|
||||||
class="form-control"
|
value="Some text"
|
||||||
id="1"
|
/>
|
||||||
type="text"
|
</div>
|
||||||
value="Some text"
|
<div
|
||||||
/>
|
class="form-text"
|
||||||
</div>
|
>
|
||||||
<small
|
<small>
|
||||||
class="form-text text-muted"
|
|
||||||
>
|
|
||||||
Some help text
|
Some help text
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 CZ.NIC z.s.p.o. (http://www.nic.cz/)
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
*
|
*
|
||||||
* This is free software, licensed under the GNU General Public License v3.
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
* See /LICENSE for more information.
|
* See /LICENSE for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Bootstrap column size for form fields */
|
/** Bootstrap column size for form fields */
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
const formFieldsSize = "card p-4 col-sm-12 col-lg-12 p-0 mb-4";
|
||||||
export const formFieldsSize = "col-sm-12 offset-lg-1 col-lg-10";
|
const buttonFormFieldsSize = "col-sm-12 col-lg-12 p-0 mb-3";
|
||||||
|
|
||||||
|
export { formFieldsSize, buttonFormFieldsSize };
|
||||||
|
157
src/common/ActionButtonWithModal/ActionButtonWithModal.js
Normal file
157
src/common/ActionButtonWithModal/ActionButtonWithModal.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { useAPIPost, useAPIPut } from "../../api/hooks";
|
||||||
|
import { API_STATE } from "../../api/utils";
|
||||||
|
import Button from "../../bootstrap/Button";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "../../bootstrap/Modal";
|
||||||
|
import { useAlert } from "../../context/alertContext/AlertContext";
|
||||||
|
|
||||||
|
ActionButtonWithModal.propTypes = {
|
||||||
|
/** Component that triggers the action. */
|
||||||
|
actionTrigger: PropTypes.elementType.isRequired,
|
||||||
|
/** Method to use for the action. */
|
||||||
|
actionMethod: PropTypes.string,
|
||||||
|
/** URL to send the action to. */
|
||||||
|
actionUrl: PropTypes.string.isRequired,
|
||||||
|
/** Title of the modal. */
|
||||||
|
modalTitle: PropTypes.string.isRequired,
|
||||||
|
/** Message of the modal. */
|
||||||
|
modalMessage: PropTypes.string.isRequired,
|
||||||
|
/** Text of the action button in the modal. */
|
||||||
|
modalActionText: PropTypes.string,
|
||||||
|
/** Props for the action button in the modal. */
|
||||||
|
modalActionProps: PropTypes.object,
|
||||||
|
/** Message to display on successful action. */
|
||||||
|
successMessage: PropTypes.string,
|
||||||
|
/** Message to display on failed action. */
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionButtonWithModal({
|
||||||
|
actionTrigger: ActionTriggerComponent,
|
||||||
|
actionMethod = "POST",
|
||||||
|
actionUrl,
|
||||||
|
modalTitle,
|
||||||
|
modalMessage,
|
||||||
|
modalActionText,
|
||||||
|
modalActionProps,
|
||||||
|
successMessage,
|
||||||
|
errorMessage,
|
||||||
|
}) {
|
||||||
|
const [triggered, setTriggered] = useState(false);
|
||||||
|
const [modalShown, setModalShown] = useState(false);
|
||||||
|
const [triggerPostActionStatus, triggerPostAction] = useAPIPost(actionUrl);
|
||||||
|
const [triggerPutActionStatus, triggerPutAction] = useAPIPut(actionUrl);
|
||||||
|
|
||||||
|
const [setAlert] = useAlert();
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
triggerPostActionStatus.state === API_STATE.SUCCESS ||
|
||||||
|
triggerPutActionStatus.state === API_STATE.SUCCESS
|
||||||
|
) {
|
||||||
|
setAlert(
|
||||||
|
successMessage || _("Action successful."),
|
||||||
|
API_STATE.SUCCESS
|
||||||
|
);
|
||||||
|
setTriggered(false);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
triggerPostActionStatus.state === API_STATE.ERROR ||
|
||||||
|
triggerPutActionStatus.state === API_STATE.ERROR
|
||||||
|
) {
|
||||||
|
setAlert(errorMessage || _("Action failed."));
|
||||||
|
setTriggered(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
triggerPostActionStatus,
|
||||||
|
triggerPutActionStatus,
|
||||||
|
setAlert,
|
||||||
|
successMessage,
|
||||||
|
errorMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const actionHandler = () => {
|
||||||
|
setTriggered(true);
|
||||||
|
if (actionMethod === "POST") {
|
||||||
|
triggerPostAction();
|
||||||
|
} else {
|
||||||
|
triggerPutAction();
|
||||||
|
}
|
||||||
|
setModalShown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionModal
|
||||||
|
shown={modalShown}
|
||||||
|
setShown={setModalShown}
|
||||||
|
onAction={actionHandler}
|
||||||
|
title={modalTitle}
|
||||||
|
message={modalMessage}
|
||||||
|
actionText={modalActionText}
|
||||||
|
actionProps={modalActionProps}
|
||||||
|
/>
|
||||||
|
<ActionTriggerComponent
|
||||||
|
loading={triggered}
|
||||||
|
disabled={triggered}
|
||||||
|
onClick={() => setModalShown(true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionModal.propTypes = {
|
||||||
|
shown: PropTypes.bool.isRequired,
|
||||||
|
setShown: PropTypes.func.isRequired,
|
||||||
|
onAction: PropTypes.func.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
actionText: PropTypes.string,
|
||||||
|
actionProps: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionModal({
|
||||||
|
shown,
|
||||||
|
setShown,
|
||||||
|
onAction,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
actionText,
|
||||||
|
actionProps,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Modal shown={shown} setShown={setShown}>
|
||||||
|
<ModalHeader setShown={setShown} title={title} />
|
||||||
|
<ModalBody>
|
||||||
|
<p className="mb-0">{message}</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => setShown(false)}
|
||||||
|
>
|
||||||
|
{_("Cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onAction} {...actionProps}>
|
||||||
|
{actionText || _("Confirm")}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActionButtonWithModal;
|
39
src/common/ActionButtonWithModal/ActionButtonWithModal.md
Normal file
39
src/common/ActionButtonWithModal/ActionButtonWithModal.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
RebootButton component is a button that opens a modal dialog to confirm the
|
||||||
|
reboot of the device.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useEffect, createContext } from "react";
|
||||||
|
|
||||||
|
import Button from "../../bootstrap/Button";
|
||||||
|
import { AlertContextProvider } from "../../context/alertContext/AlertContext";
|
||||||
|
import ActionButtonWithModal from "./ActionButtonWithModal";
|
||||||
|
|
||||||
|
window.AlertContext = React.createContext();
|
||||||
|
|
||||||
|
const RebootButtonExample = () => {
|
||||||
|
const ActionButton = (props) => {
|
||||||
|
return <Button {...props}>Action</Button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertContextProvider>
|
||||||
|
<div id="modal-container" />
|
||||||
|
<div id="alert-container" />
|
||||||
|
<ActionButtonWithModal
|
||||||
|
actionTrigger={ActionButton}
|
||||||
|
actionUrl="/reforis/api/action"
|
||||||
|
modalTitle="Warning!"
|
||||||
|
modalMessage="Are you sure you want to perform this action?"
|
||||||
|
modalActionText="Confirm action"
|
||||||
|
modalActionProps={{ className: "btn-danger" }}
|
||||||
|
successMessage="Action request succeeded."
|
||||||
|
errorMessage="Action request failed."
|
||||||
|
/>
|
||||||
|
</AlertContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
<RebootButtonExample />;
|
||||||
|
```
|
118
src/common/RichTable/RichTable.js
Normal file
118
src/common/RichTable/RichTable.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import RichTableBody from "./RichTableBody";
|
||||||
|
import RichTableColumnsDropdown from "./RichTableColumnsDropdown";
|
||||||
|
import RichTableHeader from "./RichTableHeader";
|
||||||
|
import RichTablePagination from "./RichTablePagination";
|
||||||
|
import Input from "../../bootstrap/Input";
|
||||||
|
|
||||||
|
RichTable.propTypes = {
|
||||||
|
/** Columns to be displayed in the table */
|
||||||
|
columns: PropTypes.array.isRequired,
|
||||||
|
/** Data to be displayed in the table, must be passed as a stable reference, for example, useState */
|
||||||
|
data: PropTypes.array.isRequired,
|
||||||
|
/** Whether to display pagination */
|
||||||
|
withPagination: PropTypes.bool,
|
||||||
|
/** Number of rows per page, the default is 5 */
|
||||||
|
pageSize: PropTypes.number,
|
||||||
|
/** Index of the current page */
|
||||||
|
pageIndex: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RichTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
withPagination,
|
||||||
|
pageSize = 5,
|
||||||
|
pageIndex = 0,
|
||||||
|
}) {
|
||||||
|
const tableColumns = useMemo(() => columns, [columns]);
|
||||||
|
const [sorting, setSorting] = useState([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState({});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: tableColumns,
|
||||||
|
filterFns: {
|
||||||
|
fuzzy: fuzzyFilter,
|
||||||
|
},
|
||||||
|
globalFilterFn: "fuzzy",
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
pagination,
|
||||||
|
globalFilter,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationIsNeeded = data.length > pageSize && withPagination;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
|
<Input
|
||||||
|
className="me-3"
|
||||||
|
type="text"
|
||||||
|
placeholder={_("Search…")}
|
||||||
|
value={globalFilter ?? ""}
|
||||||
|
onChange={(e) => setGlobalFilter(String(e.target.value))}
|
||||||
|
/>
|
||||||
|
<RichTableColumnsDropdown columns={table.getAllLeafColumns()} />
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-hover text-nowrap">
|
||||||
|
<RichTableHeader table={table} flexRender={flexRender} />
|
||||||
|
<RichTableBody
|
||||||
|
table={table}
|
||||||
|
columns={tableColumns}
|
||||||
|
flexRender={flexRender}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
{paginationIsNeeded && (
|
||||||
|
<RichTablePagination
|
||||||
|
table={table}
|
||||||
|
tablePageSize={pageSize}
|
||||||
|
allRows={data.length}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzyFilter(row, columnId, value, addMeta) {
|
||||||
|
const itemRank = rankItem(row.getValue(columnId), value);
|
||||||
|
addMeta({ itemRank });
|
||||||
|
return itemRank.passed;
|
||||||
|
}
|
135
src/common/RichTable/RichTable.md
Normal file
135
src/common/RichTable/RichTable.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
### Description
|
||||||
|
|
||||||
|
Rich Table is a table component based on
|
||||||
|
[Tanstack React Table](https://tanstack.com/table/). It adds some features to
|
||||||
|
the table component, such as:
|
||||||
|
|
||||||
|
- **Pagination**: The table can be paginated.
|
||||||
|
- **Sorting**: The table can be sorted by columns.
|
||||||
|
- **Row Expansion**: The table rows can be expanded. (To be implemented)
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import RichTable from "./RichTable";
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
header: "Name",
|
||||||
|
accessorKey: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Surname",
|
||||||
|
accessorKey: "surname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Age",
|
||||||
|
accessorKey: "age",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Phone",
|
||||||
|
accessorKey: "phone",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
name: "John",
|
||||||
|
surname: "Coltrane",
|
||||||
|
age: 30,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jane",
|
||||||
|
surname: "Doe",
|
||||||
|
age: 25,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice",
|
||||||
|
surname: "Smith",
|
||||||
|
age: 35,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob",
|
||||||
|
surname: "Smith",
|
||||||
|
age: 40,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Charlie",
|
||||||
|
surname: "Brown",
|
||||||
|
age: 45,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Daisy",
|
||||||
|
surname: "Brown",
|
||||||
|
age: 50,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Eve",
|
||||||
|
surname: "Johnson",
|
||||||
|
age: 55,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Frank",
|
||||||
|
surname: "Johnson",
|
||||||
|
age: 60,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Grace",
|
||||||
|
surname: "Williams",
|
||||||
|
age: 65,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Henry",
|
||||||
|
surname: "Williams",
|
||||||
|
age: 70,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ivy",
|
||||||
|
surname: "Brown",
|
||||||
|
age: 75,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jack",
|
||||||
|
surname: "Brown",
|
||||||
|
age: 80,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Kelly",
|
||||||
|
surname: "Johnson",
|
||||||
|
age: 85,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Liam",
|
||||||
|
surname: "Johnson",
|
||||||
|
age: 90,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mia",
|
||||||
|
surname: "Williams",
|
||||||
|
age: 95,
|
||||||
|
phone: "123456789",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nathan",
|
||||||
|
surname: "Williams",
|
||||||
|
age: 100,
|
||||||
|
phone: "987654321",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<RichTable columns={columns} data={data} withPagination />;
|
||||||
|
```
|
58
src/common/RichTable/RichTableBody.js
Normal file
58
src/common/RichTable/RichTableBody.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import propTypes from "prop-types";
|
||||||
|
|
||||||
|
RichTableBody.propTypes = {
|
||||||
|
table: propTypes.shape({
|
||||||
|
getRowModel: propTypes.func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
columns: propTypes.array.isRequired,
|
||||||
|
flexRender: propTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function RichTableBody({ table, columns, flexRender }) {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<tr key={row.id} className="align-middle">
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
{...(cell.column.columnDef
|
||||||
|
.className && {
|
||||||
|
className:
|
||||||
|
cell.column.columnDef.className,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="text-center py-4">
|
||||||
|
<span>{_("No results.")}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichTableBody;
|
90
src/common/RichTable/RichTableColumnsDropdown.js
Normal file
90
src/common/RichTable/RichTableColumnsDropdown.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { faCheck, faRotateLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import Button from "../../bootstrap/Button";
|
||||||
|
|
||||||
|
RichTableColumnsDropdown.propTypes = {
|
||||||
|
columns: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function RichTableColumnsDropdown({ columns }) {
|
||||||
|
return (
|
||||||
|
<div className="dropdown mb-3">
|
||||||
|
<Button
|
||||||
|
className="btn btn-outline-secondary dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
>
|
||||||
|
{_("Columns")}
|
||||||
|
</Button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
|
{columns.map((column) => {
|
||||||
|
return (
|
||||||
|
<li key={column.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dropdown-item d-flex align-items-center"
|
||||||
|
onClick={column.getToggleVisibilityHandler()}
|
||||||
|
style={{ paddingLeft: "2rem" }}
|
||||||
|
disabled={
|
||||||
|
column.columnDef?.enableHiding === false
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{column.getIsVisible() && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheck}
|
||||||
|
className="position-absolute text-secondary me-2"
|
||||||
|
style={{ left: "0.6rem" }}
|
||||||
|
width="1rem"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{column.columnDef.header}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{columns.some((column) => !column.getIsVisible()) && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dropdown-item d-flex align-items-center"
|
||||||
|
style={{ paddingLeft: "2rem" }}
|
||||||
|
onClick={() => {
|
||||||
|
// toggleVisibility for columns that are hidden
|
||||||
|
columns.forEach((column) => {
|
||||||
|
if (!column.getIsVisible()) {
|
||||||
|
column.toggleVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faRotateLeft}
|
||||||
|
className="position-absolute text-secondary me-2"
|
||||||
|
width="1rem"
|
||||||
|
style={{ left: "0.6rem" }}
|
||||||
|
/>
|
||||||
|
{_("Reset")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichTableColumnsDropdown;
|
102
src/common/RichTable/RichTableHeader.js
Normal file
102
src/common/RichTable/RichTableHeader.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
faSquareCaretUp,
|
||||||
|
faSquareCaretDown,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import propTypes from "prop-types";
|
||||||
|
|
||||||
|
RichTableHeader.propTypes = {
|
||||||
|
table: propTypes.shape({
|
||||||
|
getHeaderGroups: propTypes.func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
flexRender: propTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function RichTableHeader({ table, flexRender }) {
|
||||||
|
const getThTitle = (header) => {
|
||||||
|
if (!header.column.getCanSort()) return undefined;
|
||||||
|
|
||||||
|
const nextSortingOrder = header.column.getNextSortingOrder();
|
||||||
|
if (nextSortingOrder === "asc") return _("Sort ascending");
|
||||||
|
if (nextSortingOrder === "desc") return _("Sort descending");
|
||||||
|
return _("Clear sort");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className="table-light">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id} role="row">
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th
|
||||||
|
key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
{...(header.column.columnDef.headerClassName && {
|
||||||
|
className:
|
||||||
|
header.column.columnDef.headerClassName,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ||
|
||||||
|
header.column.columnDef.headerIsHidden ? (
|
||||||
|
<div className="d-none" aria-hidden="true">
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={
|
||||||
|
header.column.columnDef
|
||||||
|
.headerClassName === "text-center"
|
||||||
|
? { justifySelf: "center" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
className={`btn btn-link text-decoration-none text-reset fw-bold p-0 d-flex align-items-center
|
||||||
|
${
|
||||||
|
header.column.getCanSort()
|
||||||
|
? "d-flex align-items-center"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
title={getThTitle(header)}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{{
|
||||||
|
asc: (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSquareCaretUp}
|
||||||
|
className="ms-1 text-primary"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
desc: (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSquareCaretDown}
|
||||||
|
className="ms-1 text-primary"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}[header.column.getIsSorted()] ?? null}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichTableHeader;
|
128
src/common/RichTable/RichTablePagination.js
Normal file
128
src/common/RichTable/RichTablePagination.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
faAngleLeft,
|
||||||
|
faAnglesLeft,
|
||||||
|
faAngleRight,
|
||||||
|
faAnglesRight,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import propTypes from "prop-types";
|
||||||
|
|
||||||
|
RichTablePagination.propTypes = {
|
||||||
|
table: propTypes.shape({
|
||||||
|
getState: propTypes.func.isRequired,
|
||||||
|
getCanPreviousPage: propTypes.func.isRequired,
|
||||||
|
getCanNextPage: propTypes.func.isRequired,
|
||||||
|
firstPage: propTypes.func.isRequired,
|
||||||
|
previousPage: propTypes.func.isRequired,
|
||||||
|
nextPage: propTypes.func.isRequired,
|
||||||
|
lastPage: propTypes.func.isRequired,
|
||||||
|
setPageSize: propTypes.func.isRequired,
|
||||||
|
getPageCount: propTypes.func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
tablePageSize: propTypes.number,
|
||||||
|
allRows: propTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function RichTablePagination({ table, tablePageSize, allRows }) {
|
||||||
|
const { pagination } = table.getState();
|
||||||
|
const prevPagBtnDisabled = !table.getCanPreviousPage();
|
||||||
|
const nextPagBtnDisabled = !table.getCanNextPage();
|
||||||
|
|
||||||
|
const pageSizes = useMemo(() => {
|
||||||
|
return [tablePageSize ?? 5, 10, 25].filter(
|
||||||
|
(value, index, self) => self.indexOf(value) === index
|
||||||
|
);
|
||||||
|
}, [tablePageSize]);
|
||||||
|
|
||||||
|
const renderPaginationButton = (icon, ariaLabel, onClick, disabled) => (
|
||||||
|
<li
|
||||||
|
className={`page-item ${disabled ? "disabled" : ""}`}
|
||||||
|
style={{ cursor: disabled ? "not-allowed" : "pointer" }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="page-link"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label={_("Pagination navigation bar")}
|
||||||
|
className="d-flex gap-2 justify-content-start align-items-center mx-2 mb-1 text-nowrap"
|
||||||
|
>
|
||||||
|
<ul className="pagination pagination-sm mb-0">
|
||||||
|
{renderPaginationButton(
|
||||||
|
faAnglesLeft,
|
||||||
|
_("First page"),
|
||||||
|
() => table.firstPage(),
|
||||||
|
prevPagBtnDisabled
|
||||||
|
)}
|
||||||
|
{renderPaginationButton(
|
||||||
|
faAngleLeft,
|
||||||
|
_("Previous page"),
|
||||||
|
() => table.previousPage(),
|
||||||
|
prevPagBtnDisabled
|
||||||
|
)}
|
||||||
|
{renderPaginationButton(
|
||||||
|
faAngleRight,
|
||||||
|
_("Next page"),
|
||||||
|
() => table.nextPage(),
|
||||||
|
nextPagBtnDisabled
|
||||||
|
)}
|
||||||
|
{renderPaginationButton(
|
||||||
|
faAnglesRight,
|
||||||
|
_("Last page"),
|
||||||
|
() => table.lastPage(),
|
||||||
|
nextPagBtnDisabled
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<span>
|
||||||
|
{_("Page")}
|
||||||
|
<span className="fw-bold">
|
||||||
|
{pagination.pageIndex + 1}
|
||||||
|
{_("of")}
|
||||||
|
{table.getPageCount().toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="vr mx-1 align-self-center"
|
||||||
|
style={{ height: "1.5rem" }}
|
||||||
|
/>
|
||||||
|
<span>{_("Rows per page:")}</span>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm w-auto"
|
||||||
|
aria-label={_("Select rows per page")}
|
||||||
|
value={pagination.pageSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
table.setPageSize(Number(e.target.value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageSizes.map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
{pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option key={allRows} value={allRows}>
|
||||||
|
{_("All")}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichTablePagination;
|
77
src/common/WiFiSettings/ResetWiFiSettings.js
Normal file
77
src/common/WiFiSettings/ResetWiFiSettings.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { useAPIPost } from "../../api/hooks";
|
||||||
|
import { API_STATE } from "../../api/utils";
|
||||||
|
import { ALERT_TYPES } from "../../bootstrap/Alert";
|
||||||
|
import Button from "../../bootstrap/Button";
|
||||||
|
import { formFieldsSize } from "../../bootstrap/constants";
|
||||||
|
import { useAlert } from "../../context/alertContext/AlertContext";
|
||||||
|
|
||||||
|
ResetWiFiSettings.propTypes = {
|
||||||
|
ws: PropTypes.object.isRequired,
|
||||||
|
endpoint: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ResetWiFiSettings({ ws, endpoint }) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const module = "wifi";
|
||||||
|
ws.subscribe(module).bind(module, "reset", () => {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
});
|
||||||
|
}, [ws]);
|
||||||
|
|
||||||
|
const [postResetResponse, postReset] = useAPIPost(endpoint);
|
||||||
|
const [setAlert, dismissAlert] = useAlert();
|
||||||
|
useEffect(() => {
|
||||||
|
if (postResetResponse.state === API_STATE.ERROR) {
|
||||||
|
setAlert(_("An error occurred during resetting Wi-Fi settings."));
|
||||||
|
} else if (postResetResponse.state === API_STATE.SUCCESS) {
|
||||||
|
setAlert(
|
||||||
|
_("Wi-Fi settings are set to defaults."),
|
||||||
|
ALERT_TYPES.SUCCESS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [postResetResponse, setAlert]);
|
||||||
|
|
||||||
|
const onReset = () => {
|
||||||
|
dismissAlert();
|
||||||
|
setIsLoading(true);
|
||||||
|
postReset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={formFieldsSize}>
|
||||||
|
<h2>{_("Reset Wi-Fi Settings")}</h2>
|
||||||
|
<p>
|
||||||
|
{_(
|
||||||
|
"If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the current Wi-Fi configuration and restore the default values."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="text-end">
|
||||||
|
<Button
|
||||||
|
className="btn-primary"
|
||||||
|
forisFormSize
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
{_("Reset Wi-Fi Settings")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResetWiFiSettings;
|
306
src/common/WiFiSettings/WiFiForm.js
Normal file
306
src/common/WiFiSettings/WiFiForm.js
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2022 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { HELP_TEXTS, HTMODES, BANDS, ENCRYPTIONMODES } from "./constants";
|
||||||
|
import WifiGuestForm from "./WiFiGuestForm";
|
||||||
|
import WiFiQRCode from "./WiFiQRCode";
|
||||||
|
import PasswordInput from "../../bootstrap/PasswordInput";
|
||||||
|
import RadioSet from "../../bootstrap/RadioSet";
|
||||||
|
import Select from "../../bootstrap/Select";
|
||||||
|
import Switch from "../../bootstrap/Switch";
|
||||||
|
import TextInput from "../../bootstrap/TextInput";
|
||||||
|
|
||||||
|
WiFiForm.propTypes = {
|
||||||
|
formData: PropTypes.shape({ devices: PropTypes.arrayOf(PropTypes.object) })
|
||||||
|
.isRequired,
|
||||||
|
formErrors: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||||
|
setFormValue: PropTypes.func.isRequired,
|
||||||
|
hasGuestNetwork: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
WiFiForm.defaultProps = {
|
||||||
|
formData: { devices: [] },
|
||||||
|
setFormValue: () => {},
|
||||||
|
hasGuestNetwork: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WiFiForm({
|
||||||
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
hasGuestNetwork,
|
||||||
|
disabled,
|
||||||
|
}) {
|
||||||
|
return formData.devices.map((device, index) => (
|
||||||
|
<DeviceForm
|
||||||
|
key={device.id}
|
||||||
|
formData={device}
|
||||||
|
deviceIndex={index}
|
||||||
|
formErrors={(formErrors || [])[index]}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
hasGuestNetwork={hasGuestNetwork}
|
||||||
|
disabled={disabled}
|
||||||
|
divider={index + 1 !== formData.devices.length}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceForm.propTypes = {
|
||||||
|
formData: PropTypes.shape({
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
|
SSID: PropTypes.string.isRequired,
|
||||||
|
password: PropTypes.string.isRequired,
|
||||||
|
hidden: PropTypes.bool.isRequired,
|
||||||
|
band: PropTypes.string.isRequired,
|
||||||
|
htmode: PropTypes.string.isRequired,
|
||||||
|
channel: PropTypes.string.isRequired,
|
||||||
|
guest_wifi: PropTypes.object.isRequired,
|
||||||
|
encryption: PropTypes.string.isRequired,
|
||||||
|
available_bands: PropTypes.array.isRequired,
|
||||||
|
ieee80211w_disabled: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
formErrors: PropTypes.object.isRequired,
|
||||||
|
setFormValue: PropTypes.func.isRequired,
|
||||||
|
hasGuestNetwork: PropTypes.bool,
|
||||||
|
deviceIndex: PropTypes.number,
|
||||||
|
divider: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceForm.defaultProps = {
|
||||||
|
formErrors: {},
|
||||||
|
hasGuestNetwork: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function DeviceForm({
|
||||||
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
hasGuestNetwork,
|
||||||
|
deviceIndex,
|
||||||
|
divider,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const deviceID = formData.id;
|
||||||
|
const bnds = formData.available_bands;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
label={<h2 className="mb-0">{_(`Wi-Fi ${deviceID + 1}`)}</h2>}
|
||||||
|
checked={formData.enabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { enabled: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
switchHeading
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{formData.enabled && (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label="SSID"
|
||||||
|
value={formData.SSID}
|
||||||
|
error={formErrors.SSID || null}
|
||||||
|
helpText={HELP_TEXTS.ssid}
|
||||||
|
required
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: {
|
||||||
|
SSID: { $set: value },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<WiFiQRCode
|
||||||
|
SSID={formData.SSID}
|
||||||
|
password={formData.password}
|
||||||
|
/>
|
||||||
|
</TextInput>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
withEye
|
||||||
|
label={_("Password")}
|
||||||
|
value={formData.password}
|
||||||
|
error={formErrors.password}
|
||||||
|
helpText={HELP_TEXTS.password}
|
||||||
|
required
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { password: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label={_("Hide SSID")}
|
||||||
|
helpText={HELP_TEXTS.hidden}
|
||||||
|
checked={formData.hidden}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { hidden: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioSet
|
||||||
|
name={`band-${deviceID}`}
|
||||||
|
label={_("Band")}
|
||||||
|
choices={getBandChoices(formData)}
|
||||||
|
value={formData.band}
|
||||||
|
helpText={HELP_TEXTS.band}
|
||||||
|
inline
|
||||||
|
onChange={setFormValue((value) => {
|
||||||
|
// Find the selected band
|
||||||
|
const selectedBand = bnds.find(
|
||||||
|
(band) => band.band === value
|
||||||
|
);
|
||||||
|
// Get the last item in the available HT modes for the selected band
|
||||||
|
const bestHtmode =
|
||||||
|
selectedBand.available_htmodes.slice(-1)[0];
|
||||||
|
return {
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: {
|
||||||
|
band: { $set: value },
|
||||||
|
channel: { $set: "0" },
|
||||||
|
htmode: { $set: bestHtmode },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={_("802.11n/ac/ax mode")}
|
||||||
|
choices={getHtmodeChoices(formData)}
|
||||||
|
value={formData.htmode}
|
||||||
|
helpText={HELP_TEXTS.htmode}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { htmode: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={_("Channel")}
|
||||||
|
choices={getChannelChoices(formData)}
|
||||||
|
value={formData.channel}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { channel: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={_("Encryption")}
|
||||||
|
choices={getEncryptionChoices(formData)}
|
||||||
|
helpText={HELP_TEXTS.wpa3}
|
||||||
|
value={formData.encryption}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: { encryption: { $set: value } },
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(formData.encryption === "WPA3" ||
|
||||||
|
formData.encryption === "WPA2/3") && (
|
||||||
|
<Switch
|
||||||
|
label={_("Disable Management Frame Protection")}
|
||||||
|
helpText={_(
|
||||||
|
"In case you have trouble connecting to WiFi Access Point, try disabling Management Frame Protection."
|
||||||
|
)}
|
||||||
|
checked={formData.ieee80211w_disabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[deviceIndex]: {
|
||||||
|
ieee80211w_disabled: { $set: value },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasGuestNetwork && (
|
||||||
|
<WifiGuestForm
|
||||||
|
formData={{
|
||||||
|
id: deviceIndex,
|
||||||
|
...formData.guest_wifi,
|
||||||
|
}}
|
||||||
|
formErrors={formErrors.guest_wifi || {}}
|
||||||
|
setFormValue={setFormValue}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{divider && <hr />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChannelChoices(device) {
|
||||||
|
const channelChoices = {
|
||||||
|
0: _("auto"),
|
||||||
|
};
|
||||||
|
|
||||||
|
device.available_bands.forEach((availableBand) => {
|
||||||
|
if (availableBand.band !== device.band) return;
|
||||||
|
|
||||||
|
availableBand.available_channels.forEach((availableChannel) => {
|
||||||
|
channelChoices[availableChannel.number.toString()] = `
|
||||||
|
${availableChannel.number}
|
||||||
|
(${availableChannel.frequency} MHz ${
|
||||||
|
availableChannel.radar ? " ,DFS" : ""
|
||||||
|
})
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return channelChoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtmodeChoices(device) {
|
||||||
|
const htmodeChoices = {};
|
||||||
|
|
||||||
|
device.available_bands.forEach((availableBand) => {
|
||||||
|
if (availableBand.band !== device.band) return;
|
||||||
|
|
||||||
|
availableBand.available_htmodes.forEach((availableHtmod) => {
|
||||||
|
htmodeChoices[availableHtmod] = HTMODES[availableHtmod];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return htmodeChoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBandChoices(device) {
|
||||||
|
return device.available_bands.map((availableBand) => ({
|
||||||
|
label: `${BANDS[availableBand.band]} GHz`,
|
||||||
|
value: availableBand.band,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEncryptionChoices(device) {
|
||||||
|
if (device.encryption === "custom") {
|
||||||
|
ENCRYPTIONMODES.custom = _("Custom");
|
||||||
|
}
|
||||||
|
return ENCRYPTIONMODES;
|
||||||
|
}
|
112
src/common/WiFiSettings/WiFiGuestForm.js
Normal file
112
src/common/WiFiSettings/WiFiGuestForm.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { HELP_TEXTS, ENCRYPTIONMODES } from "./constants";
|
||||||
|
import WiFiQRCode from "./WiFiQRCode";
|
||||||
|
import PasswordInput from "../../bootstrap/PasswordInput";
|
||||||
|
import Select from "../../bootstrap/Select";
|
||||||
|
import Switch from "../../bootstrap/Switch";
|
||||||
|
import TextInput from "../../bootstrap/TextInput";
|
||||||
|
|
||||||
|
WifiGuestForm.propTypes = {
|
||||||
|
formData: PropTypes.shape({
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
SSID: PropTypes.string.isRequired,
|
||||||
|
password: PropTypes.string.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
|
encryption: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
formErrors: PropTypes.shape({
|
||||||
|
SSID: PropTypes.string,
|
||||||
|
password: PropTypes.string,
|
||||||
|
}),
|
||||||
|
setFormValue: PropTypes.func.isRequired,
|
||||||
|
deviceIndex: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WifiGuestForm({
|
||||||
|
formData,
|
||||||
|
formErrors,
|
||||||
|
setFormValue,
|
||||||
|
deviceIndex,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
label={_("Enable Guest Wi-Fi")}
|
||||||
|
checked={formData.enabled}
|
||||||
|
helpText={HELP_TEXTS.guest_wifi_enabled}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[formData.id]: {
|
||||||
|
guest_wifi: { enabled: { $set: value } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{formData.enabled ? (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
label="SSID"
|
||||||
|
value={formData.SSID}
|
||||||
|
error={formErrors.SSID}
|
||||||
|
helpText={HELP_TEXTS.ssid}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[formData.id]: {
|
||||||
|
guest_wifi: { SSID: { $set: value } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<WiFiQRCode
|
||||||
|
SSID={formData.SSID}
|
||||||
|
password={formData.password}
|
||||||
|
/>
|
||||||
|
</TextInput>
|
||||||
|
<PasswordInput
|
||||||
|
withEye
|
||||||
|
label={_("Password")}
|
||||||
|
value={formData.password}
|
||||||
|
helpText={HELP_TEXTS.password}
|
||||||
|
error={formErrors.password}
|
||||||
|
required
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[formData.id]: {
|
||||||
|
guest_wifi: { password: { $set: value } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={_("Encryption")}
|
||||||
|
choices={ENCRYPTIONMODES}
|
||||||
|
helpText={HELP_TEXTS.wpa3}
|
||||||
|
value={formData.encryption}
|
||||||
|
onChange={setFormValue((value) => ({
|
||||||
|
devices: {
|
||||||
|
[formData.id]: {
|
||||||
|
guest_wifi: { encryption: { $set: value } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
103
src/common/WiFiSettings/WiFiQRCode.js
Normal file
103
src/common/WiFiSettings/WiFiQRCode.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2025 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
|
||||||
|
import { createAndDownloadPdf, toQRCodeContent } from "./qrCodeHelpers";
|
||||||
|
import Button from "../../bootstrap/Button";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "../../bootstrap/Modal";
|
||||||
|
|
||||||
|
WiFiQRCode.propTypes = {
|
||||||
|
SSID: PropTypes.string.isRequired,
|
||||||
|
password: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WiFiQRCode({ SSID, password }) {
|
||||||
|
const [modal, setModal] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="input-group-text"
|
||||||
|
onClick={() => setModal(true)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="fa-solid fa-qrcode"
|
||||||
|
title={_("Show QR code")}
|
||||||
|
aria-label={_("Show QR code")}
|
||||||
|
className="text-secondary"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{modal && (
|
||||||
|
<QRCodeModal
|
||||||
|
setShown={setModal}
|
||||||
|
shown={modal}
|
||||||
|
SSID={SSID}
|
||||||
|
password={password}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRCodeModal.propTypes = {
|
||||||
|
SSID: PropTypes.string.isRequired,
|
||||||
|
password: PropTypes.string.isRequired,
|
||||||
|
shown: PropTypes.bool.isRequired,
|
||||||
|
setShown: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function QRCodeModal({ shown, setShown, SSID, password }) {
|
||||||
|
return (
|
||||||
|
<Modal setShown={setShown} shown={shown}>
|
||||||
|
<ModalHeader setShown={setShown} title={_("Wi-Fi QR Code")} />
|
||||||
|
<ModalBody>
|
||||||
|
<QRCodeSVG
|
||||||
|
className="d-block mx-auto img-logo-black"
|
||||||
|
value={toQRCodeContent(SSID, password)}
|
||||||
|
level="M"
|
||||||
|
size={350}
|
||||||
|
marginSize={0}
|
||||||
|
imageSettings={{
|
||||||
|
src: "/reforis/static/reforis/imgs/turris.svg",
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
excavate: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => setShown(false)}
|
||||||
|
>
|
||||||
|
{_("Close")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="btn-primary"
|
||||||
|
onClick={() => createAndDownloadPdf(SSID, password)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="fa-solid fa-file-download"
|
||||||
|
className="me-2"
|
||||||
|
/>
|
||||||
|
{_("Download PDF")}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
122
src/common/WiFiSettings/WiFiSettings.js
Normal file
122
src/common/WiFiSettings/WiFiSettings.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2024 CZ.NIC z.s.p.o. (https://www.nic.cz/)
|
||||||
|
*
|
||||||
|
* This is free software, licensed under the GNU General Public License v3.
|
||||||
|
* See /LICENSE for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import ResetWiFiSettings from "./ResetWiFiSettings";
|
||||||
|
import WiFiForm from "./WiFiForm";
|
||||||
|
import ForisForm from "../../form/components/ForisForm";
|
||||||
|
|
||||||
|
WiFiSettings.propTypes = {
|
||||||
|
ws: PropTypes.object.isRequired,
|
||||||
|
endpoint: PropTypes.string.isRequired,
|
||||||
|
resetEndpoint: PropTypes.string.isRequired,
|
||||||
|
hasGuestNetwork: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
function WiFiSettings({ ws, endpoint, resetEndpoint, hasGuestNetwork }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ForisForm
|
||||||
|
ws={ws}
|
||||||
|
forisConfig={{
|
||||||
|
endpoint,
|
||||||
|
wsModule: "wifi",
|
||||||
|
}}
|
||||||
|
prepData={prepData}
|
||||||
|
prepDataToSubmit={prepDataToSubmit}
|
||||||
|
validator={validator}
|
||||||
|
>
|
||||||
|
<WiFiForm hasGuestNetwork={hasGuestNetwork} />
|
||||||
|
</ForisForm>
|
||||||
|
<ResetWiFiSettings ws={ws} endpoint={resetEndpoint} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepData(formData) {
|
||||||
|
formData.devices.forEach((device, idx) => {
|
||||||
|
formData.devices[idx].channel = device.channel.toString();
|
||||||
|
});
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepDataToSubmit(formData) {
|
||||||
|
formData.devices.forEach((device, idx) => {
|
||||||
|
delete device.available_bands;
|
||||||
|
|
||||||
|
formData.devices[idx].channel = parseInt(device.channel);
|
||||||
|
|
||||||
|
if (!device.enabled) {
|
||||||
|
formData.devices[idx] = { id: device.id, enabled: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device.guest_wifi.enabled)
|
||||||
|
formData.devices[idx].guest_wifi = { enabled: false };
|
||||||
|
|
||||||
|
if (device.encryption === "WPA2") {
|
||||||
|
delete formData.devices[idx].ieee80211w_disabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function byteCount(string) {
|
||||||
|
const buffer = Buffer.from(string, "utf-8");
|
||||||
|
const count = buffer.byteLength;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validator(formData) {
|
||||||
|
const formErrors = formData.devices.map((device) => {
|
||||||
|
if (!device.enabled) return {};
|
||||||
|
|
||||||
|
const errors = {};
|
||||||
|
if (device.SSID.length > 32)
|
||||||
|
errors.SSID = _("SSID can't be longer than 32 symbols");
|
||||||
|
if (device.SSID.length === 0) errors.SSID = _("SSID can't be empty");
|
||||||
|
if (byteCount(device.SSID) > 32)
|
||||||
|
errors.SSID = _("SSID can't be longer than 32 bytes");
|
||||||
|
|
||||||
|
if (device.password.length < 8)
|
||||||
|
errors.password = _("Password must contain at least 8 symbols");
|
||||||
|
if (device.password.length >= 64)
|
||||||
|
errors.password = _(
|
||||||
|
"Password must not contain more than 63 symbols"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!device.guest_wifi.enabled) return errors;
|
||||||
|
|
||||||
|
const guest_wifi_errors = {};
|
||||||
|
if (device.guest_wifi.SSID.length > 32)
|
||||||
|
guest_wifi_errors.SSID = _("SSID can't be longer than 32 symbols");
|
||||||
|
if (device.guest_wifi.SSID.length === 0)
|
||||||
|
guest_wifi_errors.SSID = _("SSID can't be empty");
|
||||||
|
if (byteCount(device.guest_wifi.SSID) > 32)
|
||||||
|
guest_wifi_errors.SSID = _("SSID can't be longer than 32 bytes");
|
||||||
|
|
||||||
|
if (device.guest_wifi.password.length < 8)
|
||||||
|
guest_wifi_errors.password = _(
|
||||||
|
"Password must contain at least 8 symbols"
|
||||||
|
);
|
||||||
|
if (device.guest_wifi.password.length >= 64)
|
||||||
|
guest_wifi_errors.password = _(
|
||||||
|
"Password must not contain more than 63 symbols"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (guest_wifi_errors.SSID || guest_wifi_errors.password) {
|
||||||
|
errors.guest_wifi = guest_wifi_errors;
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
return JSON.stringify(formErrors).match(/\[[{},?]+\]/) ? null : formErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WiFiSettings;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user