mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-11-10 03:21:57 +01:00
Compare commits
1234 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53b72a8d1 | ||
|
|
e8ff15ad0f | ||
|
|
ec6b3ae414 |
||
|
|
07e4521c30 |
||
|
|
771857e014 |
||
|
|
14a4f1f437 |
||
|
|
600686c32b | ||
|
|
2e3ff05d83 | ||
|
|
e9dcdbc30d | ||
|
|
6cbb482c0f |
||
|
|
68aafc4d2e | ||
|
|
70354c8599 | ||
|
|
43601e060c | ||
|
|
56f82a7587 | ||
|
|
4c30cab7c1 | ||
|
|
1c8ca6c0a2 | ||
|
|
5dbe4ba00b | ||
|
|
377feaf566 | ||
|
|
05839a549c | ||
|
|
7a1a458abe | ||
|
|
c71e4cd063 | ||
|
|
d8e093e0c5 | ||
|
|
351e084ab1 | ||
|
|
bba6fff4a5 | ||
|
|
a8e92b5f46 | ||
|
|
8315e33258 | ||
|
|
3881c26ee0 | ||
|
|
028c64f6ec | ||
|
|
4088b141a6 | ||
|
|
445881bae9 | ||
|
|
b3f7e445fe | ||
|
|
1f2a7b86e5 | ||
|
|
ae787530ff | ||
|
|
6e4ae15438 | ||
|
|
e06d9da186 | ||
|
|
b035014867 | ||
|
|
746aa53bc3 | ||
|
|
7b61a00f21 | ||
|
|
aa24888ee5 | ||
|
|
c735bfdb1d | ||
|
|
41dbc27e27 | ||
|
|
4d98605e93 | ||
|
|
07166037b9 | ||
|
|
e1418dfdc1 | ||
|
|
ab92620f56 | ||
|
|
0a4b873b77 | ||
|
|
23bafa4471 | ||
|
|
436d3df83f |
||
|
|
37393dd6c9 | ||
|
|
8c15af3105 | ||
|
|
0ac1d19415 | ||
|
|
63a33d1057 | ||
|
|
a9d0caad5f | ||
|
|
6ed4ad4c8c | ||
|
|
71946afd75 | ||
|
|
919bf49ec1 | ||
|
|
001f2e97ea | ||
|
|
d2d5490aab | ||
|
|
c788fa99e3 | ||
|
|
34d284b1c4 | ||
|
|
67c736f979 |
||
|
|
6b1e7b3544 | ||
|
|
da30a6657e | ||
|
|
2e0b5edd95 | ||
|
|
1d6f0b403a | ||
|
|
df65f39d5e | ||
|
|
1bfea3c48a | ||
|
|
07db1554c7 | ||
|
|
ed1e51f694 | ||
|
|
5b71d68179 | ||
|
|
b94e28a961 | ||
|
|
1d52b7c464 | ||
|
|
0d49632b92 | ||
|
|
702e5c8732 | ||
|
|
d2b605edc0 | ||
|
|
4c28871283 | ||
|
|
1d38c50abc | ||
|
|
710569daaf | ||
|
|
92cd645945 | ||
|
|
16126c4000 | ||
|
|
eda6deff47 | ||
|
|
27a18bdc1e | ||
|
|
98b62cc81e | ||
|
|
2c195d9767 | ||
|
|
bb49c67108 | ||
|
|
f0dc80aac9 |
||
|
|
8998b006e0 | ||
|
|
b4b758c356 | ||
|
|
a399b629d1 | ||
|
|
41a7238ab7 | ||
|
|
0e99faee0a | ||
|
|
13e75808f8 | ||
|
|
1a0fab0615 | ||
|
|
fcdeb0479a | ||
|
|
79ac318d0f | ||
|
|
6765c110c6 | ||
|
|
f6f83cc111 | ||
|
|
c6d5fb3f57 | ||
|
|
4b8ef4b0fa | ||
|
|
46d8c86e0c | ||
|
|
c7102bcd8c | ||
|
|
d6ac16ede0 | ||
|
|
23cad8261b | ||
|
|
65d840c444 | ||
|
|
c52126ccf8 |
||
|
|
8eec606589 | ||
|
|
cdc58507db | ||
|
|
03f7ad66d2 | ||
|
|
3b01af1247 | ||
|
|
8d2ff6f5d7 | ||
|
|
6ff7f64384 | ||
|
|
c2cbbee0df | ||
|
|
e81c8470be | ||
|
|
ecd2abe00e | ||
|
|
0d1ae030be | ||
|
|
1f669a9c53 | ||
|
|
8ff2fc5a82 | ||
|
|
c7ec8adc31 | ||
|
|
cee6d355e8 | ||
|
|
4b00697f02 | ||
|
|
617ae03b48 | ||
|
|
71629a696c | ||
|
|
14cc0b9e9a |
||
|
|
c5a1df37b9 | ||
|
|
46d1a0cb1b | ||
|
|
a18ec373d2 | ||
|
|
ced16620ec | ||
|
|
890621b651 | ||
|
|
5a5691a8c4 | ||
|
|
fb92db8c05 | ||
|
|
2b28aa8ba9 | ||
|
|
90f83273da | ||
|
|
76f3c379b5 | ||
|
|
1d33d95c57 | ||
|
|
72e3766be5 | ||
|
|
7c1ab6460d | ||
|
|
d0f2422e0d | ||
|
|
4277f42285 | ||
|
|
0e9558e331 |
||
|
|
4e9e82d9f1 |
||
|
|
411ac500ba |
||
|
|
b1443a817b |
||
|
|
3e8ca06177 | ||
|
|
c1b7272ab1 | ||
|
|
b093866d15 | ||
|
|
065ef9f8ae | ||
|
|
9b17efc12c | ||
|
|
fe7910a2f2 | ||
|
|
eb4258053e | ||
|
|
117ff4484d | ||
|
|
ba7d139f8a | ||
|
|
d657b2ff04 | ||
|
|
0637c05053 | ||
|
|
88fbc46325 | ||
|
|
379155e839 | ||
|
|
0717239296 | ||
|
|
d3e3c4e3f8 | ||
|
|
c9a1febc56 | ||
|
|
7f099972e1 | ||
|
|
52444e05e4 | ||
|
|
4fcd55748f | ||
|
|
d57107ed3e | ||
|
|
0c7aa5e92a | ||
|
|
17f123ba8a | ||
|
|
1156bb52af | ||
|
|
71be75b3e7 | ||
|
|
5a4f151ca3 | ||
|
|
9729a43f2b | ||
|
|
4da403569c | ||
|
|
74be016b68 | ||
|
|
3896d3d9ab | ||
|
|
ed396765c8 | ||
|
|
cc9d50a8fe | ||
|
|
9b4d5e9c27 | ||
|
|
ccb837e4b4 | ||
|
|
2bc39e7791 | ||
|
|
fa7f3a1da1 | ||
|
|
c91d37d2a4 | ||
|
|
5ab7ac4d4b | ||
|
|
4c8940f9c3 | ||
|
|
aa29f10d51 | ||
|
|
78885ec3c5 | ||
|
|
1fb137e89f | ||
|
|
facfb37383 | ||
|
|
c5751b2aa6 | ||
|
|
aa4299041b | ||
|
|
c27f2246a3 | ||
|
|
a6be786d5d | ||
|
|
578a030175 | ||
|
|
f858e68f12 | ||
|
|
bdd88700d4 | ||
|
|
87cf75f67d | ||
|
|
c3cc7cb0d6 | ||
|
|
e1600cdec9 | ||
|
|
431cf23600 | ||
|
|
08ce1795fc | ||
|
|
e369ce6db9 | ||
|
|
af4ea17faa | ||
|
|
bb13ebc0ec | ||
|
|
3b42d7a2c8 | ||
|
|
e98d988c45 | ||
|
|
cc70e77dee | ||
|
|
7a86109d66 | ||
|
|
5238be1460 | ||
|
|
6edc8056ec |
||
|
|
b19cc13897 | ||
|
|
50f478f7ef | ||
|
|
80482f7294 | ||
|
|
dc864fad04 | ||
|
|
6d495b38b4 | ||
|
|
1c838d1e42 | ||
|
|
652c7abbce | ||
|
|
d925fd8913 | ||
|
|
9a8e34cbe3 | ||
|
|
34ae83cc8c | ||
|
|
e26e6da15d | ||
|
|
d45cd23c0f | ||
|
|
b60a1070e9 | ||
|
|
7f04827a0b | ||
|
|
da11c9b793 | ||
|
|
93b04fbf94 | ||
|
|
a2d94b54b1 | ||
|
|
1aedcc056f | ||
|
|
2019f44395 | ||
|
|
b217d3e4ef | ||
|
|
e3125e3afb | ||
|
|
d5c5c7c772 | ||
|
|
f945118827 | ||
|
|
ab811b1b7b | ||
|
|
1cd5d7b073 | ||
|
|
c9576f0b08 | ||
|
|
cbedb377ac | ||
|
|
6340bdd677 | ||
|
|
f41722c7eb | ||
|
|
b645c674bc | ||
|
|
e45a56c66a |
||
|
|
b4a7d18ace |
||
|
|
d70fe0fbaa | ||
|
|
48ff2494f6 | ||
|
|
ee33d743e6 | ||
|
|
eaaf44b391 | ||
|
|
a48490ac1a | ||
|
|
cb63b3bde7 | ||
|
|
f48791e961 | ||
|
|
a75a60fecd | ||
|
|
ac89489202 | ||
|
|
ae08d9539d | ||
|
|
275b695d6b | ||
|
|
d4d80de10f | ||
|
|
49ddb2938f | ||
|
|
7054c51490 | ||
|
|
808af0d3cd | ||
|
|
b14fc0e22a | ||
|
|
f7259a118b | ||
|
|
be60c4363c | ||
|
|
631db7df31 | ||
|
|
781ea45633 | ||
|
|
0eee161630 | ||
|
|
7a1b9b8ce1 | ||
|
|
3fcb5ce82e | ||
|
|
eb7aefb8c0 | ||
|
|
475cfe60f9 | ||
|
|
1e9a2e5382 | ||
|
|
9eaf5042ec | ||
|
|
d9ef9cd7b7 | ||
|
|
128b428644 | ||
|
|
23cd51c1ca | ||
|
|
d370f976a7 | ||
|
|
247fed7d74 | ||
|
|
c834058678 | ||
|
|
6c229ccb3a | ||
|
|
696eb8092a | ||
|
|
f9b85c3b85 | ||
|
|
496df89e0b | ||
|
|
58d4207b49 | ||
|
|
390736cf5a | ||
|
|
fd090b8701 | ||
|
|
286cd009de | ||
|
|
1f384a6ab8 | ||
|
|
883d38ecfc | ||
|
|
a691be6584 | ||
|
|
833a8c34f4 | ||
|
|
9fde748fbe | ||
|
|
8c22ec46bc | ||
|
|
74cc8629f6 | ||
|
|
fc3857aa67 | ||
|
|
db1b91fc32 | ||
|
|
e87720a838 | ||
|
|
74fef78120 | ||
|
|
9eb8e33e09 | ||
|
|
8bcebf57c4 | ||
|
|
e2735823a0 | ||
|
|
f3ad3c1ffe | ||
|
|
f215bd11cd | ||
|
|
dc480f755c | ||
|
|
f1d34bbc24 | ||
|
|
6665203f2a | ||
|
|
ce86863095 | ||
|
|
e2bad9e9da | ||
|
|
2cd2a481d9 | ||
|
|
78de2c5e03 | ||
|
|
dc25397469 | ||
|
|
db810445fb | ||
|
|
a3db52b184 | ||
|
|
7d96b2a611 | ||
|
|
50f4c01e99 | ||
|
|
0bc6d9986b | ||
|
|
d3c3fedac2 | ||
|
|
6137065b4e | ||
|
|
a58fcd94dd | ||
|
|
daec5aa4b1 | ||
|
|
2819b457fa | ||
|
|
1933234ed4 | ||
|
|
4e1bd486e8 | ||
|
|
53889c7813 | ||
|
|
9a3794bc83 | ||
|
|
e5cf8550ee | ||
|
|
5c4aa11b4b | ||
|
|
1b86257836 | ||
|
|
0f5fb992ef | ||
|
|
392740d79c | ||
|
|
41108bd969 | ||
|
|
40a837b165 | ||
|
|
7e6b931db4 | ||
|
|
9eb825f89a | ||
|
|
af87c1ae1d | ||
|
|
18bf68cb6a | ||
|
|
38c826713f | ||
|
|
b9c3358f7f | ||
|
|
2f2f4b2b3e | ||
|
|
423cdf6ca2 | ||
|
|
dc81a5b361 | ||
|
|
a4f2a7a7df | ||
|
|
c0c847c5be | ||
|
|
1dbcff66d1 | ||
|
|
6e28f2a74e | ||
|
|
9812671a89 | ||
|
|
8b417d6441 | ||
|
|
ff57b5b270 | ||
|
|
c00edef69c | ||
|
|
a235f05794 | ||
|
|
bd411ba13b | ||
|
|
f8bdbf1fde | ||
|
|
beea572c47 |
||
|
|
442a7aa235 | ||
|
|
2226b72d1c |
||
|
|
00a74ed96a |
||
|
|
699a5c935f |
||
|
|
b2946aee0d | ||
|
|
beb079b825 | ||
|
|
64497c4c0d | ||
|
|
70bbb47850 | ||
|
|
73253dd03c | ||
|
|
442457f11b | ||
|
|
c44535990b |
||
|
|
b8d5b83eee | ||
|
|
00da2dedc3 | ||
|
|
4ce1de079e | ||
|
|
6b9c125de4 | ||
|
|
2c4f44e808 | ||
|
|
2b694731ad | ||
|
|
7e34535e62 |
||
|
|
cf791cff1d | ||
|
|
0bb831fe88 | ||
|
|
42a32ce142 | ||
|
|
23f58b7bf4 | ||
|
|
4e9101fded | ||
|
|
9c700c77a8 | ||
|
|
cb1f674332 | ||
|
|
6823d94ffb | ||
|
|
60ab992360 | ||
|
|
f9e769a6e3 | ||
|
|
f802c6c176 | ||
|
|
dedadf0c10 | ||
|
|
c8375def1a | ||
|
|
62ebcde2de |
||
|
|
594a5779dc | ||
|
|
c0ef64fb64 | ||
|
|
48c70c3bb4 | ||
|
|
68124a340b | ||
|
|
0b5003fcf6 | ||
|
|
956ece60af | ||
|
|
53da45d7d7 | ||
|
|
57f0432a87 | ||
|
|
fb535ec6f7 | ||
|
|
4e1b1a4ffa | ||
|
|
5b111d80f1 | ||
|
|
03e1105a8e | ||
|
|
059a9683db | ||
|
|
1daf6f01f4 | ||
|
|
d3b225771c |
||
|
|
7275db27e7 | ||
|
|
49ee9131d0 | ||
|
|
e75e0c4c0b | ||
|
|
e94d4a7752 | ||
|
|
95206f427d | ||
|
|
2d7f88522a | ||
|
|
f5c17bc7c8 | ||
|
|
63e222ed40 | ||
|
|
6963ee3b8d | ||
|
|
bb5e42bf63 | ||
|
|
c48f778648 |
||
|
|
616aad6403 |
||
|
|
bcc7547d6f |
||
|
|
5a1a6e9217 |
||
|
|
eae1fcecab |
||
|
|
b53989bb9d |
||
|
|
467d50bd31 | ||
|
|
1935258978 | ||
|
|
2b5030c69f | ||
|
|
6537502696 |
||
|
|
0ba352ab0b | ||
|
|
5d3f861728 | ||
|
|
319b69f6c7 | ||
|
|
c4ba28e3a0 | ||
|
|
b38ef8ecea | ||
|
|
cb0817666d | ||
|
|
a54c2db9b9 | ||
|
|
17caf476bf | ||
|
|
42cb590c75 | ||
|
|
6fd05e1456 | ||
|
|
bec45d60e5 | ||
|
|
019e67a676 | ||
|
|
f146d88aa5 | ||
|
|
48be9a8098 | ||
|
|
29f92d9bd3 |
||
|
|
ebb977e99f | ||
|
|
94bcd3d2d3 | ||
|
|
a9bd4c5606 | ||
|
|
8d9dde0032 | ||
|
|
b2d4333fff | ||
|
|
afd5e2b95b | ||
|
|
085adf8d71 | ||
|
|
9b35b60d61 | ||
|
|
742e57cc5c | ||
|
|
a5961668fe | ||
|
|
e7394c165a | ||
|
|
7be966122f | ||
|
|
d176b68fd2 | ||
|
|
1e80be1376 | ||
|
|
3585b08d4b | ||
|
|
c51e0eb68f | ||
|
|
d05c0579a2 | ||
|
|
5e40519bc5 | ||
|
|
d13752114c | ||
|
|
90e1b809fe | ||
|
|
32b4e6812d | ||
|
|
a798aa9c24 | ||
|
|
f1c28b9f46 | ||
|
|
39bc400376 | ||
|
|
e287918121 | ||
|
|
e0bf8e5fbc | ||
|
|
376c7e7a6f | ||
|
|
5612a790fb | ||
|
|
80527e35c3 | ||
|
|
f592ab6395 | ||
|
|
d7c741c652 | ||
|
|
9502f30e1b | ||
|
|
7286c4bbef | ||
|
|
a976f97dbb | ||
|
|
2c9b8c7dea | ||
|
|
787decf4e2 | ||
|
|
2fc70b8bdd |
||
|
|
64491a9772 | ||
|
|
b724b05de6 | ||
|
|
d94c4af1be |
||
|
|
8f0f5a5eb4 |
||
|
|
edf50a71d1 |
||
|
|
d0937218b9 | ||
|
|
3247a97217 | ||
|
|
edd254ee06 | ||
|
|
42ecb83155 | ||
|
|
56f801c058 | ||
|
|
2d3d05e956 |
||
|
|
4321e51bf5 |
||
|
|
be04730906 |
||
|
|
aa06e1df04 | ||
|
|
fd7a0156bc | ||
|
|
1e19ff24ba | ||
|
|
3d4e91fc69 | ||
|
|
97aed847b6 | ||
|
|
8750573724 | ||
|
|
0f4238291f | ||
|
|
03a2a10efd | ||
|
|
04310aa2f8 | ||
|
|
e8ca11a5cf | ||
|
|
24137b30a5 | ||
|
|
4421917333 | ||
|
|
25c8660c2e | ||
|
|
190e87390d |
||
|
|
c9aefdd862 | ||
|
|
3ad088663f | ||
|
|
a29e87e5ac | ||
|
|
de0832bece | ||
|
|
614697ba84 | ||
|
|
6bdf3d891a | ||
|
|
f75704f77c | ||
|
|
9d09543eb9 | ||
|
|
a6116398a8 | ||
|
|
39763b84d5 | ||
|
|
8502df08fa | ||
|
|
bf2a776403 | ||
|
|
052190c69b | ||
|
|
8826ba6729 | ||
|
|
39b5240934 | ||
|
|
ddc1c286d9 | ||
|
|
22fba37d28 | ||
|
|
0c627a5636 | ||
|
|
53dcd24216 | ||
|
|
4b09a321ad | ||
|
|
9e85b70c17 |
||
|
|
9c99217dee | ||
|
|
afc1dbdd4b | ||
|
|
20f58fc07d | ||
|
|
e6b78dd213 | ||
|
|
63893ffabe | ||
|
|
c9e519d0b5 | ||
|
|
273bde90f2 | ||
|
|
92e4976396 |
||
|
|
e9efbff912 | ||
|
|
41089c08f8 | ||
|
|
9e23e606f8 | ||
|
|
b3f0fd368a | ||
|
|
12bd5472e2 | ||
|
|
ef64779759 | ||
|
|
b3d8076ddf | ||
|
|
f775203608 | ||
|
|
a6083688e4 | ||
|
|
50689cd4e6 | ||
|
|
255fcbac1c | ||
|
|
7f8ffa56e5 | ||
|
|
9a2a5f30a3 | ||
|
|
a9f444cbb4 | ||
|
|
164efb0551 | ||
|
|
a37b8cbb15 | ||
|
|
946032a101 | ||
|
|
a273acbecd | ||
|
|
0ceee1582e | ||
|
|
04a0369d56 | ||
|
|
31a288b44d | ||
|
|
502dc3aa1c | ||
|
|
6874d7ca55 | ||
|
|
da8f669aed | ||
|
|
0f92a69b03 |
||
|
|
8faa3251c4 |
||
|
|
56fc14003c |
||
|
|
b3499e4ea5 |
||
|
|
07b1ff9bf5 | ||
|
|
5bbf24c92e |
||
|
|
eea8b3e679 | ||
|
|
e223078af9 | ||
|
|
b554d0d851 |
||
|
|
b1ba26e0b9 | ||
|
|
ca8ad760d7 | ||
|
|
80129c0a88 | ||
|
|
7530e62dfa | ||
|
|
baf8977578 | ||
|
|
c7bf843312 | ||
|
|
ce6fee1682 | ||
|
|
2653fad488 | ||
|
|
dd54c46a29 | ||
|
|
724a0e21d3 |
||
|
|
578277d11f |
||
|
|
22258e3183 | ||
|
|
0234463b68 | ||
|
|
ef412eef92 | ||
|
|
76ebd22eab |
||
|
|
5b0ca8e346 | ||
|
|
0b6b10c27b | ||
|
|
6225d2c9b3 | ||
|
|
01fc6524a4 | ||
|
|
2575e6a160 | ||
|
|
484ba5ebd7 | ||
|
|
b42d98e9f8 | ||
|
|
65b2f045ac |
||
|
|
5e76451d46 | ||
|
|
a873ad3316 | ||
|
|
b1e03f49ee | ||
|
|
011e23f8e6 | ||
|
|
646cd8cf22 | ||
|
|
52ac8a70d5 | ||
|
|
e020334b73 | ||
|
|
7698e83f0b | ||
|
|
dd56f5e0c8 | ||
|
|
92c32eef74 |
||
|
|
08770c7dc5 | ||
|
|
808a94e4df |
||
|
|
490086d531 | ||
|
|
2ef3fbb81b | ||
|
|
7d834ac8d7 | ||
|
|
15ad0ec9c0 | ||
|
|
f0b78e8b2c | ||
|
|
e616faa76f | ||
|
|
8159f4d8ee | ||
|
|
021c576468 | ||
|
|
1b2339a82c | ||
|
|
2b6bb3f773 | ||
|
|
abc5c61a06 | ||
|
|
7145bce605 | ||
|
|
bb92e5e9ee | ||
|
|
0c47aa226c | ||
|
|
76e945bbbd | ||
|
|
4a6ec2581d | ||
|
|
3d75bf5f9f | ||
|
|
c27648b89b | ||
|
|
ccf67c0662 | ||
|
|
ca116cae91 | ||
|
|
a29d933f99 | ||
|
|
49acf3e0cf | ||
|
|
234b5abb96 | ||
|
|
839bcf91d6 | ||
|
|
58ed57fab7 |
||
|
|
fa42997733 | ||
|
|
ac416141d0 | ||
|
|
c629a85b14 | ||
|
|
7ccfea208f |
||
|
|
f3c802bcff | ||
|
|
574583bd6a | ||
|
|
84c54d0b25 | ||
|
|
86d3f87694 | ||
|
|
ddd7252051 |
||
|
|
b4e8136618 | ||
|
|
c2638991f2 | ||
|
|
8554be9abd | ||
|
|
87a518703f | ||
|
|
dd03ca943d | ||
|
|
6997861811 | ||
|
|
1cc1530b20 |
||
|
|
98597fb3aa | ||
|
|
283a445198 | ||
|
|
7db44f0ec5 | ||
|
|
abb5395cae | ||
|
|
8c8b44baef | ||
|
|
7366a33fe5 | ||
|
|
ad02d7e525 |
||
|
|
b5a0189f29 |
||
|
|
756152dd68 | ||
|
|
173a8ee680 | ||
|
|
b99777cde1 | ||
|
|
8193e7a68e | ||
|
|
f18c024daa | ||
|
|
f6577a8f33 | ||
|
|
7fc3153dde | ||
|
|
5231dbd6e7 | ||
|
|
77671550a7 | ||
|
|
e231404128 | ||
|
|
6650e2da3d | ||
|
|
1ac9641a04 | ||
|
|
ad47c8d8ed | ||
|
|
0dbf417866 | ||
|
|
a45bf22ac5 | ||
|
|
fd521acaa4 |
||
|
|
b5a6ba921c | ||
|
|
f6a2467eae | ||
|
|
79da0518c2 | ||
|
|
5e512f8935 | ||
|
|
99c10ffe85 | ||
|
|
47830dcd08 | ||
|
|
947cce78d7 | ||
|
|
74e555d25d | ||
|
|
e9973af8f4 | ||
|
|
2ab2b7f77d | ||
|
|
463812fb3d | ||
|
|
d2406726c6 | ||
|
|
a169623866 | ||
|
|
21c3c45150 | ||
|
|
1aee0a91c0 | ||
|
|
27a28d4adc | ||
|
|
d6ff22fc44 | ||
|
|
5ede61118c | ||
|
|
228549ff51 | ||
|
|
1ec5cbc301 | ||
|
|
3011cb8fae | ||
|
|
1f04d1b993 | ||
|
|
2ef46cdd34 | ||
|
|
2bc50b2888 | ||
|
|
ff78c3c9a7 | ||
|
|
d5980b7620 | ||
|
|
9e0f86788d | ||
|
|
2681c7ded3 | ||
|
|
5ab6a63492 | ||
|
|
6df7bc5f2a | ||
|
|
a0a7ca3c9c | ||
|
|
f88584e1ca | ||
|
|
3e657a7cac | ||
|
|
a8e1171108 | ||
|
|
19e5d302f4 | ||
|
|
22e2480feb | ||
|
|
88ef1788ac | ||
|
|
a54ce939ca | ||
|
|
528d0c7a26 | ||
|
|
c68a647e75 | ||
|
|
e0e4b74b6f | ||
|
|
204178740c | ||
|
|
8fdf37261d | ||
|
|
06c8e584a4 | ||
|
|
bbf7222a6a | ||
|
|
f6e955b487 | ||
|
|
32a72bfd23 | ||
|
|
25e0c22de9 | ||
|
|
2dd1843aac | ||
|
|
f3bf4ca838 | ||
|
|
b88e5e27d8 | ||
|
|
6f91ff1f28 | ||
|
|
b7b941e3a1 | ||
|
|
64414fe105 | ||
|
|
701713e298 | ||
|
|
445229976f | ||
|
|
eb02404d49 | ||
|
|
c780c0bd92 | ||
|
|
8f631cae63 | ||
|
|
07afff8db5 | ||
|
|
a4f440656a | ||
|
|
235d572f8c | ||
|
|
eebc373734 | ||
|
|
002d29a53e | ||
|
|
5074e2beac | ||
|
|
d898ca736c | ||
|
|
af325612aa | ||
|
|
590c2c3b0a | ||
|
|
20f32c7f12 | ||
|
|
4106bcef5f | ||
|
|
0e3b8a2a28 | ||
|
|
272608b4ec | ||
|
|
7a389469b9 | ||
|
|
9650969c94 | ||
|
|
6821e668e4 | ||
|
|
43a68b96ae | ||
|
|
1f6e3db09e | ||
|
|
2fabcabcc0 | ||
|
|
22855b077d | ||
|
|
2cad7a67ea | ||
|
|
8ce93a028a | ||
|
|
028307b63c | ||
|
|
7bd6cd7cec | ||
|
|
8c45a40f9e | ||
|
|
70c62dab77 |
||
|
|
b4d8d31c2d | ||
|
|
bc15135f6b | ||
|
|
16baccc2cb | ||
|
|
bb2559edc1 | ||
|
|
f6f736a91f | ||
|
|
9f477676e2 | ||
|
|
6cba6ba52e | ||
|
|
d29dbc9385 | ||
|
|
cb01302ada | ||
|
|
e3dfbf0e95 | ||
|
|
3f471d0c73 | ||
|
|
4a706ab707 | ||
|
|
4946a9ab0d | ||
|
|
123372d93f | ||
|
|
272fe0516b | ||
|
|
289c9126d0 | ||
|
|
0a482da93e | ||
|
|
8bb8118d9f | ||
|
|
9db822eabd | ||
|
|
8a42dfa154 | ||
|
|
d7a7e22e5a | ||
|
|
4f75e2641b | ||
|
|
d3dcefb645 | ||
|
|
6d1553e8d8 | ||
|
|
740985d68f | ||
|
|
7ad2fab53d | ||
|
|
6e9b337b49 | ||
|
|
c58ff5861d | ||
|
|
33a5e70b70 | ||
|
|
07f1ce5822 | ||
|
|
d41996b365 | ||
|
|
d3c9b7eae1 | ||
|
|
373a1ab0f4 | ||
|
|
58dd56a89f | ||
|
|
d7eadd9294 | ||
|
|
853e29dd83 | ||
|
|
0c1c46c045 | ||
|
|
be97ea08a2 | ||
|
|
1c8b81ca2c | ||
|
|
8266f230d7 | ||
|
|
0b53542716 | ||
|
|
eab1c7096c | ||
|
|
8ee3aaf4f4 | ||
|
|
427b8659c9 | ||
|
|
3c3af824cf | ||
|
|
d40ce470d3 | ||
|
|
07c7f07c35 | ||
|
|
fe732ecf45 | ||
|
|
971bb92a8c | ||
|
|
777f6ba738 | ||
|
|
4d927c5870 | ||
|
|
1830e9da3d | ||
|
|
5eb29746af | ||
|
|
afb816cc41 | ||
|
|
bd640c19a4 | ||
|
|
0d445b6a21 | ||
|
|
78671b0bfe | ||
|
|
60325e797d | ||
|
|
7d9be5ae76 | ||
|
|
777bfed813 | ||
|
|
43ca543651 | ||
|
|
205d5f8f58 | ||
|
|
ee37852a72 |
||
|
|
dc14b58d73 | ||
|
|
a88a2e04cf | ||
|
|
c620beb965 | ||
|
|
92cb9f70a1 | ||
|
|
b2059b6910 | ||
|
|
0b21effb13 |
||
|
|
02acafc348 | ||
|
|
d202ecf06f | ||
|
|
5402d7bedb | ||
|
|
39247f1ece | ||
|
|
55b824d777 | ||
|
|
7cc67f8bb1 | ||
|
|
0772d85918 | ||
|
|
26d83af298 | ||
|
|
4eb223c401 |
||
|
|
4876068cce | ||
|
|
08ae313dfe | ||
|
|
3967c53468 | ||
|
|
7ad077862c | ||
|
|
5a563e4f8f | ||
|
|
5a4b7c525b | ||
|
|
4a99a5e68f | ||
|
|
a96c10a13e | ||
|
|
b3b2782f45 | ||
|
|
1bf9f25ded | ||
|
|
75df0198fc | ||
|
|
b70d74ae4b | ||
|
|
dfe4568991 | ||
|
|
3fff354833 | ||
|
|
db72dac243 | ||
|
|
b886c0aeae | ||
|
|
91b7f2752f | ||
|
|
f4a67c0224 | ||
|
|
c6ac0302ed | ||
|
|
39f1960135 | ||
|
|
ffb659b228 | ||
|
|
e93148304e | ||
|
|
34940dd950 |
||
|
|
eda0e2fe26 |
||
|
|
4aecfaf4e0 | ||
|
|
767de1dd65 | ||
|
|
7a6b045030 |
||
|
|
fdf64f9e9a |
||
|
|
835b954140 |
||
|
|
9770ffa46b |
||
|
|
da75cca97c | ||
|
|
d767e7472d | ||
|
|
9b8d4c518a | ||
|
|
96f5b2beab | ||
|
|
b9dc349d9a | ||
|
|
07d6e5c1d6 | ||
|
|
488c8c5526 | ||
|
|
fb0abf3c1a | ||
|
|
a954712197 |
||
|
|
7c258d231b |
||
|
|
c4bc1a4c51 | ||
|
|
0d9c86fcd3 | ||
|
|
5454bb5b07 |
||
|
|
04d5cd741b | ||
|
|
3191028f74 | ||
|
|
49f82127aa | ||
|
|
28d86c8885 | ||
|
|
1548b9f8c8 | ||
|
|
7271c8c6f1 | ||
|
|
aa0ec15e67 | ||
|
|
7f78822a12 | ||
|
|
f8e92f98d0 | ||
|
|
8d816ab1ee | ||
|
|
19967bd42e | ||
|
|
d3a819613f | ||
|
|
e33f6a20bb | ||
|
|
02b736c92f | ||
|
|
e339c79c5a | ||
|
|
945fb9384e | ||
|
|
a792a140f7 | ||
|
|
ed4728fdb1 | ||
|
|
92a8107b9d | ||
|
|
1af8558cf4 | ||
|
|
b781150ee9 | ||
|
|
59c4248efe | ||
|
|
a5b101a0ea | ||
|
|
03854aa207 | ||
|
|
c989be2719 | ||
|
|
d22e3ddcd7 | ||
|
|
040168b3d4 | ||
|
|
ac5c5555d5 | ||
|
|
1e6065f732 | ||
|
|
cb7be460d7 | ||
|
|
206bcebdb7 | ||
|
|
18c697f472 | ||
|
|
4adb9a5266 | ||
|
|
a72a7ed1fc | ||
|
|
838997b959 | ||
|
|
5736816b2b | ||
|
|
38b5a84c4f | ||
|
|
dff1ef04bf | ||
|
|
757201cafa | ||
|
|
1da3225e1d | ||
|
|
a2cd5652d2 | ||
|
|
dc7c13479c | ||
|
|
113e5b3bcd | ||
|
|
925f5c0ce0 | ||
|
|
0c53f5e186 | ||
|
|
2bcbe1d0be | ||
|
|
0e68f0783f | ||
|
|
91e8711fdf | ||
|
|
cf39e1f259 | ||
|
|
0cd631774b | ||
|
|
df53cf2540 | ||
|
|
b734fc09f3 | ||
|
|
1fe5c56d02 | ||
|
|
af98842090 | ||
|
|
3d67ad870a | ||
|
|
7b5ae70de3 | ||
|
|
18c80f6b64 | ||
|
|
d49bb1f4b5 | ||
|
|
87533b6031 | ||
|
|
33e36f3d2b | ||
|
|
42e604245c | ||
|
|
33475dca66 | ||
|
|
7a90d3f281 | ||
|
|
e53da5ad06 | ||
|
|
f5bbb8bdd6 | ||
|
|
d5b07bbaa3 | ||
|
|
3585b8a56a | ||
|
|
0c33059c4e | ||
|
|
0824e11570 | ||
|
|
0c4f5bcb3a | ||
|
|
aad8dcbadf | ||
|
|
193d5c8663 | ||
|
|
3734541743 | ||
|
|
f3bea68011 | ||
|
|
e843286ea7 | ||
|
|
e3d2012809 | ||
|
|
ce768764cc | ||
|
|
16b3d58242 | ||
|
|
fad8f26eaa | ||
|
|
04d1e84596 | ||
|
|
934acca934 | ||
|
|
639c98237a | ||
|
|
a6db67ab33 | ||
|
|
b92553f480 | ||
|
|
f5d53dc305 | ||
|
|
b7ce91e2d8 | ||
|
|
4295141fb6 | ||
|
|
973709c2f4 | ||
|
|
a72a61091a | ||
|
|
c635637c00 | ||
|
|
77a5cadf51 | ||
|
|
e0ef376cf0 | ||
|
|
04aeab7ea6 | ||
|
|
0dea26f67d | ||
|
|
1815162907 | ||
|
|
e00988047c | ||
|
|
faadd8e9a4 | ||
|
|
409dcce3c7 | ||
|
|
746ba398a9 | ||
|
|
faa3bea3ab | ||
|
|
f70f6c39ce | ||
|
|
25a8642749 | ||
|
|
41e45599d4 | ||
|
|
293afc3998 |
||
|
|
c662dcfcd9 | ||
|
|
e16aa31ddf | ||
|
|
c87f809d12 | ||
|
|
a3dcd1a0a8 | ||
|
|
4398b8698c | ||
|
|
18eb0467ee | ||
|
|
60942c8254 | ||
|
|
c7fac06b65 | ||
|
|
2e5b2c8b7d | ||
|
|
fbd095ab50 | ||
|
|
b176cb1ae1 | ||
|
|
2b65926865 | ||
|
|
a5d184baef | ||
|
|
12e9497ccf | ||
|
|
4b7d200aa2 | ||
|
|
e937432d1e | ||
|
|
e1f1d10833 | ||
|
|
05ec7ab665 | ||
|
|
dbd841a723 | ||
|
|
e8bc93f67a |
||
|
|
0d6ab793ce | ||
|
|
eab4d6f600 | ||
|
|
861c0174df | ||
|
|
5d68922f2e | ||
|
|
20ee16e3c0 |
||
|
|
20be83c345 | ||
|
|
015b69e601 | ||
|
|
95ef97fcc2 | ||
|
|
d600cb4b9a | ||
|
|
0bbfaf9893 | ||
|
|
e3e562a154 | ||
|
|
49034fabf2 | ||
|
|
e9d03a0c95 | ||
|
|
d7e68605c9 | ||
|
|
51900b4b62 | ||
|
|
67a1d06f37 | ||
|
|
395af17d96 | ||
|
|
88f4feaa81 | ||
|
|
b6b2e44231 | ||
|
|
4d4570701b | ||
|
|
178a825e93 |
||
|
|
f67ce2b0a3 | ||
|
|
10918ccb0d | ||
|
|
698fadc00c | ||
|
|
b16760988d | ||
|
|
7e3e1e2705 | ||
|
|
e5e039a1ca | ||
|
|
43d8c3c208 | ||
|
|
3cc1fb40f0 | ||
|
|
fea8109789 | ||
|
|
ff2aafabbd | ||
|
|
e27b15691a | ||
|
|
47ddcca065 | ||
|
|
0a7a7c979c | ||
|
|
b37ae97673 | ||
|
|
fa9e571b41 | ||
|
|
f0fd3d8c1d | ||
|
|
a1bdfab80f |
||
|
|
0d5b7a0711 |
||
|
|
852107f7f6 |
||
|
|
77b017381a |
||
|
|
75ce9c4370 | ||
|
|
a8d12c9c4c | ||
|
|
3294ba5964 | ||
|
|
2bd3a6a745 | ||
|
|
0d3c670f9d | ||
|
|
998b3ed075 | ||
|
|
a390b1e2a0 | ||
|
|
fb493cc837 | ||
|
|
2f17d5f3f5 | ||
|
|
3a2f3ce1cc | ||
|
|
1b5eea4750 | ||
|
|
cb9e90ddc9 | ||
|
|
03c7a71311 | ||
|
|
e5b9ce9c25 | ||
|
|
c8cde94ee3 | ||
|
|
aad62f2599 | ||
|
|
7ae0fad356 | ||
|
|
3548f12deb | ||
|
|
a161e3a520 | ||
|
|
8018e8687b | ||
|
|
e6ae73db00 | ||
|
|
d720cde693 | ||
|
|
d111905ba0 | ||
|
|
4b41e932d2 | ||
|
|
ee3ad403fb | ||
|
|
2a7c131786 | ||
|
|
2861f4fdb8 | ||
|
|
6f24e7f077 | ||
|
|
08e7e3f2bf | ||
|
|
f0519ae0b3 | ||
|
|
631c364179 | ||
|
|
e8d670971e | ||
|
|
fa43ae91cb | ||
|
|
e8eb102feb | ||
|
|
e4634ecc2c | ||
|
|
0307a50890 | ||
|
|
af1852bb3f |
||
|
|
046803b9c4 |
||
|
|
daabb9f55e |
||
|
|
b177db82f5 |
||
|
|
6b2ebd2434 |
||
|
|
ef6dd0febc | ||
|
|
2cb8cf72ba | ||
|
|
7430ecd7a6 | ||
|
|
dcf85f9d5e | ||
|
|
427a31f1b6 | ||
|
|
be4268c6cd | ||
|
|
a1660cd888 | ||
|
|
4645227a40 | ||
|
|
1da2b9eecb | ||
|
|
f83d4103a6 | ||
|
|
9e28d39095 | ||
|
|
6d838c41ce | ||
|
|
af0931a86e | ||
|
|
a4117d024e | ||
|
|
6b122d6040 | ||
|
|
6e66a2bf7a | ||
|
|
37edb73b3e | ||
|
|
a3b5243ffc | ||
|
|
b12a2c6aab | ||
|
|
1379f896c9 | ||
|
|
0a31714717 | ||
|
|
5f8d822e8a | ||
|
|
ff6624b7b3 | ||
|
|
645837dd67 | ||
|
|
eac8f4cd37 | ||
|
|
7e4e9138d3 | ||
|
|
ede7fa3149 | ||
|
|
aa1405a64b | ||
|
|
507c2640a9 | ||
|
|
f6e403d552 | ||
|
|
c1fe3b9d97 | ||
|
|
c85882a0b1 | ||
|
|
21203fc527 | ||
|
|
69cfe51db0 | ||
|
|
9a2133d176 | ||
|
|
f2861fbcf0 | ||
|
|
83d7de0a22 |
||
|
|
d80e9bab12 |
||
|
|
d9f58b935a | ||
|
|
301ecf6c95 | ||
|
|
be1e17b3be | ||
|
|
86f19d6b25 | ||
|
|
e2146332dc | ||
|
|
e283e2db57 | ||
|
|
22377daa8d | ||
|
|
498e0ae2c7 | ||
|
|
12ea949739 | ||
|
|
436e73a2e9 | ||
|
|
1b65dcfcf1 | ||
|
|
8095f1b995 | ||
|
|
a20a3e65fa | ||
|
|
e8a047a28f | ||
|
|
d9ba631173 | ||
|
|
8dc0e014ba | ||
|
|
530476f917 | ||
|
|
8634ff26a7 | ||
|
|
c8f3acaa9d | ||
|
|
74ec7d4c9c | ||
|
|
72c21313dc | ||
|
|
b7660bc778 | ||
|
|
22b197012b | ||
|
|
78bd858ebb | ||
|
|
19819454fa | ||
|
|
26a4b57cfb | ||
|
|
f3729ef9db | ||
|
|
ab09d319e9 | ||
|
|
df23ba07ba | ||
|
|
d20b668e87 | ||
|
|
f0646597fe | ||
|
|
6d783fd581 | ||
|
|
14fbf18733 | ||
|
|
e35c7c496f | ||
|
|
a218b8fdd6 | ||
|
|
1491672cf8 | ||
|
|
f9894ffff7 | ||
|
|
7b565817d6 | ||
|
|
a03b2ecf73 | ||
|
|
dd2f74e19e | ||
|
|
c1dcaf926a | ||
|
|
c116db9593 |
||
|
|
1b92b9f171 | ||
|
|
17e79207f0 | ||
|
|
4d187741e0 | ||
|
|
85c3031fcd | ||
|
|
a3e012d754 | ||
|
|
60f8e754c2 | ||
|
|
3e13a0d9d9 | ||
|
|
cd91dc8b5a | ||
|
|
bcaf96ed59 | ||
|
|
e2437d4c33 | ||
|
|
3798217abc | ||
|
|
57423436ce | ||
|
|
e824f6376a | ||
|
|
1f4bedc9b0 |
||
|
|
aa66285909 |
||
|
|
c6229568c5 | ||
|
|
6110f5be40 | ||
|
|
ea9cc6723f | ||
|
|
b5721dcfd0 | ||
|
|
d7383539ba | ||
|
|
11cdc282d2 | ||
|
|
b23f59271b | ||
|
|
999fe48a31 | ||
|
|
963079afbf | ||
|
|
a6d508205b | ||
|
|
fdf52a59fe | ||
|
|
dd0f8ec97c | ||
|
|
641b47b189 | ||
|
|
e1120dbfa7 | ||
|
|
f9d47e0865 | ||
|
|
d991643b0e | ||
|
|
8b8079a6f1 | ||
|
|
5faeb5dd56 | ||
|
|
5b3156ccf4 | ||
|
|
dc355773c9 | ||
|
|
1daf556206 | ||
|
|
1911c62edd | ||
|
|
2fe2740b62 | ||
|
|
4d7d624033 | ||
|
|
0abe3f0e61 | ||
|
|
77a6204798 | ||
|
|
64af418be2 | ||
|
|
15411d6c81 | ||
|
|
fd645a0bce | ||
|
|
f888028823 | ||
|
|
abc554c7b8 | ||
|
|
07cc8a9534 | ||
|
|
60ecbc7c32 | ||
|
|
b7af538cbf | ||
|
|
49c8b8003b | ||
|
|
65d04d4afb | ||
|
|
a449e82a22 | ||
|
|
34fd611946 | ||
|
|
62cbc168fb | ||
|
|
74d1904df1 | ||
|
|
7d69d6ba30 | ||
|
|
bc37d11f13 | ||
|
|
1825080d9e | ||
|
|
6926f6b233 | ||
|
|
459ae163da | ||
|
|
fc7b1e6d31 | ||
|
|
3198e5d750 | ||
|
|
f31cac580a | ||
|
|
753a12765b | ||
|
|
cbffc485f3 | ||
|
|
c15ddcdf9f | ||
|
|
264ed3aaab | ||
|
|
61a5ebde6b | ||
|
|
f4b4f14a67 | ||
|
|
9994dbd9db | ||
|
|
d976865e7a | ||
|
|
0445b87567 | ||
|
|
64c86fa11d | ||
|
|
548339911f | ||
|
|
e914a32894 | ||
|
|
f28e369c01 | ||
|
|
30b2c8b841 | ||
|
|
b5c7a789a2 | ||
|
|
168b4f6c15 | ||
|
|
bf5ed030fe | ||
|
|
b76b2740a7 | ||
|
|
d5f002ac20 | ||
|
|
2ec1a10623 | ||
|
|
ee69f9e576 | ||
|
|
b7af08503c | ||
|
|
08a1ce5f64 | ||
|
|
22f8448c65 | ||
|
|
6b0f0d31b9 | ||
|
|
feca20ef77 | ||
|
|
9e04a3405f |
1009 changed files with 193457 additions and 31660 deletions
.docker
.dockerignore.editorconfig.env.env.dev.env.test.github/workflows
.gitignoreDockerfileDockerfile-frankenphpMakefileREADME.mdVERSIONassets
ckeditor
controllers
bulk_import_controller.jsbulk_job_manage_controller.js
common
csrf_protection_controller.jselements
attachment_autocomplete_controller.jsckeditor_controller.jscollection_type_controller.js
field_mapping_controller.jsdatatables
ipn_suggestion_controller.jspart_search_controller.jspart_select_controller.jsselect_controller.jsselect_multiple_controller.jsstatic_file_autocomplete_controller.jsstructural_entity_select_controller.jstagsinput_controller.jstree_controller.jspages
toggle_password_controller.jscss
app
components
email
js
tomselect
bin
composer.jsoncomposer.lockconfig
55
.docker/frankenphp/Caddyfile
Normal file
55
.docker/frankenphp/Caddyfile
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
|
||||
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
|
||||
order mercure after encode
|
||||
order vulcain after reverse_proxy
|
||||
order php_server before file_server
|
||||
}
|
||||
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
{$SERVER_NAME:localhost} {
|
||||
log {
|
||||
# Redact the authorization query parameter that can be set by Mercure
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
uri query {
|
||||
replace authorization REDACTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root * /app/public
|
||||
encode zstd br gzip
|
||||
|
||||
mercure {
|
||||
# Transport to use (default to Bolt)
|
||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||
# Publisher JWT key
|
||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||
# Subscriber JWT key
|
||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||
# Allow anonymous subscribers (double-check that it's what you want)
|
||||
anonymous
|
||||
# Enable the subscription API (double-check that it's what you want)
|
||||
subscriptions
|
||||
# Extra directives
|
||||
{$MERCURE_EXTRA_DIRECTIVES}
|
||||
}
|
||||
|
||||
vulcain
|
||||
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||
header ?Permissions-Policy "browsing-topics=()"
|
||||
|
||||
php_server
|
||||
}
|
||||
5
.docker/frankenphp/conf.d/app.dev.ini
Normal file
5
.docker/frankenphp/conf.d/app.dev.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
||||
; See https://github.com/docker/for-linux/issues/264
|
||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
||||
; Add `start_with_request=yes` to start debug session on each request
|
||||
xdebug.client_host = host.docker.internal
|
||||
18
.docker/frankenphp/conf.d/app.ini
Normal file
18
.docker/frankenphp/conf.d/app.ini
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
expose_php = 0
|
||||
date.timezone = UTC
|
||||
apc.enable_cli = 1
|
||||
session.use_strict_mode = 1
|
||||
zend.detect_unicode = 0
|
||||
|
||||
; https://symfony.com/doc/current/performance.html
|
||||
realpath_cache_size = 4096K
|
||||
realpath_cache_ttl = 600
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.memory_consumption = 256
|
||||
opcache.enable_file_override = 1
|
||||
|
||||
memory_limit = 256M
|
||||
|
||||
upload_max_filesize=256M
|
||||
post_max_size=300M
|
||||
2
.docker/frankenphp/conf.d/app.prod.ini
Normal file
2
.docker/frankenphp/conf.d/app.prod.ini
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
opcache.preload_user = root
|
||||
opcache.preload = /app/config/preload.php
|
||||
60
.docker/frankenphp/docker-entrypoint.sh
Normal file
60
.docker/frankenphp/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
# Install the project the first time PHP is started
|
||||
# After the installation, the following block can be deleted
|
||||
if [ ! -f composer.json ]; then
|
||||
rm -Rf tmp/
|
||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
||||
|
||||
cd tmp
|
||||
cp -Rp . ..
|
||||
cd -
|
||||
rm -Rf tmp/
|
||||
|
||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
||||
composer config --json extra.symfony.docker 'true'
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build -d --wait"
|
||||
sleep infinity
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
||||
composer install --prefer-dist --no-progress --no-interaction
|
||||
fi
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
||||
php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
fi
|
||||
|
||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
fi
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
||||
4
.docker/frankenphp/worker.Caddyfile
Normal file
4
.docker/frankenphp/worker.Caddyfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
worker {
|
||||
file ./public/index.php
|
||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||
}
|
||||
|
|
@ -39,8 +39,50 @@ if [ -d /var/www/html/var/db ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Start PHP-FPM
|
||||
service php8.1-fpm start
|
||||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
||||
php-fpmPHP_VERSION -F &
|
||||
|
||||
|
||||
# Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE
|
||||
if [ "$DB_AUTOMIGRATE" = "true" ]; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(sudo -E -u www-data php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
# Check if there are any available migrations to do, by executing doctrine:migrations:up-to-date
|
||||
# and checking if the exit code is 0 (up to date) or 1 (not up to date)
|
||||
if sudo -E -u www-data php bin/console doctrine:migrations:up-to-date --no-interaction; then
|
||||
echo "Database is up to date, no migrations necessary."
|
||||
else
|
||||
echo "Migrations available..."
|
||||
echo "Do backup of database..."
|
||||
|
||||
sudo -E -u www-data mkdir -p /var/www/html/uploads/.automigration-backup/
|
||||
# Backup the database
|
||||
sudo -E -u www-data php bin/console partdb:backup -n --database /var/www/html/uploads/.automigration-backup/backup-$(date +%Y-%m-%d_%H-%M-%S).zip
|
||||
|
||||
# Check if there are any migration files
|
||||
sudo -E -u www-data php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
|
|
|
|||
|
|
@ -24,25 +24,6 @@
|
|||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
# Pass the configuration from the docker env to the PHP environment (here you should list all .env options)
|
||||
PassEnv APP_ENV APP_DEBUG APP_SECRET
|
||||
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
|
||||
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR
|
||||
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES ATTACHMENT_DOWNLOAD_BY_DEFAULT
|
||||
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
|
||||
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA
|
||||
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
|
||||
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
|
||||
# In old version the SAML sp private key env, was wrongly named SAMLP_SP_PRIVATE_KEY, keep it for backward compatibility
|
||||
PassEnv SAML_ENABLED SAML_BEHIND_PROXY SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAML_SP_PRIVATE_KEY SAMLP_SP_PRIVATE_KEY
|
||||
PassEnv TABLE_DEFAULT_PAGE_SIZE TABLE_PARTS_DEFAULT_COLUMNS
|
||||
|
||||
PassEnv PROVIDER_DIGIKEY_CLIENT_ID PROVIDER_DIGIKEY_SECRET PROVIDER_DIGIKEY_CURRENCY PROVIDER_DIGIKEY_LANGUAGE PROVIDER_DIGIKEY_COUNTRY
|
||||
PassEnv PROVIDER_ELEMENT14_KEY PROVIDER_ELEMENT14_STORE_ID
|
||||
PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES
|
||||
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
|
||||
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
# enabled or disabled at a global level, it is possible to
|
||||
# include a line for only one particular virtual host. For example the
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ tests/
|
|||
docs/
|
||||
.git
|
||||
|
||||
/public/media/*
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
|
|
@ -42,3 +44,39 @@ yarn-error.log
|
|||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
|
||||
### From frankenphp
|
||||
|
||||
**/*.log
|
||||
**/*.php~
|
||||
**/*.dist.php
|
||||
**/*.dist
|
||||
**/*.cache
|
||||
**/._*
|
||||
**/.dockerignore
|
||||
**/.DS_Store
|
||||
**/.git/
|
||||
**/.gitattributes
|
||||
**/.gitignore
|
||||
**/.gitmodules
|
||||
**/compose.*.yaml
|
||||
**/compose.*.yml
|
||||
**/compose.yaml
|
||||
**/compose.yml
|
||||
**/docker-compose.*.yaml
|
||||
**/docker-compose.*.yml
|
||||
**/docker-compose.yaml
|
||||
**/docker-compose.yml
|
||||
**/Dockerfile
|
||||
**/Thumbs.db
|
||||
.github/
|
||||
public/bundles/
|
||||
var/
|
||||
vendor/
|
||||
.editorconfig
|
||||
.env.*.local
|
||||
.env.local
|
||||
.env.local.php
|
||||
.env.test
|
||||
|
||||
|
|
|
|||
17
.editorconfig
Normal file
17
.editorconfig
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
145
.env
145
.env
|
|
@ -14,41 +14,27 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
|
|||
# Uncomment this line (and comment the line above to use a MySQL database
|
||||
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db?serverVersion=5.7
|
||||
|
||||
# Set this value to 1, if you want to use SSL to connect to the MySQL server. It will be tried to use the CA certificate
|
||||
# otherwise a CA bundle shipped with PHP will be used.
|
||||
# Leave it at 0, if you do not want to use SSL or if your server does not support it
|
||||
DATABASE_MYSQL_USE_SSL_CA=0
|
||||
|
||||
# Set this value to 0, if you don't want to verify the CA certificate of the MySQL server
|
||||
# Only do this, if you know what you are doing!
|
||||
DATABASE_MYSQL_SSL_VERIFY_CERT=1
|
||||
|
||||
# Emulate natural sorting of strings even on databases that do not support it (like SQLite, MySQL or MariaDB < 10.7)
|
||||
# This can be slow on big databases and might have some problems and quirks, so use it with caution
|
||||
DATABASE_EMULATE_NATURAL_SORT=0
|
||||
|
||||
###################################################################################
|
||||
# General settings
|
||||
###################################################################################
|
||||
|
||||
# The language to use serverwide as default (en, de, ru, etc.)
|
||||
DEFAULT_LANG="en"
|
||||
# The default timezone to use serverwide (e.g. Europe/Berlin)
|
||||
DEFAULT_TIMEZONE="Europe/Berlin"
|
||||
# The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country
|
||||
BASE_CURRENCY="EUR"
|
||||
# The name of this installation. This will be shown as title in the browser and in the header of the website
|
||||
INSTANCE_NAME="Part-DB"
|
||||
# Allow users to download attachments to the server by providing an URL
|
||||
# This could be a potential security issue, as the user can retrieve any file the server has access to (via internet)
|
||||
ALLOW_ATTACHMENT_DOWNLOADS=0
|
||||
# Set this to 1, if the "download external files" checkbox should be checked by default for new attachments
|
||||
ATTACHMENT_DOWNLOAD_BY_DEFAULT=0
|
||||
# Use gravatars for user avatars, when user has no own avatar defined
|
||||
USE_GRAVATAR=0
|
||||
# The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes)
|
||||
# Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files
|
||||
MAX_ATTACHMENT_FILE_SIZE="100M"
|
||||
|
||||
# The public reachable URL of this Part-DB installation. This is used for generating links to the website in emails and so on
|
||||
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
|
||||
# This must end with a slash!
|
||||
DEFAULT_URI="https://partdb.changeme.invalid/"
|
||||
|
||||
# With this option you can configure, where users are enforced to give a change reason, which will be logged
|
||||
# This is a comma separated list of values, see documentation for available values
|
||||
# Leave this empty, to make all change reasons optional
|
||||
ENFORCE_CHANGE_COMMENTS_FOR=""
|
||||
|
||||
# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet
|
||||
CHECK_FOR_UPDATES=1
|
||||
|
||||
###################################################################################
|
||||
# Email settings
|
||||
###################################################################################
|
||||
|
|
@ -65,21 +51,6 @@ EMAIL_SENDER_NAME="Part-DB Mailer"
|
|||
# Set this to 1 to allow reset of a password per email
|
||||
ALLOW_EMAIL_PW_RESET=0
|
||||
|
||||
######################################################################################
|
||||
# History/Eventlog settings
|
||||
######################################################################################
|
||||
# If you want to use full timetrave functionality all values below have to be set to 1
|
||||
|
||||
# Save which fields were changed in a ElementEdited log entry
|
||||
HISTORY_SAVE_CHANGED_FIELDS=1
|
||||
# Save the old data in the ElementEdited log entry (warning this could increase the database size in short time)
|
||||
HISTORY_SAVE_CHANGED_DATA=1
|
||||
# Save the data of an element that gets removed into log entry. This allows to undelete an element
|
||||
HISTORY_SAVE_REMOVED_DATA=1
|
||||
# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page
|
||||
# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1
|
||||
HISTORY_SAVE_NEW_DATA=1
|
||||
|
||||
###################################################################################
|
||||
# Error pages settings
|
||||
###################################################################################
|
||||
|
|
@ -89,80 +60,12 @@ ERROR_PAGE_ADMIN_EMAIL=''
|
|||
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
|
||||
ERROR_PAGE_SHOW_HELP=1
|
||||
|
||||
##################################################################################
|
||||
# Part table settings
|
||||
##################################################################################
|
||||
|
||||
# The default page size for the part table (set to -1 to show all parts on one page)
|
||||
TABLE_DEFAULT_PAGE_SIZE=50
|
||||
# Configure which columns will be visible by default in the parts table (and in which order).
|
||||
# This is a comma separated list of column names. See documentation for available values.
|
||||
TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount
|
||||
|
||||
##################################################################################
|
||||
# Info provider settings
|
||||
##################################################################################
|
||||
|
||||
# Digikey Provider:
|
||||
# You can get your client id and secret from https://developer.digikey.com/
|
||||
PROVIDER_DIGIKEY_CLIENT_ID=
|
||||
PROVIDER_DIGIKEY_SECRET=
|
||||
# The currency to get prices in
|
||||
PROVIDER_DIGIKEY_CURRENCY=EUR
|
||||
# The language to get results in (en, de, fr, it, es, zh, ja, ko)
|
||||
PROVIDER_DIGIKEY_LANGUAGE=en
|
||||
# The country to get results for
|
||||
PROVIDER_DIGIKEY_COUNTRY=DE
|
||||
|
||||
# Farnell Provider:
|
||||
# You can get your API key from https://partner.element14.com/
|
||||
PROVIDER_ELEMENT14_KEY=
|
||||
# Configure the store domain you want to use. This decides the language and currency of results. You can get a list of available stores from https://partner.element14.com/docs/Product_Search_API_REST__Description
|
||||
PROVIDER_ELEMENT14_STORE_ID=de.farnell.com
|
||||
|
||||
# TME Provider:
|
||||
# You can get your API key from https://developers.tme.eu/en/
|
||||
PROVIDER_TME_KEY=
|
||||
PROVIDER_TME_SECRET=
|
||||
# The currency to get prices in
|
||||
PROVIDER_TME_CURRENCY=EUR
|
||||
# The language to get results in (en, de, pl)
|
||||
PROVIDER_TME_LANGUAGE=en
|
||||
# The country to get results for
|
||||
PROVIDER_TME_COUNTRY=DE
|
||||
# Set this to 1 to get gross prices (including VAT) instead of net prices
|
||||
PROVIDER_TME_GET_GROSS_PRICES=1
|
||||
|
||||
# Octopart / Nexar Provider:
|
||||
# You can get your API key from https://nexar.com/api
|
||||
PROVIDER_OCTOPART_CLIENT_ID=
|
||||
PROVIDER_OCTOPART_SECRET=
|
||||
# The currency and country to get prices for (you have to set both to get meaningful results)
|
||||
# 3 letter ISO currency code (e.g. EUR, USD, GBP)
|
||||
PROVIDER_OCTOPART_CURRENCY=EUR
|
||||
# 2 letter ISO country code (e.g. DE, US, GB)
|
||||
PROVIDER_OCTOPART_COUNTRY=DE
|
||||
# The number of results to get from Octopart while searching (please note that this counts towards your API limits)
|
||||
PROVIDER_OCTOPART_SEARCH_LIMIT=10
|
||||
# Set to false to include non authorized offers in the results
|
||||
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
|
||||
|
||||
# Mouser Provider API V2:
|
||||
# You can get your API key from https://www.mouser.it/api-hub/
|
||||
PROVIDER_MOUSER_KEY=
|
||||
# Filter search results by RoHS compliance and stock availability:
|
||||
# Available options: None | Rohs | InStock | RohsAndInStock
|
||||
PROVIDER_MOUSER_SEARCH_OPTION='None'
|
||||
# The number of results to get from Mouser while searching (please note that this value is max 50)
|
||||
PROVIDER_MOUSER_SEARCH_LIMIT=50
|
||||
# It is recommended to leave this set to 'true'. The option is not really good doumented by Mouser:
|
||||
# Used when searching for keywords in the language specified when you signed up for Search API.
|
||||
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
|
||||
|
||||
###################################################################################
|
||||
# SAML Single sign on-settings
|
||||
###################################################################################
|
||||
# Set this to 1 to enable SAML single sign on
|
||||
# Be also sure to set the correct values for DEFAULT_URI
|
||||
SAML_ENABLED=0
|
||||
|
||||
# Set to 1, if your Part-DB installation is behind a reverse proxy and you want to use SAML
|
||||
|
|
@ -207,16 +110,11 @@ DEMO_MODE=0
|
|||
# In that case all URL contains the index.php front controller in URL
|
||||
NO_URL_REWRITE_AVAILABLE=0
|
||||
|
||||
# If you want to use fixer.io for currency conversion, you have to set this to your API key
|
||||
FIXER_API_KEY=CHANGEME
|
||||
|
||||
# Override value if you want to show to show a given text on homepage.
|
||||
# When this is empty the content of config/banner.md is used as banner
|
||||
BANNER=""
|
||||
|
||||
APP_ENV=prod
|
||||
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
|
||||
# Set to 1, if Part-DB should redirect all HTTP requests to HTTPS. You dont need to configure this, if your webserver already does this.
|
||||
REDIRECT_TO_HTTPS=0
|
||||
|
||||
# Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions)
|
||||
DISABLE_YEAR2038_BUG_CHECK=0
|
||||
|
||||
# Set the trusted IPs here, when using an reverse proxy
|
||||
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
|
|
@ -232,3 +130,8 @@ LOCK_DSN=flock
|
|||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=prod
|
||||
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
|
||||
###< symfony/framework-bundle ###
|
||||
|
|
|
|||
4
.env.dev
Normal file
4
.env.dev
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=318b5d659e07a0b3f96d9b3a83b254ca
|
||||
###< symfony/framework-bundle ###
|
||||
|
|
@ -5,5 +5,11 @@ SYMFONY_DEPRECATIONS_HELPER=999999
|
|||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
|
||||
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"
|
||||
# Doctrine automatically adds an _test suffix to database name in test env
|
||||
DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
|
||||
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
|
||||
|
||||
# Disable update checks, as tests would fail, when github is not reachable
|
||||
CHECK_FOR_UPDATES=0
|
||||
|
||||
INSTANCE_NAME="Part-DB"
|
||||
19
.github/workflows/assets_artifact_build.yml
vendored
19
.github/workflows/assets_artifact_build.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: Build assets artifact
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -19,7 +22,7 @@ jobs:
|
|||
APP_ENV: prod
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -27,14 +30,14 @@ jobs:
|
|||
php-version: '8.2'
|
||||
coverage: none
|
||||
ini-values: xdebug.max_nesting_level=1000
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
@ -48,7 +51,7 @@ jobs:
|
|||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -57,9 +60,9 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: yarn install
|
||||
|
|
@ -77,13 +80,13 @@ jobs:
|
|||
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
|
||||
|
||||
- name: Upload assets artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Only dependencies and built assets
|
||||
path: /tmp/partdb_assets.zip
|
||||
|
||||
- name: Upload full artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Full Part-DB including dependencies and built assets
|
||||
path: /tmp/partdb_with_assets.zip
|
||||
|
|
|
|||
7
.github/workflows/docker_build.yml
vendored
7
.github/workflows/docker_build.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: Docker Image Build
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# - cron: '0 10 * * *' # everyday at 10am
|
||||
|
|
@ -17,7 +20,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
|
|
@ -65,7 +68,7 @@ jobs:
|
|||
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
|
|
|||
80
.github/workflows/docker_frankenphp.yml
vendored
Normal file
80
.github/workflows/docker_frankenphp.yml
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
name: Docker Image Build (FrankenPHP)
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# - cron: '0 10 * * *' # everyday at 10am
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- '!l10n_**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- 'v*.*.*-**'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
partdborg/part-db
|
||||
# Mark the image build from master as latest (as we dont have really releases yet)
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=ref,event=branch,
|
||||
type=ref,event=tag,
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.title=Part-DB
|
||||
org.opencontainers.image.description=Part-DB is a web application for managing electronic components and your inventory.
|
||||
org.opencontainers.image.url=https://github.com/Part-DB/Part-DB-server
|
||||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server
|
||||
org.opencontainers.image.authors=Jan Böhmer
|
||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-frankenphp
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
9
.github/workflows/static_analysis.yml
vendored
9
.github/workflows/static_analysis.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: Static analysis
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -16,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -24,14 +27,14 @@ jobs:
|
|||
php-version: '8.2'
|
||||
coverage: none
|
||||
ini-values: xdebug.max_nesting_level=1000
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
|
|||
54
.github/workflows/tests.yml
vendored
54
.github/workflows/tests.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: PHPUnit Tests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -16,9 +19,10 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: [ '8.1', '8.2', '8.3' ]
|
||||
db-type: [ 'mysql', 'sqlite' ]
|
||||
php-versions: ['8.2', '8.3', '8.4', '8.5' ]
|
||||
db-type: [ 'mysql', 'sqlite', 'postgres' ]
|
||||
|
||||
env:
|
||||
# Note that we set DATABASE URL later based on our db-type matrix value
|
||||
|
|
@ -26,18 +30,23 @@ jobs:
|
|||
SYMFONY_DEPRECATIONS_HELPER: disabled
|
||||
PHP_VERSION: ${{ matrix.php-versions }}
|
||||
DB_TYPE: ${{ matrix.db-type }}
|
||||
CHECK_FOR_UPDATES: false # Disable update checks for tests
|
||||
|
||||
steps:
|
||||
- name: Set Database env for MySQL
|
||||
run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/test" >> $GITHUB_ENV
|
||||
run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/partdb?serverVersion=8.0.35" >> $GITHUB_ENV
|
||||
if: matrix.db-type == 'mysql'
|
||||
|
||||
- name: Set Database env for SQLite
|
||||
run: echo "DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"" >> $GITHUB_ENV
|
||||
if: matrix.db-type == 'sqlite'
|
||||
|
||||
- name: Set Database env for PostgreSQL
|
||||
run: echo "DATABASE_URL=postgresql://postgres:postgres @127.0.0.1:5432/partdb?serverVersion=14&charset=utf8" >> $GITHUB_ENV
|
||||
if: matrix.db-type == 'postgres'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -45,10 +54,19 @@ jobs:
|
|||
php-version: ${{ matrix.php-versions }}
|
||||
coverage: pcov
|
||||
ini-values: xdebug.max_nesting_level=1000
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath
|
||||
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
|
||||
|
||||
- name: Start MySQL
|
||||
run: sudo systemctl start mysql.service
|
||||
if: matrix.db-type == 'mysql'
|
||||
|
||||
# Replace the scram-sha-256 with trust for host connections, to avoid password authentication
|
||||
- name: Start PostgreSQL
|
||||
run: |
|
||||
sudo sed -i 's/^\(host.*all.*all.*\)scram-sha-256/\1trust/' /etc/postgresql/14/main/pg_hba.conf
|
||||
sudo systemctl start postgresql.service
|
||||
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
|
||||
if: matrix.db-type == 'postgres'
|
||||
|
||||
#- name: Setup MySQL
|
||||
# uses: mirromutth/mysql-action@v1.1
|
||||
|
|
@ -63,7 +81,7 @@ jobs:
|
|||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
@ -74,7 +92,7 @@ jobs:
|
|||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -86,9 +104,9 @@ jobs:
|
|||
run: composer install --prefer-dist --no-progress
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: yarn install
|
||||
|
|
@ -98,24 +116,20 @@ jobs:
|
|||
|
||||
- name: Create DB
|
||||
run: php bin/console --env test doctrine:database:create --if-not-exists -n
|
||||
if: matrix.db-type == 'mysql'
|
||||
|
||||
# Checkinf for existance is not supported for sqlite, so do it without it
|
||||
- name: Create DB
|
||||
run: php bin/console --env test doctrine:database:create -n
|
||||
if: matrix.db-type == 'sqlite'
|
||||
if: matrix.db-type == 'mysql' || matrix.db-type == 'postgres'
|
||||
|
||||
- name: Do migrations
|
||||
run: php bin/console --env test doctrine:migrations:migrate -n
|
||||
|
||||
# Use our own custom fixtures loading command to circumvent some problems with reset the autoincrement values
|
||||
- name: Load fixtures
|
||||
run: php bin/console --env test doctrine:fixtures:load -n
|
||||
run: php bin/console --env test partdb:fixtures:load -n
|
||||
|
||||
- name: Run PHPunit and generate coverage
|
||||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
env_vars: PHP_VERSION,DB_TYPE
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -133,11 +147,11 @@ jobs:
|
|||
- name: Test check-requirements command
|
||||
run: php bin/console partdb:check-requirements -n
|
||||
|
||||
- name: Test partdb:backup command
|
||||
run: php bin/console partdb:backup -n --full /tmp/test_backup.zip
|
||||
|
||||
- name: Test legacy Part-DB import
|
||||
run: bash .github/assets/legacy_import/test_legacy_import.sh
|
||||
if: matrix.db-type == 'mysql' && matrix.php-versions == '8.2'
|
||||
env:
|
||||
DATABASE_URL: mysql://root:root@localhost:3306/legacy_db
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
|||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/.env.local.bak
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
|
|
@ -41,9 +42,12 @@ yarn-error.log
|
|||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
187
Dockerfile
187
Dockerfile
|
|
@ -1,22 +1,64 @@
|
|||
FROM debian:bullseye-slim
|
||||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG PHP_VERSION=8.4
|
||||
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
ARG PHP_VERSION
|
||||
|
||||
# Install needed dependencies for PHP build
|
||||
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip \
|
||||
RUN apt-get update && apt-get -y install \
|
||||
apt-transport-https \
|
||||
lsb-release \
|
||||
ca-certificates \
|
||||
curl \
|
||||
zip \
|
||||
mariadb-client \
|
||||
postgresql-client \
|
||||
&& curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \
|
||||
&& sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \
|
||||
&& apt-get update && apt-get upgrade -y \
|
||||
&& apt-get install -y apache2 php8.1 php8.1-fpm php8.1-opcache php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-intl php8.1-zip php8.1-xsl php8.1-sqlite3 php8.1-mysql gpg \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
ENV APACHE_CONFDIR /etc/apache2
|
||||
ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
|
||||
|
||||
&& apt-get install -y \
|
||||
apache2 \
|
||||
php${PHP_VERSION} \
|
||||
php${PHP_VERSION}-fpm \
|
||||
php${PHP_VERSION}-opcache \
|
||||
php${PHP_VERSION}-curl \
|
||||
php${PHP_VERSION}-gd \
|
||||
php${PHP_VERSION}-mbstring \
|
||||
php${PHP_VERSION}-xml \
|
||||
php${PHP_VERSION}-bcmath \
|
||||
php${PHP_VERSION}-intl \
|
||||
php${PHP_VERSION}-zip \
|
||||
php${PHP_VERSION}-xsl \
|
||||
php${PHP_VERSION}-sqlite3 \
|
||||
php${PHP_VERSION}-mysql \
|
||||
php${PHP_VERSION}-pgsql \
|
||||
gpg \
|
||||
sudo \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
|
||||
&& mkdir -p /var/www/html \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
# delete the "index.html" that installing Apache drops in here
|
||||
&& rm -rvf /var/www/html/*
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
yarn \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
ENV APACHE_CONFDIR=/etc/apache2
|
||||
ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
||||
|
||||
# Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile)
|
||||
# generically convert lines like
|
||||
|
|
@ -27,8 +69,6 @@ RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
|
|||
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
|
||||
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
||||
set -eux; . "$APACHE_ENVVARS"; \
|
||||
# delete the "index.html" that installing Apache drops in here
|
||||
rm -rvf /var/www/html/*; \
|
||||
\
|
||||
# logs should go to stdout / stderr
|
||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||
|
|
@ -36,82 +76,87 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
|
|||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
||||
|
||||
# Enable php-fpm
|
||||
RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm
|
||||
# ---
|
||||
|
||||
FROM scratch AS apache-config
|
||||
ARG PHP_VERSION
|
||||
# Configure php-fpm to log to stdout of the container (stdout of PID 1)
|
||||
# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint)
|
||||
# We also disable the clear_env option to allow the use of environment variables in php-fpm
|
||||
RUN { \
|
||||
echo '[global]'; \
|
||||
echo 'error_log = /proc/1/fd/1'; \
|
||||
echo; \
|
||||
echo '[www]'; \
|
||||
echo 'access.log = /proc/1/fd/1'; \
|
||||
echo 'catch_workers_output = yes'; \
|
||||
echo 'decorate_workers_output = no'; \
|
||||
echo 'clear_env = no'; \
|
||||
} | tee "/etc/php/8.1/fpm/pool.d/zz-docker.conf"
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf
|
||||
[global]
|
||||
error_log = /proc/1/fd/1
|
||||
|
||||
[www]
|
||||
access.log = /proc/1/fd/1
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
||||
clear_env = no
|
||||
EOF
|
||||
|
||||
# PHP files should be handled by PHP, and should be preferred over any other file type
|
||||
RUN { \
|
||||
echo '<FilesMatch \.php$>'; \
|
||||
echo '\tSetHandler application/x-httpd-php'; \
|
||||
echo '</FilesMatch>'; \
|
||||
echo; \
|
||||
echo 'DirectoryIndex disabled'; \
|
||||
echo 'DirectoryIndex index.php index.html'; \
|
||||
echo; \
|
||||
echo '<Directory /var/www/>'; \
|
||||
echo '\tOptions -Indexes'; \
|
||||
echo '\tAllowOverride All'; \
|
||||
echo '</Directory>'; \
|
||||
} | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
|
||||
&& a2enconf docker-php
|
||||
COPY <<EOF /etc/apache2/conf-available/docker-php.conf
|
||||
<FilesMatch \\.php$>
|
||||
SetHandler application/x-httpd-php
|
||||
</FilesMatch>
|
||||
|
||||
DirectoryIndex disabled
|
||||
DirectoryIndex index.php index.html
|
||||
|
||||
<Directory /var/www/>
|
||||
Options -Indexes
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
EOF
|
||||
|
||||
# Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html)
|
||||
RUN \
|
||||
{ \
|
||||
echo 'opcache.memory_consumption=256'; \
|
||||
echo 'opcache.max_accelerated_files=20000'; \
|
||||
echo 'opcache.validate_timestamp=0'; \
|
||||
# Configure Realpath cache for performance
|
||||
echo 'realpath_cache_size=4096K'; \
|
||||
echo 'realpath_cache_ttl=600'; \
|
||||
} > /etc/php/8.1/fpm/conf.d/symfony-recommended.ini
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/symfony-recommended.ini
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.validate_timestamp=0
|
||||
# Configure Realpath cache for performance
|
||||
realpath_cache_size=4096K
|
||||
realpath_cache_ttl=600
|
||||
EOF
|
||||
|
||||
# Increase upload limit and enable preloading
|
||||
RUN \
|
||||
{ \
|
||||
echo 'upload_max_filesize=256M'; \
|
||||
echo 'post_max_size=300M'; \
|
||||
echo 'opcache.preload_user=www-data'; \
|
||||
echo 'opcache.preload=/var/www/html/config/preload.php'; \
|
||||
} > /etc/php/8.1/fpm/conf.d/partdb.ini
|
||||
# Increase upload limit and enable preloading (disabled for now, as it does not seem to work properly, and require prod env anyway)
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/partdb.ini
|
||||
upload_max_filesize=256M
|
||||
post_max_size=300M
|
||||
;opcache.preload_user=www-data
|
||||
;opcache.preload=/var/www/html/config/preload.php
|
||||
log_limit=8096
|
||||
EOF
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
# ---
|
||||
|
||||
FROM base
|
||||
ARG PHP_VERSION
|
||||
|
||||
# Set working dir
|
||||
WORKDIR /var/www/html
|
||||
COPY --from=apache-config / /
|
||||
COPY --chown=www-data:www-data . .
|
||||
|
||||
# Setup apache2
|
||||
RUN a2dissite 000-default.conf
|
||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||
RUN a2ensite symfony.conf
|
||||
RUN a2enmod rewrite
|
||||
RUN a2dissite 000-default.conf && \
|
||||
a2ensite symfony.conf && \
|
||||
# Enable php-fpm
|
||||
a2enmod proxy_fcgi setenvif && \
|
||||
a2enconf php${PHP_VERSION}-fpm && \
|
||||
a2enconf docker-php && \
|
||||
a2enmod rewrite
|
||||
|
||||
# Install composer and yarn dependencies for Part-DB
|
||||
USER www-data
|
||||
RUN composer install -a --no-dev && composer clear-cache
|
||||
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
|
||||
RUN composer install -a --no-dev && \
|
||||
composer clear-cache
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
|
|
@ -119,10 +164,12 @@ ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
|
|||
|
||||
USER root
|
||||
|
||||
# Copy entrypoint to /usr/local/bin and make it executable
|
||||
RUN cp ./.docker/partdb-entrypoint.sh /usr/local/bin/partdb-entrypoint.sh && chmod +x /usr/local/bin/partdb-entrypoint.sh
|
||||
# Copy apache2-foreground to /usr/local/bin and make it executable
|
||||
RUN cp ./.docker/apache2-foreground /usr/local/bin/apache2-foreground && chmod +x /usr/local/bin/apache2-foreground
|
||||
# Replace the php version placeholder in the entry point, with our php version
|
||||
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
|
||||
|
||||
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
|
||||
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
|
||||
install ./.docker/apache2-foreground /usr/local/bin
|
||||
ENTRYPOINT ["partdb-entrypoint.sh"]
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
|
|
|
|||
121
Dockerfile-frankenphp
Normal file
121
Dockerfile-frankenphp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
||||
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl \
|
||||
ca-certificates \
|
||||
mariadb-client \
|
||||
postgresql-client \
|
||||
file \
|
||||
acl \
|
||||
git \
|
||||
gettext \
|
||||
gnupg \
|
||||
zip \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN set -eux; \
|
||||
# Prepare keyrings directory
|
||||
mkdir -p /etc/apt/keyrings; \
|
||||
\
|
||||
# Import Yarn GPG key
|
||||
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \
|
||||
| tee /etc/apt/keyrings/yarn.gpg >/dev/null; \
|
||||
chmod 644 /etc/apt/keyrings/yarn.gpg; \
|
||||
\
|
||||
# Add Yarn repo with signed-by
|
||||
echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian stable main" \
|
||||
| tee /etc/apt/sources.list.d/yarn.list; \
|
||||
\
|
||||
# Run NodeSource setup script (unchanged)
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash -; \
|
||||
\
|
||||
# Install Node.js + Yarn
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
yarn; \
|
||||
\
|
||||
# Cleanup
|
||||
apt-get -y autoremove; \
|
||||
apt-get clean autoclean; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# Install PHP
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
@composer \
|
||||
apcu \
|
||||
intl \
|
||||
opcache \
|
||||
zip \
|
||||
pdo_mysql \
|
||||
pdo_sqlite \
|
||||
pdo_pgsql \
|
||||
gd \
|
||||
bcmath \
|
||||
xsl \
|
||||
;
|
||||
|
||||
# Copy config files for php and caddy
|
||||
COPY --link .docker/frankenphp/conf.d/app.ini $PHP_INI_DIR/conf.d/
|
||||
COPY --chmod=755 .docker/frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY --link .docker/frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY --link .docker/frankenphp/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/
|
||||
COPY --link .docker/frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
|
||||
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# Install composer
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
WORKDIR /app
|
||||
|
||||
# prevent the reinstallation of vendors at every changes in the source code
|
||||
COPY --link composer.* symfony.* ./
|
||||
RUN set -eux; \
|
||||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
||||
|
||||
# copy sources
|
||||
COPY --link . ./
|
||||
|
||||
# Install composer and yarn dependencies for Part-DB
|
||||
RUN set -eux; \
|
||||
mkdir -p var/cache var/log; \
|
||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||
composer dump-env prod; \
|
||||
composer run-script --no-dev post-install-cmd; \
|
||||
chmod +x bin/console; sync;
|
||||
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
|
||||
|
||||
USER root
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint"]
|
||||
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
|
||||
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
|
||||
STOPSIGNAL SIGWINCH
|
||||
|
||||
VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]
|
||||
|
||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
ENV XDG_CONFIG_HOME /config
|
||||
ENV XDG_DATA_HOME /data
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 443/udp
|
||||
EXPOSE 2019
|
||||
91
Makefile
Normal file
91
Makefile
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# PartDB Makefile for Test Environment Management
|
||||
|
||||
.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \
|
||||
test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \
|
||||
section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
|
||||
|
||||
# Default target
|
||||
help: ## Show this help
|
||||
@awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
# Dependencies
|
||||
deps-install: ## Install PHP dependencies with unlimited memory
|
||||
@echo "📦 Installing PHP dependencies..."
|
||||
COMPOSER_MEMORY_LIMIT=-1 composer install
|
||||
yarn install
|
||||
@echo "✅ Dependencies installed"
|
||||
|
||||
# Complete test environment setup
|
||||
test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures)
|
||||
@echo "✅ Test environment setup complete!"
|
||||
|
||||
# Clean test environment
|
||||
test-clean: ## Clean test cache and database files
|
||||
@echo "🧹 Cleaning test environment..."
|
||||
rm -rf var/cache/test
|
||||
rm -f var/app_test.db
|
||||
@echo "✅ Test environment cleaned"
|
||||
|
||||
# Create test database
|
||||
test-db-create: ## Create test database (if not exists)
|
||||
@echo "🗄️ Creating test database..."
|
||||
-php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||
|
||||
# Run database migrations for test environment
|
||||
test-db-migrate: ## Run database migrations for test environment
|
||||
@echo "🔄 Running database migrations..."
|
||||
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
|
||||
|
||||
# Clear test cache
|
||||
test-cache-clear: ## Clear test cache
|
||||
@echo "🗑️ Clearing test cache..."
|
||||
rm -rf var/cache/test
|
||||
@echo "✅ Test cache cleared"
|
||||
|
||||
# Load test fixtures
|
||||
test-fixtures: ## Load test fixtures
|
||||
@echo "📦 Loading test fixtures..."
|
||||
php bin/console partdb:fixtures:load -n --env test
|
||||
|
||||
# Run PHPUnit tests
|
||||
test-run: ## Run PHPUnit tests
|
||||
@echo "🧪 Running tests..."
|
||||
php bin/phpunit
|
||||
|
||||
# Quick test reset (clean + migrate + fixtures, skip DB creation)
|
||||
test-reset: test-cache-clear test-db-migrate test-fixtures
|
||||
@echo "✅ Test environment reset complete!"
|
||||
|
||||
test-typecheck: ## Run static analysis (PHPStan)
|
||||
@echo "🧪 Running type checks..."
|
||||
COMPOSER_MEMORY_LIMIT=-1 composer phpstan
|
||||
|
||||
# Development helpers
|
||||
dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup)
|
||||
@echo "✅ Development environment setup complete!"
|
||||
|
||||
dev-clean: ## Clean development cache and database files
|
||||
@echo "🧹 Cleaning development environment..."
|
||||
rm -rf var/cache/dev
|
||||
rm -f var/app_dev.db
|
||||
@echo "✅ Development environment cleaned"
|
||||
|
||||
dev-db-create: ## Create development database (if not exists)
|
||||
@echo "🗄️ Creating development database..."
|
||||
-php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||
|
||||
dev-db-migrate: ## Run database migrations for development environment
|
||||
@echo "🔄 Running database migrations..."
|
||||
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
|
||||
|
||||
dev-cache-clear: ## Clear development cache
|
||||
@echo "🗑️ Clearing development cache..."
|
||||
rm -rf var/cache/dev
|
||||
@echo "✅ Development cache cleared"
|
||||
|
||||
dev-warmup: ## Warm up development cache
|
||||
@echo "🔥 Warming up development cache..."
|
||||
COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n
|
||||
|
||||
dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate)
|
||||
@echo "✅ Development environment reset complete!"
|
||||
56
README.md
56
README.md
|
|
@ -3,7 +3,7 @@
|
|||

|
||||
[](https://codecov.io/gh/Part-DB/Part-DB-server)
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
|
@ -30,7 +30,7 @@ If you want to test Part-DB without installing it, you can use [this](https://de
|
|||
You can log in with username: *user* and password: *user*.
|
||||
|
||||
Every change to the master branch gets automatically deployed, so it represents the current development progress and is
|
||||
maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
may not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
the page
|
||||
for the first time.
|
||||
|
||||
|
|
@ -39,44 +39,46 @@ for the first time.
|
|||
|
||||
## Features
|
||||
|
||||
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer
|
||||
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer,
|
||||
and multiple store locations and price information. Parts can be grouped using tags. You can associate various files
|
||||
like datasheets or pictures with the parts.
|
||||
* Multi-Language support (currently German, English, Russian, Japanese and French (experimental))
|
||||
* Multi-language support (currently German, English, Russian, Japanese, French, Czech, Danish, and Chinese)
|
||||
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner
|
||||
* User system with groups and detailed (fine granular) permissions.
|
||||
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups.
|
||||
Password reset via email can be setup.
|
||||
Password reset via email can be set up.
|
||||
* Optional support for single sign-on (SSO) via SAML (using an intermediate service
|
||||
like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
|
||||
* Import/Export system for parts and datastructure. BOM import for projects from KiCAD is supported.
|
||||
* Import/Export system for parts and data structure. BOM import for projects from KiCAD is supported.
|
||||
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build
|
||||
this project and directly withdraw all components needed from DB
|
||||
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older
|
||||
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
|
||||
versions.
|
||||
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
|
||||
* MySQL and SQLite supported as database backends
|
||||
* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface.
|
||||
* MySQL, SQLite and PostgreSQL are supported as database backends
|
||||
* Support for rich text descriptions and comments in parts
|
||||
* Support for multiple currencies and automatic update of exchange rates supported
|
||||
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
|
||||
* Automatic thumbnail generation for pictures
|
||||
* Use cloud providers (like Octopart, Digikey, farnell or TME) to automatically get part information, datasheets and
|
||||
* Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and
|
||||
prices for parts
|
||||
* API to access Part-DB from other applications/scripts
|
||||
* [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your
|
||||
KiCad and see available parts from Part-DB directly inside KiCad.
|
||||
|
||||
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
||||
or makerspaces, where many users have should have (controlled) access to the shared inventory.
|
||||
With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
||||
or maker spaces, where many users should have (controlled) access to the shared inventory.
|
||||
|
||||
Part-DB is also used by small companies and universities for managing their inventory.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A **web server** (like Apache2 or nginx) that is capable of
|
||||
running [Symfony 5](https://symfony.com/doc/current/reference/requirements.html),
|
||||
this includes a minimum PHP version of **PHP 8.1**
|
||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.2.2) database server if you do not want to use SQLite.
|
||||
* Shell access to your server is highly suggested!
|
||||
* For building the client side assets **yarn** and **nodejs** (>= 18.0) is needed.
|
||||
running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html),
|
||||
this includes a minimum PHP version of **PHP 8.2**
|
||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite.
|
||||
* Shell access to your server is highly recommended!
|
||||
* For building the client-side assets **yarn** and **nodejs** (>= 20.0) is needed.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -86,8 +88,8 @@ read [this](https://docs.part-db.de/upgrade_legacy.html) first.
|
|||
*Hint:* A docker image is available under [jbtronics/part-db1](https://hub.docker.com/r/jbtronics/part-db1). How to set
|
||||
up Part-DB via docker is described [here](https://docs.part-db.de/installation/installation_docker.html).
|
||||
|
||||
**Below you find some very rough outline of the installation process, see [here](https://docs.part-db.de/installation/)
|
||||
for a detailed guide how to install Part-DB.**
|
||||
**Below you find a very rough outline of the installation process, see [here](https://docs.part-db.de/installation/)
|
||||
for a detailed guide on how to install Part-DB.**
|
||||
|
||||
1. Copy or clone this repository into a folder on your server.
|
||||
2. Configure your webserver to serve from the `public/` folder.
|
||||
|
|
@ -105,13 +107,13 @@ for a detailed guide how to install Part-DB.**
|
|||
6. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup`
|
||||
7. Upgrade database to new scheme (or create it, when it was empty): `php bin/console doctrine:migrations:migrate` and
|
||||
follow the instructions given. During the process the password for the admin is user is shown. Copy it. **Caution**:
|
||||
This steps tamper with your database and could potentially destroy it. So make sure to make a backup of your
|
||||
These steps tamper with your database and could potentially destroy it. So make sure to make a backup of your
|
||||
database.
|
||||
8. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations, after
|
||||
8. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations after
|
||||
you installed/upgraded Part-DB. Check if `partdb.default_currency` matches your mainly used currency (this can not be
|
||||
changed after creating price information).
|
||||
Run `php bin/console cache:clear` when you changed something.
|
||||
9. Access Part-DB in your browser (under the URL you put it) and login with user *admin*. Password is the one outputted
|
||||
Run `php bin/console cache:clear` when you change something.
|
||||
9. Access Part-DB in your browser (under the URL you put it) and log in with user *admin*. Password is the one outputted
|
||||
during DB setup.
|
||||
If you can not remember the password, set a new one with `php bin/console app:set-password admin`. You can create
|
||||
new users with the admin user and start using Part-DB.
|
||||
|
|
@ -120,23 +122,23 @@ When you want to upgrade to a newer version, then just copy the new files into t
|
|||
and repeat the steps 4. to 7.
|
||||
|
||||
Normally a random password is generated when the admin user is created during initial database creation,
|
||||
however you can set the initial admin password, by setting the `INITIAL_ADMIN_PW` env var.
|
||||
however, you can set the initial admin password, by setting the `INITIAL_ADMIN_PW` env var.
|
||||
|
||||
You can configure Part-DB to your needs by changing environment variables in the `.env.local` file.
|
||||
See [here](https://docs.part-db.de/configuration.html) for more information.
|
||||
|
||||
### Reverse proxy
|
||||
|
||||
If you are using a reverse proxy, you have to ensure that the proxies sets the `X-Forwarded-*` headers correctly, or you
|
||||
If you are using a reverse proxy, you have to ensure that the proxies set the `X-Forwarded-*` headers correctly, or you
|
||||
will get HTTP/HTTPS mixup and wrong hostnames.
|
||||
If the reverse proxy is on a different server (or it cannot access Part-DB via localhost) you have to set
|
||||
the `TRUSTED_PROXIES` env variable to match your reverse proxies IP-address (or IP block). You can do this in
|
||||
the `TRUSTED_PROXIES` env variable to match your reverse proxy's IP address (or IP block). You can do this in
|
||||
your `.env.local` or (when using docker) in your `docker-compose.yml` file.
|
||||
|
||||
## Donate for development
|
||||
|
||||
If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name).
|
||||
There you will find various methods to support development on a monthly or a one time base.
|
||||
There you will find various methods to support development on a monthly or a one-time base.
|
||||
|
||||
## Built with
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.9.1
|
||||
2.2.1
|
||||
|
|
|
|||
1
assets/ckeditor/emojis.json
Normal file
1
assets/ckeditor/emojis.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,66 +1,63 @@
|
|||
/**
|
||||
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
|
||||
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
|
||||
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
|
||||
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
|
||||
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
|
||||
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
|
||||
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
|
||||
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
|
||||
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
|
||||
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
|
||||
import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
|
||||
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
|
||||
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
|
||||
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
|
||||
import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
|
||||
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
|
||||
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
|
||||
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
|
||||
import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
|
||||
import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
|
||||
import Image from '@ckeditor/ckeditor5-image/src/image.js';
|
||||
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
|
||||
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
|
||||
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
|
||||
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
|
||||
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
|
||||
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
|
||||
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
|
||||
import Link from '@ckeditor/ckeditor5-link/src/link.js';
|
||||
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
|
||||
import List from '@ckeditor/ckeditor5-list/src/list.js';
|
||||
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
|
||||
import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
|
||||
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
|
||||
import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
|
||||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
|
||||
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
|
||||
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
|
||||
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
|
||||
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
|
||||
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
|
||||
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
|
||||
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
|
||||
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
|
||||
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
|
||||
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
|
||||
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
|
||||
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
|
||||
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
|
||||
import Table from '@ckeditor/ckeditor5-table/src/table.js';
|
||||
import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
|
||||
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
|
||||
import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
|
||||
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
|
||||
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
|
||||
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
|
||||
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
|
||||
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
|
||||
import {ClassicEditor} from 'ckeditor5'
|
||||
import {Alignment} from 'ckeditor5';
|
||||
import {Autoformat} from 'ckeditor5';
|
||||
import {Base64UploadAdapter} from 'ckeditor5';
|
||||
import {BlockQuote} from 'ckeditor5';
|
||||
import {Bold} from 'ckeditor5';
|
||||
import {Code} from 'ckeditor5';
|
||||
import {CodeBlock} from 'ckeditor5';
|
||||
import {Essentials} from 'ckeditor5';
|
||||
import {FindAndReplace} from 'ckeditor5';
|
||||
import {FontBackgroundColor} from 'ckeditor5';
|
||||
import {FontColor} from 'ckeditor5';
|
||||
import {FontFamily} from 'ckeditor5';
|
||||
import {FontSize} from 'ckeditor5';
|
||||
import {GeneralHtmlSupport} from 'ckeditor5';
|
||||
import {Heading} from 'ckeditor5';
|
||||
import {Highlight} from 'ckeditor5';
|
||||
import {HorizontalLine} from 'ckeditor5';
|
||||
import {HtmlComment} from 'ckeditor5';
|
||||
import {HtmlEmbed} from 'ckeditor5';
|
||||
import {Image} from 'ckeditor5';
|
||||
import {ImageResize} from 'ckeditor5';
|
||||
import {ImageStyle} from 'ckeditor5';
|
||||
import {ImageToolbar} from 'ckeditor5';
|
||||
import {ImageUpload} from 'ckeditor5';
|
||||
import {Indent} from 'ckeditor5';
|
||||
import {IndentBlock} from 'ckeditor5';
|
||||
import {Italic} from 'ckeditor5';
|
||||
import {Link} from 'ckeditor5';
|
||||
import {LinkImage} from 'ckeditor5';
|
||||
import {List} from 'ckeditor5';
|
||||
import {ListProperties} from 'ckeditor5';
|
||||
import {Markdown} from 'ckeditor5';
|
||||
import {MediaEmbed} from 'ckeditor5';
|
||||
import {MediaEmbedToolbar} from 'ckeditor5';
|
||||
import {Paragraph} from 'ckeditor5';
|
||||
import {PasteFromOffice} from 'ckeditor5';
|
||||
import {RemoveFormat} from 'ckeditor5';
|
||||
import {SourceEditing} from 'ckeditor5';
|
||||
import {SpecialCharacters} from 'ckeditor5';
|
||||
import {SpecialCharactersArrows} from 'ckeditor5';
|
||||
import {SpecialCharactersCurrency} from 'ckeditor5';
|
||||
import {SpecialCharactersEssentials} from 'ckeditor5';
|
||||
import {SpecialCharactersLatin} from 'ckeditor5';
|
||||
import {SpecialCharactersMathematical} from 'ckeditor5';
|
||||
import {SpecialCharactersText} from 'ckeditor5';
|
||||
import {Strikethrough} from 'ckeditor5';
|
||||
import {Subscript} from 'ckeditor5';
|
||||
import {Superscript} from 'ckeditor5';
|
||||
import {Table} from 'ckeditor5';
|
||||
import {TableCaption} from 'ckeditor5';
|
||||
import {TableCellProperties} from 'ckeditor5';
|
||||
import {TableColumnResize} from 'ckeditor5';
|
||||
import {TableProperties} from 'ckeditor5';
|
||||
import {TableToolbar} from 'ckeditor5';
|
||||
import {Underline} from 'ckeditor5';
|
||||
import {WordCount} from 'ckeditor5';
|
||||
import {EditorWatchdog} from 'ckeditor5';
|
||||
import PartDBLabel from "./plugins/PartDBLabel/PartDBLabel";
|
||||
import SpecialCharactersGreek from "./plugins/special_characters_emoji";
|
||||
|
||||
class Editor extends ClassicEditor {}
|
||||
|
||||
|
|
@ -122,7 +119,8 @@ Editor.builtinPlugins = [
|
|||
Underline,
|
||||
WordCount,
|
||||
|
||||
PartDBLabel
|
||||
PartDBLabel,
|
||||
SpecialCharactersGreek
|
||||
];
|
||||
|
||||
// Editor configuration.
|
||||
|
|
|
|||
|
|
@ -2,68 +2,69 @@
|
|||
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
|
||||
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
|
||||
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
|
||||
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
|
||||
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
|
||||
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
|
||||
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
|
||||
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
|
||||
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
|
||||
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
|
||||
import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
|
||||
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
|
||||
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
|
||||
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
|
||||
import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
|
||||
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
|
||||
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
|
||||
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
|
||||
import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
|
||||
import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
|
||||
import Image from '@ckeditor/ckeditor5-image/src/image.js';
|
||||
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
|
||||
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
|
||||
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
|
||||
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
|
||||
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
|
||||
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
|
||||
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
|
||||
import Link from '@ckeditor/ckeditor5-link/src/link.js';
|
||||
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
|
||||
import List from '@ckeditor/ckeditor5-list/src/list.js';
|
||||
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
|
||||
import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
|
||||
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
|
||||
import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
|
||||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
|
||||
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
|
||||
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
|
||||
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
|
||||
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
|
||||
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
|
||||
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
|
||||
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
|
||||
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
|
||||
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
|
||||
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
|
||||
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
|
||||
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
|
||||
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
|
||||
import Table from '@ckeditor/ckeditor5-table/src/table.js';
|
||||
import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
|
||||
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
|
||||
import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
|
||||
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
|
||||
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
|
||||
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
|
||||
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
|
||||
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
|
||||
import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
|
||||
import {ClassicEditor} from 'ckeditor5';
|
||||
import {Alignment} from 'ckeditor5';
|
||||
import {Autoformat} from 'ckeditor5';
|
||||
import {Base64UploadAdapter} from 'ckeditor5';
|
||||
import {BlockQuote} from 'ckeditor5';
|
||||
import {Bold} from 'ckeditor5';
|
||||
import {Code} from 'ckeditor5';
|
||||
import {CodeBlock} from 'ckeditor5';
|
||||
import {Essentials} from 'ckeditor5';
|
||||
import {FindAndReplace} from 'ckeditor5';
|
||||
import {FontBackgroundColor} from 'ckeditor5';
|
||||
import {FontColor} from 'ckeditor5';
|
||||
import {FontFamily} from 'ckeditor5';
|
||||
import {FontSize} from 'ckeditor5';
|
||||
import {GeneralHtmlSupport} from 'ckeditor5';
|
||||
import {Heading} from 'ckeditor5';
|
||||
import {Highlight} from 'ckeditor5';
|
||||
import {HorizontalLine} from 'ckeditor5';
|
||||
import {HtmlComment} from 'ckeditor5';
|
||||
import {HtmlEmbed} from 'ckeditor5';
|
||||
import {Image} from 'ckeditor5';
|
||||
import {ImageResize} from 'ckeditor5';
|
||||
import {ImageStyle} from 'ckeditor5';
|
||||
import {ImageToolbar} from 'ckeditor5';
|
||||
import {ImageUpload} from 'ckeditor5';
|
||||
import {Indent} from 'ckeditor5';
|
||||
import {IndentBlock} from 'ckeditor5';
|
||||
import {Italic} from 'ckeditor5';
|
||||
import {Link} from 'ckeditor5';
|
||||
import {LinkImage} from 'ckeditor5';
|
||||
import {List} from 'ckeditor5';
|
||||
import {ListProperties} from 'ckeditor5';
|
||||
import {Markdown} from 'ckeditor5';
|
||||
import {MediaEmbed} from 'ckeditor5';
|
||||
import {MediaEmbedToolbar} from 'ckeditor5';
|
||||
import {Paragraph} from 'ckeditor5';
|
||||
import {PasteFromOffice} from 'ckeditor5';
|
||||
import {RemoveFormat} from 'ckeditor5';
|
||||
import {SourceEditing} from 'ckeditor5';
|
||||
import {SpecialCharacters} from 'ckeditor5';
|
||||
import {SpecialCharactersArrows} from 'ckeditor5';
|
||||
import {SpecialCharactersCurrency} from 'ckeditor5';
|
||||
import {SpecialCharactersEssentials} from 'ckeditor5';
|
||||
import {SpecialCharactersLatin} from 'ckeditor5';
|
||||
import {SpecialCharactersMathematical} from 'ckeditor5';
|
||||
import {SpecialCharactersText} from 'ckeditor5';
|
||||
import {Strikethrough} from 'ckeditor5';
|
||||
import {Subscript} from 'ckeditor5';
|
||||
import {Superscript} from 'ckeditor5';
|
||||
import {Table} from 'ckeditor5';
|
||||
import {TableCaption} from 'ckeditor5';
|
||||
import {TableCellProperties} from 'ckeditor5';
|
||||
import {TableColumnResize} from 'ckeditor5';
|
||||
import {TableProperties} from 'ckeditor5';
|
||||
import {TableToolbar} from 'ckeditor5';
|
||||
import {Underline} from 'ckeditor5';
|
||||
import {WordCount} from 'ckeditor5';
|
||||
import {EditorWatchdog} from 'ckeditor5';
|
||||
import {TodoList} from 'ckeditor5';
|
||||
|
||||
import ExtendedMarkdown from "./plugins/extendedMarkdown.js";
|
||||
import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
|
||||
import SpecialCharactersGreek from "./plugins/special_characters_emoji";
|
||||
import {Mention, Emoji} from "ckeditor5";
|
||||
|
||||
class Editor extends ClassicEditor {}
|
||||
|
||||
|
|
@ -117,9 +118,11 @@ Editor.builtinPlugins = [
|
|||
Underline,
|
||||
TodoList,
|
||||
|
||||
Mention, Emoji,
|
||||
|
||||
//Our own extensions
|
||||
ExtendedMarkdown,
|
||||
SpecialCharactersEmoji
|
||||
SpecialCharactersGreek
|
||||
];
|
||||
|
||||
// Editor configuration.
|
||||
|
|
@ -148,6 +151,7 @@ Editor.defaultConfig = {
|
|||
'indent',
|
||||
'|',
|
||||
'specialCharacters',
|
||||
"emoji",
|
||||
'horizontalLine',
|
||||
'|',
|
||||
'imageUpload',
|
||||
|
|
|
|||
|
|
@ -2,35 +2,36 @@
|
|||
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
|
||||
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
|
||||
import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js';
|
||||
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
|
||||
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
|
||||
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
|
||||
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
|
||||
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
|
||||
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
|
||||
import Link from '@ckeditor/ckeditor5-link/src/link.js';
|
||||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
|
||||
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
|
||||
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
|
||||
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
|
||||
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
|
||||
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
|
||||
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
|
||||
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
|
||||
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
|
||||
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
|
||||
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
|
||||
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
|
||||
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
|
||||
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
|
||||
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
|
||||
import {ClassicEditor} from 'ckeditor5';
|
||||
import {Autoformat} from 'ckeditor5';
|
||||
import {AutoLink} from 'ckeditor5';
|
||||
import {Bold} from 'ckeditor5';
|
||||
import {Code} from 'ckeditor5';
|
||||
import {Essentials} from 'ckeditor5';
|
||||
import {FindAndReplace} from 'ckeditor5';
|
||||
import {Highlight} from 'ckeditor5';
|
||||
import {Italic} from 'ckeditor5';
|
||||
import {Link} from 'ckeditor5';
|
||||
import {Paragraph} from 'ckeditor5';
|
||||
import {RemoveFormat} from 'ckeditor5';
|
||||
import {SourceEditing} from 'ckeditor5';
|
||||
import {SpecialCharacters} from 'ckeditor5';
|
||||
import {SpecialCharactersArrows} from 'ckeditor5';
|
||||
import {SpecialCharactersCurrency} from 'ckeditor5';
|
||||
import {SpecialCharactersEssentials} from 'ckeditor5';
|
||||
import {SpecialCharactersLatin} from 'ckeditor5';
|
||||
import {SpecialCharactersMathematical} from 'ckeditor5';
|
||||
import {SpecialCharactersText} from 'ckeditor5';
|
||||
import {Strikethrough} from 'ckeditor5';
|
||||
import {Subscript} from 'ckeditor5';
|
||||
import {Superscript} from 'ckeditor5';
|
||||
import {Underline} from 'ckeditor5';
|
||||
import {EditorWatchdog} from 'ckeditor5';
|
||||
import {Mention, Emoji} from "ckeditor5";
|
||||
|
||||
import ExtendedMarkdownInline from "./plugins/extendedMarkdownInline";
|
||||
import SingleLinePlugin from "./plugins/singleLine";
|
||||
import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
|
||||
import SpecialCharactersGreek from "./plugins/special_characters_emoji";
|
||||
|
||||
class Editor extends ClassicEditor {}
|
||||
|
||||
|
|
@ -62,7 +63,8 @@ Editor.builtinPlugins = [
|
|||
|
||||
ExtendedMarkdownInline,
|
||||
SingleLinePlugin,
|
||||
SpecialCharactersEmoji
|
||||
SpecialCharactersGreek,
|
||||
Mention, Emoji
|
||||
];
|
||||
|
||||
// Editor configuration.
|
||||
|
|
@ -81,6 +83,7 @@ Editor.defaultConfig = {
|
|||
'link',
|
||||
'code',
|
||||
'specialCharacters',
|
||||
'emoji',
|
||||
'|',
|
||||
'undo',
|
||||
'redo',
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import PartDBLabelEditing from "./PartDBLabelEditing";
|
|||
|
||||
import "./PartDBLabel.css";
|
||||
|
||||
import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
|
||||
import {Plugin} from "ckeditor5";
|
||||
|
||||
export default class PartDBLabel extends Plugin {
|
||||
static get requires() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Command from '@ckeditor/ckeditor5-core/src/command';
|
||||
import {Command} from 'ckeditor5';
|
||||
|
||||
export default class PartDBLabelCommand extends Command {
|
||||
execute( { value } ) {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import {Plugin} from 'ckeditor5';
|
||||
import PartDBLabelCommand from "./PartDBLabelCommand";
|
||||
|
||||
import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
|
||||
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
|
||||
import { toWidget } from 'ckeditor5';
|
||||
import {Widget} from 'ckeditor5';
|
||||
|
||||
export default class PartDBLabelEditing extends Plugin {
|
||||
static get requires() { // ADDED
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import {Plugin} from 'ckeditor5';
|
||||
|
||||
require('./lang/de.js');
|
||||
require('./lang/en.js');
|
||||
|
||||
import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
|
||||
import { addListToDropdown, createDropdown } from 'ckeditor5';
|
||||
|
||||
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
|
||||
import Model from '@ckeditor/ckeditor5-ui/src/model';
|
||||
import {Collection} from 'ckeditor5';
|
||||
import {UIModel} from 'ckeditor5';
|
||||
|
||||
export default class PartDBLabelUI extends Plugin {
|
||||
init() {
|
||||
|
|
@ -128,6 +129,8 @@ const PLACEHOLDERS = [
|
|||
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
||||
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
||||
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
||||
['[[BARCODE_C93]]', 'Code 93 barcode linking to this element'],
|
||||
['[[BARCODE_DATAMATRIX]]', 'Datamatrix code linking to this element'],
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -149,18 +152,28 @@ const PLACEHOLDERS = [
|
|||
function getDropdownItemsDefinitions(t) {
|
||||
const itemDefinitions = new Collection();
|
||||
|
||||
let first = true;
|
||||
|
||||
for ( const group of PLACEHOLDERS) {
|
||||
|
||||
//Add group header
|
||||
|
||||
//Skip separator for first group
|
||||
if (!first) {
|
||||
|
||||
itemDefinitions.add({
|
||||
'type': 'separator',
|
||||
model: new Model( {
|
||||
model: new UIModel( {
|
||||
withText: true,
|
||||
})
|
||||
});
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
itemDefinitions.add({
|
||||
type: 'button',
|
||||
model: new Model( {
|
||||
model: new UIModel( {
|
||||
label: t(group.label),
|
||||
withText: true,
|
||||
isEnabled: false,
|
||||
|
|
@ -171,7 +184,7 @@ function getDropdownItemsDefinitions(t) {
|
|||
for ( const entry of group.entries) {
|
||||
const definition = {
|
||||
type: 'button',
|
||||
model: new Model( {
|
||||
model: new UIModel( {
|
||||
commandParam: entry[0],
|
||||
label: t(entry[1]),
|
||||
tooltip: entry[0],
|
||||
|
|
|
|||
|
|
@ -17,15 +17,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Make sure that the global object is defined. If not, define it.
|
||||
window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {};
|
||||
import {add} from "ckeditor5";
|
||||
|
||||
// Make sure that the dictionary for Polish translations exist.
|
||||
window.CKEDITOR_TRANSLATIONS[ 'de' ] = window.CKEDITOR_TRANSLATIONS[ 'de' ] || {};
|
||||
window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary = window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary || {};
|
||||
|
||||
// Extend the dictionary for Polish translations with your translations:
|
||||
Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
||||
add( "de", {
|
||||
'Label Placeholder': 'Label Platzhalter',
|
||||
'Part': 'Bauteil',
|
||||
|
||||
|
|
@ -69,6 +63,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
|||
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
||||
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
||||
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
||||
'Code 93 barcode linking to this element': 'Code 93 Barcode verknüpft mit diesem Element',
|
||||
'Datamatrix code linking to this element': 'Datamatrix Code verknüpft mit diesem Element',
|
||||
|
||||
'Location ID': 'Lagerort ID',
|
||||
'Name': 'Name',
|
||||
|
|
@ -86,5 +82,4 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
|||
'Instance name': 'Instanzname',
|
||||
'Target type': 'Zieltyp',
|
||||
'URL of this Part-DB instance': 'URL dieser Part-DB Instanz',
|
||||
|
||||
} );
|
||||
});
|
||||
|
|
|
|||
84
assets/ckeditor/plugins/PartDBLabel/lang/en.js
Normal file
84
assets/ckeditor/plugins/PartDBLabel/lang/en.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {add} from "ckeditor5";
|
||||
|
||||
add( "en", {
|
||||
'Label Placeholder': 'Label placeholder',
|
||||
'Part': 'Part',
|
||||
|
||||
'Database ID': 'Database ID',
|
||||
'Part name': 'Part name',
|
||||
'Category': 'Category',
|
||||
'Category (Full path)': 'Category (full path)',
|
||||
'Manufacturer': 'Manufacturer',
|
||||
'Manufacturer (Full path)': 'Manufacturer (full path)',
|
||||
'Footprint': 'Footprint',
|
||||
'Footprint (Full path)': 'Footprint (full path)',
|
||||
'Mass': 'Mass',
|
||||
'Manufacturer Product Number (MPN)': 'Manufacturer Product Number (MPN)',
|
||||
'Internal Part Number (IPN)': 'Internal Part Number (IPN)',
|
||||
'Tags': 'Tags',
|
||||
'Manufacturing status': 'Manufacturing status',
|
||||
'Description': 'Description',
|
||||
'Description (plain text)': 'Description (plain text)',
|
||||
'Comment': 'Comment',
|
||||
'Comment (plain text)': 'Comment (plain text)',
|
||||
'Last modified datetime': 'Last modified datetime',
|
||||
'Creation datetime': 'Creation datetime',
|
||||
'IPN as QR code': 'IPN as QR code',
|
||||
'IPN as Code 128 barcode': 'IPN as Code 128 barcode',
|
||||
'IPN as Code 39 barcode': 'IPN as Code 39 barcode',
|
||||
|
||||
'Lot ID': 'Lot ID',
|
||||
'Lot name': 'Lot name',
|
||||
'Lot comment': 'Lot comment',
|
||||
'Lot expiration date': 'Lot expiration date',
|
||||
'Lot amount': 'Lot amount',
|
||||
'Storage location': 'Storage location',
|
||||
'Storage location (Full path)': 'Storage location (full path)',
|
||||
'Full name of the lot owner': 'Full name of the lot owner',
|
||||
'Username of the lot owner': 'Username of the lot owner',
|
||||
|
||||
'Barcodes': 'Barcodes',
|
||||
'Content of the 1D barcodes (like Code 39)': 'Content of the 1D barcodes (like Code 39)',
|
||||
'Content of the 2D barcodes (QR codes)': 'Content of the 2D barcodes (QR codes)',
|
||||
'QR code linking to this element': 'QR code linking to this element',
|
||||
'Code 128 barcode linking to this element': 'Code 128 barcode linking to this element',
|
||||
'Code 39 barcode linking to this element': 'Code 39 barcode linking to this element',
|
||||
'Code 93 barcode linking to this element': 'Code 93 barcode linking to this element',
|
||||
'Datamatrix code linking to this element': 'Datamatrix code linking to this element',
|
||||
|
||||
'Location ID': 'Location ID',
|
||||
'Name': 'Name',
|
||||
'Full path': 'Full path',
|
||||
'Parent name': 'Parent name',
|
||||
'Parent full path': 'Parent full path',
|
||||
'Full name of the location owner': 'Full name of the location owner',
|
||||
'Username of the location owner': 'Username of the location owner',
|
||||
|
||||
'Username': 'Username',
|
||||
'Username (including name)': 'Username (including name)',
|
||||
'Current datetime': 'Current datetime',
|
||||
'Current date': 'Current date',
|
||||
'Current time': 'Current time',
|
||||
'Instance name': 'Instance name',
|
||||
'Target type': 'Target type',
|
||||
'URL of this Part-DB instance': 'URL of this Part-DB instance',
|
||||
} );
|
||||
|
|
@ -17,8 +17,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Plugin } from 'ckeditor5/src/core';
|
||||
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
|
||||
import { Plugin, MarkdownGfmDataProcessor } from 'ckeditor5';
|
||||
|
||||
const ALLOWED_TAGS = [
|
||||
//Common elements
|
||||
|
|
@ -34,7 +33,6 @@ const ALLOWED_TAGS = [
|
|||
|
||||
//Block elements
|
||||
'span',
|
||||
'p',
|
||||
'img',
|
||||
|
||||
|
||||
|
|
@ -57,7 +55,7 @@ export default class ExtendedMarkdown extends Plugin {
|
|||
constructor( editor ) {
|
||||
super( editor );
|
||||
|
||||
editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
|
||||
editor.data.processor = new MarkdownGfmDataProcessor( editor.data.viewDocument );
|
||||
for (const tag of ALLOWED_TAGS) {
|
||||
editor.data.processor.keepHtml(tag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Plugin } from 'ckeditor5/src/core';
|
||||
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
|
||||
import {Plugin} from 'ckeditor5';
|
||||
import {MarkdownGfmDataProcessor} from 'ckeditor5';
|
||||
|
||||
const ALLOWED_TAGS = [
|
||||
//Common elements
|
||||
|
|
@ -46,7 +46,7 @@ export default class ExtendedMarkdownInline extends Plugin {
|
|||
constructor( editor ) {
|
||||
super( editor );
|
||||
|
||||
editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
|
||||
editor.data.processor = new MarkdownGfmDataProcessor( editor.data.viewDocument );
|
||||
for (const tag of ALLOWED_TAGS) {
|
||||
editor.data.processor.keepHtml(tag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import {Plugin} from 'ckeditor5';
|
||||
|
||||
export default class SingleLinePlugin extends Plugin {
|
||||
init() {
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
|
||||
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
|
||||
import SpecialCharacters from 'ckeditor5';
|
||||
import SpecialCharactersEssentials from 'ckeditor5';
|
||||
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import {Plugin} from 'ckeditor5';
|
||||
|
||||
const emoji = require('emoji.json');
|
||||
|
||||
export default class SpecialCharactersEmoji extends Plugin {
|
||||
export default class SpecialCharactersGreek extends Plugin {
|
||||
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
|
|
@ -32,9 +30,6 @@ export default class SpecialCharactersEmoji extends Plugin {
|
|||
|
||||
//Add greek characters to special characters
|
||||
specialCharsPlugin.addItems('Greek', this.getGreek());
|
||||
|
||||
//Add Emojis to special characters
|
||||
specialCharsPlugin.addItems('Emoji', this.getEmojis());
|
||||
}
|
||||
|
||||
getGreek() {
|
||||
|
|
@ -96,14 +91,4 @@ export default class SpecialCharactersEmoji extends Plugin {
|
|||
{ title: 'san', character: 'Ϻ' },
|
||||
];
|
||||
}
|
||||
|
||||
getEmojis() {
|
||||
//Map our emoji data to the format the plugin expects
|
||||
return emoji.map(emoji => {
|
||||
return {
|
||||
title: emoji.name,
|
||||
character: emoji.char
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
359
assets/controllers/bulk_import_controller.js
Normal file
359
assets/controllers/bulk_import_controller.js
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import { generateCsrfHeaders } from "./csrf_protection_controller"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["progressBar", "progressText"]
|
||||
static values = {
|
||||
jobId: Number,
|
||||
partId: Number,
|
||||
researchUrl: String,
|
||||
researchAllUrl: String,
|
||||
markCompletedUrl: String,
|
||||
markSkippedUrl: String,
|
||||
markPendingUrl: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Auto-refresh progress if job is in progress
|
||||
if (this.hasProgressBarTarget) {
|
||||
this.startProgressUpdates()
|
||||
}
|
||||
|
||||
// Restore scroll position after page reload (if any)
|
||||
this.restoreScrollPosition()
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
|
||||
// Add CSRF headers if available
|
||||
const form = document.querySelector('form')
|
||||
if (form) {
|
||||
const csrfHeaders = generateCsrfHeaders(form)
|
||||
Object.assign(headers, csrfHeaders)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async fetchWithErrorHandling(url, options = {}, timeout = 30000) {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: { ...this.getHeaders(), ...options.headers },
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server error (${response.status}): ${errorText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out. Please try again.')
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
throw new Error('Network error. Please check your connection and try again.')
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval)
|
||||
}
|
||||
}
|
||||
|
||||
startProgressUpdates() {
|
||||
// Progress updates are handled via page reload for better reliability
|
||||
// No need for periodic updates since state changes trigger page refresh
|
||||
}
|
||||
|
||||
restoreScrollPosition() {
|
||||
const savedPosition = sessionStorage.getItem('bulkImportScrollPosition')
|
||||
if (savedPosition) {
|
||||
// Restore scroll position after a small delay to ensure page is fully loaded
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, parseInt(savedPosition))
|
||||
// Clear the saved position so it doesn't interfere with normal navigation
|
||||
sessionStorage.removeItem('bulkImportScrollPosition')
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
async markCompleted(event) {
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
|
||||
try {
|
||||
const url = this.markCompletedUrlValue.replace('__PART_ID__', partId)
|
||||
const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgressDisplay(data)
|
||||
this.markRowAsCompleted(partId)
|
||||
|
||||
if (data.job_completed) {
|
||||
this.showJobCompletedMessage()
|
||||
}
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Failed to mark part as completed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking part as completed:', error)
|
||||
this.showErrorMessage(error.message || 'Failed to mark part as completed')
|
||||
}
|
||||
}
|
||||
|
||||
async markSkipped(event) {
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
const reason = prompt('Reason for skipping (optional):') || ''
|
||||
|
||||
try {
|
||||
const url = this.markSkippedUrlValue.replace('__PART_ID__', partId)
|
||||
const data = await this.fetchWithErrorHandling(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason })
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgressDisplay(data)
|
||||
this.markRowAsSkipped(partId)
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Failed to mark part as skipped')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking part as skipped:', error)
|
||||
this.showErrorMessage(error.message || 'Failed to mark part as skipped')
|
||||
}
|
||||
}
|
||||
|
||||
async markPending(event) {
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
|
||||
try {
|
||||
const url = this.markPendingUrlValue.replace('__PART_ID__', partId)
|
||||
const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgressDisplay(data)
|
||||
this.markRowAsPending(partId)
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Failed to mark part as pending')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking part as pending:', error)
|
||||
this.showErrorMessage(error.message || 'Failed to mark part as pending')
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressDisplay(data) {
|
||||
if (this.hasProgressBarTarget) {
|
||||
this.progressBarTarget.style.width = `${data.progress}%`
|
||||
this.progressBarTarget.setAttribute('aria-valuenow', data.progress)
|
||||
}
|
||||
|
||||
if (this.hasProgressTextTarget) {
|
||||
this.progressTextTarget.textContent = `${data.completed_count} / ${data.total_count} completed`
|
||||
}
|
||||
}
|
||||
|
||||
markRowAsCompleted(partId) {
|
||||
// Save scroll position and refresh page to show updated state
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
markRowAsSkipped(partId) {
|
||||
// Save scroll position and refresh page to show updated state
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
markRowAsPending(partId) {
|
||||
// Save scroll position and refresh page to show updated state
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
showJobCompletedMessage() {
|
||||
const alert = document.createElement('div')
|
||||
alert.className = 'alert alert-success alert-dismissible fade show'
|
||||
alert.innerHTML = `
|
||||
<i class="fas fa-check-circle"></i>
|
||||
Job completed! All parts have been processed.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`
|
||||
|
||||
const container = document.querySelector('.card-body')
|
||||
container.insertBefore(alert, container.firstChild)
|
||||
}
|
||||
|
||||
async researchPart(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
const spinner = event.currentTarget.querySelector(`[data-research-spinner="${partId}"]`)
|
||||
const button = event.currentTarget
|
||||
|
||||
// Show loading state
|
||||
if (spinner) {
|
||||
spinner.style.display = 'inline-block'
|
||||
}
|
||||
button.disabled = true
|
||||
|
||||
try {
|
||||
const url = this.researchUrlValue.replace('__PART_ID__', partId)
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server error (${response.status}): ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Research completed for part. Found ${data.results_count} results.`)
|
||||
// Save scroll position and reload to show updated results
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Research failed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error researching part:', error)
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
this.showErrorMessage('Research timed out. Please try again.')
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
this.showErrorMessage('Network error. Please check your connection and try again.')
|
||||
} else {
|
||||
this.showErrorMessage(error.message || 'Research failed due to an unexpected error')
|
||||
}
|
||||
} finally {
|
||||
// Hide loading state
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none'
|
||||
}
|
||||
button.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
async researchAllParts(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const spinner = document.getElementById('research-all-spinner')
|
||||
const button = event.currentTarget
|
||||
|
||||
// Show loading state
|
||||
if (spinner) {
|
||||
spinner.style.display = 'inline-block'
|
||||
}
|
||||
button.disabled = true
|
||||
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 120000) // 2 minute timeout for bulk operations
|
||||
|
||||
const response = await fetch(this.researchAllUrlValue, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server error (${response.status}): ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Research completed for ${data.researched_count} parts.`)
|
||||
// Save scroll position and reload to show updated results
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Bulk research failed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error researching all parts:', error)
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
this.showErrorMessage('Bulk research timed out. This may happen with large batches. Please try again or process smaller batches.')
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
this.showErrorMessage('Network error. Please check your connection and try again.')
|
||||
} else {
|
||||
this.showErrorMessage(error.message || 'Bulk research failed due to an unexpected error')
|
||||
}
|
||||
} finally {
|
||||
// Hide loading state
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none'
|
||||
}
|
||||
button.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
this.showToast('success', message)
|
||||
}
|
||||
|
||||
showErrorMessage(message) {
|
||||
this.showToast('error', message)
|
||||
}
|
||||
|
||||
showToast(type, message) {
|
||||
// Create a simple alert that doesn't disrupt layout
|
||||
const alertId = 'alert-' + Date.now()
|
||||
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle'
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger'
|
||||
|
||||
const alertHTML = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show position-fixed"
|
||||
style="top: 20px; right: 20px; z-index: 9999; max-width: 400px;"
|
||||
id="${alertId}">
|
||||
<i class="fas ${iconClass} me-2"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close" onclick="this.parentElement.remove()" aria-label="Close"></button>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Add alert to body
|
||||
document.body.insertAdjacentHTML('beforeend', alertHTML)
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
const alertElement = document.getElementById(alertId)
|
||||
if (alertElement) {
|
||||
alertElement.remove()
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
92
assets/controllers/bulk_job_manage_controller.js
Normal file
92
assets/controllers/bulk_job_manage_controller.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import { generateCsrfHeaders } from "./csrf_protection_controller"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
deleteUrl: String,
|
||||
stopUrl: String,
|
||||
deleteConfirmMessage: String,
|
||||
stopConfirmMessage: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Controller initialized
|
||||
}
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
|
||||
// Add CSRF headers if available
|
||||
const form = document.querySelector('form')
|
||||
if (form) {
|
||||
const csrfHeaders = generateCsrfHeaders(form)
|
||||
Object.assign(headers, csrfHeaders)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
async deleteJob(event) {
|
||||
const jobId = event.currentTarget.dataset.jobId
|
||||
const confirmMessage = this.deleteConfirmMessageValue || 'Are you sure you want to delete this job?'
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
try {
|
||||
const deleteUrl = this.deleteUrlValue.replace('__JOB_ID__', jobId)
|
||||
|
||||
const response = await fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
location.reload()
|
||||
} else {
|
||||
alert('Error deleting job: ' + (data.error || 'Unknown error'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting job:', error)
|
||||
alert('Error deleting job: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async stopJob(event) {
|
||||
const jobId = event.currentTarget.dataset.jobId
|
||||
const confirmMessage = this.stopConfirmMessageValue || 'Are you sure you want to stop this job?'
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
try {
|
||||
const stopUrl = this.stopUrlValue.replace('__JOB_ID__', jobId)
|
||||
|
||||
const response = await fetch(stopUrl, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
location.reload()
|
||||
} else {
|
||||
alert('Error stopping job: ' + (data.error || 'Unknown error'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping job:', error)
|
||||
alert('Error stopping job: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,5 +88,8 @@ export default class extends Controller {
|
|||
} else {
|
||||
this.hideSidebar();
|
||||
}
|
||||
|
||||
//Hide the tootip on the button
|
||||
this._toggle_button.blur();
|
||||
}
|
||||
}
|
||||
|
|
@ -20,18 +20,26 @@
|
|||
'use strict';
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { marked } from "marked";
|
||||
import { Marked } from "marked";
|
||||
import { mangle } from "marked-mangle";
|
||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import "../../css/app/markdown.css";
|
||||
|
||||
export default class extends Controller {
|
||||
export default class MarkdownController extends Controller {
|
||||
|
||||
static _marked = new Marked([
|
||||
{
|
||||
gfm: true,
|
||||
},
|
||||
gfmHeadingId(),
|
||||
mangle(),
|
||||
])
|
||||
;
|
||||
|
||||
connect()
|
||||
{
|
||||
this.configureMarked();
|
||||
this.render();
|
||||
|
||||
//Dispatch an event that we are now finished
|
||||
|
|
@ -45,9 +53,12 @@ export default class extends Controller {
|
|||
let raw = this.element.dataset['markdown'];
|
||||
|
||||
//Apply purified parsed markdown
|
||||
this.element.innerHTML = DOMPurify.sanitize(marked(this.unescapeHTML(raw)));
|
||||
this.element.innerHTML = DOMPurify.sanitize(MarkdownController._marked.parse(this.unescapeHTML(raw)));
|
||||
|
||||
for(let a of this.element.querySelectorAll('a')) {
|
||||
// test if link is absolute
|
||||
var r = new RegExp('^(?:[a-z+]+:)?//', 'i');
|
||||
if (r.test(a.getAttribute('href'))) {
|
||||
//Mark all links as external
|
||||
a.classList.add('link-external');
|
||||
//Open links in new tag
|
||||
|
|
@ -55,6 +66,7 @@ export default class extends Controller {
|
|||
//Dont track
|
||||
a.setAttribute('rel', 'noopener');
|
||||
}
|
||||
}
|
||||
|
||||
//Apply bootstrap styles to tables
|
||||
for(let table of this.element.querySelectorAll('table')) {
|
||||
|
|
@ -81,8 +93,17 @@ export default class extends Controller {
|
|||
/**
|
||||
* Configure the marked parser
|
||||
*/
|
||||
configureMarked()
|
||||
/*static newMarked()
|
||||
{
|
||||
const marked = new Marked([
|
||||
{
|
||||
gfm: true,
|
||||
},
|
||||
gfmHeadingId(),
|
||||
mangle(),
|
||||
])
|
||||
;
|
||||
|
||||
marked.use(mangle());
|
||||
marked.use(gfmHeadingId({
|
||||
}));
|
||||
|
|
@ -90,5 +111,5 @@ export default class extends Controller {
|
|||
marked.setOptions({
|
||||
gfm: true,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
}
|
||||
79
assets/controllers/csrf_protection_controller.js
Normal file
79
assets/controllers/csrf_protection_controller.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
||||
Object.keys(h).map(function (k) {
|
||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
||||
});
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
||||
});
|
||||
|
||||
export function generateCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
let csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCsrfHeaders (formElement) {
|
||||
const headers = {};
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function removeCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
||||
|
|
@ -23,11 +23,22 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
persistent: false,
|
||||
create: true,
|
||||
|
|
@ -36,6 +47,7 @@ export default class extends Controller {
|
|||
selectOnTab: true,
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
dropdownParent: dropdownParent,
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
return '<span>' + escape(data.label) + '</span>';
|
||||
|
|
@ -46,6 +58,12 @@ export default class extends Controller {
|
|||
}
|
||||
return '<div>' + escape(data.label) + '</div>';
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
"restore_on_backspace": {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,32 @@ import { default as FullEditor } from "../../ckeditor/markdown_full";
|
|||
import { default as SingleLineEditor} from "../../ckeditor/markdown_single_line";
|
||||
import { default as HTMLLabelEditor } from "../../ckeditor/html_label";
|
||||
|
||||
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
|
||||
import {EditorWatchdog} from 'ckeditor5';
|
||||
|
||||
import "ckeditor5/ckeditor5.css";;
|
||||
import "../../css/components/ckeditor.css";
|
||||
|
||||
const translationContext = require.context(
|
||||
'ckeditor5/translations',
|
||||
false,
|
||||
//Only load the translation files we will really need
|
||||
/(de|it|fr|ru|ja|cs|da|zh|pl|hu)\.js$/
|
||||
);
|
||||
|
||||
function loadTranslation(language) {
|
||||
if (!language || language === 'en') {
|
||||
return null;
|
||||
}
|
||||
const lang = language.slice(0, 2);
|
||||
const path = `./${lang}.js`;
|
||||
if (translationContext.keys().includes(path)) {
|
||||
const module = translationContext(path);
|
||||
return module.default;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
|
|
@ -51,8 +73,22 @@ export default class extends Controller {
|
|||
|
||||
const language = document.body.dataset.locale ?? "en";
|
||||
|
||||
const emojiURL = new URL('../../ckeditor/emojis.json', import.meta.url).href;
|
||||
|
||||
const config = {
|
||||
language: language,
|
||||
licenseKey: "GPL",
|
||||
|
||||
emoji: {
|
||||
definitionsUrl: emojiURL
|
||||
}
|
||||
}
|
||||
|
||||
//Load translations if not english
|
||||
let translations = loadTranslation(language);
|
||||
if (translations) {
|
||||
//Keep existing translations (e.g. from other plugins), if any
|
||||
config.translations = [window.CKEDITOR_TRANSLATIONS, translations];
|
||||
}
|
||||
|
||||
const watchdog = new EditorWatchdog();
|
||||
|
|
@ -70,7 +106,18 @@ export default class extends Controller {
|
|||
editor_div.classList.add(...new_classes.split(","));
|
||||
}
|
||||
|
||||
console.log(editor);
|
||||
// Automatic synchronization of source input
|
||||
editor.model.document.on("change:data", () => {
|
||||
editor.updateSourceElement();
|
||||
|
||||
// Dispatch the input event for further treatment
|
||||
const event = new Event("input");
|
||||
this.element.dispatchEvent(event);
|
||||
});
|
||||
|
||||
//This return is important! Otherwise we get mysterious errors in the console
|
||||
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
|
||||
return editor;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -75,13 +75,49 @@ export default class extends Controller {
|
|||
|
||||
//Insert new html after the last child element
|
||||
//If the table has a tbody, insert it there
|
||||
//Afterwards return the newly created row
|
||||
if(targetTable.tBodies[0]) {
|
||||
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
||||
return targetTable.tBodies[0].lastElementChild;
|
||||
} else { //Otherwise just insert it
|
||||
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
||||
return targetTable.lastElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action opens a file dialog to select multiple files and then creates a new element for each file, where
|
||||
* the file is assigned to the input field.
|
||||
* This should only be used for attachments collection types
|
||||
* @param event
|
||||
*/
|
||||
uploadMultipleFiles(event) {
|
||||
//Open a file dialog to select multiple files
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
|
||||
input.addEventListener('change', (event) => {
|
||||
//Create a element for each file
|
||||
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
const file = input.files[i];
|
||||
|
||||
const newElement = this.createElement(event);
|
||||
const rowInput = newElement.querySelector("input[type='file']");
|
||||
|
||||
//We can not directly assign the file to the input, so we have to create a new DataTransfer object
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
rowInput.files = dataTransfer.files;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to createEvent Pricedetails need some special handling to fill min amount
|
||||
* @param event
|
||||
|
|
|
|||
|
|
@ -24,18 +24,25 @@ import 'datatables.net-bs5/css/dataTables.bootstrap5.css'
|
|||
import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css'
|
||||
import 'datatables.net-fixedheader-bs5/css/fixedHeader.bootstrap5.css'
|
||||
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.css';
|
||||
import 'datatables.net-select-bs5/css/select.bootstrap5.css';
|
||||
|
||||
//Use our own styles for the select extension which fit the bootstrap theme better
|
||||
//import 'datatables.net-select-bs5/css/select.bootstrap5.css';
|
||||
import '../../../css/components/datatables_select_bs5.css';
|
||||
|
||||
//JS
|
||||
import 'datatables.net-bs5';
|
||||
import 'datatables.net-buttons-bs5';
|
||||
import 'datatables.net-buttons/js/buttons.colVis.js';
|
||||
import 'datatables.net-fixedheader-bs5';
|
||||
import 'datatables.net-select-bs5';
|
||||
import 'datatables.net-colreorder-bs5';
|
||||
import 'datatables.net-responsive-bs5';
|
||||
import '../../../js/lib/datatables';
|
||||
|
||||
//import 'datatables.net-select-bs5';
|
||||
//Use the local version containing the fix for the select extension
|
||||
import '../../../js/lib/dataTables.select.mjs';
|
||||
|
||||
|
||||
const EVENT_DT_LOADED = 'dt:loaded';
|
||||
|
||||
export default class extends Controller {
|
||||
|
|
@ -132,7 +139,7 @@ export default class extends Controller {
|
|||
if(this.isSelectable()) {
|
||||
options.select = {
|
||||
style: 'multi+shift',
|
||||
selector: 'td.select-checkbox'
|
||||
selector: 'td.dt-select',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +152,12 @@ export default class extends Controller {
|
|||
|
||||
//Fix height of the length selector
|
||||
promise.then((dt) => {
|
||||
|
||||
//Draw the rows to make sure the correct status text is displayed ("No matching records found" instead of "Loading...")
|
||||
if (dt.data().length === 0) {
|
||||
dt.rows().draw()
|
||||
}
|
||||
|
||||
//Find all length selectors (select with name dt_length), which are inside a label
|
||||
const lengthSelectors = document.querySelectorAll('label select[name="dt_length"]');
|
||||
//And remove the surrounding label, while keeping the select with all event handlers
|
||||
|
|
@ -180,27 +193,6 @@ export default class extends Controller {
|
|||
dt.fixedHeader.headerOffset($("#navbar").outerHeight());
|
||||
});
|
||||
|
||||
//Register event handler to selectAllRows checkbox if available
|
||||
if (this.isSelectable()) {
|
||||
promise.then((dt) => {
|
||||
const selectAllCheckbox = this.element.querySelector('thead th.select-checkbox');
|
||||
selectAllCheckbox.addEventListener('click', () => {
|
||||
if(selectAllCheckbox.parentElement.classList.contains('selected')) {
|
||||
dt.rows().deselect();
|
||||
selectAllCheckbox.parentElement.classList.remove('selected');
|
||||
} else {
|
||||
dt.rows().select();
|
||||
selectAllCheckbox.parentElement.classList.add('selected');
|
||||
}
|
||||
});
|
||||
|
||||
//When any row is deselected, also deselect the selectAll checkbox
|
||||
dt.on('deselect.dt', () => {
|
||||
selectAllCheckbox.parentElement.classList.remove('selected');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//Allow to further configure the datatable
|
||||
promise.then(this._afterLoaded.bind(this));
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ export default class extends DatatablesController {
|
|||
//Hide/Unhide panel with the selection tools
|
||||
if (count > 0) {
|
||||
selectPanel.classList.remove('d-none');
|
||||
selectPanel.classList.add('sticky-select-bar');
|
||||
} else {
|
||||
selectPanel.classList.add('d-none');
|
||||
selectPanel.classList.remove('sticky-select-bar');
|
||||
}
|
||||
|
||||
//Update selection count text
|
||||
|
|
|
|||
250
assets/controllers/elements/ipn_suggestion_controller.js
Normal file
250
assets/controllers/elements/ipn_suggestion_controller.js
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import "../../css/components/autocomplete_bootstrap_theme.css";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input"];
|
||||
static values = {
|
||||
partId: Number,
|
||||
partCategoryId: Number,
|
||||
partDescription: String,
|
||||
suggestions: Object,
|
||||
commonSectionHeader: String, // Dynamic header for common Prefixes
|
||||
partIncrementHeader: String, // Dynamic header for new possible part increment
|
||||
suggestUrl: String,
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.configureAutocomplete();
|
||||
this.watchCategoryChanges();
|
||||
this.watchDescriptionChanges();
|
||||
}
|
||||
|
||||
templates = {
|
||||
commonSectionHeader({ title, html }) {
|
||||
return html`
|
||||
<section class="aa-Source">
|
||||
<div class="aa-SourceHeader">
|
||||
<span class="aa-SourceHeaderTitle">${title}</span>
|
||||
<div class="aa-SourceHeaderLine"></div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
},
|
||||
partIncrementHeader({ title, html }) {
|
||||
return html`
|
||||
<section class="aa-Source">
|
||||
<div class="aa-SourceHeader">
|
||||
<span class="aa-SourceHeaderTitle">${title}</span>
|
||||
<div class="aa-SourceHeaderLine"></div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
},
|
||||
list({ html }) {
|
||||
return html`
|
||||
<ul class="aa-List" role="listbox"></ul>
|
||||
`;
|
||||
},
|
||||
item({ suggestion, description, html }) {
|
||||
return html`
|
||||
<li class="aa-Item" role="option" data-suggestion="${suggestion}" aria-selected="false">
|
||||
<div class="aa-ItemWrapper">
|
||||
<div class="aa-ItemContent">
|
||||
<div class="aa-ItemIcon aa-ItemIcon--noBorder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="aa-ItemContentBody">
|
||||
<div class="aa-ItemContentTitle">${suggestion}</div>
|
||||
<div class="aa-ItemContentDescription">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
configureAutocomplete() {
|
||||
const inputField = this.inputTarget;
|
||||
const commonPrefixes = this.suggestionsValue.commonPrefixes || [];
|
||||
const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || [];
|
||||
const commonHeader = this.commonSectionHeaderValue;
|
||||
const partIncrementHeader = this.partIncrementHeaderValue;
|
||||
|
||||
if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return;
|
||||
|
||||
// Check whether the panel should be created at the update
|
||||
if (this.isPanelInitialized) {
|
||||
const existingPanel = inputField.parentNode.querySelector(".aa-Panel");
|
||||
if (existingPanel) {
|
||||
// Only remove the panel in the update phase
|
||||
|
||||
existingPanel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Create panel
|
||||
const panel = document.createElement("div");
|
||||
panel.classList.add("aa-Panel");
|
||||
panel.style.display = "none";
|
||||
|
||||
// Create panel layout
|
||||
const panelLayout = document.createElement("div");
|
||||
panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable");
|
||||
|
||||
// Section for prefixes part increment
|
||||
if (prefixesPartIncrement.length) {
|
||||
const partIncrementSection = document.createElement("section");
|
||||
partIncrementSection.classList.add("aa-Source");
|
||||
|
||||
const partIncrementHeaderHtml = this.templates.partIncrementHeader({
|
||||
title: partIncrementHeader,
|
||||
html: String.raw,
|
||||
});
|
||||
partIncrementSection.innerHTML += partIncrementHeaderHtml;
|
||||
|
||||
const partIncrementList = document.createElement("ul");
|
||||
partIncrementList.classList.add("aa-List");
|
||||
partIncrementList.setAttribute("role", "listbox");
|
||||
|
||||
prefixesPartIncrement.forEach((prefix) => {
|
||||
const itemHTML = this.templates.item({
|
||||
suggestion: prefix.title,
|
||||
description: prefix.description,
|
||||
html: String.raw,
|
||||
});
|
||||
partIncrementList.innerHTML += itemHTML;
|
||||
});
|
||||
|
||||
partIncrementSection.appendChild(partIncrementList);
|
||||
panelLayout.appendChild(partIncrementSection);
|
||||
}
|
||||
|
||||
// Section for common prefixes
|
||||
if (commonPrefixes.length) {
|
||||
const commonSection = document.createElement("section");
|
||||
commonSection.classList.add("aa-Source");
|
||||
|
||||
const commonSectionHeader = this.templates.commonSectionHeader({
|
||||
title: commonHeader,
|
||||
html: String.raw,
|
||||
});
|
||||
commonSection.innerHTML += commonSectionHeader;
|
||||
|
||||
const commonList = document.createElement("ul");
|
||||
commonList.classList.add("aa-List");
|
||||
commonList.setAttribute("role", "listbox");
|
||||
|
||||
commonPrefixes.forEach((prefix) => {
|
||||
const itemHTML = this.templates.item({
|
||||
suggestion: prefix.title,
|
||||
description: prefix.description,
|
||||
html: String.raw,
|
||||
});
|
||||
commonList.innerHTML += itemHTML;
|
||||
});
|
||||
|
||||
commonSection.appendChild(commonList);
|
||||
panelLayout.appendChild(commonSection);
|
||||
}
|
||||
|
||||
panel.appendChild(panelLayout);
|
||||
inputField.parentNode.appendChild(panel);
|
||||
|
||||
inputField.addEventListener("focus", () => {
|
||||
panel.style.display = "block";
|
||||
});
|
||||
|
||||
inputField.addEventListener("blur", () => {
|
||||
setTimeout(() => {
|
||||
panel.style.display = "none";
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Selection of an item
|
||||
panelLayout.addEventListener("mousedown", (event) => {
|
||||
const target = event.target.closest("li");
|
||||
|
||||
if (target) {
|
||||
inputField.value = target.dataset.suggestion;
|
||||
panel.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
this.isPanelInitialized = true;
|
||||
};
|
||||
|
||||
watchCategoryChanges() {
|
||||
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
|
||||
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
|
||||
this.previousCategoryId = Number(this.partCategoryIdValue);
|
||||
|
||||
if (categoryField) {
|
||||
categoryField.addEventListener("change", () => {
|
||||
const categoryId = Number(categoryField.value);
|
||||
const description = String(descriptionField?.value ?? '');
|
||||
|
||||
// Check whether the category has changed compared to the previous ID
|
||||
if (categoryId !== this.previousCategoryId) {
|
||||
this.fetchNewSuggestions(categoryId, description);
|
||||
this.previousCategoryId = categoryId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watchDescriptionChanges() {
|
||||
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
|
||||
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
|
||||
this.previousDescription = String(this.partDescriptionValue);
|
||||
|
||||
if (descriptionField) {
|
||||
descriptionField.addEventListener("input", () => {
|
||||
const categoryId = Number(categoryField.value);
|
||||
const description = String(descriptionField?.value ?? '');
|
||||
|
||||
// Check whether the description has changed compared to the previous one
|
||||
if (description !== this.previousDescription) {
|
||||
this.fetchNewSuggestions(categoryId, description);
|
||||
this.previousDescription = description;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchNewSuggestions(categoryId, description) {
|
||||
const baseUrl = this.suggestUrlValue;
|
||||
const partId = this.partIdValue;
|
||||
const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description;
|
||||
const encodedDescription = this.base64EncodeUtf8(truncatedDescription);
|
||||
const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : '');
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.suggestionsValue = data;
|
||||
this.configureAutocomplete();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Errors when loading the new IPN-suggestions:", error);
|
||||
});
|
||||
};
|
||||
|
||||
base64EncodeUtf8(text) {
|
||||
const utf8Bytes = new TextEncoder().encode(text);
|
||||
return btoa(String.fromCharCode(...utf8Bytes));
|
||||
};
|
||||
}
|
||||
200
assets/controllers/elements/part_search_controller.js
Normal file
200
assets/controllers/elements/part_search_controller.js
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
import { autocomplete } from '@algolia/autocomplete-js';
|
||||
//import "@algolia/autocomplete-theme-classic/dist/theme.css";
|
||||
import "../../css/components/autocomplete_bootstrap_theme.css";
|
||||
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
|
||||
import {marked} from "marked";
|
||||
|
||||
import {
|
||||
trans,
|
||||
SEARCH_PLACEHOLDER,
|
||||
SEARCH_SUBMIT,
|
||||
STATISTICS_PARTS
|
||||
} from '../../translator';
|
||||
|
||||
|
||||
/**
|
||||
* This controller is responsible for the search fields in the navbar and the homepage.
|
||||
* It uses the Algolia Autocomplete library to provide a fast and responsive search.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = ["input"];
|
||||
|
||||
_autocomplete;
|
||||
|
||||
// Highlight the search query in the results
|
||||
_highlight = (text, query) => {
|
||||
if (!text) return text;
|
||||
if (!query) return text;
|
||||
|
||||
const HIGHLIGHT_PRE_TAG = '__aa-highlight__'
|
||||
const HIGHLIGHT_POST_TAG = '__/aa-highlight__'
|
||||
|
||||
const escaped = query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const regex = new RegExp(escaped, 'gi');
|
||||
|
||||
return text.replace(regex, (match) => `${HIGHLIGHT_PRE_TAG}${match}${HIGHLIGHT_POST_TAG}`);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// The endpoint for searching parts
|
||||
const base_url = this.element.dataset.autocomplete;
|
||||
// The URL template for the part detail pages
|
||||
const part_detail_uri_template = this.element.dataset.detailUrl;
|
||||
|
||||
//The URL of the placeholder picture
|
||||
const placeholder_image = this.element.dataset.placeholderImage;
|
||||
|
||||
//If the element is in navbar mode, or not
|
||||
const navbar_mode = this.element.dataset.navbarMode === "true";
|
||||
|
||||
const that = this;
|
||||
|
||||
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
|
||||
key: 'RECENT_SEARCH',
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
this._autocomplete = autocomplete({
|
||||
container: this.element,
|
||||
//Place the panel in the navbar, if the element is in navbar mode
|
||||
panelContainer: navbar_mode ? document.getElementById("navbar-search-form") : document.body,
|
||||
panelPlacement: this.element.dataset.panelPlacement,
|
||||
plugins: [recentSearchesPlugin],
|
||||
openOnFocus: true,
|
||||
placeholder: trans(SEARCH_PLACEHOLDER),
|
||||
translations: {
|
||||
submitButtonTitle: trans(SEARCH_SUBMIT)
|
||||
},
|
||||
|
||||
// Use a navigator compatible with turbo:
|
||||
navigator: {
|
||||
navigate({ itemUrl }) {
|
||||
window.Turbo.visit(itemUrl, { action: "advance" });
|
||||
},
|
||||
navigateNewTab({ itemUrl }) {
|
||||
const windowReference = window.open(itemUrl, '_blank', 'noopener');
|
||||
|
||||
if (windowReference) {
|
||||
windowReference.focus();
|
||||
}
|
||||
},
|
||||
navigateNewWindow({ itemUrl }) {
|
||||
window.open(itemUrl, '_blank', 'noopener');
|
||||
},
|
||||
},
|
||||
|
||||
// If the form is submitted, forward the term to the form
|
||||
onSubmit({state, event, ...setters}) {
|
||||
//Put the current text into each target input field
|
||||
const input = that.inputTarget;
|
||||
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Do not submit the form, if the input is empty
|
||||
if (state.query === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = state.query;
|
||||
input.form.requestSubmit();
|
||||
},
|
||||
|
||||
|
||||
getSources({ query }) {
|
||||
return [
|
||||
// The parts source
|
||||
{
|
||||
sourceId: 'parts',
|
||||
getItems() {
|
||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
||||
|
||||
const data = fetch(url)
|
||||
.then((response) => response.json())
|
||||
;
|
||||
|
||||
//Iterate over all fields besides the id and highlight them
|
||||
const fields = ["name", "description", "category", "footprint"];
|
||||
|
||||
data.then((items) => {
|
||||
items.forEach((item) => {
|
||||
for (const field of fields) {
|
||||
item[field] = that._highlight(item[field], query);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
getItemUrl({ item }) {
|
||||
return part_detail_uri_template.replace('__ID__', item.id);
|
||||
},
|
||||
templates: {
|
||||
header({ html }) {
|
||||
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PARTS)}</span>
|
||||
<div class="aa-SourceHeaderLine" />`;
|
||||
},
|
||||
item({item, components, html}) {
|
||||
const details_url = part_detail_uri_template.replace('__ID__', item.id);
|
||||
|
||||
return html`
|
||||
<a class="aa-ItemLink" href="${details_url}">
|
||||
<div class="aa-ItemContent">
|
||||
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
||||
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
||||
</div>
|
||||
<div class="aa-ItemContentBody">
|
||||
<div class="aa-ItemContentTitle">
|
||||
<b>
|
||||
${components.Highlight({hit: item, attribute: 'name'})}
|
||||
</b>
|
||||
</div>
|
||||
<div class="aa-ItemContentDescription">
|
||||
${components.Highlight({hit: item, attribute: 'description'})}
|
||||
${item.category ? html`<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span>${components.Highlight({hit: item, attribute: 'category'})}</p>` : ""}
|
||||
${item.footprint ? html`<p class="m-0"><span class="fa-solid fa-microchip fa-fw"></span>${components.Highlight({hit: item, attribute: 'footprint'})}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
|
||||
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
|
||||
const inputs = this.element.getElementsByClassName('aa-Input');
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('blur', () => {
|
||||
this._autocomplete.setIsOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,19 @@ export default class extends Controller {
|
|||
|
||||
connect() {
|
||||
|
||||
//Check if tomselect is inside an modal and do not attach the dropdown to body in that case (as it breaks the modal)
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
allowEmptyOption: true,
|
||||
plugins: ['dropdown_input'],
|
||||
searchField: ["name", "description", "category", "footprint"],
|
||||
valueField: "id",
|
||||
labelField: "name",
|
||||
dropdownParent: dropdownParent,
|
||||
preload: "focus",
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
|
|
|
|||
|
|
@ -38,11 +38,17 @@ export default class extends Controller {
|
|||
this._emptyMessage = this.element.getAttribute('title');
|
||||
}
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
plugins: ["clear_button"],
|
||||
allowEmptyOption: true,
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
|
|
@ -50,7 +56,24 @@ export default class extends Controller {
|
|||
}
|
||||
};
|
||||
|
||||
//Load the drag_drop plugin if the select is ordered
|
||||
if (this.element.dataset.orderedValue) {
|
||||
settings.plugins.push('drag_drop');
|
||||
settings.plugins.push("caret_position");
|
||||
}
|
||||
|
||||
//If multiple items can be selected, enable the remove_button plugin
|
||||
if (this.element.multiple) {
|
||||
settings.plugins.push('remove_button');
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
|
||||
//If the select is ordered, we need to update the value field (with the decoded value from the orderedValue field)
|
||||
if (this.element.dataset.orderedValue) {
|
||||
const data = JSON.parse(this.element.dataset.orderedValue);
|
||||
this._tomSelect.setValue(data);
|
||||
}
|
||||
}
|
||||
|
||||
getTomSelect() {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,21 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
// TODO: Merge with select_controller.js
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, {
|
||||
maxItems: 1000,
|
||||
allowEmptyOption: true,
|
||||
dropdownParent: dropdownParent,
|
||||
plugins: ['remove_button'],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
/**
|
||||
* This is the frontend controller for StaticFileAutocompleteType form element.
|
||||
* Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete.
|
||||
* The file is just a list of strings, one per line, which will be used as the autocomplete options.
|
||||
* Lines starting with # will be ignored.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
persistent: false,
|
||||
create: true,
|
||||
maxItems: 1,
|
||||
maxOptions: 100,
|
||||
createOnBlur: true,
|
||||
selectOnTab: true,
|
||||
valueField: 'text',
|
||||
searchField: 'text',
|
||||
orderField: 'text',
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
plugins: {
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.element.dataset.url) {
|
||||
const url = this.element.dataset.url;
|
||||
settings.load = (query, callback) => {
|
||||
const self = this;
|
||||
if (self.loading > 1) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
// Convert the text file to array
|
||||
let lines = text.split("\n");
|
||||
//Remove all lines beginning with #
|
||||
lines = lines.filter(x => !x.startsWith("#"));
|
||||
|
||||
//Convert the array to an object, where each line is in the text field
|
||||
lines = lines.map(x => {
|
||||
return {text: x};
|
||||
});
|
||||
|
||||
|
||||
//Unset the load function to prevent endless recursion
|
||||
self._tomSelect.settings.load = null;
|
||||
|
||||
callback(lines);
|
||||
}).catch(() => {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
super.disconnect();
|
||||
//Destroy the TomSelect instance
|
||||
this._tomSelect.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ import {Controller} from "@hotwired/stimulus";
|
|||
|
||||
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
|
||||
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
|
@ -38,11 +40,24 @@ export default class extends Controller {
|
|||
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
||||
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
|
||||
let settings = {
|
||||
allowEmptyOption: true,
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
create: allowAdd ? this.createItem.bind(this) : false,
|
||||
createFilter: this.createFilter.bind(this),
|
||||
|
||||
// This three options allow us to paste element names with commas: (see issue #538)
|
||||
maxItems: 1,
|
||||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||
splitOn: null,
|
||||
dropdownParent: dropdownParent,
|
||||
|
||||
searchField: [
|
||||
{field: "text", weight : 2},
|
||||
|
|
@ -53,7 +68,21 @@ export default class extends Controller {
|
|||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
option: this.renderOption.bind(this),
|
||||
option_create: function(data, escape) {
|
||||
option_create: (data, escape) => {
|
||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
||||
//This here handles the display part, while the createItem function handles the actual creation
|
||||
if (data.input.startsWith("->")) {
|
||||
//Get current selected value
|
||||
const current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
||||
//Prepend it to the input
|
||||
if (current) {
|
||||
data.input = current + " " + data.input;
|
||||
} else {
|
||||
//If there is no current value, we remove the "->"
|
||||
data.input = data.input.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i> <strong>' + escape(data.input) + '</strong>… ' +
|
||||
'<small class="text-muted float-end">(' + addHint +')</small>' +
|
||||
'</div>';
|
||||
|
|
@ -63,14 +92,39 @@ export default class extends Controller {
|
|||
//Add callbacks to update validity
|
||||
onInitialize: this.updateValidity.bind(this),
|
||||
onChange: this.updateValidity.bind(this),
|
||||
|
||||
plugins: {
|
||||
"autoselect_typed": {},
|
||||
}
|
||||
};
|
||||
|
||||
//Add clear button plugin, if an empty option is present
|
||||
if (this.element.querySelector("option[value='']") !== null) {
|
||||
settings.plugins["clear_button"] = {};
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
//Do not do a sync here as this breaks the initial rendering of the empty option
|
||||
//this._tomSelect.sync();
|
||||
}
|
||||
|
||||
createItem(input, callback) {
|
||||
|
||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
||||
if (input.startsWith("->")) {
|
||||
//Get current selected value
|
||||
let current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
||||
//Replace no break spaces with normal spaces
|
||||
current = current.replaceAll("\u00A0", " ");
|
||||
//Prepend it to the input
|
||||
if (current) {
|
||||
input = current + " " + input;
|
||||
} else {
|
||||
//If there is no current value, we remove the "->"
|
||||
input = input.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
callback({
|
||||
//$%$ is a special value prefix, that is used to identify items, that are not yet in the DB
|
||||
value: '$%$' + input,
|
||||
|
|
@ -79,6 +133,31 @@ export default class extends Controller {
|
|||
});
|
||||
}
|
||||
|
||||
createFilter(input) {
|
||||
|
||||
//Normalize the input (replace spacing around arrows)
|
||||
if (input.includes("->")) {
|
||||
const inputs = input.split("->");
|
||||
inputs.forEach((value, index) => {
|
||||
inputs[index] = value.trim();
|
||||
});
|
||||
input = inputs.join("->");
|
||||
} else {
|
||||
input = input.trim();
|
||||
}
|
||||
|
||||
const options = this._tomSelect.options;
|
||||
//Iterate over all options and check if the input is already present
|
||||
for (let index in options) {
|
||||
const option = options[index];
|
||||
if (option.path === input) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
updateValidity() {
|
||||
//Mark this input as invalid, if the selected option is disabled
|
||||
|
|
|
|||
|
|
@ -23,19 +23,32 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
let settings = {
|
||||
plugins: {
|
||||
remove_button:{
|
||||
}
|
||||
remove_button:{},
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
},
|
||||
persistent: false,
|
||||
selectOnTab: true,
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
dropdownParent: dropdownParent,
|
||||
};
|
||||
|
||||
if(this.element.dataset.autocomplete) {
|
||||
|
|
|
|||
|
|
@ -94,13 +94,15 @@ export default class extends Controller {
|
|||
showTags: this._showTags,
|
||||
data: data,
|
||||
showIcon: true,
|
||||
preventUnselect: true,
|
||||
allowReselect: true,
|
||||
onNodeSelected: (event) => {
|
||||
const node = event.detail.node;
|
||||
if (node.href) {
|
||||
window.Turbo.visit(node.href, {action: "advance"});
|
||||
this._registerURLWatcher(node);
|
||||
}
|
||||
},
|
||||
//onNodeContextmenu: contextmenu_handler,
|
||||
}, [BS5Theme, BS53Theme, FAIconTheme]);
|
||||
|
||||
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
||||
|
|
@ -108,12 +110,42 @@ export default class extends Controller {
|
|||
const treeView = event.detail.treeView;
|
||||
treeView.revealNode(treeView.getSelected());
|
||||
|
||||
//Add the url watcher to all selected nodes
|
||||
for (const node of treeView.getSelected()) {
|
||||
this._registerURLWatcher(node);
|
||||
}
|
||||
|
||||
//Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click
|
||||
treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
_registerURLWatcher(node)
|
||||
{
|
||||
//Register a watcher for a location change, which will unselect the node, if the location changes
|
||||
const desired_url = node.href;
|
||||
|
||||
//Ensure that the node is unselected, if the location changes
|
||||
const unselectNode = () => {
|
||||
//Parse url so we can properly compare them
|
||||
const desired = new URL(node.href, window.location.origin);
|
||||
|
||||
//We only compare the pathname, because the hash and parameters should not matter
|
||||
if(window.location.pathname !== desired.pathname) {
|
||||
//The ignore parameter is important here, otherwise the node will not be unselected
|
||||
node.setSelected(false, {silent: true, ignorePreventUnselect: true});
|
||||
|
||||
//Unregister the watcher
|
||||
document.removeEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
};
|
||||
|
||||
//Register the watcher via hotwire turbo
|
||||
//We must just load to have the new url in window.location
|
||||
document.addEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
|
||||
_onContextMenu(event)
|
||||
{
|
||||
//Find the node that was clicked and open link in new tab
|
||||
|
|
|
|||
136
assets/controllers/field_mapping_controller.js
Normal file
136
assets/controllers/field_mapping_controller.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["tbody", "addButton", "submitButton"]
|
||||
static values = {
|
||||
mappingIndex: Number,
|
||||
maxMappings: Number,
|
||||
prototype: String,
|
||||
maxMappingsReachedMessage: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.updateAddButtonState()
|
||||
this.updateFieldOptions()
|
||||
this.attachEventListeners()
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Add event listeners to existing field selects
|
||||
const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
|
||||
fieldSelects.forEach(select => {
|
||||
select.addEventListener('change', this.updateFieldOptions.bind(this))
|
||||
})
|
||||
|
||||
// Note: Add button click is handled by Stimulus action in template (data-action="click->field-mapping#addMapping")
|
||||
// No manual event listener needed
|
||||
|
||||
// Form submit handler
|
||||
const form = this.element.querySelector('form')
|
||||
if (form && this.hasSubmitButtonTarget) {
|
||||
form.addEventListener('submit', this.handleFormSubmit.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
addMapping() {
|
||||
const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
|
||||
|
||||
if (currentMappings >= this.maxMappingsValue) {
|
||||
alert(this.maxMappingsReachedMessageValue)
|
||||
return
|
||||
}
|
||||
|
||||
const newRowHtml = this.prototypeValue.replace(/__name__/g, this.mappingIndexValue)
|
||||
const tempDiv = document.createElement('div')
|
||||
tempDiv.innerHTML = newRowHtml
|
||||
|
||||
const fieldWidget = tempDiv.querySelector('select[name*="[field]"]') || tempDiv.children[0]
|
||||
const providerWidget = tempDiv.querySelector('select[name*="[providers]"]') || tempDiv.children[1]
|
||||
const priorityWidget = tempDiv.querySelector('input[name*="[priority]"]') || tempDiv.children[2]
|
||||
|
||||
const newRow = document.createElement('tr')
|
||||
newRow.className = 'mapping-row'
|
||||
newRow.innerHTML = `
|
||||
<td>${fieldWidget ? fieldWidget.outerHTML : ''}</td>
|
||||
<td>${providerWidget ? providerWidget.outerHTML : ''}</td>
|
||||
<td>${priorityWidget ? priorityWidget.outerHTML : ''}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-action="click->field-mapping#removeMapping">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
|
||||
this.tbodyTarget.appendChild(newRow)
|
||||
this.mappingIndexValue++
|
||||
|
||||
const newFieldSelect = newRow.querySelector('select[name*="[field]"]')
|
||||
if (newFieldSelect) {
|
||||
newFieldSelect.value = ''
|
||||
newFieldSelect.addEventListener('change', this.updateFieldOptions.bind(this))
|
||||
}
|
||||
|
||||
this.updateFieldOptions()
|
||||
this.updateAddButtonState()
|
||||
}
|
||||
|
||||
removeMapping(event) {
|
||||
const row = event.target.closest('tr')
|
||||
row.remove()
|
||||
this.updateFieldOptions()
|
||||
this.updateAddButtonState()
|
||||
}
|
||||
|
||||
updateFieldOptions() {
|
||||
const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
|
||||
|
||||
const selectedFields = Array.from(fieldSelects)
|
||||
.map(select => select.value)
|
||||
.filter(value => value && value !== '')
|
||||
|
||||
fieldSelects.forEach(select => {
|
||||
Array.from(select.options).forEach(option => {
|
||||
const isCurrentValue = option.value === select.value
|
||||
const isEmptyOption = !option.value || option.value === ''
|
||||
const isAlreadySelected = selectedFields.includes(option.value)
|
||||
|
||||
if (!isEmptyOption && isAlreadySelected && !isCurrentValue) {
|
||||
option.disabled = true
|
||||
option.style.display = 'none'
|
||||
} else {
|
||||
option.disabled = false
|
||||
option.style.display = ''
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateAddButtonState() {
|
||||
const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
|
||||
|
||||
if (this.hasAddButtonTarget) {
|
||||
if (currentMappings >= this.maxMappingsValue) {
|
||||
this.addButtonTarget.disabled = true
|
||||
this.addButtonTarget.title = this.maxMappingsReachedMessageValue
|
||||
} else {
|
||||
this.addButtonTarget.disabled = false
|
||||
this.addButtonTarget.title = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit(event) {
|
||||
if (this.hasSubmitButtonTarget) {
|
||||
this.submitButtonTarget.disabled = true
|
||||
|
||||
// Disable the entire form to prevent changes during processing
|
||||
const form = event.target
|
||||
const formElements = form.querySelectorAll('input, select, textarea, button')
|
||||
formElements.forEach(element => {
|
||||
if (element !== this.submitButtonTarget) {
|
||||
element.disabled = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
//import * as ZXing from "@zxing/library";
|
||||
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode";
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
|
@ -50,7 +50,7 @@ export default class extends Controller {
|
|||
});
|
||||
|
||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||
fps: 2,
|
||||
fps: 10,
|
||||
qrbox: qrboxFunction,
|
||||
experimentalFeatures: {
|
||||
//This option improves reading quality on android chrome
|
||||
|
|
@ -61,6 +61,11 @@ export default class extends Controller {
|
|||
this._scanner.render(this.onScanSuccess.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._scanner.pause();
|
||||
this._scanner.clear();
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
//Put our decoded Text into the input box
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
|
|
|
|||
|
|
@ -25,9 +25,23 @@ import "katex/dist/katex.css";
|
|||
export default class extends Controller {
|
||||
static targets = ["input", "preview"];
|
||||
|
||||
static values = {
|
||||
unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units
|
||||
}
|
||||
|
||||
updatePreview()
|
||||
{
|
||||
katex.render(this.inputTarget.value, this.previewTarget, {
|
||||
let value = "";
|
||||
if (this.unitValue) {
|
||||
//Escape percentage signs
|
||||
value = this.inputTarget.value.replace(/%/g, '\\%');
|
||||
|
||||
value = "\\mathrm{" + value + "}";
|
||||
} else {
|
||||
value = this.inputTarget.value;
|
||||
}
|
||||
|
||||
katex.render(value, this.previewTarget, {
|
||||
throwOnError: false,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ import TomSelect from "tom-select";
|
|||
import katex from "katex";
|
||||
import "katex/dist/katex.css";
|
||||
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller
|
||||
{
|
||||
|
|
@ -53,7 +60,10 @@ export default class extends Controller
|
|||
connect() {
|
||||
const settings = {
|
||||
plugins: {
|
||||
clear_button:{}
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
},
|
||||
persistent: false,
|
||||
maxItems: 1,
|
||||
|
|
@ -75,7 +85,9 @@ export default class extends Controller
|
|||
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
|
||||
}
|
||||
if (data.unit) {
|
||||
tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>'
|
||||
let unit = data.unit.replace(/%/g, '\\%');
|
||||
unit = "\\mathrm{" + unit + "}";
|
||||
tmp += '<span class="ms-2">' + katex.renderToString('[' + unit + ']') + '</span>'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
86
assets/controllers/toggle_password_controller.js
Normal file
86
assets/controllers/toggle_password_controller.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import '../css/components/toggle_password.css';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
visibleLabel: { type: String, default: 'Show' },
|
||||
visibleIcon: { type: String, default: 'Default' },
|
||||
hiddenLabel: { type: String, default: 'Hide' },
|
||||
hiddenIcon: { type: String, default: 'Default' },
|
||||
buttonClasses: Array,
|
||||
};
|
||||
|
||||
isDisplayed = false;
|
||||
visibleIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
|
||||
</svg>`;
|
||||
hiddenIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
|
||||
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
|
||||
</svg>`;
|
||||
|
||||
connect() {
|
||||
if (this.visibleIconValue !== 'Default') {
|
||||
this.visibleIcon = this.visibleIconValue;
|
||||
}
|
||||
|
||||
if (this.hiddenIconValue !== 'Default') {
|
||||
this.hiddenIcon = this.hiddenIconValue;
|
||||
}
|
||||
|
||||
const button = this.createButton();
|
||||
|
||||
this.element.insertAdjacentElement('afterend', button);
|
||||
this.dispatchEvent('connect', { element: this.element, button });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
createButton() {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.classList.add(...this.buttonClassesValue);
|
||||
button.setAttribute('tabindex', '-1');
|
||||
button.addEventListener('click', this.toggle.bind(this));
|
||||
button.innerHTML = `${this.visibleIcon} ${this.visibleLabelValue}`;
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle input type between "text" or "password" and update label accordingly
|
||||
*/
|
||||
toggle(event) {
|
||||
this.isDisplayed = !this.isDisplayed;
|
||||
const toggleButtonElement = event.currentTarget;
|
||||
toggleButtonElement.innerHTML = this.isDisplayed
|
||||
? `${this.hiddenIcon} ${this.hiddenLabelValue}`
|
||||
: `${this.visibleIcon} ${this.visibleLabelValue}`;
|
||||
this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password');
|
||||
this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement });
|
||||
}
|
||||
|
||||
dispatchEvent(name, payload) {
|
||||
this.dispatch(name, { detail: payload, prefix: 'toggle-password' });
|
||||
}
|
||||
}
|
||||
|
|
@ -121,3 +121,10 @@ del {
|
|||
background-color: #f09595;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* Password toggle
|
||||
****************************************/
|
||||
.toggle-password-button {
|
||||
top: 0.7rem !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,3 +112,10 @@ ul.structural_link li a:hover {
|
|||
background-color: var(--bs-success);
|
||||
border-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Katex rendering with same height as text
|
||||
***********************************************/
|
||||
.katex-same-height-as-text .katex {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
.hoverpic {
|
||||
min-width: 10px;
|
||||
max-width: 30px;
|
||||
min-width: var(--table-image-preview-min-size, 20px);
|
||||
max-width: var(--table-image-preview-max-size, 35px);
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
|
@ -49,9 +49,8 @@
|
|||
}
|
||||
|
||||
.part-table-image {
|
||||
max-height: 40px;
|
||||
max-height: calc(1.2*var(--table-image-preview-max-size, 35px)); /** Aspect ratio of maximum 1.2 */
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.part-info-image {
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ body {
|
|||
.back-to-top {
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
bottom: 60px;
|
||||
right: 40px;
|
||||
display:none;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/****************************************
|
||||
* Action bar
|
||||
****************************************/
|
||||
|
||||
.sticky-select-bar {
|
||||
position: sticky;
|
||||
top: 120px;
|
||||
z-index: 1000; /* Ensure the bar is above other content */
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* Tables
|
||||
****************************************/
|
||||
|
|
@ -63,10 +73,6 @@ table.dataTable > tbody > tr.selected > td > a {
|
|||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.card-footer-table {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
|
@ -84,16 +90,31 @@ th.select-checkbox {
|
|||
* Datatables definitions/overrides
|
||||
********************************************************************/
|
||||
|
||||
.dataTables_length {
|
||||
.dt-length {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/** Add spacing between column visibility button and length menu */
|
||||
.buttons-colvis {
|
||||
margin-right: 0.2em !important;
|
||||
}
|
||||
|
||||
/** Fix datatables select-checkbox position */
|
||||
table.dataTable tr.selected td.select-checkbox:after
|
||||
{
|
||||
margin-top: -20px !important;
|
||||
}
|
||||
|
||||
/** Show pagination right aligned */
|
||||
.dt-paging .pagination {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/** Fix table row coloring */
|
||||
table.table.dataTable > :not(caption) > * > * {
|
||||
background-color: var(--bs-table-bg);
|
||||
}
|
||||
|
||||
|
||||
/******************************************************
|
||||
Classes for Datatables export
|
||||
|
|
@ -104,52 +125,3 @@ Classes for Datatables export
|
|||
.export-helper{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/******************************************************
|
||||
* Styling for the select all checkbox in the parts table
|
||||
* Should match the styling of the select checkbox
|
||||
******************************************************/
|
||||
table.dataTable > thead > tr > th.select-checkbox {
|
||||
position: relative;
|
||||
}
|
||||
table.dataTable > thead > tr > th.select-checkbox:before,
|
||||
table.dataTable > thead > tr > th.select-checkbox:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0.9em;
|
||||
left: 50%;
|
||||
width: 1em !important;
|
||||
height: 1em !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable > thead > tr > th.select-checkbox:before {
|
||||
content: " ";
|
||||
margin-top: -5px;
|
||||
margin-left: -6px;
|
||||
border: 2px solid var(--bs-tertiary-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > th.select-checkbox:before {
|
||||
border: 2px solid var(--bs-tertiary-color) !important;
|
||||
}
|
||||
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after, table.dataTable > tbody > tr > th.select-checkbox:before, table.dataTable > tbody > tr > th.select-checkbox:after {
|
||||
width: 1em !important;
|
||||
height: 1em !important;
|
||||
}
|
||||
|
||||
table.dataTable > thead > tr.selected > th.select-checkbox:after {
|
||||
content: "✓";
|
||||
font-size: 20px;
|
||||
margin-top: -20px;
|
||||
margin-left: -6px;
|
||||
text-align: center;
|
||||
/*text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; */
|
||||
}
|
||||
table.dataTable.compact > thead > tr > th.select-checkbox:before {
|
||||
margin-top: -12px;
|
||||
}
|
||||
table.dataTable.compact > thead > tr.selected > th.select-checkbox:after {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
|
|
|||
1120
assets/css/components/autocomplete_bootstrap_theme.css
Normal file
1120
assets/css/components/autocomplete_bootstrap_theme.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -24,9 +24,8 @@
|
|||
/** Should be the same settings, as in label_style.css */
|
||||
.ck-html-label .ck-content {
|
||||
font-family: "DejaVu Sans Mono", monospace;
|
||||
font-size: 12px;
|
||||
font-size: 12pt;
|
||||
line-height: 1.0;
|
||||
font-size-adjust: 1.5;
|
||||
}
|
||||
|
||||
.ck-html-label .ck-content p {
|
||||
|
|
@ -72,6 +71,8 @@
|
|||
--ck-color-button-on-hover-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-active-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-disabled-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-color: var(--bs-primary)
|
||||
--ck-color-button-on-color: var(--bs-primary);
|
||||
|
||||
--ck-content-font-color: var(--ck-color-base-text);
|
||||
|
||||
}
|
||||
69
assets/css/components/datatables_select_bs5.css
Normal file
69
assets/css/components/datatables_select_bs5.css
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/******************************************************************************************
|
||||
* This styles the checkboxes of the select extension exactly like the ones in bootstrap 5
|
||||
******************************************************************************************/
|
||||
|
||||
table.dataTable > tbody > tr > .selected {
|
||||
background-color: var(--bs-primary-bg-subtle) !important;
|
||||
color: white;
|
||||
}
|
||||
table.dataTable > tbody > tr > .dt-select {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
table.dataTable > thead > tr > .dt-select {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable input.dt-select-checkbox {
|
||||
--bs-form-check-bg: var(--bs-body-bg);
|
||||
flex-shrink: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
vertical-align: top;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--bs-form-check-bg);
|
||||
background-image: var(--bs-form-check-bg-image);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
table.dataTable input.dt-select-checkbox:checked {
|
||||
background-color: rgb(var(--bs-secondary-rgb));
|
||||
border-color: rgb(var(--bs-secondary-rgb));
|
||||
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
table.dataTable input.dt-select-checkbox:indeterminate {
|
||||
background-color: rgb(var(--bs-secondary-rgb));
|
||||
border-color: rgb(var(--bs-secondary-rgb));
|
||||
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
div.dt-container span.select-info,
|
||||
div.dt-container span.select-item {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
div.dt-container span.select-info,
|
||||
div.dt-container span.select-item {
|
||||
margin-left: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
table.dataTable.table-sm tbody td.select-checkbox::before {
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
||||
41
assets/css/components/toggle_password.css
Normal file
41
assets/css/components/toggle_password.css
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.toggle-password-container {
|
||||
position: relative;
|
||||
}
|
||||
.toggle-password-icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
.toggle-password-button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
column-gap: 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 0.875rem;
|
||||
justify-items: center;
|
||||
height: 1rem;
|
||||
line-height: 1.25rem;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: -1.25rem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -44,4 +44,18 @@ import "./register_events";
|
|||
import "./tristate_checkboxes";
|
||||
|
||||
//Define jquery globally
|
||||
window.$ = window.jQuery = require("jquery")
|
||||
window.$ = window.jQuery = require("jquery");
|
||||
|
||||
//Use the local WASM file for the ZXing library
|
||||
import {
|
||||
setZXingModuleOverrides,
|
||||
} from "barcode-detector/ponyfill";
|
||||
import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm";
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path, prefix) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return wasmFile;
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
1538
assets/js/lib/dataTables.select.mjs
Normal file
1538
assets/js/lib/dataTables.select.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -75,11 +75,10 @@
|
|||
request._dt = config.name;
|
||||
|
||||
//Try to resolve the original column index when the column was reordered (using the ColReorder plugin)
|
||||
//Only do this when _ColReorder_iOrigCol is available
|
||||
if (settings.aoColumns && settings.aoColumns.length && settings.aoColumns[0]._ColReorder_iOrigCol !== undefined) {
|
||||
if (dt.colReorder && dt.colReorder.transpose) {
|
||||
if (request.order && request.order.length) {
|
||||
request.order.forEach(function (order) {
|
||||
order.column = settings.aoColumns[order.column]._ColReorder_iOrigCol;
|
||||
order.column = dt.colReorder.transpose(order.column, "toOriginal");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +97,15 @@
|
|||
dtOpts = config.options(dtOpts);
|
||||
}
|
||||
|
||||
//Choose the column where the className contains "select-column" and apply the select extension to its render field
|
||||
//Added for Part-DB
|
||||
for (let column of dtOpts.columns) {
|
||||
if (column.className && column.className.includes('dt-select')) {
|
||||
column.render = $.fn.dataTable.render.select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
root.html(data.template);
|
||||
dt = $('table', root).DataTable(dtOpts);
|
||||
if (config.state !== 'none') {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import {Dropdown} from "bootstrap";
|
||||
import ClipboardJS from "clipboard";
|
||||
import {Modal} from "bootstrap";
|
||||
|
||||
class RegisterEventHelper {
|
||||
constructor() {
|
||||
|
|
@ -31,9 +32,11 @@ class RegisterEventHelper {
|
|||
//Initialize ClipboardJS
|
||||
this.registerLoadHandler(() => {
|
||||
new ClipboardJS('.btn');
|
||||
})
|
||||
});
|
||||
|
||||
this.registerModalDropRemovalOnFormSubmit();
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerModalDropRemovalOnFormSubmit() {
|
||||
|
|
@ -43,6 +46,15 @@ class RegisterEventHelper {
|
|||
if (back_drop) {
|
||||
back_drop.remove();
|
||||
}
|
||||
|
||||
//Remove scroll-lock if it is still active
|
||||
if (document.body.classList.contains('modal-open')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
|
||||
//Remove the padding-right and overflow:hidden from the body
|
||||
document.body.style.paddingRight = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
63
assets/tomselect/autoselect_typed/autoselect_typed.js
Normal file
63
assets/tomselect/autoselect_typed/autoselect_typed.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Autoselect Typed plugin for Tomselect
|
||||
*
|
||||
* This plugin allows automatically selecting an option matching the typed text when the Tomselect element goes out of
|
||||
* focus (is blurred) and/or when the delimiter is typed.
|
||||
*
|
||||
* #select_on_blur option
|
||||
* Tomselect natively supports the "createOnBlur" option. This option picks up any remaining text in the input field
|
||||
* and uses it to create a new option and selects that option. It does behave a bit strangely though, in that it will
|
||||
* not select an already existing option when the input is blurred, so if you typed something that matches an option in
|
||||
* the list and then click outside the box (without pressing enter) the entered text is just removed (unless you have
|
||||
* allow duplicates on in which case it will create a new option).
|
||||
* This plugin fixes that, such that Tomselect will first try to select an option matching the remaining uncommitted
|
||||
* text and only when no matching option is found tries to create a new one (if createOnBlur and create is on)
|
||||
*
|
||||
* #select_on_delimiter option
|
||||
* Normally when typing the delimiter (space by default) Tomselect will try to create a new option (and select it) (if
|
||||
* create is on), but if the typed text matches an option (and allow duplicates is off) it refuses to react at all until
|
||||
* you press enter. With this option, the delimiter will also allow selecting an option, not just creating it.
|
||||
*/
|
||||
function select_current_input(self){
|
||||
if(self.isLocked){
|
||||
return
|
||||
}
|
||||
|
||||
const val = self.inputValue()
|
||||
//Do nothing if the input is empty
|
||||
if (!val) {
|
||||
return
|
||||
}
|
||||
|
||||
if (self.options[val]) {
|
||||
self.addItem(val)
|
||||
self.setTextboxValue()
|
||||
}
|
||||
}
|
||||
|
||||
export default function(plugin_options_) {
|
||||
const plugin_options = Object.assign({
|
||||
//Autoselect the typed text when the input element goes out of focus
|
||||
select_on_blur: true,
|
||||
//Autoselect the typed text when the delimiter is typed
|
||||
select_on_delimiter: true,
|
||||
}, plugin_options_);
|
||||
|
||||
const self = this
|
||||
|
||||
if(plugin_options.select_on_blur) {
|
||||
this.hook("before", "onBlur", function () {
|
||||
select_current_input(self)
|
||||
})
|
||||
}
|
||||
|
||||
if(plugin_options.select_on_delimiter) {
|
||||
this.hook("before", "onKeyPress", function (e) {
|
||||
const character = String.fromCharCode(e.keyCode || e.which);
|
||||
if (self.settings.mode === 'multi' && character === self.settings.delimiter) {
|
||||
select_current_input(self)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
93
assets/tomselect/click_to_edit/click_to_edit.js
Normal file
93
assets/tomselect/click_to_edit/click_to_edit.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* click_to_edit plugin for Tomselect
|
||||
*
|
||||
* This plugin allows editing (and selecting text in) any selected item by clicking it.
|
||||
*
|
||||
* Usually, when the user typed some text and created an item in Tomselect that item cannot be edited anymore. To make
|
||||
* a change, the item has to be deleted and retyped completely. There is also generally no way to copy text out of a
|
||||
* tomselect item. The "restore_on_backspace" plugin improves that somewhat, by allowing the user to edit an item after
|
||||
* pressing backspace. However, it is somewhat confusing to first have to focus the field an then hit backspace in order
|
||||
* to copy a piece of text. It may also not be immediately obvious for editing.
|
||||
* This plugin transforms an item into editable text when it is clicked, e.g. when the user tries to place the caret
|
||||
* within an item or when they try to drag across the text to highlight it.
|
||||
* It also plays nice with the remove_button plugin which still removes (deselects) an option entirely.
|
||||
*
|
||||
* It is recommended to also enable the autoselect_typed plugin when using this plugin. Without it, the text in the
|
||||
* input field (i.e. the item that was just clicked) is lost when the user clicks outside the field. Also, when the user
|
||||
* clicks an option (making it text) and then tries to enter another one by entering the delimiter (e.g. space) nothing
|
||||
* happens until enter is pressed or the text is changed from what it was.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return a dom element from either a dom query string, jQuery object, a dom element or html string
|
||||
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
|
||||
*
|
||||
* param query should be {}
|
||||
*/
|
||||
const getDom = query => {
|
||||
if (query.jquery) {
|
||||
return query[0];
|
||||
}
|
||||
if (query instanceof HTMLElement) {
|
||||
return query;
|
||||
}
|
||||
if (isHtmlString(query)) {
|
||||
var tpl = document.createElement('template');
|
||||
tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result
|
||||
return tpl.content.firstChild;
|
||||
}
|
||||
return document.querySelector(query);
|
||||
};
|
||||
const isHtmlString = arg => {
|
||||
if (typeof arg === 'string' && arg.indexOf('<') > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function plugin(plugin_options_) {
|
||||
const self = this
|
||||
|
||||
const plugin_options = Object.assign({
|
||||
//If there is unsubmitted text in the input field, should that text be automatically used to select a matching
|
||||
//element? If this is off, clicking on item1 and then clicking on item2 will result in item1 being deselected
|
||||
auto_select_before_edit: true,
|
||||
//If there is unsubmitted text in the input field, should that text be automatically used to create a matching
|
||||
//element if no matching element was found or auto_select_before_edit is off?
|
||||
auto_create_before_edit: true,
|
||||
//customize this function to change which text the item is replaced with when clicking on it
|
||||
text: option => {
|
||||
return option[self.settings.labelField];
|
||||
}
|
||||
}, plugin_options_);
|
||||
|
||||
|
||||
self.hook('after', 'setupTemplates', () => {
|
||||
const orig_render_item = self.settings.render.item;
|
||||
self.settings.render.item = (data, escape) => {
|
||||
const item = getDom(orig_render_item.call(self, data, escape));
|
||||
|
||||
item.addEventListener('click', evt => {
|
||||
if (self.isLocked) {
|
||||
return;
|
||||
}
|
||||
const val = self.inputValue();
|
||||
|
||||
if (self.options[val]) {
|
||||
self.addItem(val)
|
||||
} else if (self.settings.create) {
|
||||
self.createItem();
|
||||
}
|
||||
const option = self.options[item.dataset.value]
|
||||
self.setTextboxValue(plugin_options.text.call(self, option));
|
||||
self.focus();
|
||||
self.removeItem(item);
|
||||
}
|
||||
);
|
||||
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
export { plugin as default };
|
||||
|
|
@ -4,6 +4,10 @@
|
|||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
//Increase xdebug.max_nesting_level to 1000 if required (see issue #411)
|
||||
//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower
|
||||
if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ if (!ini_get('date.timezone')) {
|
|||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
|
|
|
|||
153
composer.json
153
composer.json
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"name": "part-db/part-db-server",
|
||||
"type": "project",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
|
|
@ -10,107 +11,112 @@
|
|||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"api-platform/core": "^3.1",
|
||||
"amphp/http-client": "^5.1",
|
||||
"api-platform/doctrine-orm": "^4.1",
|
||||
"api-platform/json-api": "^4.0.0",
|
||||
"api-platform/symfony": "^4.0.0",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "^0.11.0",
|
||||
"brick/math": "^0.13.1",
|
||||
"composer/ca-bundle": "^1.5",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/annotations": "1.14.3",
|
||||
"doctrine/data-fixtures": "^1.6.6",
|
||||
"doctrine/dbal": "^3.4.6",
|
||||
"doctrine/data-fixtures": "^2.0.0",
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.16",
|
||||
"dompdf/dompdf": "dev-master#87bea32efe0b0db309e1d31537201f64d5508280 as v2.0.3",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"florianv/swap": "^4.0",
|
||||
"florianv/swap-bundle": "dev-master",
|
||||
"doctrine/orm": "^3.2.0",
|
||||
"dompdf/dompdf": "^3.1.2",
|
||||
"gregwar/captcha-bundle": "^2.1.0",
|
||||
"jbtronics/2fa-webauthn": "^v2.0.0",
|
||||
"hshn/base64-encoded-file": "^5.0",
|
||||
"jbtronics/2fa-webauthn": "^3.0.0",
|
||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||
"jbtronics/settings-bundle": "^3.0.0",
|
||||
"jfcherng/php-diff": "^6.14",
|
||||
"knpuniversity/oauth2-client-bundle": "^2.15",
|
||||
"league/commonmark": "^2.7",
|
||||
"league/csv": "^9.8.0",
|
||||
"league/html-to-markdown": "^5.0.1",
|
||||
"liip/imagine-bundle": "^2.2",
|
||||
"nbgrp/onelogin-saml-bundle": "^1.3",
|
||||
"maennchen/zipstream-php": "2.1",
|
||||
"nbgrp/onelogin-saml-bundle": "^v2.0.2",
|
||||
"nelexa/zip": "^4.0",
|
||||
"nelmio/cors-bundle": "^2.3",
|
||||
"nelmio/security-bundle": "^3.0",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"ocramius/proxy-manager": "2.2.*",
|
||||
"omines/datatables-bundle": "^0.7.2",
|
||||
"omines/datatables-bundle": "^0.10.0",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"php-translation/symfony-bundle": "^0.14.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"phpstan/phpdoc-parser": "^1.23",
|
||||
"part-db/swap-bundle": "^6.0.0",
|
||||
"phpoffice/phpspreadsheet": "^5.0.0",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
"scheb/2fa-bundle": "^6.8.0",
|
||||
"scheb/2fa-google-authenticator": "^6.8.0",
|
||||
"scheb/2fa-trusted-device": "^6.8.0",
|
||||
"scheb/2fa-backup-code": "^v7.11.0",
|
||||
"scheb/2fa-bundle": "^v7.11.0",
|
||||
"scheb/2fa-google-authenticator": "^v7.11.0",
|
||||
"scheb/2fa-trusted-device": "^v7.11.0",
|
||||
"shivas/versioning-bundle": "^4.0",
|
||||
"spatie/db-dumper": "^3.3.1",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "6.3.*",
|
||||
"symfony/console": "6.3.*",
|
||||
"symfony/dotenv": "6.3.*",
|
||||
"symfony/expression-language": "6.3.*",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/dom-crawler": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"symfony/flex": "^v2.3.1",
|
||||
"symfony/form": "6.3.*",
|
||||
"symfony/framework-bundle": "6.3.*",
|
||||
"symfony/http-client": "6.3.*",
|
||||
"symfony/http-kernel": "6.3.*",
|
||||
"symfony/mailer": "6.3.*",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/http-kernel": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/process": "6.3.*",
|
||||
"symfony/property-access": "6.3.*",
|
||||
"symfony/property-info": "6.3.*",
|
||||
"symfony/proxy-manager-bridge": "6.3.*",
|
||||
"symfony/rate-limiter": "6.3.*",
|
||||
"symfony/runtime": "6.3.*",
|
||||
"symfony/security-bundle": "6.3.*",
|
||||
"symfony/serializer": "6.3.*",
|
||||
"symfony/string": "6.3.*",
|
||||
"symfony/translation": "6.3.*",
|
||||
"symfony/twig-bundle": "6.3.*",
|
||||
"symfony/polyfill-php82": "^1.28",
|
||||
"symfony/process": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/rate-limiter": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/string": "7.3.*",
|
||||
"symfony/translation": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-translator": "^2.10",
|
||||
"symfony/ux-turbo": "^2.0",
|
||||
"symfony/validator": "6.3.*",
|
||||
"symfony/web-link": "6.3.*",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/web-link": "7.3.*",
|
||||
"symfony/webpack-encore-bundle": "^v2.0.1",
|
||||
"symfony/yaml": "6.3.*",
|
||||
"tecnickcom/tc-lib-barcode": "^1.15",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"symplify/easy-coding-standard": "^12.5.20",
|
||||
"tecnickcom/tc-lib-barcode": "^2.1.4",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.0",
|
||||
"twig/html-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.8",
|
||||
"twig/html-extra": "^3.8",
|
||||
"twig/inky-extra": "^3.0",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/markdown-extra": "^3.0",
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0",
|
||||
"webmozart/assert": "^1.4"
|
||||
"twig/intl-extra": "^3.8",
|
||||
"twig/markdown-extra": "^3.8",
|
||||
"twig/string-extra": "^3.8",
|
||||
"web-auth/webauthn-symfony-bundle": "^5.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^7.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"ekino/phpstan-banned-code": "^v1.0.0",
|
||||
"dama/doctrine-test-bundle": "^v8.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
|
||||
"ekino/phpstan-banned-code": "^v3.0.0",
|
||||
"jbtronics/translation-editor-bundle": "^1.0",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^1.4.7",
|
||||
"phpstan/phpstan-doctrine": "^1.2.11",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan-symfony": "^1.1.7",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psalm/plugin-symfony": "^v5.0.1",
|
||||
"rector/rector": "^0.18.0",
|
||||
"phpstan/phpstan": "^2.0.4",
|
||||
"phpstan/phpstan-doctrine": "^2.0.1",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.1",
|
||||
"phpstan/phpstan-symfony": "^2.0.0",
|
||||
"phpunit/phpunit": "^11.5.0",
|
||||
"rector/rector": "^2.0.4",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "6.3.*",
|
||||
"symfony/css-selector": "6.3.*",
|
||||
"symfony/debug-bundle": "6.3.*",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/debug-bundle": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.13",
|
||||
"symfony/phpunit-bridge": "6.3.*",
|
||||
"symfony/stopwatch": "6.3.*",
|
||||
"symfony/web-profiler-bundle": "6.3.*",
|
||||
"symplify/easy-coding-standard": "^12.0",
|
||||
"vimeo/psalm": "^5.6.0"
|
||||
"symfony/phpunit-bridge": "7.3.*",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Used to improve price calculation performance",
|
||||
|
|
@ -121,7 +127,7 @@
|
|||
"*": "dist"
|
||||
},
|
||||
"platform": {
|
||||
"php": "8.1.0"
|
||||
"php": "8.2.0"
|
||||
},
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
|
|
@ -153,7 +159,7 @@
|
|||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
|
||||
"phpstan": "php -d memory_limit=1G vendor/bin/phpstan analyse src --level 5"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
|
|
@ -161,7 +167,8 @@
|
|||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "6.3.*"
|
||||
"require": "7.3.*",
|
||||
"docker": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13423
composer.lock
generated
13423
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,4 @@
|
|||
Welcome to Part-DB.
|
||||
**Attention**:
|
||||
Since Version 2.0.0 this file is no longer used.
|
||||
|
||||
<small>If you want to change this banner, edit `config/banner.md` file or set the `BANNER` environment variable.</small>
|
||||
|
||||
<blockquote class="pb-0">
|
||||
<p style="font-size: 12px">
|
||||
And God said <br>
|
||||
$\nabla \cdot \vec{D} = \rho$,
|
||||
$\nabla \cdot \vec{B} = 0$,
|
||||
$\nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t}$,
|
||||
$\nabla \times \vec{H} = \vec{j} + \frac{\partial \vec{D}}{\partial t}$, <br>
|
||||
and then there was light.
|
||||
</p>
|
||||
</blockquote>
|
||||
You can now set the banner text directly in the admin interface, or by setting the `BANNER` environment variable.
|
||||
|
|
|
|||
|
|
@ -18,13 +18,11 @@ return [
|
|||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true],
|
||||
Translation\Bundle\TranslationBundle::class => ['all' => true],
|
||||
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
|
||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true],
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true],
|
||||
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
|
||||
Nbgrp\OneloginSamlBundle\NbgrpOneloginSamlBundle::class => ['all' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
|
|
@ -32,5 +30,7 @@ return [
|
|||
Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true],
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
|
||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
api_platform:
|
||||
|
||||
title: 'Part-DB API'
|
||||
description: 'API of Part-DB'
|
||||
|
||||
version: '0.1.0'
|
||||
|
||||
# eager_loading:
|
||||
# max_joins: 100
|
||||
formats:
|
||||
jsonld: ['application/ld+json']
|
||||
json: ['application/json']
|
||||
jsonapi: ['application/vnd.api+json']
|
||||
|
||||
keep_legacy_inflector: false
|
||||
docs_formats:
|
||||
jsonld: ['application/ld+json']
|
||||
jsonopenapi: ['application/vnd.openapi+json']
|
||||
html: ['text/html']
|
||||
json: ['application/vnd.openapi+json']
|
||||
|
||||
swagger:
|
||||
api_keys:
|
||||
|
|
@ -24,5 +28,13 @@ api_platform:
|
|||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
extra_properties:
|
||||
standard_put: true
|
||||
rfc_7807_compliant_errors: true
|
||||
|
||||
pagination_client_items_per_page: true # Allow clients to override the default items per page
|
||||
|
||||
# Need to be true, or some tests will fail
|
||||
use_symfony_listeners: true
|
||||
|
||||
serializer:
|
||||
# Change this to false later, to remove the hydra prefix on the API
|
||||
hydra_prefix: true
|
||||
|
|
|
|||
12
config/packages/csrf.yaml
Normal file
12
config/packages/csrf.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
check_header: true
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
5
config/packages/dama_doctrine_test_bundle.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
when@test:
|
||||
dama_doctrine_test:
|
||||
enable_static_connection: true
|
||||
enable_static_meta_data_cache: true
|
||||
enable_static_query_cache: true
|
||||
|
|
@ -8,17 +8,17 @@ datatables:
|
|||
|
||||
# Set options, as documented at https://datatables.net/reference/option/
|
||||
options:
|
||||
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
|
||||
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
#dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>"
|
||||
dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>>
|
||||
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
|
||||
#pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
pageLength: 50 #TODO
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
<'card'
|
||||
rt
|
||||
<'card-footer card-footer-table text-muted' i >
|
||||
>
|
||||
<'row'<'col mt-2 input-group' B l> <'col mt-2' <'pull-right' p>>>"
|
||||
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>"
|
||||
pagingType: 'simple_numbers'
|
||||
searching: true
|
||||
searching: false
|
||||
stateSave: true
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
translation:
|
||||
symfony_profiler:
|
||||
enabled: true
|
||||
webui:
|
||||
enabled: true
|
||||
33
config/packages/doctrine.php
Normal file
33
config/packages/doctrine.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+.
|
||||
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
|
||||
*/
|
||||
|
||||
return static function(\Symfony\Config\DoctrineConfig $doctrine) {
|
||||
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
$doctrine->orm()->enableNativeLazyObjects(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -2,14 +2,24 @@ doctrine:
|
|||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# Required for DAMA doctrine test bundle
|
||||
use_savepoints: true
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
|
||||
types:
|
||||
# UTC datetimes
|
||||
datetime:
|
||||
class: App\Doctrine\Types\UTCDateTimeType
|
||||
date:
|
||||
class: App\Doctrine\Types\UTCDateTimeType
|
||||
|
||||
datetime_immutable:
|
||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
||||
date_immutable:
|
||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
||||
|
||||
big_decimal:
|
||||
class: App\Doctrine\Types\BigDecimalType
|
||||
tinyint:
|
||||
|
|
@ -25,21 +35,27 @@ doctrine:
|
|||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
controller_resolver:
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
|
||||
dql:
|
||||
string_functions:
|
||||
regexp: DoctrineExtensions\Query\Mysql\Regexp
|
||||
ifnull: DoctrineExtensions\Query\Mysql\IfNull
|
||||
regexp: App\Doctrine\Functions\Regexp
|
||||
field: DoctrineExtensions\Query\Mysql\Field
|
||||
field2: App\Doctrine\Functions\Field2
|
||||
natsort: App\Doctrine\Functions\Natsort
|
||||
array_position: App\Doctrine\Functions\ArrayPosition
|
||||
ilike: App\Doctrine\Functions\ILike
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
csrf_protection: true
|
||||
handle_all_throwables: true
|
||||
|
||||
# We set this header by ourself, so we can disable it here
|
||||
# We set this header by ourselves, so we can disable it here
|
||||
disallow_search_engine_index: false
|
||||
|
||||
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
|
||||
|
|
@ -26,12 +24,14 @@ framework:
|
|||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
|
||||
form: { csrf_protection: { token_id: 'submit' } }
|
||||
csrf_protection:
|
||||
stateless_token_ids: ['submit', 'authenticate', 'logout']
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ knpu_oauth2_client:
|
|||
type: generic
|
||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
||||
|
||||
client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%'
|
||||
client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%'
|
||||
client_id: '%env(settings:digikey:clientId)%'
|
||||
client_secret: '%env(settings:digikey:secret)%'
|
||||
|
||||
redirect_route: 'oauth_client_check'
|
||||
redirect_params: {name: 'ip_digikey_oauth'}
|
||||
|
|
@ -26,8 +26,8 @@ knpu_oauth2_client:
|
|||
type: generic
|
||||
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
|
||||
|
||||
client_id: '%env(PROVIDER_OCTOPART_CLIENT_ID)%'
|
||||
client_secret: '%env(PROVIDER_OCTOPART_SECRET)%'
|
||||
client_id: '%env(settings:octopart:clientId)%'
|
||||
client_secret: '%env(settings:octopart:secret)%'
|
||||
|
||||
redirect_route: 'oauth_client_check'
|
||||
redirect_params: { name: 'ip_octopart_oauth' }
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ when@prod:
|
|||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
|
@ -70,11 +69,11 @@ when@docker:
|
|||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
include_stacktraces: true
|
||||
channels: ["!deprecation"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "php://stderr"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ nbgrp_onelogin_saml:
|
|||
privateKey: '%env(string:default:saml.sp.privateKey:string:SAMLP_SP_PRIVATE_KEY)%'
|
||||
|
||||
# Optional settings
|
||||
#baseurl: 'http://myapp.com'
|
||||
baseurl: '%partdb.default_uri%saml/'
|
||||
strict: true
|
||||
debug: false
|
||||
security:
|
||||
|
|
|
|||
|
|
@ -20,12 +20,6 @@ nelmio_security:
|
|||
- 'digikey.com'
|
||||
- 'nexar.com'
|
||||
|
||||
# forces Microsoft's XSS-Protection with
|
||||
# its block mode
|
||||
xss_protection:
|
||||
enabled: true
|
||||
mode_block: true
|
||||
|
||||
# Send a full URL in the `Referer` header when performing a same-origin request,
|
||||
# only send the origin of the document to secure destination (HTTPS->HTTPS),
|
||||
# and send no header to a less secure destination (HTTPS->HTTP).
|
||||
|
|
@ -51,12 +45,16 @@ nelmio_security:
|
|||
img-src:
|
||||
- '*'
|
||||
- 'data:'
|
||||
# Required for be able to load pictures in the QR code scanner
|
||||
- 'blob:'
|
||||
style-src:
|
||||
- 'self'
|
||||
- 'unsafe-inline'
|
||||
- 'data:'
|
||||
script-src:
|
||||
- 'self'
|
||||
# Required for loading the Wasm for the barcode scanner:
|
||||
- 'wasm-unsafe-eval'
|
||||
object-src:
|
||||
- 'self'
|
||||
- 'data:'
|
||||
|
|
@ -65,9 +63,3 @@ nelmio_security:
|
|||
- 'data:'
|
||||
block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport
|
||||
# upgrade-insecure-requests: true # defaults to false, upgrades HTTP requests to HTTPS transport
|
||||
|
||||
when@dev:
|
||||
# disables the Content-Security-Policy header
|
||||
nelmio_security:
|
||||
csp:
|
||||
enabled: false
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
translation:
|
||||
locales: ["en", "de"]
|
||||
edit_in_place:
|
||||
enabled: false
|
||||
config_name: app
|
||||
configs:
|
||||
app:
|
||||
dirs: ["%kernel.project_dir%/templates", "%kernel.project_dir%/src"]
|
||||
output_dir: "%kernel.project_dir%/translations"
|
||||
excluded_names: ["*TestCase.php", "*Test.php"]
|
||||
excluded_dirs: [cache, data, logs]
|
||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
default_uri: '%env(DEFAULT_URI)%'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ scheb_two_factor:
|
|||
server_name: '$$DOMAIN$$' # This field is replaced by the domain name of the server in DecoratedGoogleAuthenticator
|
||||
issuer: '%partdb.title%' # Issuer name used in QR code
|
||||
digits: 6 # Number of digits in authentication code
|
||||
window: 1 # How many codes before/after the current one would be accepted as valid
|
||||
leeway: 5 # Acceptable time drift in seconds
|
||||
template: security/2fa_form.html.twig
|
||||
|
||||
backup_codes:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue