Compare commits
580 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
047af45cc1 | |
![]() |
355b3d2c9c | |
![]() |
7140fac0af | |
![]() |
6a060c9b11 | |
![]() |
10c3a80a7d | |
![]() |
fc82cc962f | |
![]() |
daec71f7eb | |
![]() |
bb944efb16 | |
![]() |
b55dc1864d | |
![]() |
db6659ce4b | |
![]() |
432b0c1aec | |
![]() |
75e9f4cb04 | |
![]() |
5fc0aea585 | |
![]() |
748a31a0b6 | |
![]() |
0b71235389 | |
![]() |
8b0f41bcd1 | |
![]() |
a31a06cd72 | |
![]() |
b45ec9c8d1 | |
![]() |
c0253b5498 | |
![]() |
1e78cca1d3 | |
![]() |
251d8df336 | |
![]() |
8a7d4baca8 | |
![]() |
8becc53ddf | |
![]() |
a2a136a336 | |
![]() |
3835908542 | |
![]() |
c6e865bec1 | |
![]() |
5fa1fc7a1f | |
![]() |
07ee4ebd90 | |
![]() |
60b7cb9614 | |
![]() |
1ae52f0ebd | |
![]() |
18856cc719 | |
![]() |
7317a3e14d | |
![]() |
fefe1bd93a | |
![]() |
954c3fb1d1 | |
![]() |
69b1633af8 | |
![]() |
790228f9e4 | |
![]() |
d330a80e60 | |
![]() |
68fd241829 | |
![]() |
2e54e4461f | |
![]() |
c70db6153b | |
![]() |
a8808f2e1c | |
![]() |
d3382d172a | |
![]() |
67b6b0e511 | |
![]() |
a6df4bf396 | |
![]() |
7bf7e41d7f | |
![]() |
a41f4ddc5e | |
![]() |
ba5552830e | |
![]() |
3fe2f7aff0 | |
![]() |
a96ed745cc | |
![]() |
53436c2c03 | |
![]() |
44471af96e | |
![]() |
7ee80fd923 | |
![]() |
c51568fdde | |
![]() |
307d5c179b | |
![]() |
eb0024c8cc | |
![]() |
516c1068a3 | |
![]() |
2039105c88 | |
![]() |
cf35548edf | |
![]() |
fb4a28c068 | |
![]() |
c17ea4234c | |
![]() |
b54537cd34 | |
![]() |
3da9ad26c5 | |
![]() |
0168a343d3 | |
![]() |
c2dafa17c4 | |
![]() |
8c839138f6 | |
![]() |
6c65d64b81 | |
![]() |
bd2805b23b | |
![]() |
10ef97a4d4 | |
![]() |
cbe5ed9386 | |
![]() |
f738ca5cf5 | |
![]() |
5947a1ba42 | |
![]() |
05941739bf | |
![]() |
b25fe0cefb | |
![]() |
54bec393bf | |
![]() |
7a0d228f77 | |
![]() |
4e81ebe622 | |
![]() |
83cf8ed165 | |
![]() |
f0d9ab32bb | |
![]() |
c46fcc9db8 | |
![]() |
e22906afa4 | |
![]() |
b7d31ddb0c | |
![]() |
f731691cd1 | |
![]() |
11cde0b638 | |
![]() |
fc5df403af | |
![]() |
4e4613d8ab | |
![]() |
24bbf0788b | |
![]() |
35463b9631 | |
![]() |
f194c394d2 | |
![]() |
592e934fe8 | |
![]() |
c30f034cf2 | |
![]() |
4193e13c7c | |
![]() |
14ed79b7b9 | |
![]() |
acbfdbb89d | |
![]() |
2261b9915a | |
![]() |
dbed0cb27d | |
![]() |
8990c9971a | |
![]() |
750f2c3fe0 | |
![]() |
e0211d2164 | |
![]() |
78acb059c2 | |
![]() |
fc32b5b54c | |
![]() |
f01398e80a | |
![]() |
9b83d227db | |
![]() |
89ff246e64 | |
![]() |
d9198099a7 | |
![]() |
5e0ccf8e3d | |
![]() |
c1ad39d7ad | |
![]() |
39c4a66b75 | |
![]() |
79ca64ab34 | |
![]() |
0d630b21ec | |
![]() |
14a0ae877b | |
![]() |
02d5c20616 | |
![]() |
d9fd9585ad | |
![]() |
7ccf4cd274 | |
![]() |
16529e049c | |
![]() |
1072afb31a | |
![]() |
775733eff8 | |
![]() |
0f6ac10c91 | |
![]() |
a34246352e | |
![]() |
c989e3f371 | |
![]() |
5de1accc77 | |
![]() |
abfaaa8e5a | |
![]() |
20794eaa21 | |
![]() |
cc0e71988a | |
![]() |
203fb4e7d1 | |
![]() |
c02262184f | |
![]() |
829cbb0ae2 | |
![]() |
2a72cb1bb5 | |
![]() |
8ba4dbb728 | |
![]() |
a794d30892 | |
![]() |
9911e78b50 | |
![]() |
3a71083a05 | |
![]() |
6e6b6cfba7 | |
![]() |
d735151d9b | |
![]() |
38563dbe6d | |
![]() |
e74594a970 | |
![]() |
79a4a0b41c | |
![]() |
fff5a765fe | |
![]() |
4f0ceccb85 | |
![]() |
d41f292e2d | |
![]() |
2b6bf4de94 | |
![]() |
75f53344d2 | |
![]() |
b14ee74c36 | |
![]() |
00da5012d8 | |
![]() |
11662c8421 | |
![]() |
a6444d411d | |
![]() |
e3e2d3e0af | |
![]() |
e69e39e436 | |
![]() |
e1aefbf5b6 | |
![]() |
750c1a2895 | |
![]() |
da2f064383 | |
![]() |
5c6ac137f2 | |
![]() |
3a635e2d44 | |
![]() |
767b409886 | |
![]() |
c29660bf5d | |
![]() |
5d8ce7a028 | |
![]() |
af9427823d | |
![]() |
46c9f83508 | |
![]() |
8cca77377a | |
![]() |
0a239323f1 | |
![]() |
e445967d82 | |
![]() |
1dea4bda2f | |
![]() |
e987b8d710 | |
![]() |
3c2d27310d | |
![]() |
7374c2758e | |
![]() |
645580b5ca | |
![]() |
3b91e4b035 | |
![]() |
8ef3217f5f | |
![]() |
c913705752 | |
![]() |
75634a30d2 | |
![]() |
e4e3825def | |
![]() |
e9628ec2ba | |
![]() |
ceb504bb5f | |
![]() |
5e1e2a6386 | |
![]() |
dc3ab4b1b6 | |
![]() |
e8db8c6107 | |
![]() |
1ccd3d28f2 | |
![]() |
9387c810ba | |
![]() |
dfb6852eb3 | |
![]() |
b2299346f8 | |
![]() |
00bc2dde8a | |
![]() |
fb897c79b8 | |
![]() |
7ab1ad86e9 | |
![]() |
e3f551e965 | |
![]() |
0965a94fe6 | |
![]() |
980593dc54 | |
![]() |
9c729759a2 | |
![]() |
943b83be90 | |
![]() |
5ad9e8f0f6 | |
![]() |
fd63444630 | |
![]() |
1c6f990d4d | |
![]() |
b76187562d | |
![]() |
ab68450bea | |
![]() |
60e2a1a7ea | |
![]() |
4ee4e74bd7 | |
![]() |
2a59690aae | |
![]() |
ab82a0dbf6 | |
![]() |
9178f1e902 | |
![]() |
0b4974a338 | |
![]() |
6212b949ab | |
![]() |
60fca4899f | |
![]() |
ae6402803a | |
![]() |
dfb5522024 | |
![]() |
f8631f9685 | |
![]() |
15b265a37c | |
![]() |
769e2c3be3 | |
![]() |
e06ed0b968 | |
![]() |
6821110d7e | |
![]() |
2063f88507 | |
![]() |
c2ab9288c1 | |
![]() |
0f527048cc | |
![]() |
9ad372ff59 | |
![]() |
338ed7ce39 | |
![]() |
65cc1caba1 | |
![]() |
8f6e9da135 | |
![]() |
af666ee2e5 | |
![]() |
f828a9c198 | |
![]() |
7ac23fb2be | |
![]() |
8930a537b9 | |
![]() |
200f4f276c | |
![]() |
3d5680e2c9 | |
![]() |
a5c6bf15dd | |
![]() |
3872a19c58 | |
![]() |
c77d2988b9 | |
![]() |
1585fbfa45 | |
![]() |
b51c2e91ec | |
![]() |
3318c4589d | |
![]() |
7e78f04c24 | |
![]() |
e2147ea5b6 | |
![]() |
dd6e2f6c81 | |
![]() |
a4b767f412 | |
![]() |
88a40e7dfb | |
![]() |
a8e2180713 | |
![]() |
19f5ba15f8 | |
![]() |
991da15d52 | |
![]() |
5b9eaee29f | |
![]() |
81f22c6c93 | |
![]() |
1acf0eee19 | |
![]() |
9333e8518b | |
![]() |
3b7084fa87 | |
![]() |
7573cfd75c | |
![]() |
27b349a82a | |
![]() |
d101e3cc43 | |
![]() |
3be7479750 | |
![]() |
28b7ba5811 | |
![]() |
12970ae0d8 | |
![]() |
08437acb0b | |
![]() |
eb97eaeb38 | |
![]() |
efe6834d99 | |
![]() |
4da4952695 | |
![]() |
58654fc769 | |
![]() |
89599dd1b2 | |
![]() |
301e5d742a | |
![]() |
80e2b3c4b2 | |
![]() |
9f6ae048da | |
![]() |
e882e848f1 | |
![]() |
c1ab9abd47 | |
![]() |
c8d7237446 | |
![]() |
c959e957f2 | |
![]() |
4e2820fcdf | |
![]() |
c28542949c | |
![]() |
e16417537d | |
![]() |
2fa41164dc | |
![]() |
fbe19433eb | |
![]() |
1ebf191c6e | |
![]() |
a153f87fba | |
![]() |
4581bf1751 | |
![]() |
d3bcb1cd7c | |
![]() |
c9f6843f96 | |
![]() |
10ea389822 | |
![]() |
a63cfa23bf | |
![]() |
ca41ec07a4 | |
![]() |
b35817e074 | |
![]() |
c0582bd756 | |
![]() |
0c2086e33a | |
![]() |
c269b4847e | |
![]() |
378cd7bb58 | |
![]() |
69597c7464 | |
![]() |
f6677886c3 | |
![]() |
8e9cbdd6aa | |
![]() |
5199e7adc8 | |
![]() |
b9527532b6 | |
![]() |
0a10b1907d | |
![]() |
724233cbb5 | |
![]() |
34426f5172 | |
![]() |
5776400033 | |
![]() |
0915e28fc8 | |
![]() |
151b1b4eba | |
![]() |
6771a17e70 | |
![]() |
510312d347 | |
![]() |
e6c2db66de | |
![]() |
c2f3561263 | |
![]() |
7cc8ed3526 | |
![]() |
e9d0911cf9 | |
![]() |
15c14fc081 | |
![]() |
39d4dc81cf | |
![]() |
761024cc74 | |
![]() |
879e8908a0 | |
![]() |
6312517dba | |
![]() |
1c3d6b8ff5 | |
![]() |
3591cfaec5 | |
![]() |
473fd60d99 | |
![]() |
61e9ff1b2f | |
![]() |
a41cf84083 | |
![]() |
a9169268d2 | |
![]() |
b4f20185e0 | |
![]() |
8f11f7eefd | |
![]() |
97439432cc | |
![]() |
af0eabdf2f | |
![]() |
b419c90924 | |
![]() |
b9e9dab146 | |
![]() |
99ca73d6b4 | |
![]() |
6e72ae02b9 | |
![]() |
3eb063f81a | |
![]() |
3cd0ca29b6 | |
![]() |
eeeb8cd11a | |
![]() |
948cf7cedb | |
![]() |
ceadfbbdc6 | |
![]() |
c8edb09ff1 | |
![]() |
c17a87fdb0 | |
![]() |
91b589917a | |
![]() |
5d2f8b6c09 | |
![]() |
ae0d5b18a4 | |
![]() |
d64af3f022 | |
![]() |
f7a64d562d | |
![]() |
468303cb81 | |
![]() |
68ad03a2da | |
![]() |
a9761955fe | |
![]() |
b7cc366312 | |
![]() |
f300183630 | |
![]() |
3f02eca41a | |
![]() |
eef725684d | |
![]() |
63ff9bb2e4 | |
![]() |
ad7fc93b84 | |
![]() |
8ef4b7bc8b | |
![]() |
ca910cfdaf | |
![]() |
adae7af409 | |
![]() |
7206577eed | |
![]() |
bfd571a7a9 | |
![]() |
78c812d31f | |
![]() |
3c0c6d7fdb | |
![]() |
8f9b344604 | |
![]() |
0e25b7ad6d | |
![]() |
dc2ea316c5 | |
![]() |
1dfc90a9c0 | |
![]() |
76ab9949aa | |
![]() |
4d44e03a18 | |
![]() |
daf073e418 | |
![]() |
345e55feb7 | |
![]() |
3c8869007c | |
![]() |
f5c5f79b56 | |
![]() |
5b7f4f3d9d | |
![]() |
27b0c9b65c | |
![]() |
d7009289a8 | |
![]() |
a6035e16ae | |
![]() |
829a50492c | |
![]() |
56ee854e71 | |
![]() |
43ca2e68a8 | |
![]() |
a7bfd3fd4b | |
![]() |
c9e45c3909 | |
![]() |
ee6fbbcfc7 | |
![]() |
6cd9a55a52 | |
![]() |
0869b37c0d | |
![]() |
fea127f7b2 | |
![]() |
2adc044c9a | |
![]() |
47f84385e1 | |
![]() |
a44db2781d | |
![]() |
f4d689e431 | |
![]() |
c0445de79a | |
![]() |
5e40de20b9 | |
![]() |
a375d707cd | |
![]() |
4f0b28ebb8 | |
![]() |
59e23e02ab | |
![]() |
ffe78a67d2 | |
![]() |
8ad30f8301 | |
![]() |
7573a07d76 | |
![]() |
63ac38c926 | |
![]() |
37ef6f6c69 | |
![]() |
df61e0c5a0 | |
![]() |
651d01031c | |
![]() |
30c678e770 | |
![]() |
5b1a7bc301 | |
![]() |
010ae19bc4 | |
![]() |
ae329b91a3 | |
![]() |
a83607ada3 | |
![]() |
c1899b5689 | |
![]() |
1d2f1760c9 | |
![]() |
2a8218d99f | |
![]() |
ef74b725cc | |
![]() |
dbbdeb5f5b | |
![]() |
dbe3ea8c88 | |
![]() |
8abf651a46 | |
![]() |
6a05393c11 | |
![]() |
f28d80d920 | |
![]() |
d0982f6390 | |
![]() |
02e0d5bd13 | |
![]() |
c983f416eb | |
![]() |
87cb4a3ea8 | |
![]() |
5b1d0fc50d | |
![]() |
4b9687a454 | |
![]() |
997badd17c | |
![]() |
3aa1497786 | |
![]() |
3678c8e3c1 | |
![]() |
a0b43f392a | |
![]() |
c71531eb5c | |
![]() |
05f037cb64 | |
![]() |
da88e5c957 | |
![]() |
dddce06dea | |
![]() |
51445a4ac9 | |
![]() |
215b4e1e71 | |
![]() |
7a4f6c329a | |
![]() |
c53cf0a319 | |
![]() |
db51be5b8f | |
![]() |
2826c97756 | |
![]() |
2c161ccfb3 | |
![]() |
1ca5152665 | |
![]() |
9ac2167ff3 | |
![]() |
6183ad0818 | |
![]() |
a40f8542f0 | |
![]() |
6468044213 | |
![]() |
5bea30b700 | |
![]() |
2b61463060 | |
![]() |
e31d1ebca0 | |
![]() |
c93546b594 | |
![]() |
724f56f20b | |
![]() |
c2768194cd | |
![]() |
31ab881b05 | |
![]() |
08a962cf48 | |
![]() |
d8185bde99 | |
![]() |
4e62cf6ee8 | |
![]() |
cf24889fdd | |
![]() |
7bae51f34d | |
![]() |
2e1cf3f92e | |
![]() |
96bd32226f | |
![]() |
f8f620a13e | |
![]() |
4eaf565ee4 | |
![]() |
453ba13514 | |
![]() |
c9a5ce81ba | |
![]() |
6748253759 | |
![]() |
dc51405ee1 | |
![]() |
5cdf4de4fb | |
![]() |
b8b187acf7 | |
![]() |
f081c23e6d | |
![]() |
bfccce21de | |
![]() |
1734977a11 | |
![]() |
14eddc5459 | |
![]() |
7b9b664cdf | |
![]() |
75197b9070 | |
![]() |
00a2c4af34 | |
![]() |
3644a03338 | |
![]() |
776eda5355 | |
![]() |
32a12b884f | |
![]() |
6dd1f4556f | |
![]() |
eae79c1684 | |
![]() |
50d691f76d | |
![]() |
f167e664e3 | |
![]() |
99a6d46777 | |
![]() |
60e300ff31 | |
![]() |
50c5934e83 | |
![]() |
9b37c81336 | |
![]() |
c7559082c3 | |
![]() |
b0f709695b | |
![]() |
90ccde4601 | |
![]() |
fcdcdf9919 | |
![]() |
4783b42d0f | |
![]() |
1d9c834c9d | |
![]() |
c88f6f02e3 | |
![]() |
4efa1f30ff | |
![]() |
792a58722d | |
![]() |
17c68c2c36 | |
![]() |
187a090866 | |
![]() |
15c770e9d0 | |
![]() |
b07ffa6281 | |
![]() |
a1d9d53517 | |
![]() |
962952f662 | |
![]() |
7af07bfcf0 | |
![]() |
ccef257c32 | |
![]() |
3be97749f9 | |
![]() |
b3d9ad640b | |
![]() |
6f8268f3ac | |
![]() |
35081eb85d | |
![]() |
7c5a337037 | |
![]() |
5a5df08fca | |
![]() |
68aa8b57e0 | |
![]() |
d11d1f0de6 | |
![]() |
ad68519ac2 | |
![]() |
ab8b6699d8 | |
![]() |
d20f82faa9 | |
![]() |
5411c68538 | |
![]() |
17aa9b4cbd | |
![]() |
c62eeec1d8 | |
![]() |
7609cf4ecf | |
![]() |
6282e4e401 | |
![]() |
f408122d82 | |
![]() |
160ce5216a | |
![]() |
e1a9e016e9 | |
![]() |
a08b2a2f01 | |
![]() |
7748ca0d43 | |
![]() |
15eb3df690 | |
![]() |
cd8a71f584 | |
![]() |
dea1963199 | |
![]() |
eff6cb50eb | |
![]() |
db9745ef81 | |
![]() |
3d6bd45a6d | |
![]() |
cc2a7b0a39 | |
![]() |
ecb580ec95 | |
![]() |
835fbe17af | |
![]() |
2477bdd345 | |
![]() |
17b4be7f7d | |
![]() |
05433c9c8c | |
![]() |
c9dd10762f | |
![]() |
74498e6d1c | |
![]() |
52dd4b4f55 | |
![]() |
10c361b27e | |
![]() |
d5b8b0b719 | |
![]() |
41a9bfdf6c | |
![]() |
75711f7da4 | |
![]() |
8c3c2e7e3f | |
![]() |
9028adde12 | |
![]() |
f771baf2c0 | |
![]() |
86b6c8e70f | |
![]() |
7a5a8843e3 | |
![]() |
2ab3909bae | |
![]() |
93863f99d1 | |
![]() |
f78d6643b7 | |
![]() |
9f358cb80b | |
![]() |
965ef42eac | |
![]() |
821a6a02bf | |
![]() |
9c33ed50de | |
![]() |
c1d48d752c | |
![]() |
54a47d42bd | |
![]() |
6c8ffe0a64 | |
![]() |
d0e02c6ead | |
![]() |
ce7505532d | |
![]() |
b28f34fd32 | |
![]() |
3077b35a79 | |
![]() |
4af4f45ebf | |
![]() |
b9e6a37df8 | |
![]() |
86439e3cd8 | |
![]() |
b98c97b788 | |
![]() |
b807caa328 | |
![]() |
32764a90ed | |
![]() |
e62f440b65 | |
![]() |
567e29f3c9 | |
![]() |
6223ac42eb | |
![]() |
2db6203c64 | |
![]() |
8b0f688d3f | |
![]() |
4d90117796 | |
![]() |
75c4f97889 | |
![]() |
4590c8fd6e | |
![]() |
f692fa5aaa | |
![]() |
b6f1f354ea | |
![]() |
84c0afab0c | |
![]() |
148a92e6e1 | |
![]() |
1587a979ab | |
![]() |
85b08b5d2c | |
![]() |
3466ac2e06 | |
![]() |
dc2631967f | |
![]() |
00e9e8fcdf | |
![]() |
7da2f96bba | |
![]() |
43c8678845 | |
![]() |
a1704d079b | |
![]() |
76d114242c | |
![]() |
2b20791a46 | |
![]() |
76d6260dc0 | |
![]() |
401a58dc71 | |
![]() |
5a2c27129b | |
![]() |
b939749f7a | |
![]() |
4d9ef128fc | |
![]() |
6eb6a2d80d | |
![]() |
cc7da36de0 | |
![]() |
cce963ec18 | |
![]() |
5e08aaaec4 | |
![]() |
51140602ac | |
![]() |
3986ade2c8 | |
![]() |
8ca6317b74 | |
![]() |
850c18ce58 | |
![]() |
8a705e91b9 | |
![]() |
4f0cce1b54 | |
![]() |
c85fa56223 | |
![]() |
4ba6ae7d69 |
|
@ -1,7 +1,12 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = false
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
end_of_line = LF
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
|
@ -0,0 +1,8 @@
|
||||||
|
**/node_modules
|
||||||
|
**/public
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/config
|
||||||
|
**/scripts
|
||||||
|
**/docs
|
||||||
|
**/playground
|
|
@ -0,0 +1,529 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["react-app", "airbnb"],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"plugins": [
|
||||||
|
"babel",
|
||||||
|
"react",
|
||||||
|
"jsx-a11y",
|
||||||
|
"import",
|
||||||
|
"react-hooks"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"strapi": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.cy.*",
|
||||||
|
"./cypress/**/*.*"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:cypress/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.cypress.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/no-unresolved": [2, {
|
||||||
|
"ignore": [
|
||||||
|
"@strapi/strapi/admin",
|
||||||
|
"@strapi/icons/symbols",
|
||||||
|
"@strapi/admin/strapi-admin"
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
|
||||||
|
"template-curly-spacing" : "off",
|
||||||
|
|
||||||
|
"indent" : "off",
|
||||||
|
|
||||||
|
"react/jsx-fragments": "off",
|
||||||
|
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
||||||
|
"react/no-unused-prop-types": "warn",
|
||||||
|
|
||||||
|
"react/jsx-no-target-blank": "error",
|
||||||
|
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
|
||||||
|
"babel/no-invalid-this": "error",
|
||||||
|
|
||||||
|
"arrow-spacing": "warn",
|
||||||
|
|
||||||
|
"implicit-arrow-linebreak": "warn",
|
||||||
|
|
||||||
|
"react/no-unused-state": "warn",
|
||||||
|
|
||||||
|
"react/boolean-prop-naming": "off",
|
||||||
|
|
||||||
|
"react/destructuring-assignment": ["warn", "always", { "ignoreClassFields": true }],
|
||||||
|
|
||||||
|
"react/no-access-state-in-setstate": "warn",
|
||||||
|
|
||||||
|
"operator-linebreak": "warn",
|
||||||
|
|
||||||
|
"no-useless-constructor": "warn",
|
||||||
|
|
||||||
|
"react/no-danger": "off",
|
||||||
|
|
||||||
|
"react/jsx-indent-props": "warn",
|
||||||
|
|
||||||
|
"react/jsx-curly-brace-presence": "warn",
|
||||||
|
|
||||||
|
"react/jsx-key": "error",
|
||||||
|
|
||||||
|
"react/jsx-boolean-value": "warn",
|
||||||
|
|
||||||
|
"react/jsx-closing-tag-location": "warn",
|
||||||
|
|
||||||
|
"import/extensions": "error",
|
||||||
|
|
||||||
|
"newline-per-chained-call": "warn",
|
||||||
|
|
||||||
|
"prefer-arrow-callback": "warn",
|
||||||
|
|
||||||
|
"block-spacing": "warn",
|
||||||
|
|
||||||
|
"one-var-declaration-per-line": "warn",
|
||||||
|
|
||||||
|
"prefer-const": "warn",
|
||||||
|
|
||||||
|
"import/first": "off",
|
||||||
|
|
||||||
|
"react/jsx-max-props-per-line": 1,
|
||||||
|
|
||||||
|
"react/jsx-first-prop-new-line": "warn",
|
||||||
|
|
||||||
|
"react/jsx-equals-spacing": "warn",
|
||||||
|
|
||||||
|
"react/jsx-indent": "warn",
|
||||||
|
|
||||||
|
"react/jsx-closing-bracket-location": "off",
|
||||||
|
|
||||||
|
"import/no-mutable-exports": "error",
|
||||||
|
|
||||||
|
"import/no-extraneous-dependencies": "off",
|
||||||
|
|
||||||
|
"object-shorthand": ["off", "never"],
|
||||||
|
|
||||||
|
"object-curly-newline": "off",
|
||||||
|
|
||||||
|
"arrow-body-style": "off",
|
||||||
|
|
||||||
|
"comma-dangle": ["warn", "always-multiline"],
|
||||||
|
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
|
||||||
|
"no-cond-assign": "warn",
|
||||||
|
|
||||||
|
"no-confusing-arrow": "off",
|
||||||
|
|
||||||
|
"no-console": "off",
|
||||||
|
|
||||||
|
"no-constant-condition": "warn",
|
||||||
|
|
||||||
|
"no-control-regex": "warn",
|
||||||
|
|
||||||
|
"no-continue": "warn",
|
||||||
|
|
||||||
|
"react/forbid-prop-types": "warn",
|
||||||
|
|
||||||
|
"no-debugger": "warn",
|
||||||
|
|
||||||
|
"no-dupe-args": "error",
|
||||||
|
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
|
||||||
|
"no-empty": "warn",
|
||||||
|
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
|
||||||
|
"no-extra-boolean-cast": "warn",
|
||||||
|
|
||||||
|
"no-extra-semi": "warn",
|
||||||
|
|
||||||
|
"no-func-assign": "error",
|
||||||
|
|
||||||
|
"no-inner-declarations": "error",
|
||||||
|
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
|
||||||
|
"no-mixed-operators": "off",
|
||||||
|
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
|
||||||
|
"no-negated-in-lhs": "error",
|
||||||
|
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
|
||||||
|
"no-regex-spaces": "warn",
|
||||||
|
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
|
||||||
|
"use-isnan": "error",
|
||||||
|
|
||||||
|
"valid-jsdoc": "warn",
|
||||||
|
|
||||||
|
"valid-typeof": "error",
|
||||||
|
|
||||||
|
"array-callback-return": "off",
|
||||||
|
|
||||||
|
"block-scoped-var": "off",
|
||||||
|
|
||||||
|
"prefer-destructuring": "warn",
|
||||||
|
|
||||||
|
"complexity": "off",
|
||||||
|
|
||||||
|
"consistent-return": "off",
|
||||||
|
|
||||||
|
"curly": "warn",
|
||||||
|
|
||||||
|
"default-case": "warn",
|
||||||
|
|
||||||
|
"dot-notation": "off",
|
||||||
|
|
||||||
|
"eqeqeq": "warn",
|
||||||
|
|
||||||
|
"guard-for-in": "off",
|
||||||
|
|
||||||
|
"no-alert": "warn",
|
||||||
|
|
||||||
|
"no-caller": "error",
|
||||||
|
|
||||||
|
"no-div-regex": "off",
|
||||||
|
|
||||||
|
"no-else-return": "off",
|
||||||
|
|
||||||
|
"no-eq-null": "error",
|
||||||
|
|
||||||
|
"no-eval": "error",
|
||||||
|
|
||||||
|
"no-extend-native": "error",
|
||||||
|
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
|
||||||
|
"no-iterator": "error",
|
||||||
|
|
||||||
|
"no-labels": "off",
|
||||||
|
|
||||||
|
"no-lone-blocks": "warn",
|
||||||
|
|
||||||
|
"no-loop-func": "error",
|
||||||
|
|
||||||
|
"no-multi-spaces": "warn",
|
||||||
|
|
||||||
|
"no-multi-str": "error",
|
||||||
|
|
||||||
|
"no-native-reassign": "error",
|
||||||
|
|
||||||
|
"no-new": "warn",
|
||||||
|
|
||||||
|
"no-new-func": "error",
|
||||||
|
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
|
||||||
|
"no-octal": "error",
|
||||||
|
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
|
||||||
|
"no-process-env": "off",
|
||||||
|
|
||||||
|
"no-proto": "error",
|
||||||
|
|
||||||
|
"no-redeclare": "error",
|
||||||
|
|
||||||
|
"no-return-assign": "off",
|
||||||
|
|
||||||
|
"arrow-parens": ["warn", "always", { "requireForBlockBody": false }],
|
||||||
|
|
||||||
|
"no-script-url": "error",
|
||||||
|
|
||||||
|
"no-self-compare": "error",
|
||||||
|
|
||||||
|
"no-sequences": "error",
|
||||||
|
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
|
||||||
|
"no-unused-expressions": "warn",
|
||||||
|
|
||||||
|
"no-void": "error",
|
||||||
|
|
||||||
|
"no-with": "error",
|
||||||
|
|
||||||
|
"radix": "off",
|
||||||
|
|
||||||
|
"vars-on-top": "off",
|
||||||
|
|
||||||
|
"wrap-iife": "error",
|
||||||
|
|
||||||
|
"yoda": "warn",
|
||||||
|
|
||||||
|
"strict": "off",
|
||||||
|
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
|
||||||
|
"no-delete-var": "error",
|
||||||
|
|
||||||
|
"no-label-var": "error",
|
||||||
|
|
||||||
|
"no-shadow": "warn",
|
||||||
|
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
|
||||||
|
"no-undef": "error",
|
||||||
|
|
||||||
|
"no-undef-init": "error",
|
||||||
|
|
||||||
|
"no-multi-assign": "warn",
|
||||||
|
|
||||||
|
"no-undefined": "error",
|
||||||
|
|
||||||
|
"no-unused-vars": ["warn", { "args": "none", "ignoreRestSiblings": true }],
|
||||||
|
|
||||||
|
"no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{ "functions": false, "classes": true, "variables": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
"no-restricted-properties": "warn",
|
||||||
|
|
||||||
|
"no-restricted-syntax": "warn",
|
||||||
|
|
||||||
|
"brace-style": "off",
|
||||||
|
|
||||||
|
"camelcase": "warn",
|
||||||
|
|
||||||
|
"comma-spacing": ["warn", { "before": false, "after": true }],
|
||||||
|
|
||||||
|
"comma-style": ["warn", "last"],
|
||||||
|
|
||||||
|
"consistent-this": ["off", "_this"],
|
||||||
|
|
||||||
|
"eol-last": "warn",
|
||||||
|
|
||||||
|
"func-names": "off",
|
||||||
|
|
||||||
|
"func-style": ["warn", "declaration", { "allowArrowFunctions": true }],
|
||||||
|
|
||||||
|
"key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
|
||||||
|
|
||||||
|
"max-nested-callbacks": ["warn", 5],
|
||||||
|
|
||||||
|
"new-cap": ["warn", { "newIsCap": true, "capIsNew": false }],
|
||||||
|
|
||||||
|
"new-parens": "warn",
|
||||||
|
|
||||||
|
"newline-after-var": "off",
|
||||||
|
|
||||||
|
"no-array-constructor": "off",
|
||||||
|
|
||||||
|
"no-inline-comments": "off",
|
||||||
|
|
||||||
|
"no-lonely-if": "warn",
|
||||||
|
|
||||||
|
"no-mixed-spaces-and-tabs": "warn",
|
||||||
|
|
||||||
|
"no-multiple-empty-lines": ["warn", { "max": 2 }],
|
||||||
|
|
||||||
|
"no-nested-ternary": "warn",
|
||||||
|
|
||||||
|
"no-new-object": "off",
|
||||||
|
|
||||||
|
"no-spaced-func": "warn",
|
||||||
|
|
||||||
|
"no-ternary": "off",
|
||||||
|
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
|
||||||
|
"no-extra-parens": "off",
|
||||||
|
|
||||||
|
"padding-line-between-statements": "off",
|
||||||
|
|
||||||
|
"one-var": ["warn", "never"],
|
||||||
|
|
||||||
|
"operator-assignment": ["off", "never"],
|
||||||
|
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
|
||||||
|
"padded-blocks": ["off", "never"],
|
||||||
|
|
||||||
|
"lines-between-class-members": ["warn", "always"],
|
||||||
|
|
||||||
|
"quote-props": ["warn", "as-needed"],
|
||||||
|
|
||||||
|
"quotes": ["off", "single"],
|
||||||
|
|
||||||
|
"semi": ["warn", "always"],
|
||||||
|
|
||||||
|
"semi-spacing": ["warn", { "before": false, "after": true }],
|
||||||
|
|
||||||
|
"sort-vars": "off",
|
||||||
|
|
||||||
|
"keyword-spacing": ["warn", { "before": true, "after": true }],
|
||||||
|
|
||||||
|
"space-before-blocks": ["warn", "always"],
|
||||||
|
|
||||||
|
"function-paren-newline": "off",
|
||||||
|
|
||||||
|
"space-before-function-paren": ["warn", { "anonymous": "never", "named": "never" }],
|
||||||
|
|
||||||
|
"object-curly-spacing": ["warn", "always"],
|
||||||
|
|
||||||
|
"array-bracket-spacing": ["warn", "never"],
|
||||||
|
|
||||||
|
"computed-property-spacing": ["warn", "never"],
|
||||||
|
|
||||||
|
"space-in-parens": ["warn", "never"],
|
||||||
|
|
||||||
|
"space-infix-ops": "warn",
|
||||||
|
|
||||||
|
"space-unary-ops": ["warn", { "words": true, "nonwords": false }],
|
||||||
|
|
||||||
|
"spaced-comment": ["warn", "always"],
|
||||||
|
|
||||||
|
"wrap-regex": "off",
|
||||||
|
|
||||||
|
"no-var": "error",
|
||||||
|
|
||||||
|
"generator-star-spacing": ["error", "before"],
|
||||||
|
|
||||||
|
"max-depth": ["warn", 4],
|
||||||
|
|
||||||
|
"max-len": ["off", 80, 2],
|
||||||
|
|
||||||
|
"max-params": ["off", 99],
|
||||||
|
|
||||||
|
"max-statements": "off",
|
||||||
|
|
||||||
|
"no-bitwise": "off",
|
||||||
|
|
||||||
|
"no-plusplus": "off",
|
||||||
|
|
||||||
|
"react/display-name": "off",
|
||||||
|
|
||||||
|
"react/jsx-tag-spacing": "warn",
|
||||||
|
|
||||||
|
"jsx-quotes": ["warn", "prefer-double"],
|
||||||
|
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
|
||||||
|
"react/jsx-sort-props": "off",
|
||||||
|
|
||||||
|
"react/jsx-uses-react": "error",
|
||||||
|
|
||||||
|
"react/prefer-stateless-function": "warn",
|
||||||
|
|
||||||
|
"react/jsx-uses-vars": "error",
|
||||||
|
|
||||||
|
"react/jsx-no-bind": "error",
|
||||||
|
|
||||||
|
"react/no-did-mount-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-will-update-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-did-update-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-multi-comp": "off",
|
||||||
|
|
||||||
|
"react/no-unknown-property": "warn",
|
||||||
|
|
||||||
|
"react/prop-types": "off",
|
||||||
|
|
||||||
|
"react/react-in-jsx-scope": "error",
|
||||||
|
|
||||||
|
"react/self-closing-comp": "warn",
|
||||||
|
|
||||||
|
"react/jsx-wrap-multilines": "warn",
|
||||||
|
|
||||||
|
"react/no-array-index-key": "warn",
|
||||||
|
|
||||||
|
"react/no-unescaped-entities": "warn",
|
||||||
|
|
||||||
|
"react/sort-comp": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-static-element-interactions": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/click-events-have-key-events": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||||
|
|
||||||
|
"react/jsx-one-expression-per-line": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/anchor-is-valid": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/alt-text": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/label-has-for": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"required": {
|
||||||
|
"some": ["nesting", "id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"jsx-a11y/img-redundant-alt": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/no-autofocus": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/iframe-has-title": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/anchor-has-content": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/label-has-associated-control": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/mouse-events-have-key-events": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/interactive-supports-focus": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-distracting-elements": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/heading-has-content": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/html-has-lang": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/href-no-hash": "off",
|
||||||
|
|
||||||
|
"react/jsx-filename-extension": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/media-has-caption": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: 🐛 Bug Report
|
||||||
|
about: Create a report to help improve this plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting an issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Bug report
|
||||||
|
|
||||||
|
### Describe the bug
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
### Steps to reproduce the behavior
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
### Code snippets
|
||||||
|
|
||||||
|
If applicable, add code samples to help explain your problem.
|
||||||
|
|
||||||
|
### System
|
||||||
|
|
||||||
|
- Node.js version: <!-- Please ensure you are using the Node LTS version (v12 / v14) -->
|
||||||
|
- NPM version:
|
||||||
|
- Strapi version:
|
||||||
|
- Plugin version:
|
||||||
|
- Database:
|
||||||
|
- Operating system:
|
||||||
|
|
||||||
|
### Additional context
|
||||||
|
|
||||||
|
Add any other context about the problem here.
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: 🚀 Feature Request
|
||||||
|
about: Suggest an idea to help make this plugin even better!
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting a feature request.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Feature request
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Quick summary what's this feature request about.
|
||||||
|
|
||||||
|
### Why is it needed?
|
||||||
|
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
### Suggested solution(s)
|
||||||
|
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
### Related issue(s)/PR(s)
|
||||||
|
|
||||||
|
Let us know if this is related to any issue/pull request.
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting a pull request.
|
||||||
|
|
||||||
|
To help us merge your PR, make sure to follow the instructions below:
|
||||||
|
|
||||||
|
- Create or update the documentation.
|
||||||
|
- Create or update the tests.
|
||||||
|
- Refer to the issue you are closing in the PR description - fix #issue
|
||||||
|
- Specify if the PR is in WIP (work in progress) state or ready to be merged
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What does it do?
|
||||||
|
|
||||||
|
Describe the technical changes you did.
|
||||||
|
|
||||||
|
### Why is it needed?
|
||||||
|
|
||||||
|
Describe the issue you are solving.
|
||||||
|
|
||||||
|
### How to test it?
|
||||||
|
|
||||||
|
Provide information about the environment and the path to verify the behaviour.
|
||||||
|
|
||||||
|
### Related issue(s)/PR(s)
|
||||||
|
|
||||||
|
Let us know if this is related to any issue/pull request
|
Binary file not shown.
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 164 KiB |
|
@ -0,0 +1,52 @@
|
||||||
|
name: Deploy Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: docs.pluginpal.io
|
||||||
|
url: https://docs.pluginpal.io/config-sync
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
|
||||||
|
- name: Build a Docker image
|
||||||
|
run: |
|
||||||
|
cd docs
|
||||||
|
docker build \
|
||||||
|
-t docs-config-sync:latest .
|
||||||
|
docker save -o ../docs-config-sync-latest.tar docs-config-sync:latest
|
||||||
|
|
||||||
|
- name: Transfer the Docker image to the Dokku server
|
||||||
|
uses: appleboy/scp-action@v0.1.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_CI_USERNAME }}
|
||||||
|
password: ${{ secrets.SSH_CI_PASSWORD }}
|
||||||
|
source: docs-config-sync-latest.tar
|
||||||
|
target: /var/lib/dokku/data/storage/docs/docker-images
|
||||||
|
|
||||||
|
- name: Deploy the Dokku app based on the Docker image
|
||||||
|
uses: appleboy/ssh-action@v0.1.10
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_CI_USERNAME }}
|
||||||
|
password: ${{ secrets.SSH_CI_PASSWORD }}
|
||||||
|
script_stop: true
|
||||||
|
script: |
|
||||||
|
sudo docker load -i /var/lib/dokku/data/storage/docs/docker-images/docs-config-sync-latest.tar
|
||||||
|
DOCS_CONFIG_SYNC_LATEST_IMAGE=$(sudo docker images --format "{{.ID}}" docs-config-sync:latest)
|
||||||
|
sudo docker tag docs-config-sync:latest docs-config-sync:$DOCS_CONFIG_SYNC_LATEST_IMAGE
|
||||||
|
dokku git:from-image docs-config-sync docs-config-sync:$DOCS_CONFIG_SYNC_LATEST_IMAGE
|
||||||
|
sudo docker system prune --all --force
|
|
@ -0,0 +1,55 @@
|
||||||
|
name: Publish to NPM
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
always-auth: true
|
||||||
|
node-version: 18
|
||||||
|
cache: 'yarn'
|
||||||
|
registry-url: 'https://registry.npmjs.org/'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
- name: Build the plugin
|
||||||
|
run: yarn build
|
||||||
|
- name: Get the release tag version
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||||
|
- name: Extract pre-release tag if any
|
||||||
|
id: extract_tag
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.get_version.outputs.VERSION }}"
|
||||||
|
if [[ $VERSION == *-* ]]; then
|
||||||
|
# Extract everything between hyphen and last period (or end of string)
|
||||||
|
PRETAG=$(echo $VERSION | sed -E 's/.*-([^.]+).*/\1/')
|
||||||
|
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "NPM_TAG=$PRETAG" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "NPM_TAG=latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Get source branch
|
||||||
|
id: get_branch
|
||||||
|
run: |
|
||||||
|
RELEASE_COMMIT=$(git rev-list -n 1 ${{ steps.get_version.outputs.VERSION }})
|
||||||
|
SOURCE_BRANCH=$(git branch -r --contains $RELEASE_COMMIT | grep -v HEAD | head -n 1 | sed 's/.*origin\///')
|
||||||
|
echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
- name: Set package version
|
||||||
|
run: yarn version --new-version "${{ steps.get_version.outputs.VERSION }}" --no-git-tag-version
|
||||||
|
- name: Publish package
|
||||||
|
run: yarn publish --access public --tag ${{ steps.extract_tag.outputs.NPM_TAG }}
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
- name: Push version bump
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
commit_message: 'chore: Bump version to ${{ steps.get_version.outputs.VERSION }}'
|
||||||
|
file_pattern: 'package.json'
|
||||||
|
branch: master
|
|
@ -0,0 +1,79 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- beta
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: 'lint'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: [18, 20]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
- name: Run eslint
|
||||||
|
run: yarn run eslint
|
||||||
|
test:
|
||||||
|
name: 'test'
|
||||||
|
needs: [lint]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: [18, 20]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies plugin
|
||||||
|
run: yarn --no-lockfile --unsafe-perm
|
||||||
|
- name: Push the package to yalc
|
||||||
|
run: yarn build
|
||||||
|
- name: Add yalc package to the playground
|
||||||
|
run: yarn playground:yalc-add
|
||||||
|
- name: Install dependencies playground
|
||||||
|
run: cd playground && yarn install --unsafe-perm
|
||||||
|
- name: Build playground
|
||||||
|
run: yarn playground:build
|
||||||
|
# - name: Run unit tests
|
||||||
|
# run: yarn test:unit
|
||||||
|
- name: Run integration tests
|
||||||
|
run: yarn run -s test:integration
|
||||||
|
- name: Run end-to-end tests
|
||||||
|
uses: cypress-io/github-action@v6
|
||||||
|
with:
|
||||||
|
start: yarn playground:start
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: cypress-screenshots
|
||||||
|
path: cypress/screenshots
|
||||||
|
if-no-files-found: ignore # 'warn' or 'error' are also available, defaults to `warn`
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: cypress-videos
|
||||||
|
path: cypress/videos
|
||||||
|
if-no-files-found: ignore # 'warn' or 'error' are also available, defaults to `warn`
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV }}
|
||||||
|
flags: unit
|
||||||
|
verbose: true
|
||||||
|
fail_ci_if_error: true
|
|
@ -3,9 +3,19 @@ coverage
|
||||||
node_modules
|
node_modules
|
||||||
stats.json
|
stats.json
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
files
|
||||||
|
|
||||||
# Cruft
|
# Cruft
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
bundle
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/screenshots/
|
||||||
|
cypress/videos/
|
||||||
|
cypress/downloads/
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
[@boazpoolman](https://twitter.com/boazpoolman) on twitter.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
This plugin provides a local development instance of Strapi to develop it's features. We call this instance `playground` and it can be found in the playground folder in the root of the project. For that reason it is not needed to have your own Strapi instance running to work on this plugin. Just clone the repo and you're ready to go!
|
||||||
|
|
||||||
|
#### 1. Fork the [repository](https://github.com/pluginpal/strapi-plugin-config-sync)
|
||||||
|
|
||||||
|
[Go to the repository](https://github.com/pluginpal/strapi-plugin-config-sync) and fork it to your own GitHub account.
|
||||||
|
|
||||||
|
#### 2. Clone the forked repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:YOUR_USERNAME/strapi-plugin-config-sync.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Install the dependencies
|
||||||
|
|
||||||
|
Go to the folder and install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd strapi-plugin-config-sync && yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Install the playground dependencies
|
||||||
|
|
||||||
|
Run this in the root of the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn playground:install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Run the compiler of the plugin
|
||||||
|
|
||||||
|
We use `yalc` to publish the package to a local registry. Run the following command o watch for changes and push to `yalc` every time a change is made:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn develop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Start the playground instance
|
||||||
|
|
||||||
|
Leave the watcher running, open up a new terminal window and browse back to the root of the plugin repo. Run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn playground:develop
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the playground instance that will have the plugin installed from the `yalc` registry. Browse to http://localhost:1337 and create a test admin user to log in to the playground.
|
||||||
|
|
||||||
|
#### 7. Start your contribution!
|
||||||
|
|
||||||
|
You can now start working on your contribution. If you had trouble setting up this testing environment please feel free to report an issue on Github.
|
||||||
|
|
||||||
|
### Commit message convention
|
||||||
|
|
||||||
|
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
|
||||||
|
|
||||||
|
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
|
||||||
|
- `feat`: new features, e.g. add new method to the module.
|
||||||
|
- `refactor`: code refactor, e.g. migrate from class components to hooks.
|
||||||
|
- `docs`: changes into documentation, e.g. add usage example for the module..
|
||||||
|
- `test`: adding or updating tests, eg add integration tests using detox.
|
||||||
|
- `chore`: tooling changes, e.g. change CI config.
|
||||||
|
|
||||||
|
### Linting and tests
|
||||||
|
|
||||||
|
[ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
We use [ESLint](https://eslint.org/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
The `package.json` file contains various scripts for common tasks:
|
||||||
|
|
||||||
|
- `yarn eslint`: lint files with ESLint.
|
||||||
|
- `yarn eslint:fix`: auto-fix ESLint issues.
|
||||||
|
- `yarn test:integration`: run integration tests with Jest.
|
||||||
|
|
||||||
|
### Sending a pull request
|
||||||
|
|
||||||
|
When you're sending a pull request:
|
||||||
|
|
||||||
|
- Prefer small pull requests focused on one change.
|
||||||
|
- Verify that linters and tests are passing.
|
||||||
|
- Review the documentation to make sure it looks good.
|
||||||
|
- Follow the pull request template when opening a pull request.
|
||||||
|
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
|
127
README.md
127
README.md
|
@ -1,74 +1,105 @@
|
||||||
# Strapi Plugin Config Sync
|
<div align="center">
|
||||||
|
<h1>Strapi config-sync plugin</h1>
|
||||||
|
|
||||||
A lot of configuration of your Strapi project is stored in the database. Like core_store, user permissions, user roles & webhooks. Things you might want to have the same on all environments. But when you update them locally, you will have to manually update them on all other environments too.
|
<p style="margin-top: 0;">This plugin is a multi-purpose tool to manage your Strapi database records through JSON files. Mostly used to version controlconfig data for automated deployment, automated tests and data sharing for collaboration purposes.</p>
|
||||||
|
|
||||||
That's where this plugin comes in to play. It allows you to export these configs as individual JSON files for each config, and write them somewhere in your project. With the configs written in your filesystem your can keep track of them through version control (git), and easily pull and import them across environments.
|
<a href="https://docs.pluginpal.io/config-sync">Read the documentation</a>
|
||||||
|
|
||||||
Importing, exporting and keeping track of config changes is done in the admin page of the plugin.
|
<p>
|
||||||
|
<a href="https://www.npmjs.org/package/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/npm/v/strapi-plugin-config-sync/latest.svg" alt="NPM Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.org/package/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/npm/dm/strapi-plugin-config-sync" alt="Monthly download on NPM" />
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/github/actions/workflow/status/boazpoolman/strapi-plugin-config-sync/tests.yml?branch=master" alt="CI build status" />
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync">
|
||||||
|
<img src="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync/coverage.svg?branch=master" alt="codecov.io" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
*Currently only the core_store changes are being tracked.*
|
## ✨ Features
|
||||||
|
|
||||||
**THIS PLUGIN IS STILL IN DEVELOPMENT**
|
- **CLI** - `config-sync` CLI for syncing the config from the command line
|
||||||
|
- **GUI** - Settings page for syncing the config in Strapi admin
|
||||||
|
- **Partial sync** - Import or export only specific portions of config
|
||||||
|
- **Custom types** - Include your custom collection types in the sync process
|
||||||
|
- **Import on bootstrap** - Easy automated deployment with `importOnBootstrap`
|
||||||
|
- **Exclusion** - Exclude single config entries or all entries of a given type
|
||||||
|
- **Diff viewer** - A git-style diff viewer to inspect the config changes
|
||||||
|
|
||||||
**PLEASE USE WITH CARE**
|
## ⏳ Getting started
|
||||||
|
|
||||||

|
[Read the Getting Started tutorial](https://docs.pluginpal.io/config-sync) or follow the steps below:
|
||||||
|
|
||||||
## Installation
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn add strapi-plugin-config-sync
|
||||||
|
|
||||||
Use `npm` or `yarn` to install and build the plugin.
|
# using npm
|
||||||
|
npm install strapi-plugin-config-sync --save
|
||||||
|
```
|
||||||
|
|
||||||
yarn add strapi-plugin-config-sync
|
Add the export path to the `watchIgnoreFiles` list in the `config/admin.js` file.
|
||||||
yarn build
|
This way your app won't reload when you export the config in development.
|
||||||
yarn develop
|
|
||||||
|
|
||||||
## Configuration
|
##### `config/admin.js`:
|
||||||
Some settings for the plugin are able to be modified by creating a file `extensions/config-sync/config/config.json` and changing the following settings:
|
```
|
||||||
|
module.exports = ({ env }) => ({
|
||||||
|
// ...
|
||||||
|
watchIgnoreFiles: [
|
||||||
|
'**/config/sync/**',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
#### `destination`
|
After successful installation you have to rebuild the admin UI so it'll include this plugin. To rebuild and restart Strapi run:
|
||||||
|
|
||||||
The path for reading and writing the config JSON files.
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn build
|
||||||
|
yarn develop
|
||||||
|
|
||||||
> `required:` NO | `type:` string | `default:` extensions/config-sync/files/
|
# using npm
|
||||||
|
npm run build
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
#### `minify`
|
The **Config Sync** plugin should now appear in the **Settings** section of your Strapi app.
|
||||||
|
|
||||||
Setting to minify the JSON that's being exported. It defaults to false for better readability in git commits.
|
To start tracking your config changes you have to make the first export. This will dump all your configuration data to the `/config/sync` directory. You can export either through [the CLI](https://docs.pluginpal.io/config-sync/cli) or [Strapi admin panel](https://docs.pluginpal.io/config-sync/admin-gui)
|
||||||
|
|
||||||
> `required:` NO | `type:` bool | `default:` false
|
Enjoy 🎉
|
||||||
|
|
||||||
#### `importOnBootstrap`
|
## 📓 Documentation
|
||||||
|
|
||||||
Allows you to let the config be imported automaticly when strapi is bootstrapping (on `yarn start`). This setting should only be used in production, and should be handled very carefully as it can unintendedly overwrite the changes in your database.
|
See our dedicated [repository](https://github.com/pluginpal/docs) for all of PluginPal's documentation, or view the Config Sync documentation live:
|
||||||
|
|
||||||
PLEASE USE WITH CARE.
|
- [Config Sync documentation](https://docs.pluginpal.io/config-sync)
|
||||||
|
|
||||||
> `required:` NO | `type:` bool | `default:` false
|
## 🤝 Contributing
|
||||||
|
|
||||||
#### `exclude`
|
Feel free to fork and make a pull request of this plugin. All the input is welcome!
|
||||||
|
|
||||||
You might not want all your database config exported and managed in git. This settings allows you to add an array of config names which should not be tracked by the config-sync plugin.
|
|
||||||
|
|
||||||
For now only the `core_store` table is being tracked. Add the `key` value of a `core_store` item to the array to exclude it from being tracked.
|
|
||||||
|
|
||||||
> `required:` NO | `type:` array | `default:` []
|
|
||||||
|
|
||||||
## TODOs
|
|
||||||
- Exporting of user roles & permissions
|
|
||||||
- Exporting of webhooks
|
|
||||||
- Specify which tables you want to track in the plugin configurations
|
|
||||||
- Add partial import/export functionality
|
|
||||||
- Add CLI commands for importing/exporting
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [MIT License](LICENSE.md)
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
|
|
||||||
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
|
|
||||||
|
|
||||||
## ⭐️ Show your support
|
## ⭐️ Show your support
|
||||||
|
|
||||||
Give a star if this project helped you.
|
Give a star if this project helped you.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- [PluginPal marketplace](https://www.pluginpal.io/plugin/config-sync)
|
||||||
|
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
|
||||||
|
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
|
||||||
|
- [Strapi marketplace](https://market.strapi.io/plugins/strapi-plugin-config-sync)
|
||||||
|
|
||||||
|
## 🌎 Community support
|
||||||
|
|
||||||
|
- For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/).
|
||||||
|
- For support with this plugin you can DM me in the Strapi Discord [channel](https://discord.strapi.io/).
|
||||||
|
|
||||||
|
## 📝 Resources
|
||||||
|
|
||||||
|
- [MIT License](https://github.com/pluginpal/strapi-plugin-config-sync/blob/master/LICENSE.md)
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { Button } from '@buffetjs/core';
|
|
||||||
import ConfirmModal from '../ConfirmModal';
|
|
||||||
import { exportAllConfig, importAllConfig } from '../../state/actions/Config';
|
|
||||||
|
|
||||||
const ActionButtons = ({ diff }) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
|
||||||
const [actionType, setActionType] = useState('');
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setActionType('');
|
|
||||||
setModalIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = (type) => {
|
|
||||||
setActionType(type);
|
|
||||||
setModalIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionButtonsStyling>
|
|
||||||
<Button disabled={isEmpty(diff)} color="primary" label="Import" onClick={() => openModal('import')} />
|
|
||||||
<Button disabled={isEmpty(diff)} color="primary" label="Export" onClick={() => openModal('export')} />
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={modalIsOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
type={actionType}
|
|
||||||
onSubmit={() => actionType === 'import' ? dispatch(importAllConfig()) : dispatch(exportAllConfig())}
|
|
||||||
/>
|
|
||||||
</ActionButtonsStyling>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActionButtonsStyling = styled.div`
|
|
||||||
padding: 10px 0 20px 0;
|
|
||||||
|
|
||||||
> button {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ActionButtons;
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { Button, Typography } from '@strapi/design-system';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
import { getFetchClient, useNotification } from '@strapi/strapi/admin';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
import { exportAllConfig, importAllConfig, downloadZip } from '../../state/actions/Config';
|
||||||
|
|
||||||
|
const ActionButtons = () => {
|
||||||
|
const { post, get } = getFetchClient();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { toggleNotification } = useNotification();
|
||||||
|
const partialDiff = useSelector((state) => state.getIn(['config', 'partialDiff'], Map({}))).toJS();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButtonsStyling>
|
||||||
|
<ConfirmModal
|
||||||
|
type="import"
|
||||||
|
trigger={(
|
||||||
|
<Button disabled={isEmpty(partialDiff)}>
|
||||||
|
{formatMessage({ id: 'config-sync.Buttons.Import' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onSubmit={(force) => dispatch(importAllConfig(partialDiff, force, toggleNotification, formatMessage, post, get))}
|
||||||
|
/>
|
||||||
|
<ConfirmModal
|
||||||
|
type="export"
|
||||||
|
trigger={(
|
||||||
|
<Button disabled={isEmpty(partialDiff)}>
|
||||||
|
{formatMessage({ id: 'config-sync.Buttons.Export' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onSubmit={(force) => dispatch(exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get))}
|
||||||
|
/>
|
||||||
|
{!isEmpty(partialDiff) && (
|
||||||
|
<Typography variant="epsilon">{Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"}</Typography>
|
||||||
|
)}
|
||||||
|
<Button onClick={() => dispatch(downloadZip(toggleNotification, formatMessage, post, get))}>{formatMessage({ id: 'config-sync.Buttons.DownloadConfig' })}</Button>
|
||||||
|
</ActionButtonsStyling>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActionButtonsStyling = styled.div`
|
||||||
|
padding: 10px 0 20px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
> button:last-of-type {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default ActionButtons;
|
|
@ -1,49 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
|
|
||||||
import { AttributeIcon } from '@buffetjs/core';
|
|
||||||
import {
|
|
||||||
HeaderModal,
|
|
||||||
HeaderModalTitle,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from 'strapi-helper-plugin';
|
|
||||||
|
|
||||||
const ConfigDiff = ({ isOpen, onClose, onToggle, oldValue, newValue, configName }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClosed={onClose}
|
|
||||||
onToggle={onToggle}
|
|
||||||
>
|
|
||||||
<HeaderModal>
|
|
||||||
<section style={{ alignItems: 'center' }}>
|
|
||||||
<AttributeIcon type='enum' />
|
|
||||||
<HeaderModalTitle style={{ marginLeft: 15 }}>Config changes for {configName}</HeaderModalTitle>
|
|
||||||
</section>
|
|
||||||
</HeaderModal>
|
|
||||||
<ModalBody style={{
|
|
||||||
paddingTop: '0.5rem',
|
|
||||||
paddingBottom: '3rem'
|
|
||||||
}}>
|
|
||||||
<div className="container-fluid">
|
|
||||||
<section style={{ marginTop: 20 }}>
|
|
||||||
<ReactDiffViewer
|
|
||||||
oldValue={JSON.stringify(oldValue, null, 2)}
|
|
||||||
newValue={JSON.stringify(newValue, null, 2)}
|
|
||||||
splitView={true}
|
|
||||||
compareMethod={DiffMethod.WORDS}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<section style={{ alignItems: 'center' }}>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConfigDiff;
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import RDV, { DiffMethod } from 'react-diff-viewer-continued';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An issue with the diff-viewer library causes a difference in the way the library is exported.
|
||||||
|
* Depending on whether the library is loaded through the browser or through the server, the default export may or may not be present.
|
||||||
|
* This causes issues with SSR and the way the library is imported.
|
||||||
|
*
|
||||||
|
* Below a workaround to fix this issue.
|
||||||
|
*
|
||||||
|
* @see https://github.com/Aeolun/react-diff-viewer-continued/issues/43
|
||||||
|
*/
|
||||||
|
let ReactDiffViewer;
|
||||||
|
if (typeof RDV.default !== 'undefined') {
|
||||||
|
ReactDiffViewer = RDV.default;
|
||||||
|
} else {
|
||||||
|
ReactDiffViewer = RDV;
|
||||||
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
|
||||||
|
const ConfigDiff = ({ oldValue, newValue, configName, trigger }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Root>
|
||||||
|
<Modal.Trigger>
|
||||||
|
{trigger}
|
||||||
|
</Modal.Trigger>
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Header>
|
||||||
|
<Typography variant="omega" fontWeight="bold" textColor="neutral800">
|
||||||
|
{formatMessage({ id: 'config-sync.ConfigDiff.Title' })} {configName}
|
||||||
|
</Typography>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Grid.Root paddingBottom={4} style={{ textAlign: 'center' }}>
|
||||||
|
<Grid.Item col={6}>
|
||||||
|
<Typography variant="delta" style={{ width: '100%' }}>{formatMessage({ id: 'config-sync.ConfigDiff.SyncDirectory' })}</Typography>
|
||||||
|
</Grid.Item>
|
||||||
|
<Grid.Item col={6}>
|
||||||
|
<Typography variant="delta" style={{ width: '100%' }}>{formatMessage({ id: 'config-sync.ConfigDiff.Database' })}</Typography>
|
||||||
|
</Grid.Item>
|
||||||
|
</Grid.Root>
|
||||||
|
<Typography variant="pi">
|
||||||
|
<ReactDiffViewer
|
||||||
|
oldValue={JSON.stringify(oldValue, null, 2)}
|
||||||
|
newValue={JSON.stringify(newValue, null, 2)}
|
||||||
|
splitView
|
||||||
|
compareMethod={DiffMethod.WORDS}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigDiff;
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Tr, Td, Checkbox, Typography } from '@strapi/design-system';
|
||||||
|
|
||||||
|
const CustomRow = ({ row, checked, updateValue, ...props }) => {
|
||||||
|
const { configName, configType, state, onClick } = row;
|
||||||
|
|
||||||
|
const stateStyle = (stateStr) => {
|
||||||
|
const style = {
|
||||||
|
display: 'inline-flex',
|
||||||
|
padding: '0 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
height: '24px',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontWeight: '500',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stateStr === 'Only in DB') {
|
||||||
|
style.backgroundColor = '#cbf2d7';
|
||||||
|
style.color = '#1b522b';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateStr === 'Only in sync dir') {
|
||||||
|
style.backgroundColor = '#f0cac7';
|
||||||
|
style.color = '#3d302f';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateStr === 'Different') {
|
||||||
|
style.backgroundColor = '#e8e6b7';
|
||||||
|
style.color = '#4a4934';
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
{...props}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target.type !== 'checkbox') {
|
||||||
|
onClick(configType, configName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<Checkbox
|
||||||
|
aria-label={`Select ${configName}`}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={updateValue}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td onClick={(e) => props.onClick(e)}>
|
||||||
|
<Typography variant="omega">{configName}</Typography>
|
||||||
|
</Td>
|
||||||
|
<Td onClick={(e) => props.onClick(e)}>
|
||||||
|
<Typography variant="omega">{configType}</Typography>
|
||||||
|
</Td>
|
||||||
|
<Td onClick={(e) => props.onClick(e)}>
|
||||||
|
<Typography variant="omega" style={stateStyle(state)}>{state}</Typography>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomRow;
|
|
@ -1,73 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Table } from '@buffetjs/core';
|
|
||||||
import ConfigDiff from '../ConfigDiff';
|
|
||||||
|
|
||||||
const headers = [
|
|
||||||
{
|
|
||||||
name: 'Config name',
|
|
||||||
value: 'config_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Database table',
|
|
||||||
value: 'table_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Change',
|
|
||||||
value: 'change_type',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ConfigList = ({ fileConfig, databaseConfig, diff, isLoading }) => {
|
|
||||||
const [openModal, setOpenModal] = useState(false);
|
|
||||||
const [originalConfig, setOriginalConfig] = useState({});
|
|
||||||
const [newConfig, setNewConfig] = useState({});
|
|
||||||
const [configName, setConfigName] = useState('');
|
|
||||||
const [rows, setRows] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let formattedRows = [];
|
|
||||||
Object.keys(diff).map((config) => {
|
|
||||||
// @TODO implement different config types, roles/permissions e.g.
|
|
||||||
formattedRows.push({
|
|
||||||
config_name: config,
|
|
||||||
table_name: 'core_store',
|
|
||||||
change_type: ''
|
|
||||||
});
|
|
||||||
});
|
|
||||||
setRows(formattedRows);
|
|
||||||
}, [diff]);
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setOriginalConfig({});
|
|
||||||
setNewConfig({});
|
|
||||||
setConfigName('');
|
|
||||||
setOpenModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ConfigDiff
|
|
||||||
isOpen={openModal}
|
|
||||||
oldValue={originalConfig}
|
|
||||||
newValue={newConfig}
|
|
||||||
onClose={closeModal}
|
|
||||||
onToggle={closeModal}
|
|
||||||
configName={configName}
|
|
||||||
/>
|
|
||||||
<Table
|
|
||||||
headers={headers}
|
|
||||||
onClickRow={(e, data) => {
|
|
||||||
setOriginalConfig(fileConfig.get(data.config_name));
|
|
||||||
setNewConfig(databaseConfig.get(data.config_name));
|
|
||||||
setConfigName(data.config_name);
|
|
||||||
setOpenModal(true);
|
|
||||||
}}
|
|
||||||
rows={!isLoading ? rows : []}
|
|
||||||
isLoading={isLoading}
|
|
||||||
tableEmptyText="No config changes. You are up to date!"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConfigList;
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
Loader,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
|
||||||
|
import ConfigDiff from '../ConfigDiff';
|
||||||
|
import FirstExport from '../FirstExport';
|
||||||
|
import NoChanges from '../NoChanges';
|
||||||
|
import ConfigListRow from './ConfigListRow';
|
||||||
|
import { setConfigPartialDiffInState } from '../../state/actions/Config';
|
||||||
|
|
||||||
|
|
||||||
|
const ConfigList = ({ diff, isLoading }) => {
|
||||||
|
const [originalConfig, setOriginalConfig] = useState({});
|
||||||
|
const [newConfig, setNewConfig] = useState({});
|
||||||
|
const [cName, setCname] = useState('');
|
||||||
|
const [rows, setRows] = useState([]);
|
||||||
|
const [checkedItems, setCheckedItems] = useState([]);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const getConfigState = (configName) => {
|
||||||
|
if (
|
||||||
|
diff.fileConfig[configName]
|
||||||
|
&& diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
return formatMessage({ id: 'config-sync.ConfigList.Different' });
|
||||||
|
} else if (
|
||||||
|
diff.fileConfig[configName]
|
||||||
|
&& !diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
return formatMessage({ id: 'config-sync.ConfigList.OnlyDir' });
|
||||||
|
} else if (
|
||||||
|
!diff.fileConfig[configName]
|
||||||
|
&& diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
return formatMessage({ id: 'config-sync.ConfigList.OnlyDB' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(diff.diff)) {
|
||||||
|
setRows([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedRows = [];
|
||||||
|
const newCheckedItems = [];
|
||||||
|
Object.keys(diff.diff).map((name) => {
|
||||||
|
const type = name.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const formattedName = name.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
newCheckedItems.push(true);
|
||||||
|
|
||||||
|
formattedRows.push({
|
||||||
|
configName: formattedName,
|
||||||
|
configType: type,
|
||||||
|
state: getConfigState(name),
|
||||||
|
onClick: (configType, configName) => {
|
||||||
|
setOriginalConfig(diff.fileConfig[`${configType}.${configName}`]);
|
||||||
|
setNewConfig(diff.databaseConfig[`${configType}.${configName}`]);
|
||||||
|
setCname(`${configType}.${configName}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setCheckedItems(newCheckedItems);
|
||||||
|
|
||||||
|
setRows(formattedRows);
|
||||||
|
}, [diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newPartialDiff = [];
|
||||||
|
checkedItems.map((item, index) => {
|
||||||
|
if (item && rows[index]) newPartialDiff.push(`${rows[index].configType}.${rows[index].configName}`);
|
||||||
|
});
|
||||||
|
dispatch(setConfigPartialDiffInState(newPartialDiff));
|
||||||
|
}, [checkedItems]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 40 }}>
|
||||||
|
<Loader>{formatMessage({ id: 'config-sync.ConfigList.Loading' })}</Loader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !isEmpty(diff.message)) {
|
||||||
|
return <FirstExport />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && isEmpty(diff.diff)) {
|
||||||
|
return <NoChanges />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChecked = checkedItems && checkedItems.every(Boolean);
|
||||||
|
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table colCount={4} rowCount={rows.length + 1}>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>
|
||||||
|
<Checkbox
|
||||||
|
aria-label={formatMessage({ id: 'config-sync.ConfigList.SelectAll' })}
|
||||||
|
checked={isIndeterminate ? "indeterminate" : allChecked}
|
||||||
|
onCheckedChange={(value) => setCheckedItems(checkedItems.map(() => value))}
|
||||||
|
/>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}</Typography>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}</Typography>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.State' })}</Typography>
|
||||||
|
</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<ConfigDiff
|
||||||
|
key={row.configName}
|
||||||
|
oldValue={originalConfig}
|
||||||
|
newValue={newConfig}
|
||||||
|
configName={cName}
|
||||||
|
trigger={(
|
||||||
|
<ConfigListRow
|
||||||
|
row={row}
|
||||||
|
checked={checkedItems[index]}
|
||||||
|
updateValue={() => {
|
||||||
|
checkedItems[index] = !checkedItems[index];
|
||||||
|
setCheckedItems([...checkedItems]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigList;
|
|
@ -1,34 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
ModalConfirm,
|
|
||||||
} from 'strapi-helper-plugin';
|
|
||||||
|
|
||||||
import getTrad from '../../helpers/getTrad';
|
|
||||||
|
|
||||||
const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalConfirm
|
|
||||||
confirmButtonLabel={{
|
|
||||||
id: getTrad(`popUpWarning.button.${type}`),
|
|
||||||
}}
|
|
||||||
isOpen={isOpen}
|
|
||||||
toggle={onClose}
|
|
||||||
onClosed={onClose}
|
|
||||||
onConfirm={() => {
|
|
||||||
onClose();
|
|
||||||
onSubmit();
|
|
||||||
}}
|
|
||||||
type="success"
|
|
||||||
content={{
|
|
||||||
id: getTrad(`popUpWarning.warning.${type}`),
|
|
||||||
values: {
|
|
||||||
br: () => <br />,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConfirmModal;
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
Flex,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Divider,
|
||||||
|
Box,
|
||||||
|
Field,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
import { WarningCircle } from '@strapi/icons';
|
||||||
|
|
||||||
|
const ConfirmModal = ({ onClose, onSubmit, type, trigger }) => {
|
||||||
|
const soft = useSelector((state) => state.getIn(['config', 'appEnv', 'config', 'soft'], false));
|
||||||
|
const [force, setForce] = useState(false);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger>
|
||||||
|
{trigger}
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>{formatMessage({ id: "config-sync.popUpWarning.Confirmation" })}</Dialog.Header>
|
||||||
|
<Dialog.Body>
|
||||||
|
<WarningCircle fill="danger600" width="32px" height="32px" />
|
||||||
|
<Flex size={2}>
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<Typography variant="omega" id="confirm-description" style={{ textAlign: 'center' }}>
|
||||||
|
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_1` })}<br />
|
||||||
|
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_2` })}
|
||||||
|
</Typography>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{(soft && type === 'import') && (
|
||||||
|
<Box width="100%">
|
||||||
|
<Divider marginTop={4} />
|
||||||
|
<Box paddingTop={6}>
|
||||||
|
<Field.Root hint="Check this to ignore the soft setting.">
|
||||||
|
<Checkbox
|
||||||
|
onValueChange={(value) => setForce(value)}
|
||||||
|
value={force}
|
||||||
|
name="force"
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'config-sync.popUpWarning.force' })}
|
||||||
|
</Checkbox>
|
||||||
|
<Field.Hint />
|
||||||
|
</Field.Root>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Dialog.Cancel>
|
||||||
|
<Button fullWidth variant="tertiary">
|
||||||
|
{formatMessage({ id: 'config-sync.popUpWarning.button.cancel' })}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Cancel>
|
||||||
|
<Dialog.Action>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onSubmit(force);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatMessage({ id: `config-sync.popUpWarning.button.${type}` })}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Action>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmModal;
|
|
@ -1,24 +0,0 @@
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const ContainerFluid = styled.div`
|
|
||||||
padding: 18px 30px;
|
|
||||||
> div:first-child {
|
|
||||||
max-height: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonOutline {
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 15px;
|
|
||||||
border: 1px solid #dfe0e1;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
&:before {
|
|
||||||
margin-right: 10px;
|
|
||||||
content: '\f08e';
|
|
||||||
font-family: 'FontAwesome';
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ContainerFluid;
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { getFetchClient, useNotification } from '@strapi/strapi/admin';
|
||||||
|
import { Button, EmptyStateLayout } from '@strapi/design-system';
|
||||||
|
import { EmptyDocuments } from '@strapi/icons/symbols';
|
||||||
|
|
||||||
|
|
||||||
|
import { exportAllConfig } from '../../state/actions/Config';
|
||||||
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
|
const FirstExport = () => {
|
||||||
|
const { post, get } = getFetchClient();
|
||||||
|
const { toggleNotification } = useNotification();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EmptyStateLayout
|
||||||
|
content={formatMessage({ id: 'config-sync.FirstExport.Message' })}
|
||||||
|
action={(
|
||||||
|
<ConfirmModal
|
||||||
|
type="export"
|
||||||
|
onSubmit={() => dispatch(exportAllConfig([], toggleNotification, formatMessage, post, get))}
|
||||||
|
trigger={(
|
||||||
|
<Button>{formatMessage({ id: 'config-sync.FirstExport.Button' })}</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
icon={<EmptyDocuments width={160} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FirstExport;
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* HeaderComponent
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
|
||||||
import { Header } from '@buffetjs/custom';
|
|
||||||
import { useGlobalContext } from 'strapi-helper-plugin';
|
|
||||||
|
|
||||||
const HeaderComponent = () => {
|
|
||||||
const { formatMessage } = useGlobalContext();
|
|
||||||
|
|
||||||
const headerProps = {
|
|
||||||
title: {
|
|
||||||
label: formatMessage({ id: 'config-sync.Header.Title' }),
|
|
||||||
},
|
|
||||||
content: formatMessage({ id: 'config-sync.Header.Description' }),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Header {...headerProps} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(HeaderComponent);
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* HeaderComponent
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Layouts } from '@strapi/admin/strapi-admin';
|
||||||
|
import { Box } from '@strapi/design-system';
|
||||||
|
|
||||||
|
const HeaderComponent = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box background="neutral100">
|
||||||
|
<Layouts.Header
|
||||||
|
title={formatMessage({ id: 'config-sync.Header.Title' })}
|
||||||
|
subtitle={formatMessage({ id: 'config-sync.Header.Description' })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(HeaderComponent);
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { EmptyStateLayout } from '@strapi/design-system';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { EmptyDocuments } from '@strapi/icons/symbols';
|
||||||
|
|
||||||
|
const NoChanges = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
return (
|
||||||
|
<EmptyStateLayout
|
||||||
|
content={formatMessage({ id: 'config-sync.NoChanges.Message', defaultMessage: 'No differences between DB and sync directory. You are up-to-date!' })}
|
||||||
|
icon={<EmptyDocuments width={160} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoChanges;
|
|
@ -1 +1 @@
|
||||||
export const __DEBUG__ = strapi.env === 'development';
|
export const __DEBUG__ = true; // TODO: set actual env.
|
||||||
|
|
|
@ -7,20 +7,21 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import ContainerFluid from '../../components/Container';
|
import { Page } from '@strapi/strapi/admin';
|
||||||
import Header from '../../components/Header';
|
|
||||||
|
|
||||||
|
import pluginPermissions from '../../permissions';
|
||||||
|
import Header from '../../components/Header';
|
||||||
import { store } from "../../helpers/configureStore";
|
import { store } from "../../helpers/configureStore";
|
||||||
import ConfigPage from '../ConfigPage';
|
import ConfigPage from '../ConfigPage';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
<Page.Protect permissions={pluginPermissions.settings}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ContainerFluid>
|
|
||||||
<Header />
|
<Header />
|
||||||
<ConfigPage />
|
<ConfigPage />
|
||||||
</ContainerFluid>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
|
</Page.Protect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { Map } from 'immutable';
|
|
||||||
|
|
||||||
import { getAllConfig } from '../../state/actions/Config';
|
|
||||||
import ConfigList from '../../components/ConfigList';
|
|
||||||
import ActionButtons from '../../components/ActionButtons';
|
|
||||||
import difference from '../../helpers/getObjectDiff';
|
|
||||||
|
|
||||||
const ConfigPage = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading']), Map());
|
|
||||||
const fileConfig = useSelector((state) => state.getIn(['config', 'fileConfig']), Map());
|
|
||||||
const databaseConfig = useSelector((state) => state.getIn(['config', 'databaseConfig']), Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(getAllConfig());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const diff = difference(fileConfig.toJS(), databaseConfig.toJS());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ActionButtons diff={diff} />
|
|
||||||
<ConfigList fileConfig={fileConfig} databaseConfig={databaseConfig} isLoading={isLoading} diff={diff} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConfigPage;
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Alert,
|
||||||
|
Typography,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
import { useNotification } from '@strapi/strapi/admin';
|
||||||
|
import { getFetchClient, Layouts } from '@strapi/admin/strapi-admin';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { getAllConfigDiff, getAppEnv } from '../../state/actions/Config';
|
||||||
|
import ConfigList from '../../components/ConfigList';
|
||||||
|
import ActionButtons from '../../components/ActionButtons';
|
||||||
|
|
||||||
|
const ConfigPage = () => {
|
||||||
|
const { toggleNotification } = useNotification();
|
||||||
|
const { get } = getFetchClient();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading'], Map({})));
|
||||||
|
const configDiff = useSelector((state) => state.getIn(['config', 'configDiff'], Map({})));
|
||||||
|
const appEnv = useSelector((state) => state.getIn(['config', 'appEnv', 'env']));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
|
||||||
|
dispatch(getAppEnv(toggleNotification, formatMessage, get));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layouts.Content paddingBottom={8}>
|
||||||
|
{appEnv === 'production' && (
|
||||||
|
<Box paddingBottom={4}>
|
||||||
|
<Alert variant="danger">
|
||||||
|
<Typography variant="omega" fontWeight="bold">You're in the production environment</Typography><br />
|
||||||
|
Please be careful when syncing your config in production.<br />
|
||||||
|
Make sure you are not overriding critical config changes on import.
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<ActionButtons />
|
||||||
|
<ConfigList isLoading={isLoading} diff={configDiff.toJS()} />
|
||||||
|
</Layouts.Content>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigPage;
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function b64toBlob(dataURI, type) {
|
||||||
|
const byteString = atob(dataURI);
|
||||||
|
const ab = new ArrayBuffer(byteString.length);
|
||||||
|
const ia = new Uint8Array(ab);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new Blob([ab], { type });
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import { createLogger } from 'redux-logger';
|
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
import rootReducer from '../state/reducers';
|
import rootReducer from '../state/reducers';
|
||||||
import loggerConfig from '../config/logger';
|
|
||||||
import { __DEBUG__ } from '../config/constants';
|
import { __DEBUG__ } from '../config/constants';
|
||||||
|
|
||||||
const configureStore = () => {
|
const configureStore = () => {
|
||||||
let initialStoreState = Map();
|
const initialStoreState = Map();
|
||||||
|
|
||||||
const enhancers = [];
|
const enhancers = [];
|
||||||
const middlewares = [
|
const middlewares = [
|
||||||
|
@ -27,30 +25,12 @@ const configureStore = () => {
|
||||||
if (devtools) {
|
if (devtools) {
|
||||||
console.info('[setup] ✓ Enabling Redux DevTools Extension');
|
console.info('[setup] ✓ Enabling Redux DevTools Extension');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[setup] ✓ Enabling state logger');
|
|
||||||
const loggerMiddleware = createLogger({
|
|
||||||
level: 'info',
|
|
||||||
collapsed: true,
|
|
||||||
stateTransformer: (state) => state.toJS(),
|
|
||||||
predicate: (getState, action) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const showBlacklisted = state.getIn(['debug', 'logs', 'blacklisted']);
|
|
||||||
if (loggerConfig.blacklist.indexOf(action.type) !== -1 && !showBlacklisted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.getIn(['debug', 'logs', 'enabled']);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
middlewares.push(loggerMiddleware);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const composedEnhancers = devtools || compose;
|
const composedEnhancers = devtools || compose;
|
||||||
const storeEnhancers = composedEnhancers(
|
const storeEnhancers = composedEnhancers(
|
||||||
applyMiddleware(...middlewares),
|
applyMiddleware(...middlewares),
|
||||||
...enhancers
|
...enhancers,
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { transform, isEqual, isArray, isObject } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find difference between two objects
|
|
||||||
* @param {object} origObj - Source object to compare newObj against
|
|
||||||
* @param {object} newObj - New object with potential changes
|
|
||||||
* @return {object} differences
|
|
||||||
*/
|
|
||||||
const difference = (origObj, newObj) => {
|
|
||||||
function changes(newObj, origObj) {
|
|
||||||
let arrayIndexCounter = 0
|
|
||||||
return transform(newObj, function (result, value, key) {
|
|
||||||
if (!isEqual(value, origObj[key])) {
|
|
||||||
let resultKey = isArray(origObj) ? arrayIndexCounter++ : key
|
|
||||||
result[resultKey] = (isObject(value) && isObject(origObj[key])) ? changes(value, origObj[key]) : value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return changes(newObj, origObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default difference;
|
|
|
@ -1,5 +1,5 @@
|
||||||
import pluginId from './pluginId';
|
import pluginId from './pluginId';
|
||||||
|
|
||||||
const getTrad = id => `${pluginId}.${id}`;
|
const getTrad = (id) => `${pluginId}.${id}`;
|
||||||
|
|
||||||
export default getTrad;
|
export default getTrad;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
const pluginPkg = require('../../../package.json');
|
import pluginPkg from '../../../package.json';
|
||||||
|
|
||||||
const pluginId = pluginPkg.name.replace(
|
const pluginId = pluginPkg.name.replace(
|
||||||
/^strapi-plugin-/i,
|
/^strapi-plugin-/i,
|
||||||
''
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = pluginId;
|
export default pluginId;
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const prefixPluginTranslations = (trad, pluginId) => {
|
||||||
|
if (!pluginId) {
|
||||||
|
throw new TypeError("pluginId can't be empty");
|
||||||
|
}
|
||||||
|
return Object.keys(trad).reduce((acc, current) => {
|
||||||
|
acc[`${pluginId}.${current}`] = trad[current];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { prefixPluginTranslations };
|
|
@ -0,0 +1,74 @@
|
||||||
|
// <reference types="cypress" />
|
||||||
|
|
||||||
|
describe('Config Sync', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('deleteFolder', 'playground/config/sync');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check the config diff', () => {
|
||||||
|
cy.login();
|
||||||
|
cy.navigateToInterface();
|
||||||
|
cy.initialExport();
|
||||||
|
|
||||||
|
cy.makeConfigChanges();
|
||||||
|
|
||||||
|
cy.navigateToInterface();
|
||||||
|
|
||||||
|
cy.get('tbody tr').contains('plugin_users-permissions_advanced').click();
|
||||||
|
|
||||||
|
cy.contains('"unique_email": true,');
|
||||||
|
cy.contains('"unique_email": false,');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Download the config as zip', () => {
|
||||||
|
cy.login();
|
||||||
|
cy.navigateToInterface();
|
||||||
|
cy.initialExport();
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/config-sync/zip',
|
||||||
|
}).as('getConfigZip');
|
||||||
|
|
||||||
|
cy.get('button').contains('Download Config').click();
|
||||||
|
|
||||||
|
cy.wait('@getConfigZip').then((interception) => {
|
||||||
|
const configZipResponse = interception.response.body;
|
||||||
|
const downloadsFolder = Cypress.config('downloadsFolder');
|
||||||
|
cy.readFile(`${downloadsFolder}/${configZipResponse.name.replaceAll(':', '_')}`).should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Partial import & export', () => {
|
||||||
|
cy.login();
|
||||||
|
cy.navigateToInterface();
|
||||||
|
cy.initialExport();
|
||||||
|
|
||||||
|
cy.makeConfigChanges();
|
||||||
|
|
||||||
|
cy.navigateToInterface();
|
||||||
|
|
||||||
|
cy.get('button[aria-label="Select all entries"]').click();
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/config-sync/import',
|
||||||
|
}).as('importConfig');
|
||||||
|
cy.get('button[aria-label="Select plugin_upload_settings"]').click();
|
||||||
|
cy.get('button').contains('Import').click();
|
||||||
|
cy.get('button').contains('Yes, import').click();
|
||||||
|
cy.wait('@importConfig').its('response.statusCode').should('equal', 200);
|
||||||
|
cy.contains('plugin_users-permissions_advanced');
|
||||||
|
cy.contains('plugin_users-permissions_email');
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/config-sync/export',
|
||||||
|
}).as('exportConfig');
|
||||||
|
cy.get('button[aria-label="Select plugin_users-permissions_advanced"]').click();
|
||||||
|
cy.get('button').contains('Export').click();
|
||||||
|
cy.get('button').contains('Yes, export').click();
|
||||||
|
cy.wait('@exportConfig').its('response.statusCode').should('equal', 200);
|
||||||
|
cy.contains('plugin_users-permissions_email');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,54 +1,65 @@
|
||||||
import React from 'react';
|
|
||||||
import pluginPkg from '../../package.json';
|
import pluginPkg from '../../package.json';
|
||||||
import pluginId from './helpers/pluginId';
|
import pluginId from './helpers/pluginId';
|
||||||
import App from './containers/App';
|
import { prefixPluginTranslations } from './helpers/prefixPluginTranslations';
|
||||||
import Initializer from './containers/Initializer';
|
import pluginPermissions from './permissions';
|
||||||
import trads from './translations';
|
// import pluginIcon from './components/PluginIcon';
|
||||||
|
// import getTrad from './helpers/getTrad';
|
||||||
|
|
||||||
function Comp(props) {
|
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
|
||||||
return <App {...props} />;
|
const { name } = pluginPkg.strapi;
|
||||||
}
|
|
||||||
|
|
||||||
export default strapi => {
|
export default {
|
||||||
const pluginDescription =
|
register(app) {
|
||||||
pluginPkg.strapi.description || pluginPkg.description;
|
app.registerPlugin({
|
||||||
|
|
||||||
const icon = pluginPkg.strapi.icon;
|
|
||||||
const name = pluginPkg.strapi.name;
|
|
||||||
|
|
||||||
const plugin = {
|
|
||||||
icon,
|
|
||||||
name,
|
|
||||||
destination: `/plugins/${pluginId}`,
|
|
||||||
blockerComponent: null,
|
|
||||||
blockerComponentProps: {},
|
|
||||||
description: pluginDescription,
|
description: pluginDescription,
|
||||||
icon: pluginPkg.strapi.icon,
|
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
initializer: Initializer,
|
isReady: true,
|
||||||
injectedComponents: [],
|
isRequired: pluginPkg.strapi.required || false,
|
||||||
isReady: false,
|
|
||||||
layout: null,
|
|
||||||
leftMenuLinks: [],
|
|
||||||
leftMenuSections: [],
|
|
||||||
mainComponent: Comp,
|
|
||||||
name: pluginPkg.strapi.name,
|
|
||||||
preventComponentRendering: false,
|
|
||||||
trads,
|
|
||||||
menu: {
|
|
||||||
pluginsSectionLinks: [
|
|
||||||
{
|
|
||||||
destination: `/plugins/${pluginId}`, // Endpoint of the link
|
|
||||||
icon,
|
|
||||||
name,
|
name,
|
||||||
label: {
|
});
|
||||||
id: `${pluginId}.plugin.name`, // Refers to a i18n
|
|
||||||
|
app.createSettingSection(
|
||||||
|
{
|
||||||
|
id: pluginId,
|
||||||
|
intlLabel: {
|
||||||
|
id: `${pluginId}.plugin.name`,
|
||||||
defaultMessage: 'Config Sync',
|
defaultMessage: 'Config Sync',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
[
|
||||||
|
{
|
||||||
|
intlLabel: {
|
||||||
|
id: `${pluginId}.Settings.Tool.Title`,
|
||||||
|
defaultMessage: 'Interface',
|
||||||
},
|
},
|
||||||
|
id: 'config-sync-page',
|
||||||
|
to: `${pluginId}`,
|
||||||
|
Component: () => import('./containers/App'),
|
||||||
|
permissions: pluginPermissions['settings'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
bootstrap(app) {},
|
||||||
|
async registerTrads({ locales }) {
|
||||||
|
const importedTrads = await Promise.all(
|
||||||
|
locales.map((locale) => {
|
||||||
|
return import(`./translations/${locale}.json`)
|
||||||
|
.then(({ default: data }) => {
|
||||||
|
return {
|
||||||
|
data: prefixPluginTranslations(data, pluginId),
|
||||||
|
locale,
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return {
|
||||||
|
data: {},
|
||||||
|
locale,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return strapi.registerPlugin(plugin);
|
return Promise.resolve(importedTrads);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
const pluginPermissions = {
|
||||||
|
// This permission regards the main component (App) and is used to tell
|
||||||
|
// If the plugin link should be displayed in the menu
|
||||||
|
// And also if the plugin is accessible. This use case is found when a user types the url of the
|
||||||
|
// plugin directly in the browser
|
||||||
|
'menu-link': [{ action: 'plugin::config-sync.menu-link', subject: null }],
|
||||||
|
settings: [{ action: 'plugin::config-sync.settings.read', subject: null }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pluginPermissions;
|
|
@ -3,74 +3,88 @@
|
||||||
* Main actions
|
* Main actions
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { b64toBlob } from '../../helpers/blob';
|
||||||
|
|
||||||
import { request } from 'strapi-helper-plugin';
|
export function getAllConfigDiff(toggleNotification, formatMessage, get) {
|
||||||
import { Map } from 'immutable';
|
|
||||||
|
|
||||||
export function getAllConfig() {
|
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const databaseConfig = await request('/config-sync/all/from-database', { method: 'GET' });
|
const configDiff = await get('/config-sync/diff');
|
||||||
const fileConfig = await request('/config-sync/all/from-files', { method: 'GET' });
|
dispatch(setConfigPartialDiffInState([]));
|
||||||
dispatch(setFileConfigInState(fileConfig));
|
dispatch(setConfigDiffInState(configDiff.data));
|
||||||
dispatch(setDatabaseConfigInState(databaseConfig));
|
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
strapi.notification.error('notification.error');
|
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_DATABASE_CONFIG_IN_STATE = 'SET_DATABASE_CONFIG_IN_STATE';
|
export const SET_CONFIG_DIFF_IN_STATE = 'SET_CONFIG_DIFF_IN_STATE';
|
||||||
export function setDatabaseConfigInState(config) {
|
export function setConfigDiffInState(config) {
|
||||||
return {
|
return {
|
||||||
type: SET_DATABASE_CONFIG_IN_STATE,
|
type: SET_CONFIG_DIFF_IN_STATE,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_FILE_CONFIG_IN_STATE = 'SET_FILE_CONFIG_IN_STATE';
|
export const SET_CONFIG_PARTIAL_DIFF_IN_STATE = 'SET_CONFIG_PARTIAL_DIFF_IN_STATE';
|
||||||
export function setFileConfigInState(config) {
|
export function setConfigPartialDiffInState(config) {
|
||||||
return {
|
return {
|
||||||
type: SET_FILE_CONFIG_IN_STATE,
|
type: SET_CONFIG_PARTIAL_DIFF_IN_STATE,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportAllConfig() {
|
export function exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get) {
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const { message } = await request('/config-sync/export', { method: 'GET' });
|
const response = await post('/config-sync/export', partialDiff);
|
||||||
dispatch(setFileConfigInState(Map({})));
|
toggleNotification({ type: 'success', message: response.data.message });
|
||||||
dispatch(setDatabaseConfigInState(Map({})));
|
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
|
||||||
|
|
||||||
strapi.notification.success(message);
|
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
strapi.notification.error('notification.error');
|
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importAllConfig() {
|
export function downloadZip(toggleNotification, formatMessage, post, get) {
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const { message } = await request('/config-sync/import', { method: 'GET' });
|
const { message, base64Data, name } = (await get('/config-sync/zip')).data;
|
||||||
dispatch(setFileConfigInState(Map({})));
|
toggleNotification({ type: 'success', message });
|
||||||
dispatch(setDatabaseConfigInState(Map({})));
|
if (base64Data) {
|
||||||
|
saveAs(b64toBlob(base64Data, 'application/zip'), name, { type: 'application/zip' });
|
||||||
|
}
|
||||||
|
dispatch(setLoadingState(false));
|
||||||
|
} catch (err) {
|
||||||
|
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
|
||||||
|
dispatch(setLoadingState(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
strapi.notification.success(message);
|
export function importAllConfig(partialDiff, force, toggleNotification, formatMessage, post, get) {
|
||||||
|
return async function(dispatch) {
|
||||||
|
dispatch(setLoadingState(true));
|
||||||
|
try {
|
||||||
|
const response = await post('/config-sync/import', {
|
||||||
|
force,
|
||||||
|
config: partialDiff,
|
||||||
|
});
|
||||||
|
toggleNotification({ type: 'success', message: response.data.message });
|
||||||
|
dispatch(getAllConfigDiff(toggleNotification, formatMessage, get));
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
strapi.notification.error('notification.error');
|
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
|
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
|
||||||
|
@ -80,3 +94,22 @@ export function setLoadingState(value) {
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppEnv(toggleNotification, formatMessage, get) {
|
||||||
|
return async function(dispatch) {
|
||||||
|
try {
|
||||||
|
const envVars = await get('/config-sync/app-env');
|
||||||
|
dispatch(setAppEnvInState(envVars.data));
|
||||||
|
} catch (err) {
|
||||||
|
toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SET_APP_ENV_IN_STATE = 'SET_APP_ENV_IN_STATE';
|
||||||
|
export function setAppEnvInState(value) {
|
||||||
|
return {
|
||||||
|
type: SET_APP_ENV_IN_STATE,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -4,26 +4,35 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fromJS, Map } from 'immutable';
|
import { fromJS, Map, List } from 'immutable';
|
||||||
import { SET_DATABASE_CONFIG_IN_STATE, SET_FILE_CONFIG_IN_STATE, SET_LOADING_STATE } from '../../actions/Config';
|
import {
|
||||||
|
SET_CONFIG_DIFF_IN_STATE,
|
||||||
|
SET_CONFIG_PARTIAL_DIFF_IN_STATE,
|
||||||
|
SET_LOADING_STATE,
|
||||||
|
SET_APP_ENV_IN_STATE,
|
||||||
|
} from '../../actions/Config';
|
||||||
|
|
||||||
const initialState = fromJS({
|
const initialState = fromJS({
|
||||||
databaseConfig: Map({}),
|
configDiff: Map({}),
|
||||||
fileConfig: Map({}),
|
partialDiff: List([]),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
appEnv: Map({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function configReducer(state = initialState, action) {
|
export default function configReducer(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_DATABASE_CONFIG_IN_STATE:
|
case SET_CONFIG_DIFF_IN_STATE:
|
||||||
return state
|
return state
|
||||||
.update('databaseConfig', () => fromJS(action.config))
|
.update('configDiff', () => fromJS(action.config));
|
||||||
case SET_FILE_CONFIG_IN_STATE:
|
case SET_CONFIG_PARTIAL_DIFF_IN_STATE:
|
||||||
return state
|
return state
|
||||||
.update('fileConfig', () => fromJS(action.config))
|
.update('partialDiff', () => fromJS(action.config));
|
||||||
case SET_LOADING_STATE:
|
case SET_LOADING_STATE:
|
||||||
return state
|
return state
|
||||||
.update('isLoading', () => fromJS(action.value))
|
.update('isLoading', () => fromJS(action.value));
|
||||||
|
case SET_APP_ENV_IN_STATE:
|
||||||
|
return state
|
||||||
|
.update('appEnv', () => fromJS(action.value));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,40 @@
|
||||||
{
|
{
|
||||||
"popUpWarning.warning.import": "If you continue all your local config files<br></br>will be imported into the database.",
|
"popUpWarning.warning.import_1": "If you continue all your local config files",
|
||||||
"popUpWarning.warning.export": "If you continue all your database config<br></br>will be written into config files.",
|
"popUpWarning.warning.import_2": "will be imported into the database.",
|
||||||
|
"popUpWarning.warning.export_1": "If you continue all your database config",
|
||||||
|
"popUpWarning.warning.export_2": "will be written into config files.",
|
||||||
"popUpWarning.button.import": "Yes, import",
|
"popUpWarning.button.import": "Yes, import",
|
||||||
"popUpWarning.button.export": "Yes, export",
|
"popUpWarning.button.export": "Yes, export",
|
||||||
|
"popUpWarning.button.cancel": "Cancel",
|
||||||
|
"popUpWarning.force": "Force",
|
||||||
|
"popUpWarning.Confirmation": "Confirmation",
|
||||||
|
|
||||||
"Header.Title": "Config Sync",
|
"Header.Title": "Config Sync",
|
||||||
"Header.Description": "Manage your database config across environments.",
|
"Header.Description": "Manage your database config across environments.",
|
||||||
|
|
||||||
|
"ConfigList.Loading": "Loading content...",
|
||||||
|
"ConfigList.SelectAll": "Select all entries",
|
||||||
|
"ConfigList.ConfigName": "Config name",
|
||||||
|
"ConfigList.ConfigType": "Config type",
|
||||||
|
"ConfigList.State": "State",
|
||||||
|
"ConfigList.Different": "Different",
|
||||||
|
"ConfigList.OnlyDir": "Only in sync dir",
|
||||||
|
"ConfigList.OnlyDB": "Only in DB",
|
||||||
|
|
||||||
|
"NoChanges.Message": "No differences between DB and sync directory. You are up-to-date!",
|
||||||
|
|
||||||
|
"ConfigDiff.Title": "Config changes for",
|
||||||
|
"ConfigDiff.SyncDirectory": "Sync directory",
|
||||||
|
"ConfigDiff.Database": "Database",
|
||||||
|
|
||||||
|
"Buttons.Export": "Export",
|
||||||
|
"Buttons.DownloadConfig": "Download Config",
|
||||||
|
"Buttons.Import": "Import",
|
||||||
|
|
||||||
|
"FirstExport.Message": "Looks like this is your first time using config-sync for this project.",
|
||||||
|
"FirstExport.Button": "Make the initial export",
|
||||||
|
|
||||||
|
"Settings.Tool.Title": "Interface",
|
||||||
|
|
||||||
"plugin.name": "Config Sync"
|
"plugin.name": "Config Sync"
|
||||||
}
|
}
|
|
@ -1 +1,39 @@
|
||||||
{}
|
{
|
||||||
|
"popUpWarning.warning.import_1": "Si continuas todos tus ficheros de configuración locales",
|
||||||
|
"popUpWarning.warning.import_2": "se importarán a la base de datos.",
|
||||||
|
"popUpWarning.warning.export_1": "Si continuas las configuraciones de tu base de datos",
|
||||||
|
"popUpWarning.warning.export_2": "se escribirán en ficheros de configuración locales.",
|
||||||
|
"popUpWarning.button.import": "Sí, importar",
|
||||||
|
"popUpWarning.button.export": "Sí, exportar",
|
||||||
|
"popUpWarning.button.cancel": "Cancelar",
|
||||||
|
"popUpWarning.force": "Forzar",
|
||||||
|
"popUpWarning.Confirmation": "Confirmación",
|
||||||
|
|
||||||
|
"Header.Title": "Config Sync",
|
||||||
|
"Header.Description": "Gestiona las configuraciones de tu base de datos entre diferentes entornos o instancias.",
|
||||||
|
|
||||||
|
"ConfigList.Loading": "Cargando contenido...",
|
||||||
|
"ConfigList.SelectAll": "Seleccionar todas las entradas",
|
||||||
|
"ConfigList.ConfigName": "Nombre",
|
||||||
|
"ConfigList.ConfigType": "Tipo",
|
||||||
|
"ConfigList.State": "Estado",
|
||||||
|
"ConfigList.Different": "Diferentes",
|
||||||
|
"ConfigList.OnlyDir": "Sólo en directorio de sincronización",
|
||||||
|
"ConfigList.OnlyDB": "Sólo en la base de datos",
|
||||||
|
|
||||||
|
"NoChanges.Message": "No hay diferencia entre la base de datos y el directorio de sincronización. ¡Estás actualizado!",
|
||||||
|
|
||||||
|
"ConfigDiff.Title": "Cambios en la configuración para",
|
||||||
|
"ConfigDiff.SyncDirectory": "Directorio de sincronización",
|
||||||
|
"ConfigDiff.Database": "Base de datos",
|
||||||
|
|
||||||
|
"Buttons.Import": "Importar",
|
||||||
|
"Buttons.Export": "Exportar",
|
||||||
|
|
||||||
|
"FirstExport.Message": "Parece ser la primera vez que se usa config-sync en este proyecto.",
|
||||||
|
"FirstExport.Button": "Hacer la exportación inicial",
|
||||||
|
|
||||||
|
"Settings.Tool.Title": "Interfaz",
|
||||||
|
|
||||||
|
"plugin.name": "Config Sync"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('../dist/cli');
|
|
@ -0,0 +1,4 @@
|
||||||
|
comment:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"destination": "extensions/config-sync/files/",
|
|
||||||
"minify": false,
|
|
||||||
"importOnBootstrap": false,
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An asynchronous bootstrap function that runs before
|
|
||||||
* your application gets started.
|
|
||||||
*
|
|
||||||
* This gives you an opportunity to set up your data model,
|
|
||||||
* run jobs, or perform some special logic.
|
|
||||||
*
|
|
||||||
* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
if (strapi.plugins['config-sync'].config.importOnBootstrap) {
|
|
||||||
if (fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
|
||||||
|
|
||||||
configFiles.map((file) => {
|
|
||||||
strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/export",
|
|
||||||
"handler": "config.export",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/import",
|
|
||||||
"handler": "config.import",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/all/from-files",
|
|
||||||
"handler": "config.getConfigsFromFiles",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/all/from-database",
|
|
||||||
"handler": "config.getConfigsFromDatabase",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main controllers for config import/export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Export all config, from db to filesystem.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export: async (ctx) => {
|
|
||||||
const coreStoreAPI = strapi.query('core_store');
|
|
||||||
const coreStore = await coreStoreAPI.find({ _limit: -1 });
|
|
||||||
|
|
||||||
Object.values(coreStore).map(async ({ key, value }) => {
|
|
||||||
await strapi.plugins['config-sync'].services.config.writeConfigFile(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send({
|
|
||||||
message: `Config was successfully exported to ${strapi.plugins['config-sync'].config.destination}.`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all config, from filesystem to db.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
import: async (ctx) => {
|
|
||||||
// Check for existance of the config file destination dir.
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
ctx.send({
|
|
||||||
message: 'No config files were found.'
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
|
||||||
|
|
||||||
configFiles.map((file) => {
|
|
||||||
strapi.plugins['config-sync'].services.config.importFromFile(file.slice(0, -5));
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send({
|
|
||||||
message: 'Config was successfully imported.'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all configs as defined in your filesystem.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getConfigsFromFiles: async (ctx) => {
|
|
||||||
// Check for existance of the config file destination dir.
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
ctx.send({
|
|
||||||
message: 'No config files were found.'
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
|
||||||
let formattedConfigs = {};
|
|
||||||
|
|
||||||
const getConfigs = async () => {
|
|
||||||
return Promise.all(configFiles.map(async (file) => {
|
|
||||||
const formattedConfigName = file.slice(0, -5); // remove the .json extension.
|
|
||||||
const fileContents = await strapi.plugins['config-sync'].services.config.readConfigFile(formattedConfigName);
|
|
||||||
formattedConfigs[formattedConfigName] = fileContents;
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
await getConfigs();
|
|
||||||
|
|
||||||
ctx.send(formattedConfigs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all configs as defined in your database.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getConfigsFromDatabase: async (ctx) => {
|
|
||||||
const coreStoreAPI = strapi.query('core_store');
|
|
||||||
const coreStore = await coreStoreAPI.find({ _limit: -1 });
|
|
||||||
|
|
||||||
let formattedConfigs = {};
|
|
||||||
Object.values(coreStore).map(async ({ key, value }) => {
|
|
||||||
formattedConfigs[key] = JSON.parse(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.send(formattedConfigs);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
const { defineConfig } = require('cypress');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:1337',
|
||||||
|
specPattern: '**/*.cy.{js,ts,jsx,tsx}',
|
||||||
|
video: true,
|
||||||
|
defaultCommandTimeout: 30000,
|
||||||
|
requestTimeout: 30000,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here.
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
require('cypress-terminal-report/src/installLogsPrinter')(on);
|
||||||
|
|
||||||
|
on('task', {
|
||||||
|
deleteFolder(folderName) {
|
||||||
|
console.log(`deleting folder ${folderName}`);
|
||||||
|
|
||||||
|
return fs.remove(folderName)
|
||||||
|
.then(() => {
|
||||||
|
console.log(`folder ${folderName} deleted`);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error deleting folder ${folderName}`, err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,129 @@
|
||||||
|
// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
//
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (path) => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/users/me',
|
||||||
|
}).as('sessionCheck');
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/init',
|
||||||
|
}).as('adminInit');
|
||||||
|
|
||||||
|
// Wait for the initial request to complete.
|
||||||
|
cy.wait('@adminInit').its('response.statusCode').should('equal', 200);
|
||||||
|
|
||||||
|
// Wait for the form to render.
|
||||||
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('body').then(($body) => {
|
||||||
|
// Login
|
||||||
|
if ($body.text().includes('Log in to your Strapi account')) {
|
||||||
|
cy.get('input[name="email"]').type('johndoe@example.com');
|
||||||
|
cy.get('input[name="password"]').type('Abc12345678');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.wait('@sessionCheck').its('response.statusCode').should('equal', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register
|
||||||
|
if ($body.text().includes('Credentials are only used to authenticate in Strapi')) {
|
||||||
|
cy.get('input[name="firstname"]').type('John');
|
||||||
|
cy.get('input[name="email"]').type('johndoe@example.com');
|
||||||
|
cy.get('input[name="password"]').type('Abc12345678');
|
||||||
|
cy.get('input[name="confirmPassword"]').type('Abc12345678');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.wait('@sessionCheck').its('response.statusCode').should('equal', 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('navigateToInterface', (path) => {
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/config-sync/diff',
|
||||||
|
}).as('getConfigDiff');
|
||||||
|
|
||||||
|
cy.get('a[href="/admin/settings"]').click();
|
||||||
|
cy.get('a[href="/admin/settings/config-sync"]').click();
|
||||||
|
|
||||||
|
cy.wait('@getConfigDiff').its('response.statusCode').should('equal', 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Cypress.Commands.add('initialExport', (path) => {
|
||||||
|
cy.intercept({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/config-sync/export',
|
||||||
|
}).as('exportConfig');
|
||||||
|
|
||||||
|
cy.get('button').contains('Make the initial export').click();
|
||||||
|
cy.get('button').contains('Yes, export').click();
|
||||||
|
|
||||||
|
cy.wait('@exportConfig').its('response.statusCode').should('equal', 200);
|
||||||
|
|
||||||
|
cy.contains('Config was successfully exported to config/sync/.');
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('makeConfigChanges', (path) => {
|
||||||
|
// Change a setting in the UP advanced settings
|
||||||
|
cy.intercept({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/users-permissions/advanced',
|
||||||
|
}).as('saveUpAdvanced');
|
||||||
|
cy.get('a[href="/admin/settings/users-permissions/advanced-settings"]').click();
|
||||||
|
cy.get('input[name="unique_email"').click();
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.wait('@saveUpAdvanced').its('response.statusCode').should('equal', 200);
|
||||||
|
|
||||||
|
// Change a setting in the media library settings
|
||||||
|
cy.intercept({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/upload/settings',
|
||||||
|
}).as('saveMediaLibrarySettings');
|
||||||
|
cy.get('a[href="/admin/settings/media-library"]').click();
|
||||||
|
cy.get('input[name="responsiveDimensions"').click();
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.wait('@saveMediaLibrarySettings').its('response.statusCode').should('equal', 200);
|
||||||
|
|
||||||
|
// Change a setting in the email templates
|
||||||
|
cy.intercept({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/users-permissions/email-templates',
|
||||||
|
}).as('saveUpEmailTemplates');
|
||||||
|
cy.get('a[href="/admin/settings/users-permissions/email-templates"]').click();
|
||||||
|
cy.get('tbody tr').contains('Reset password').click();
|
||||||
|
cy.get('input[name="options.response_email"]').clear();
|
||||||
|
cy.get('input[name="options.response_email"]').type(`${Math.random().toString(36).substring(2, 15)}@example.com`);
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.wait('@saveUpEmailTemplates').its('response.statusCode').should('equal', 200);
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/e2e.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands';
|
||||||
|
|
||||||
|
require('cypress-terminal-report/src/installLogsCollector')();
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
ignore:
|
||||||
|
- dependency-name: '\*'
|
||||||
|
update-types: ["version-update:semver-patch"]
|
||||||
|
groups:
|
||||||
|
strapi:
|
||||||
|
patterns:
|
||||||
|
- "@strapi/*"
|
|
@ -0,0 +1,52 @@
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: docs.pluginpal.io
|
||||||
|
url: https://docs.pluginpal.io
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
|
||||||
|
- name: Build a Docker image
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
-t pluginpal-docs:latest .
|
||||||
|
docker save -o pluginpal-docs-latest.tar pluginpal-docs:latest
|
||||||
|
|
||||||
|
- name: Transfer the Docker image to the Dokku server
|
||||||
|
uses: appleboy/scp-action@v0.1.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_CI_USERNAME }}
|
||||||
|
password: ${{ secrets.SSH_CI_PASSWORD }}
|
||||||
|
source: pluginpal-docs-latest.tar
|
||||||
|
target: /var/lib/dokku/data/storage/docs/docker-images
|
||||||
|
|
||||||
|
- name: Deploy the Dokku app based on the Docker image
|
||||||
|
uses: appleboy/ssh-action@v0.1.10
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_CI_USERNAME }}
|
||||||
|
password: ${{ secrets.SSH_CI_PASSWORD }}
|
||||||
|
script_stop: true
|
||||||
|
script: |
|
||||||
|
sudo docker load -i /var/lib/dokku/data/storage/docs/docker-images/pluginpal-docs-latest.tar
|
||||||
|
DOCS_LATEST_IMAGE=$(sudo docker images --format "{{.ID}}" pluginpal-docs:latest)
|
||||||
|
sudo docker tag pluginpal-docs:latest pluginpal-docs:$DOCS_LATEST_IMAGE
|
||||||
|
dokku git:from-image docs pluginpal-docs:$DOCS_LATEST_IMAGE
|
||||||
|
sudo docker system prune --all --force
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
|
@ -0,0 +1,29 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Stage 1: Base image.
|
||||||
|
## Start with a base image containing NodeJS so we can build Docusaurus.
|
||||||
|
FROM node:18-alpine3.18 as base
|
||||||
|
## Disable colour output from yarn to make logs easier to read.
|
||||||
|
ENV FORCE_COLOR=0
|
||||||
|
## Enable corepack.
|
||||||
|
RUN corepack enable
|
||||||
|
## Set the working directory to `/opt/docusaurus`.
|
||||||
|
WORKDIR /opt/docusaurus
|
||||||
|
|
||||||
|
# Stage 2b: Production build mode.
|
||||||
|
FROM base as prod
|
||||||
|
## Set the working directory to `/opt/docusaurus`.
|
||||||
|
WORKDIR /opt/docusaurus
|
||||||
|
## Copy over the source code.
|
||||||
|
COPY . /opt/docusaurus/
|
||||||
|
## Install dependencies with `--immutable` to ensure reproducibility.
|
||||||
|
RUN yarn install
|
||||||
|
## Build the static site.
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Stage 3a: Serve with `docusaurus serve`.
|
||||||
|
FROM prod as serve
|
||||||
|
## Expose the port that Docusaurus will run on.
|
||||||
|
EXPOSE 3000
|
||||||
|
## Run the production server.
|
||||||
|
CMD ["yarn", "serve", "--host", "0.0.0.0", "--no-open"]
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
};
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
slug: first-blog-post
|
||||||
|
title: First Blog Post
|
||||||
|
authors: [slorber, yangshun]
|
||||||
|
tags: [hola, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet...
|
||||||
|
|
||||||
|
<!-- truncate -->
|
||||||
|
|
||||||
|
...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
slug: long-blog-post
|
||||||
|
title: Long Blog Post
|
||||||
|
authors: yangshun
|
||||||
|
tags: [hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the summary of a very long blog post,
|
||||||
|
|
||||||
|
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||||
|
|
||||||
|
<!-- truncate -->
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
slug: mdx-blog-post
|
||||||
|
title: MDX Blog Post
|
||||||
|
authors: [slorber]
|
||||||
|
tags: [docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
Use the power of React to create interactive blog posts.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
{/* truncate */}
|
||||||
|
|
||||||
|
For example, use JSX to create an interactive button:
|
||||||
|
|
||||||
|
```js
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
slug: welcome
|
||||||
|
title: Welcome
|
||||||
|
authors: [slorber, yangshun]
|
||||||
|
tags: [facebook, hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||||
|
|
||||||
|
Here are a few tips you might find useful.
|
||||||
|
|
||||||
|
<!-- truncate -->
|
||||||
|
|
||||||
|
Simply add Markdown files (or folders) to the `blog` directory.
|
||||||
|
|
||||||
|
Regular blog authors can be added to `authors.yml`.
|
||||||
|
|
||||||
|
The blog post date can be extracted from filenames, such as:
|
||||||
|
|
||||||
|
- `2019-05-30-welcome.md`
|
||||||
|
- `2019-05-30-welcome/index.md`
|
||||||
|
|
||||||
|
A blog post folder can be convenient to co-locate blog post images:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The blog supports tags as well!
|
||||||
|
|
||||||
|
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
|
@ -0,0 +1,23 @@
|
||||||
|
yangshun:
|
||||||
|
name: Yangshun Tay
|
||||||
|
title: Front End Engineer @ Facebook
|
||||||
|
url: https://github.com/yangshun
|
||||||
|
image_url: https://github.com/yangshun.png
|
||||||
|
page: true
|
||||||
|
socials:
|
||||||
|
x: yangshunz
|
||||||
|
github: yangshun
|
||||||
|
|
||||||
|
slorber:
|
||||||
|
name: Sébastien Lorber
|
||||||
|
title: Docusaurus maintainer
|
||||||
|
url: https://sebastienlorber.com
|
||||||
|
image_url: https://github.com/slorber.png
|
||||||
|
page:
|
||||||
|
# customize the url of the author page at /blog/authors/<permalink>
|
||||||
|
permalink: '/all-sebastien-lorber-articles'
|
||||||
|
socials:
|
||||||
|
x: sebastienlorber
|
||||||
|
linkedin: sebastienlorber
|
||||||
|
github: slorber
|
||||||
|
newsletter: https://thisweekinreact.com
|
|
@ -0,0 +1,19 @@
|
||||||
|
facebook:
|
||||||
|
label: Facebook
|
||||||
|
permalink: /facebook
|
||||||
|
description: Facebook tag description
|
||||||
|
|
||||||
|
hello:
|
||||||
|
label: Hello
|
||||||
|
permalink: /hello
|
||||||
|
description: Hello tag description
|
||||||
|
|
||||||
|
docusaurus:
|
||||||
|
label: Docusaurus
|
||||||
|
permalink: /docusaurus
|
||||||
|
description: Docusaurus tag description
|
||||||
|
|
||||||
|
hola:
|
||||||
|
label: Hola
|
||||||
|
permalink: /hola
|
||||||
|
description: Hola tag description
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Plugin config types'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /api/plugin-config-types
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plugin config types
|
||||||
|
|
||||||
|
When you're writing a plugin, which registers a content type, you might want to consider that content type as a config type as defined in the Config Sync specification.
|
||||||
|
|
||||||
|
## Register a config type programatically
|
||||||
|
|
||||||
|
You can register a config type by adding some code to the register function of your plugin.
|
||||||
|
|
||||||
|
```md title="register.js"
|
||||||
|
// Register the config type when using the config-sync plugin.
|
||||||
|
if (strapi.plugin('config-sync')) {
|
||||||
|
if (!strapi.plugin('config-sync').pluginTypes) {
|
||||||
|
strapi.plugin('config-sync').pluginTypes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
strapi.plugin('config-sync').pluginTypes.push({
|
||||||
|
configName: 'url-pattern',
|
||||||
|
queryString: 'plugin::webtools.url-pattern',
|
||||||
|
uid: 'code',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to read more about what the different values of a config type actually mean please read the in depth [custom types](/config-types#custom-types) docs
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Custom types'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/custom-types
|
||||||
|
---
|
||||||
|
|
||||||
|
# Custom types
|
||||||
|
|
||||||
|
With this setting you can register your own custom config types. This is an array which expects objects with at least the `configName`, `queryString` and `uid` properties. Read more about registering custom types in the [Custom config types](/config-types#custom-types) documentation.
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `customTypes` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | array |
|
||||||
|
| Default | `[]` |
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Excluded config'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/excluded-config
|
||||||
|
---
|
||||||
|
|
||||||
|
# Excluded config
|
||||||
|
|
||||||
|
Specify the names of configs you want to exclude from the syncing process. By default the API tokens for users-permissions, which are stored in core_store, are excluded. This setting expects the config names to comply with the naming convention.
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `excludedConfig` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | array |
|
||||||
|
| Default | `['core-store.plugin_users-permissions_grant', 'core-store.plugin_upload_metrics', 'core-store.strapi_content_types_schema', 'core-store.ee_information',]` |
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Excluded types'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/excluded-types
|
||||||
|
---
|
||||||
|
|
||||||
|
# Excluded types
|
||||||
|
|
||||||
|
This setting will exclude all the config from a given type from the syncing process. The config types are specified by the `configName` of the type.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
excludedTypes: ['admin-role']
|
||||||
|
```
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `excludedTypes` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | array |
|
||||||
|
| Default | `[]` |
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Import on bootstrap'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/import-on-bootstrap
|
||||||
|
---
|
||||||
|
|
||||||
|
# Import on bootstrap
|
||||||
|
|
||||||
|
Allows you to let the config be imported automaticly when strapi is bootstrapping (on `strapi start`).
|
||||||
|
|
||||||
|
:::danger
|
||||||
|
This setting can't be used locally and should be handled very carefully as it can unintendedly overwrite the changes in your database. **PLEASE USE WITH CARE**.
|
||||||
|
:::
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `importOnBootstrap` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | bool |
|
||||||
|
| Default | `false` |
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Introduction'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔧 Configuration
|
||||||
|
The settings of the plugin can be overridden in the `config/plugins.js` file.
|
||||||
|
In the example below you can see how, and also what the default settings are.
|
||||||
|
|
||||||
|
```md title="config/plugins.js"
|
||||||
|
module.exports = ({ env }) => ({
|
||||||
|
// ...
|
||||||
|
'config-sync': {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
syncDir: "config/sync/",
|
||||||
|
minify: false,
|
||||||
|
soft: false,
|
||||||
|
importOnBootstrap: false,
|
||||||
|
customTypes: [],
|
||||||
|
excludedTypes: [],
|
||||||
|
excludedConfig: [
|
||||||
|
"core-store.plugin_users-permissions_grant",
|
||||||
|
"core-store.plugin_upload_metrics",
|
||||||
|
"core-store.strapi_content_types_schema",
|
||||||
|
"core-store.ee_information",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Minify'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/minify
|
||||||
|
---
|
||||||
|
|
||||||
|
# Minify
|
||||||
|
|
||||||
|
When enabled all the exported JSON files will be minified.
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `minify` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | bool |
|
||||||
|
| Default | `false` |
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Soft'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/soft
|
||||||
|
---
|
||||||
|
|
||||||
|
# Soft
|
||||||
|
|
||||||
|
When enabled the import action will be limited to only create new entries. Entries to be deleted, or updated will be skipped from the import process and will remain in it's original state.
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `soft` |
|
||||||
|
| Required | false |
|
||||||
|
| Type | bool |
|
||||||
|
| Default | `false` |
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Sync dir'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /configuration/sync-dir
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sync dir
|
||||||
|
|
||||||
|
The path for reading and writing the sync files.
|
||||||
|
|
||||||
|
| Name | Details |
|
||||||
|
| ---- | ------- |
|
||||||
|
| Key | `syncDir` |
|
||||||
|
| Required | true |
|
||||||
|
| Type | string |
|
||||||
|
| Default | `config/sync/` |
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Admin GUI'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /admin-gui
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🖥️ Admin panel (GUI)
|
||||||
|
This plugin ships with a React app which can be accessed from the settings page in Strapi admin panel. On this page you can pretty much do the same as you can from the CLI. You can import, export and see the difference between the config as found in the sync directory, and the config as found in the database.
|
||||||
|
|
||||||
|
**Pro tip:**
|
||||||
|
By clicking on one of the items in the diff table you can see the exact difference between sync dir and database in a git-style diff viewer.
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,138 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'CLI'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /cli
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔌 Command line interface (CLI)
|
||||||
|
|
||||||
|
Add the `config-sync` command as a script to the `package.json` of your Strapi project:
|
||||||
|
|
||||||
|
```
|
||||||
|
"scripts": {
|
||||||
|
// ...
|
||||||
|
"cs": "config-sync"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now run all the `config-sync` commands like this:
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn cs --help
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm run cs -- --help
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## ⬆️ Import ⬇️ Export
|
||||||
|
|
||||||
|
> _Command:_ `import` _Alias:_ `i`
|
||||||
|
>
|
||||||
|
> _Command:_ `export` _Alias:_ `e`
|
||||||
|
|
||||||
|
These commands are used to sync the config in your Strapi project.
|
||||||
|
|
||||||
|
_Example:_
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn cs import
|
||||||
|
yarn cs export
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm run cs import
|
||||||
|
npm run cs export
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
:::info
|
||||||
|
When you're using `npm` to run these commands, please note that you need an extra `--` to forward the flags to the script.
|
||||||
|
More information about this topic can be found on the <a href="https://docs.npmjs.com/cli/commands/npm-run-script">NPM documentation</a>.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
npm run cs import -- --yes
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Flag: `-y`, `--yes`
|
||||||
|
|
||||||
|
Use this flag to skip the confirm prompt and go straight to syncing the config.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flag: `-t`, `--type`
|
||||||
|
|
||||||
|
Use this flag to specify the type of config you want to sync.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --type user-role
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flag: `-p`, `--partial`
|
||||||
|
|
||||||
|
Use this flag to sync a specific set of configs by giving the CLI a comma-separated string of config names.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --partial user-role.public,i18n-locale.en
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flag: `-f`, `--force`
|
||||||
|
|
||||||
|
If you're using the soft setting to gracefully import config, you can use this flag to ignore the setting for the current command and forcefully import all changes anyway.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## ↔️ Diff
|
||||||
|
|
||||||
|
> _Command:_ `diff` | _Alias:_ `d`
|
||||||
|
|
||||||
|
This command is used to see the difference between the config as found in the sync directory, and the config as found in the database.
|
||||||
|
|
||||||
|
_Example:_
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn cs diff
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm run cs diff
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Argument: `<single>`
|
||||||
|
|
||||||
|
Add a single config name as the argument of the `diff` command to see the difference of that single file in a git-style diff viewer.
|
||||||
|
|
||||||
|
_Example:_
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn cs diff user-role.public
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm run cs diff user-role.public
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
|
@ -0,0 +1,125 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Config Types'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /config-types
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚀 Config types
|
||||||
|
|
||||||
|
By default the plugin will track 4 (official) types.
|
||||||
|
|
||||||
|
To track your own custom types you can register them by setting some plugin config.
|
||||||
|
|
||||||
|
## Default types
|
||||||
|
|
||||||
|
These 4 types are by default registered in the sync process.
|
||||||
|
|
||||||
|
### Admin role
|
||||||
|
|
||||||
|
> Config name: `admin-role` | UID: `code` | Query string: `admin::role`
|
||||||
|
|
||||||
|
### User role
|
||||||
|
|
||||||
|
> Config name: `user-role` | UID: `type` | Query string: `plugin::users-permissions.role`
|
||||||
|
|
||||||
|
### Core store
|
||||||
|
|
||||||
|
> Config name: `core-store` | UID: `key` | Query string: `strapi::core-store`
|
||||||
|
|
||||||
|
### I18n locale
|
||||||
|
|
||||||
|
> Config name: `i18n-locale` | UID: `code` | Query string: `plugin::i18n.locale`
|
||||||
|
|
||||||
|
## Custom types
|
||||||
|
|
||||||
|
Your custom types can be registered through the `customTypes` plugin config. This is a setting that can be set in the `config/plugins.js` file in your project.
|
||||||
|
|
||||||
|
_Read more about the `config/plugins.js` file [here](/configuration)._
|
||||||
|
|
||||||
|
You can register a type by giving the `customTypes` array an object which contains at least the following 3 properties:
|
||||||
|
|
||||||
|
```
|
||||||
|
customTypes: [{
|
||||||
|
configName: 'webhook',
|
||||||
|
queryString: 'webhook',
|
||||||
|
uid: 'name',
|
||||||
|
}],
|
||||||
|
```
|
||||||
|
|
||||||
|
_The example above will register the Strapi webhook type._
|
||||||
|
|
||||||
|
### Config name
|
||||||
|
|
||||||
|
The name of the config type. This value will be used as the first part of the filename for all config of this type. It should be unique from the other types and is preferably written in kebab-case.
|
||||||
|
|
||||||
|
##### Key: `configName`
|
||||||
|
|
||||||
|
> `required:` YES | `type:` string
|
||||||
|
|
||||||
|
### Query string
|
||||||
|
|
||||||
|
This is the query string of the type. Each type in Strapi has its own query string you can use to programatically preform CRUD actions on the entries of the type. Often for custom types in Strapi the format is something like `api::custom-api.custom-type`.
|
||||||
|
|
||||||
|
##### Key: `queryString`
|
||||||
|
|
||||||
|
> `required:` YES | `type:` string
|
||||||
|
|
||||||
|
### UID
|
||||||
|
|
||||||
|
The UID represents a field on the registered type. The value of this field will act as a unique identifier to identify the entries across environments. Therefore it should be unique and preferably un-editable after initial creation.
|
||||||
|
|
||||||
|
Mind that you can not use an auto-incremental value like the `id` as auto-increment does not play nice when you try to match entries across different databases.
|
||||||
|
|
||||||
|
If you do not have a single unique value, you can also pass in an array of keys for a combined uid key. This is for example the case for all content types which use i18n features (An example config would be `uid: ['productId', 'locale']`).
|
||||||
|
|
||||||
|
##### Key: `uid`
|
||||||
|
|
||||||
|
> `required:` YES | `type:` string | string[]
|
||||||
|
|
||||||
|
### Relations
|
||||||
|
|
||||||
|
The relations array specifies the relations you want to include in the sync process.
|
||||||
|
This feature is used to sync the relations between `roles` and `permissions`. See https://github.com/boazpoolman/strapi-plugin-config-sync/blob/master/server/config/types.js#L16.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
configName: 'admin-role',
|
||||||
|
queryString: 'admin::role',
|
||||||
|
uid: 'code',
|
||||||
|
relations: [{
|
||||||
|
queryString: 'admin::permission',
|
||||||
|
relationName: 'permissions',
|
||||||
|
parentName: 'role',
|
||||||
|
relationSortFields: ['action', 'subject'],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Key: `relations`
|
||||||
|
|
||||||
|
> `required:` NO | `type:` array
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
This property can accept an array of component names from the type. Strapi Components can be included in the export/import process. With "." nested components can also be included in the process.
|
||||||
|
```
|
||||||
|
customTypes: [{
|
||||||
|
configName: 'webhook',
|
||||||
|
queryString: 'webhook',
|
||||||
|
uid: 'name',
|
||||||
|
components: ['ParentComponentA', 'ParentComponentA.ChildComponent', 'ParentComponentB']
|
||||||
|
}],
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Key: `components`
|
||||||
|
|
||||||
|
> `required:` NO | `type:` array
|
||||||
|
|
||||||
|
### JSON fields
|
||||||
|
|
||||||
|
This property can accept an array of field names from the type. It is meant to specify the JSON fields on the type so the plugin can better format the field values when calculating the config difference.
|
||||||
|
|
||||||
|
##### Key: `jsonFields`
|
||||||
|
|
||||||
|
> `required:` NO | `type:` array
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Installation'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⏳ Installation
|
||||||
|
|
||||||
|
:::prerequisites
|
||||||
|
Complete installation requirements are the exact same as for Strapi itself and can be found in the Strapi documentation.
|
||||||
|
|
||||||
|
**Supported Strapi versions:**
|
||||||
|
|
||||||
|
Strapi v5 use `strapi-plugin-config-sync@^3`
|
||||||
|
|
||||||
|
Strapi v4 use `strapi-plugin-config-sync@^1`
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Install the plugin in your Strapi project.
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn add strapi-plugin-config-sync
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm install strapi-plugin-config-sync --save
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Add the export path to the `watchIgnoreFiles` list in the `config/admin.js` file.
|
||||||
|
This way your app won't reload when you export the config in development.
|
||||||
|
|
||||||
|
```md title="config/admin.js"
|
||||||
|
module.exports = ({ env }) => ({
|
||||||
|
// ...
|
||||||
|
watchIgnoreFiles: [
|
||||||
|
'**/config/sync/**',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
After successful installation you have to rebuild the admin UI so it'll include this plugin. To rebuild and restart Strapi run:
|
||||||
|
|
||||||
|
<Tabs groupId="yarn-npm">
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
yarn develop
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="npm" label="NPM">
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
The **Config Sync** plugin should now appear in the **Settings** section of your Strapi app.
|
||||||
|
|
||||||
|
To start tracking your config changes you have to make the first export. This will dump all your configuration data to the `/config/sync` directory. You can export either through [the CLI](/cli) or [Strapi admin panel](/admin-gui)
|
||||||
|
|
||||||
|
Enjoy 🎉
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Motivation'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /motivation
|
||||||
|
---
|
||||||
|
|
||||||
|
# 💡 Motivation
|
||||||
|
|
||||||
|
In Strapi we come across what I would call config types. These are models of which the records are stored in our database, just like content types. Though the big difference here is that your code often relies on the database records of these types.
|
||||||
|
|
||||||
|
Having said that, it makes sense that these records can be exported, added to git, and be migrated across environments. This way we can make sure we have all the data our code relies on, on each environment.
|
||||||
|
|
||||||
|
Examples of these types are:
|
||||||
|
|
||||||
|
- Admin roles _(admin::role)_
|
||||||
|
- User roles _(plugin::users-permissions.role)_
|
||||||
|
- Admin settings _(strapi::core-store)_
|
||||||
|
- I18n locale _(plugin::i18n.locale)_
|
||||||
|
|
||||||
|
This plugin gives you the tools to sync this data. You can export the data as JSON files on one env, and import them on every other env. By writing this data as JSON files you can easily track them in your version control system (git).
|
||||||
|
|
||||||
|
_With great power comes great responsibility - Uncle Ben_
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Naming convention'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /naming-convention
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔍 Naming convention
|
||||||
|
All the config files written in the sync directory have the same naming convention. It goes as follows:
|
||||||
|
|
||||||
|
[config-type].[identifier].json
|
||||||
|
|
||||||
|
- `config-type` - Corresponds to the `configName` of the config type.
|
||||||
|
- `identifier` - Corresponds to the value of the `uid` field of the config type.
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Workflow'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /workflow
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⌨️ Usage / Workflow
|
||||||
|
This plugin works best when you use `git` for the version control of your Strapi project.
|
||||||
|
|
||||||
|
_The following workflows are assuming you're using `git`._
|
||||||
|
|
||||||
|
### Intro
|
||||||
|
All database records tracked with this plugin will be exported to JSON files. Once exported each change to the file or the record will be tracked. Meaning you can now do one of two things:
|
||||||
|
|
||||||
|
- Change the file(s), and run an import. You have now imported from filesystem -> database.
|
||||||
|
- Change the record(s), and run an export. You have now exported from database -> filesystem.
|
||||||
|
|
||||||
|
### Local development
|
||||||
|
When building a new feature locally for your Strapi project you'd use the following workflow:
|
||||||
|
|
||||||
|
- Build the feature.
|
||||||
|
- Export the config.
|
||||||
|
- Commit and push the files to git.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
When deploying the newly created feature - to either a server, or a co-worker's machine - you'd use the following workflow:
|
||||||
|
|
||||||
|
- Pull the latest file changes to the environment.
|
||||||
|
- (Re)start your Strapi instance.
|
||||||
|
- Import the config.
|
||||||
|
|
||||||
|
## Production deployment
|
||||||
|
The production deployment will be the same as a regular deployment. You just have to be careful before running the import. Ideally making sure the are no open changes before you pull the new code to the environment.
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
sidebar_label: 'Generic update'
|
||||||
|
displayed_sidebar: configSyncSidebar
|
||||||
|
slug: /upgrading/generic-update
|
||||||
|
---
|
||||||
|
|
||||||
|
# Updating Config Sync
|
||||||
|
|
||||||
|
We are always working to make Config Sync better by fixing bugs and introducing new features. These changes will be released as minor or patch versions as defined in the Semantic Versioning specification.
|
||||||
|
|
||||||
|
## Bump a minor/patch version
|
||||||
|
|
||||||
|
When you're updating Config Sync you'll have to follow these steps:
|
||||||
|
|
||||||
|
1. Make sure there are no config changes before starting. Either export or import all staged changes.
|
||||||
|
2. Update the version of the `strapi-plugin-config-sync` package in your `package.json` using your package manager of choice (yarn/npm/pnpm)
|
||||||
|
3. After you've bumped the version make sure to export any new changes that are now shown. It is possible that new configs are introduced, or old ones are updated/removed.
|
||||||
|
4. You're now ready to push these changes an commit them to your source control!
|
|
@ -0,0 +1,147 @@
|
||||||
|
import {themes as prismThemes} from 'prism-react-renderer';
|
||||||
|
import type {Config} from '@docusaurus/types';
|
||||||
|
import type * as Preset from '@docusaurus/preset-classic';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
title: 'Strapi Config Sync',
|
||||||
|
tagline: "Documentation for the config-sync plugin for Strapi",
|
||||||
|
favicon: 'img/favicon.jpg',
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
'docusaurus-plugin-sass',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set the production url of your site here
|
||||||
|
url: 'https://docs.pluginpal.io',
|
||||||
|
// Set the /<baseUrl>/ pathname under which your site is served
|
||||||
|
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||||
|
baseUrl: '/config-sync/',
|
||||||
|
|
||||||
|
// GitHub pages deployment config.
|
||||||
|
// If you aren't using GitHub pages, you don't need these.
|
||||||
|
organizationName: 'pluginpal', // Usually your GitHub org/user name.
|
||||||
|
|
||||||
|
onBrokenLinks: 'throw',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
|
|
||||||
|
// Even if you don't use internationalization, you can use this field to set
|
||||||
|
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||||
|
// may want to replace "en" with "zh-Hans".
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// themes: ['@docusaurus/theme-live-codeblock', '@docusaurus/theme-mermaid'],
|
||||||
|
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'classic',
|
||||||
|
{
|
||||||
|
docs: {
|
||||||
|
routeBasePath: '/',
|
||||||
|
sidebarPath: './sidebars.ts',
|
||||||
|
// Please change this to your repo.
|
||||||
|
// Remove this to remove the "edit this page" links.
|
||||||
|
editUrl:
|
||||||
|
'https://github.com/pluginpal/strapi-plugin-config-sync/tree/master/docs',
|
||||||
|
admonitions: {
|
||||||
|
keywords: [
|
||||||
|
// Admonitions defaults
|
||||||
|
'note',
|
||||||
|
'tip',
|
||||||
|
'info',
|
||||||
|
'caution',
|
||||||
|
'danger',
|
||||||
|
|
||||||
|
// Admonitions custom
|
||||||
|
'callout',
|
||||||
|
'prerequisites',
|
||||||
|
'strapi',
|
||||||
|
'warning',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blog: false,
|
||||||
|
sitemap: {
|
||||||
|
lastmod: 'date',
|
||||||
|
changefreq: 'weekly',
|
||||||
|
priority: 0.6,
|
||||||
|
// ignorePatterns: ['/tags/**'],
|
||||||
|
filename: 'sitemap.xml',
|
||||||
|
createSitemapItems: async (params) => {
|
||||||
|
const {defaultCreateSitemapItems, ...rest} = params;
|
||||||
|
const items = await defaultCreateSitemapItems(rest);
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
customCss: './src/scss/__index.scss',
|
||||||
|
},
|
||||||
|
} satisfies Preset.Options,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
// Replace with your project's social card
|
||||||
|
// image: 'img/docusaurus-social-card.jpg',
|
||||||
|
navbar: {
|
||||||
|
title: 'Strapi Config Sync',
|
||||||
|
logo: {
|
||||||
|
alt: 'Config Sync logo',
|
||||||
|
src: 'img/logo.png',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
href: 'https://github.com/pluginpal/strapi-plugin-config-sync',
|
||||||
|
label: 'GitHub',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
style: 'dark',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Community',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
href: 'https://discord.com/invite/strapi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Forum',
|
||||||
|
href: 'https://forum.strapi.io/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'More',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Website',
|
||||||
|
href: 'https://www.pluginpal.io',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/pluginpal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
algolia: {
|
||||||
|
appId: 'ADLP623G89',
|
||||||
|
apiKey: '8f91ceaf54e8e8db14479fd79a420a8c',
|
||||||
|
indexName: 'pluginpal',
|
||||||
|
},
|
||||||
|
|
||||||
|
prism: {
|
||||||
|
theme: prismThemes.github,
|
||||||
|
darkTheme: prismThemes.dracula,
|
||||||
|
},
|
||||||
|
} satisfies Preset.ThemeConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "pluginpal-docs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"docusaurus": "docusaurus",
|
||||||
|
"start": "docusaurus start",
|
||||||
|
"build": "docusaurus build",
|
||||||
|
"swizzle": "docusaurus swizzle",
|
||||||
|
"deploy": "docusaurus deploy",
|
||||||
|
"clear": "docusaurus clear",
|
||||||
|
"serve": "docusaurus serve",
|
||||||
|
"write-translations": "docusaurus write-translations",
|
||||||
|
"write-heading-ids": "docusaurus write-heading-ids",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@docusaurus/core": "3.5.2",
|
||||||
|
"@docusaurus/plugin-sitemap": "^3.5.2",
|
||||||
|
"@docusaurus/preset-classic": "3.5.2",
|
||||||
|
"@docusaurus/theme-live-codeblock": "^3.5.2",
|
||||||
|
"@docusaurus/theme-mermaid": "^3.5.2",
|
||||||
|
"@docusaurus/theme-search-algolia": "^3.5.2",
|
||||||
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"docusaurus-plugin-sass": "^0.2.5",
|
||||||
|
"prism-react-renderer": "^2.3.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"sass": "^1.78.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "3.5.2",
|
||||||
|
"@docusaurus/tsconfig": "3.5.2",
|
||||||
|
"@docusaurus/types": "3.5.2",
|
||||||
|
"typescript": "~5.5.2"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.5%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 3 chrome version",
|
||||||
|
"last 3 firefox version",
|
||||||
|
"last 5 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
- create an ordered group of docs
|
||||||
|
- render a sidebar for each doc of that group
|
||||||
|
- provide next/previous navigation
|
||||||
|
|
||||||
|
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||||
|
|
||||||
|
Create as many sidebars as you want.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||||
|
const sidebars = {
|
||||||
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
|
// tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
|
||||||
|
|
||||||
|
// But you can create a sidebar manually
|
||||||
|
configSyncSidebar: [
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
collapsed: false,
|
||||||
|
label: "🚀 Getting Started",
|
||||||
|
items: [
|
||||||
|
"getting-started/installation",
|
||||||
|
"getting-started/motivation",
|
||||||
|
"getting-started/cli",
|
||||||
|
"getting-started/admin-gui",
|
||||||
|
"getting-started/config-types",
|
||||||
|
"getting-started/workflow",
|
||||||
|
"getting-started/naming-convention",
|
||||||
|
// "dev-docs/usage-information",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
collapsed: false,
|
||||||
|
label: "⚙️ Configuration",
|
||||||
|
items: [
|
||||||
|
"configuration/introduction",
|
||||||
|
"configuration/sync-dir",
|
||||||
|
"configuration/minify",
|
||||||
|
"configuration/import-on-bootstrap",
|
||||||
|
"configuration/custom-types",
|
||||||
|
"configuration/soft",
|
||||||
|
"configuration/excluded-types",
|
||||||
|
"configuration/excluded-config",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
collapsed: false,
|
||||||
|
label: "📦 API",
|
||||||
|
items: [
|
||||||
|
"api/plugin-config-types",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
collapsed: false,
|
||||||
|
label: "♻️ Upgrading",
|
||||||
|
items: [
|
||||||
|
"upgrading/generic-update",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = sidebars;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function ApiCall({
|
||||||
|
children,
|
||||||
|
noSideBySide = false,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'api-call',
|
||||||
|
(noSideBySide && 'api-call--no-side-by-side'),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export default function Badge({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
link = '',
|
||||||
|
noLink = false,
|
||||||
|
variant = '',
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const variantNormalized = variant.toLowerCase().replace(/\W/g, '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'badge',
|
||||||
|
'badge--feature',
|
||||||
|
(variantNormalized && `badge--${variantNormalized.toLowerCase()}`),
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{(noLink || !link) ? (
|
||||||
|
<>
|
||||||
|
{variant}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a className="badge__link" href={link}>
|
||||||
|
{variant}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlphaBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Alpha"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BetaBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Beta"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FutureBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Future"
|
||||||
|
link="/dev-docs/configurations/features"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnterpriseBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Enterprise"
|
||||||
|
link="https://strapi.io/pricing-self-hosted"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CloudProBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Strapi Cloud Pro"
|
||||||
|
link="https://strapi.io/pricing-cloud"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CloudTeamBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Strapi Cloud Team"
|
||||||
|
link="https://strapi.io/pricing-cloud"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function CloudDevBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Strapi Cloud Dev"
|
||||||
|
link="https://strapi.io/pricing-cloud"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="New ✨"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function UpdatedBadge(props) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="Updated ️🖌"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import styles from './button.module.scss';
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
href,
|
||||||
|
to,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
decorative,
|
||||||
|
size = '',
|
||||||
|
variant = 'primary',
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const ButtonElement = (to ? Link : (href ? 'a' : 'button'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonElement
|
||||||
|
{...rest}
|
||||||
|
{...(!href ? {} : { href, target: '_blank' })}
|
||||||
|
{...(!to ? {} : { to })}
|
||||||
|
className={clsx(
|
||||||
|
'button',
|
||||||
|
(variant && styles[`button--${variant}`]),
|
||||||
|
(size && styles[`button--${size}`]),
|
||||||
|
styles.button,
|
||||||
|
styles[variant],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{decorative && (
|
||||||
|
<span className={styles.button__decorative}>
|
||||||
|
{decorative}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ButtonElement>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/** Component: Button */
|
||||||
|
|
||||||
|
@import '../../scss/_mixins.scss';
|
||||||
|
|
||||||
|
.button {
|
||||||
|
--strapi-button-background-color: var(--strapi-primary-600);
|
||||||
|
--strapi-button-border-color: var(--strapi-primary-600);
|
||||||
|
--strapi-button-border-radius: 4px;
|
||||||
|
--strapi-button-box-shadow: 0 0 0 transparent;
|
||||||
|
--strapi-button-color: #fff;
|
||||||
|
--strapi-button-font-size: 12px;
|
||||||
|
--strapi-button-font-weight: 600;
|
||||||
|
--strapi-button-line-height: 16px;
|
||||||
|
--strapi-button-position: relative;
|
||||||
|
--strapi-button-py: 7px;
|
||||||
|
--strapi-button-px: 15px;
|
||||||
|
--strapi-button-transition-property: color, background, border-color, box-shadow;
|
||||||
|
|
||||||
|
--strapi-button-hover-background-color: var(--strapi-primary-700);
|
||||||
|
--strapi-button-hover-border-color: var(--strapi-primary-700);
|
||||||
|
--strapi-button-hover-box-shadow: 0px 9px 10px rgba(44, 56, 148, 0.2475);
|
||||||
|
--strapi-button-hover-color: #fff;
|
||||||
|
|
||||||
|
--ifm-button-color: var(--strapi-button-color);
|
||||||
|
--ifm-button-background-color: var(--strapi-button-background-color);
|
||||||
|
--ifm-button-border-color: var(--strapi-button-border-color);
|
||||||
|
--ifm-button-border-radius: var(--strapi-button-border-radius);
|
||||||
|
--ifm-button-font-weight: var(--strapi-button-font-weight);
|
||||||
|
--ifm-button-padding-horizontal: var(--strapi-button-px);
|
||||||
|
--ifm-button-padding-vertical: var(--strapi-button-py);
|
||||||
|
--ifm-button-size-multiplier: 1;
|
||||||
|
|
||||||
|
--ifm-color-primary-darker: var(--strapi-primary-200);
|
||||||
|
|
||||||
|
--ifm-link-hover-color: var(--strapi-button-color);
|
||||||
|
--ifm-link-hover-decoration: none;
|
||||||
|
|
||||||
|
position: var(--strapi-button-position);
|
||||||
|
font-size: var(--strapi-button-font-size);
|
||||||
|
line-height: var(--strapi-button-line-height);
|
||||||
|
box-shadow: var(--strapi-button-box-shadow);
|
||||||
|
transition-property: var(--strapi-button-transition-property);
|
||||||
|
|
||||||
|
&__decorative {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
bottom: -16px;
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled),
|
||||||
|
&:not([aria-disabled="true"]) {
|
||||||
|
&:focus, &:hover {
|
||||||
|
--strapi-button-box-shadow: var(--strapi-button-hover-box-shadow);
|
||||||
|
--strapi-button-background-color: var(--strapi-button-hover-background-color);
|
||||||
|
--strapi-button-border-color: var(--strapi-button-hover-border-color);
|
||||||
|
--strapi-button-color: var(--strapi-button-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sizes */
|
||||||
|
&--huge {
|
||||||
|
--strapi-button-border-radius: 6px;
|
||||||
|
--strapi-button-font-size: 15px;
|
||||||
|
--strapi-button-line-height: 23px;
|
||||||
|
--strapi-button-py: 11px;
|
||||||
|
--strapi-button-px: 71px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Variants */
|
||||||
|
&--secondary {
|
||||||
|
--strapi-button-background-color: #f0f0ff;
|
||||||
|
--strapi-button-border-color: #d9d8ff;
|
||||||
|
--strapi-button-color: var(--strapi-primary-600);
|
||||||
|
|
||||||
|
--strapi-button-hover-background-color: var(--strapi-neutral-0);
|
||||||
|
--strapi-button-hover-border-color: #d9d8ff;
|
||||||
|
--strapi-button-hover-box-shadow: none;
|
||||||
|
--strapi-button-hover-color: var(--strapi-primary-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dark mode */
|
||||||
|
@include dark {
|
||||||
|
.button {
|
||||||
|
/** Dark mode Variants */
|
||||||
|
&--secondary {
|
||||||
|
--strapi-button-background-color: var(--strapi-neutral-100);
|
||||||
|
--strapi-button-border-color: var(--strapi-neutral-200);
|
||||||
|
|
||||||
|
--strapi-button-hover-background-color: var(--strapi-neutral-0);
|
||||||
|
--strapi-button-hover-border-color: var(--strapi-neutral-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import styles from './card.module.scss';
|
||||||
|
import IconArrow from '@site/static/img/assets/icons/arrow-right.svg';
|
||||||
|
|
||||||
|
export function CardTitle({
|
||||||
|
as,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
withArrow,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const TitleElement = (as || 'h3');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TitleElement
|
||||||
|
className={clsx(
|
||||||
|
styles.card__title,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{withArrow && (
|
||||||
|
<span className={styles.card__title__arrow}>
|
||||||
|
<IconArrow />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TitleElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardDescription({
|
||||||
|
as,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const DescriptionElement = (as || 'div');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionElement
|
||||||
|
className={clsx(
|
||||||
|
styles.card__description,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardImgBg({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={clsx(
|
||||||
|
styles['card__img-bg'],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardImg({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={clsx(
|
||||||
|
styles['card__img'],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
isContentDelimited,
|
||||||
|
to,
|
||||||
|
variant,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const asCallToAction = !!(href || to);
|
||||||
|
const CardElement = (to ? Link : (href ? 'a' : 'div'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardElement
|
||||||
|
{...(!href ? {} : { href, target: '_blank' })}
|
||||||
|
{...(!to ? {} : { to })}
|
||||||
|
className={clsx(
|
||||||
|
styles.card,
|
||||||
|
(asCallToAction && styles['card--cta']),
|
||||||
|
(isContentDelimited && styles['card--content-delimited']),
|
||||||
|
(variant && styles[`card--${variant}`]),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
/** Component: Card */
|
||||||
|
|
||||||
|
@import '../../scss/_mixins.scss';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--strapi-card-background: var(--strapi-neutral-0);
|
||||||
|
--strapi-card-border-color: #EDEDFF;
|
||||||
|
--strapi-card-border-radius: 10px;
|
||||||
|
--strapi-card-box-shadow: 0 0 0 transparent;
|
||||||
|
--strapi-card-content-delimited: 395px;
|
||||||
|
--strapi-card-img-border-width: 5px;
|
||||||
|
--strapi-card-img-border-radius: 5px 5px 0 0;
|
||||||
|
--strapi-card-img-bg-scale: 1;
|
||||||
|
--strapi-card-justify-content: center;
|
||||||
|
--strapi-card-position: relative;
|
||||||
|
--strapi-card-overflow: hidden;
|
||||||
|
--strapi-card-text-align: center;
|
||||||
|
--strapi-card-gap: var(--strapi-spacing-2);
|
||||||
|
--strapi-card-px: var(--strapi-spacing-6);
|
||||||
|
--strapi-card-py: var(--strapi-spacing-6);
|
||||||
|
--strapi-card-title-arrow-left: var(--strapi-card-gap);
|
||||||
|
--strapi-card-title-color: #1D1B84;
|
||||||
|
--strapi-card-title-font-size: 17px;
|
||||||
|
--strapi-card-title-font-weight: 700;
|
||||||
|
--strapi-card-title-line-height: 26px;
|
||||||
|
--strapi-card-description-color: #4E6294;
|
||||||
|
--strapi-card-description-font-size: 15px;
|
||||||
|
--strapi-card-description-line-height: 24px;
|
||||||
|
--strapi-card-hover-border-color: #D6D6FF;
|
||||||
|
--strapi-card-hover-img-bg-scale: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: var(--strapi-card-position);
|
||||||
|
overflow: var(--strapi-card-overflow);
|
||||||
|
background: var(--strapi-card-background);
|
||||||
|
border-radius: var(--strapi-card-border-radius);
|
||||||
|
border: 1px solid var(--strapi-card-border-color);
|
||||||
|
box-shadow: var(--strapi-card-box-shadow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--strapi-card-gap);
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: var(--strapi-card-justify-content);
|
||||||
|
text-align: var(--strapi-card-text-align);
|
||||||
|
padding: var(--strapi-card-py) var(--strapi-card-px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:focus, &:hover {
|
||||||
|
--strapi-card-border-color: var(--strapi-card-hover-border-color);
|
||||||
|
--strapi-card-title-arrow-left: var(--strapi-spacing-3);
|
||||||
|
--strapi-card-img-bg-scale: var(--strapi-card-hover-img-bg-scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: var(--strapi-card-title-color);
|
||||||
|
font-size: var(--strapi-card-title-font-size);
|
||||||
|
font-weight: var(--strapi-card-title-font-weight);
|
||||||
|
line-height: var(--strapi-card-title-line-height);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
margin-left: var(--strapi-card-title-arrow-left);
|
||||||
|
transition: margin-left 0.1s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
--ifm-link-color: var(--strapi-card-description-color);
|
||||||
|
--ifm-link-decoration: underline;
|
||||||
|
|
||||||
|
color: var(--strapi-card-description-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: var(--strapi-card-description-font-size);
|
||||||
|
line-height: var(--strapi-card-description-line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: 0 1px 10px 0 #7A78B61A;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cta {
|
||||||
|
--ifm-link-color: currentColor;
|
||||||
|
--strapi-card-background:
|
||||||
|
linear-gradient(
|
||||||
|
310deg,
|
||||||
|
rgba(168, 166, 255, 0.15) 1.16%,
|
||||||
|
rgba(226, 225, 255, 0.15) 69.23%
|
||||||
|
),
|
||||||
|
#FFFFFF
|
||||||
|
;
|
||||||
|
--strapi-card-text-align: left;
|
||||||
|
--strapi-card-gap: var(--strapi-spacing-2);
|
||||||
|
--strapi-card-title-font-size: 21px;
|
||||||
|
--strapi-card-title-font-weight: 600;
|
||||||
|
--strapi-card-title-line-height: 28px;
|
||||||
|
--ifm-link-decoration: none;
|
||||||
|
--ifm-link-hover-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--content-delimited {
|
||||||
|
.card {
|
||||||
|
&__title,
|
||||||
|
&__description {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--strapi-card-content-delimited);
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Responsive */
|
||||||
|
@include medium-up {
|
||||||
|
:root {
|
||||||
|
--strapi-card-px: var(--strapi-spacing-8);
|
||||||
|
--strapi-card-py: var(--strapi-spacing-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
&__title {
|
||||||
|
&__arrow {
|
||||||
|
transition: margin-left 0.2s ease;
|
||||||
|
will-change: margin-left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus, &:hover {
|
||||||
|
&.card--cta {
|
||||||
|
--strapi-card-border-color: #D6D6FF;
|
||||||
|
--strapi-card-box-shadow: 0px 1px 4px rgba(33, 33, 52, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--cta {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
will-change: border-color, box-shadow, color;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
&__img {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
will-change: border-radius, transform;
|
||||||
|
transform:
|
||||||
|
scale(var(--strapi-card-img-scale, 1))
|
||||||
|
translate(var(--strapi-card-img-translate, '0, 0'))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dark mode */
|
||||||
|
@include dark {
|
||||||
|
--strapi-card-border-color: var(--strapi-neutral-150);
|
||||||
|
--strapi-card-title-color: var(--strapi-netral-1000);
|
||||||
|
--strapi-card-description-color: var(--strapi-netral-1000);
|
||||||
|
--strapi-card-img-border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
--strapi-card-hover-border-color: #49494D;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
&--cta {
|
||||||
|
--strapi-card-background: var(--strapi-neutral-0);
|
||||||
|
|
||||||
|
&:focus, &:hover {
|
||||||
|
--strapi-card-border-color: #49494D;
|
||||||
|
--strapi-card-color: var(--strapi-neutral-1000);
|
||||||
|
|
||||||
|
--ifm-link-hover-color: var(--strapi-neutral-1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import styles from './container.module.scss';
|
||||||
|
|
||||||
|
export function Container({ className, ...rest }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.container,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** Component: Container */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--strapi-container-px: var(--ifm-spacing-horizontal);
|
||||||
|
--strapi-container-mw: calc(863px + calc(var(--strapi-container-px) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: var(--strapi-container-px);
|
||||||
|
padding-left: var(--strapi-container-px);
|
||||||
|
max-width: var(--strapi-container-mw);
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default function CustomDocCard(props) {
|
||||||
|
const { title, description, link, emoji, small = false } = props;
|
||||||
|
const linkClasses = classNames({
|
||||||
|
card: true,
|
||||||
|
cardContainer: true,
|
||||||
|
'padding--lg': !small,
|
||||||
|
'padding--md': small,
|
||||||
|
});
|
||||||
|
const cardClasses = classNames({
|
||||||
|
'custom-doc-card': true,
|
||||||
|
'margin-bottom--lg': !small,
|
||||||
|
'margin-bottom--sm': small,
|
||||||
|
'custom-doc-card--small': small,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<article className={ cardClasses }>
|
||||||
|
<a className={ linkClasses }
|
||||||
|
href={ link }
|
||||||
|
>
|
||||||
|
<h2 className="text--truncate cardTitle" title={title}>
|
||||||
|
{emoji ? emoji : '📄️'} {title}
|
||||||
|
</h2>
|
||||||
|
<p className="text--truncate cardDescription" title={ description }>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function CustomDocCardsWrapper({ children }) {
|
||||||
|
return (
|
||||||
|
<div className="custom-cards-wrapper">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './features-list.module.scss';
|
||||||
|
import { LinkWithArrow } from '../LinkWithArrow/LinkWithArrow';
|
||||||
|
|
||||||
|
export function FeatureListItem({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
icon: Icon,
|
||||||
|
iconColor,
|
||||||
|
label,
|
||||||
|
to,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const ContentElement = ((href || to) ? LinkWithArrow : 'span');
|
||||||
|
const IconElement = ((href || to) ? 'a' : 'span');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={clsx(
|
||||||
|
styles['features-list__item'],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Icon && (
|
||||||
|
<IconElement
|
||||||
|
className={clsx(
|
||||||
|
styles['features-list__item__icon'],
|
||||||
|
(iconColor && styles[`features-list__item__icon--${iconColor}`]),
|
||||||
|
)}
|
||||||
|
href={href}
|
||||||
|
to={to}
|
||||||
|
{...(IconElement === 'a' ? { href: to || href } : {})}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</IconElement>
|
||||||
|
)}
|
||||||
|
<ContentElement
|
||||||
|
className={styles['features-list__item__content']}
|
||||||
|
href={href}
|
||||||
|
to={to}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children || label}
|
||||||
|
</ContentElement>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeaturesList({
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
icon,
|
||||||
|
iconColor,
|
||||||
|
items,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const defaultId = `featureListItem${Math.random()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className={clsx(
|
||||||
|
styles['features-list'],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{items?.map((featureListItem, featureListItemIndex) => {
|
||||||
|
return (
|
||||||
|
<FeatureListItem
|
||||||
|
key={`${id || defaultId}${featureListItemIndex}`}
|
||||||
|
icon={icon}
|
||||||
|
iconColor={iconColor}
|
||||||
|
{...featureListItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue