mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
970 Commits
2.5.3
...
revert-646
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6703402207 | ||
|
|
f93059412a | ||
|
|
294ce7bb1b | ||
|
|
a558bfdb2f | ||
|
|
86bcd1b681 | ||
|
|
33c260c35b | ||
|
|
94312bb35c | ||
|
|
70b1989f10 | ||
|
|
c0eaa75232 | ||
|
|
dbb9971482 | ||
|
|
0828994840 | ||
|
|
e6b5618bd3 | ||
|
|
9c24d3a521 | ||
|
|
480e0887e3 | ||
|
|
e0e381bdfc | ||
|
|
5199b306aa | ||
|
|
8febfaa21e | ||
|
|
69062f03ee | ||
|
|
eb04fa19e1 | ||
|
|
03b4fe3558 | ||
|
|
b349d08155 | ||
|
|
15e9d92094 | ||
|
|
5e675dbf0b | ||
|
|
a5660819de | ||
|
|
d277fefc87 | ||
|
|
1383010826 | ||
|
|
59982c9056 | ||
|
|
afcff84e38 | ||
|
|
fc5d9659d6 | ||
|
|
8fa7fa8c4b | ||
|
|
23bde76192 | ||
|
|
ca7a80fb83 | ||
|
|
cf0d9a433d | ||
|
|
bc20598b3d | ||
|
|
9de8ebb52c | ||
|
|
568af52ebc | ||
|
|
eddabc0d68 | ||
|
|
6b300d516b | ||
|
|
e271caa32b | ||
|
|
9be569c54c | ||
|
|
7e6a621484 | ||
|
|
66b47f9444 | ||
|
|
694a2084e2 | ||
|
|
fef19a3c80 | ||
|
|
3da8b945ca | ||
|
|
8f27b82edd | ||
|
|
8b529d308c | ||
|
|
71aa8e5a86 | ||
|
|
ab01f0b274 | ||
|
|
e203536506 | ||
|
|
b71b9edee7 | ||
|
|
bd514c0594 | ||
|
|
36e1ad287c | ||
|
|
92f5b5f92b | ||
|
|
0b7b6e2c23 | ||
|
|
46709fb02e | ||
|
|
61eb2f4a19 | ||
|
|
3fe16bd8f9 | ||
|
|
a9725a1aac | ||
|
|
8f9298fac8 | ||
|
|
c3e76817cd | ||
|
|
8bdec66927 | ||
|
|
66ee9edaf8 | ||
|
|
ffd7bc883d | ||
|
|
1bcfa4b8dc | ||
|
|
99e325acaf | ||
|
|
8badd1f2eb | ||
|
|
44bf276c49 | ||
|
|
a65aa5ea44 | ||
|
|
689063cfb2 | ||
|
|
e146ce7be4 | ||
|
|
0fbd9812b3 | ||
|
|
ccd7b3bdce | ||
|
|
60f8cfd492 | ||
|
|
7359b800ce | ||
|
|
0032639831 | ||
|
|
d9cdd020e6 | ||
|
|
10d021b15e | ||
|
|
3be750410e | ||
|
|
47552830b1 | ||
|
|
0fb41f54b0 | ||
|
|
5b777921a6 | ||
|
|
42dcc81767 | ||
|
|
0f3a4db71e | ||
|
|
751bed4117 | ||
|
|
ea095a98ba | ||
|
|
348a9c82bf | ||
|
|
e2918f4148 | ||
|
|
c45187eedd | ||
|
|
eeea5f2cc8 | ||
|
|
05b6aeef3e | ||
|
|
6323031b40 | ||
|
|
6ccb6cafaa | ||
|
|
be26985ca5 | ||
|
|
2aa2525d0e | ||
|
|
7cb2f307d8 | ||
|
|
f1a557c372 | ||
|
|
202337b135 | ||
|
|
4e3abcbd45 | ||
|
|
122e5a4b57 | ||
|
|
1981946480 | ||
|
|
7d327d23a2 | ||
|
|
500c27859b | ||
|
|
c6f68e6ed1 | ||
|
|
b48faf8fe0 | ||
|
|
fa24ced3a3 | ||
|
|
b9ea2425b9 | ||
|
|
1abaff9c52 | ||
|
|
6f2ccabaa2 | ||
|
|
1c77126fe6 | ||
|
|
7196be2a23 | ||
|
|
d509b840dc | ||
|
|
61c23877c1 | ||
|
|
0e61398d67 | ||
|
|
f12656463d | ||
|
|
ba9fc37226 | ||
|
|
60f754f172 | ||
|
|
3a22545158 | ||
|
|
1d0020f6e6 | ||
|
|
f3c3f3e2d8 | ||
|
|
9ba0ae5532 | ||
|
|
db73c2eea0 | ||
|
|
753823c0b3 | ||
|
|
44e8eacb8d | ||
|
|
33bcbd89f1 | ||
|
|
b0cbe3cec8 | ||
|
|
3ca76c9ef7 | ||
|
|
93199e1a70 | ||
|
|
93a601a1e7 | ||
|
|
3d864c4ff1 | ||
|
|
da2f519805 | ||
|
|
230e330eb2 | ||
|
|
4f6dffabb4 | ||
|
|
09c3490cae | ||
|
|
1fc0203c38 | ||
|
|
f545d7b3ea | ||
|
|
b242eb5b32 | ||
|
|
be9e3fa355 | ||
|
|
fac93e4ff8 | ||
|
|
8609db2182 | ||
|
|
ec73bd640c | ||
|
|
cba65972dd | ||
|
|
e62231cfed | ||
|
|
3249fb43c3 | ||
|
|
ee0ba15f9e | ||
|
|
784aecd1a1 | ||
|
|
173d6c23b0 | ||
|
|
abc1241402 | ||
|
|
f30441626e | ||
|
|
5ae125db94 | ||
|
|
093fa18839 | ||
|
|
81f18ad7f4 | ||
|
|
875e019d4f | ||
|
|
8e18a0880e | ||
|
|
36b78e5e21 | ||
|
|
86a498fc29 | ||
|
|
aae81b8a04 | ||
|
|
486f036a11 | ||
|
|
a2c9d307df | ||
|
|
e52fd90963 | ||
|
|
f8602810eb | ||
|
|
219ddfabaf | ||
|
|
d8b3b216e9 | ||
|
|
88e5209856 | ||
|
|
c2b13a6d5d | ||
|
|
9eefe13e8b | ||
|
|
7eab6a2f1d | ||
|
|
2306df5fb7 | ||
|
|
56ecacee21 | ||
|
|
a60b3d4b08 | ||
|
|
b14798b405 | ||
|
|
8382a88efe | ||
|
|
53057c4bf2 | ||
|
|
3e0f38e8c3 | ||
|
|
a5bbe765b9 | ||
|
|
4455adc813 | ||
|
|
abca763aa5 | ||
|
|
5c74349de0 | ||
|
|
4a7b72dae1 | ||
|
|
23e17d7f30 | ||
|
|
37cf829188 | ||
|
|
f213ffabe1 | ||
|
|
a1921bb767 | ||
|
|
213c04bc8a | ||
|
|
916eb530a0 | ||
|
|
1f0644ea91 | ||
|
|
b20147255a | ||
|
|
38728eb342 | ||
|
|
18c7890f65 | ||
|
|
1c224609b9 | ||
|
|
4b81468c9c | ||
|
|
cffac2a56a | ||
|
|
05c0f8d69f | ||
|
|
5db5bc65de | ||
|
|
952ab032f9 | ||
|
|
2df6f2b8b1 | ||
|
|
62a12a64a3 | ||
|
|
6935d54870 | ||
|
|
049427c6ca | ||
|
|
65e8526ee2 | ||
|
|
8ce71e792e | ||
|
|
202762027f | ||
|
|
d95551e651 | ||
|
|
44d68ad723 | ||
|
|
9e4c9d3101 | ||
|
|
050692952e | ||
|
|
c96fbfdcd6 | ||
|
|
ab90d9d01c | ||
|
|
281c0068d9 | ||
|
|
e7b74939cb | ||
|
|
c2ae58bf08 | ||
|
|
14e8026e30 | ||
|
|
eb29a42209 | ||
|
|
6fdaad1bf4 | ||
|
|
dfa6c502dc | ||
|
|
b958dcb891 | ||
|
|
6e5d64d403 | ||
|
|
3e0c2bf1a1 | ||
|
|
9c4896d72b | ||
|
|
283cdee5d6 | ||
|
|
ab5e01e54a | ||
|
|
01fec1a0cf | ||
|
|
caf13eb774 | ||
|
|
373248e304 | ||
|
|
fef342b489 | ||
|
|
6e9adece1f | ||
|
|
80308ceafa | ||
|
|
f65518f865 | ||
|
|
c0315e2c30 | ||
|
|
2f20ccf289 | ||
|
|
1a7d60bb88 | ||
|
|
90b1895f19 | ||
|
|
7945a95522 | ||
|
|
40fe6369cb | ||
|
|
f155042958 | ||
|
|
1dd23a3f47 | ||
|
|
55da3ee275 | ||
|
|
1194e40222 | ||
|
|
05fac41534 | ||
|
|
3f85e89f62 | ||
|
|
ee0f8ad19a | ||
|
|
b7d7cf233a | ||
|
|
38a708e12b | ||
|
|
53dcd94f0d | ||
|
|
a3ccc3dfef | ||
|
|
bd208c31e2 | ||
|
|
151dc352c8 | ||
|
|
77d8504baf | ||
|
|
ccbf17106d | ||
|
|
95c4d95fd3 | ||
|
|
484772e3b2 | ||
|
|
5c7a1fb407 | ||
|
|
064981ff3d | ||
|
|
fe003d7496 | ||
|
|
fae1df7f4b | ||
|
|
a72c07b657 | ||
|
|
0bff76e5f1 | ||
|
|
c7b062f483 | ||
|
|
83f72f3e41 | ||
|
|
d8b71d76dd | ||
|
|
a6d2f385af | ||
|
|
db9e397531 | ||
|
|
18dea6c3a3 | ||
|
|
8ebaecc472 | ||
|
|
708492afeb | ||
|
|
1305ab3cc6 | ||
|
|
29cc6b4f9c | ||
|
|
cc7f0b145c | ||
|
|
e69c0c3e27 | ||
|
|
a209966427 | ||
|
|
d5abbd4220 | ||
|
|
43a75b64b4 | ||
|
|
70a23a14c4 | ||
|
|
93c81ea49c | ||
|
|
3a741d1c14 | ||
|
|
ba442e1549 | ||
|
|
8c15296d07 | ||
|
|
d26464c810 | ||
|
|
8ee202e5a1 | ||
|
|
689cddfd0c | ||
|
|
25950bb5a5 | ||
|
|
1da623e63f | ||
|
|
4bf9e24d43 | ||
|
|
b41a7b8547 | ||
|
|
f500a00d04 | ||
|
|
64a2a08d24 | ||
|
|
1f58f96e88 | ||
|
|
dc3d802d3d | ||
|
|
5765d1c56c | ||
|
|
abcd050c69 | ||
|
|
f40ef26c69 | ||
|
|
fccd1a5bd7 | ||
|
|
16012a3881 | ||
|
|
ddc41027ab | ||
|
|
4f931fbe6a | ||
|
|
7ada3692cf | ||
|
|
1ab5d5027f | ||
|
|
1f16816fe4 | ||
|
|
daf048e258 | ||
|
|
f3d13005b2 | ||
|
|
25a44e1387 | ||
|
|
9e9612cf1f | ||
|
|
304c44048f | ||
|
|
99e64ad387 | ||
|
|
0f0c45af8e | ||
|
|
f2977cf938 | ||
|
|
bb80da137d | ||
|
|
f4b16a255c | ||
|
|
b49a4734ff | ||
|
|
ec8c30f060 | ||
|
|
7990400c7a | ||
|
|
2aaa2f3033 | ||
|
|
202b9f3075 | ||
|
|
eee5cf5fb4 | ||
|
|
be0814cdac | ||
|
|
f5c699ab7a | ||
|
|
80d719353c | ||
|
|
fa3fc12594 | ||
|
|
422a9db07b | ||
|
|
a4145a30f5 | ||
|
|
dcf1a593f7 | ||
|
|
785b61be2f | ||
|
|
39c7782019 | ||
|
|
1c5c51907a | ||
|
|
e004671346 | ||
|
|
a59014cad0 | ||
|
|
38e5c161e7 | ||
|
|
87650de9bc | ||
|
|
ee5596067e | ||
|
|
870fec6bbd | ||
|
|
686ab14b43 | ||
|
|
fced0cf3b1 | ||
|
|
25dd53290c | ||
|
|
a7c1f7ba69 | ||
|
|
de8e27feb8 | ||
|
|
e7144142e5 | ||
|
|
b6f2a434cf | ||
|
|
caf558f8dd | ||
|
|
6f2f1b7a76 | ||
|
|
744ef1958b | ||
|
|
08b44e1857 | ||
|
|
580b60550c | ||
|
|
e9755d437e | ||
|
|
20f695e8d7 | ||
|
|
1d7ff1f9e4 | ||
|
|
5b18f1d76d | ||
|
|
d880307a9b | ||
|
|
e5db66351e | ||
|
|
89153eef23 | ||
|
|
97c24c8b9c | ||
|
|
43535ae573 | ||
|
|
b7a8677036 | ||
|
|
61643f676c | ||
|
|
9b8c8c4971 | ||
|
|
033ca0d56b | ||
|
|
28a6797595 | ||
|
|
9ff2160c77 | ||
|
|
953db56a0d | ||
|
|
82cf474863 | ||
|
|
4c77b32171 | ||
|
|
34141ce9af | ||
|
|
edfcac3d5c | ||
|
|
774e11c827 | ||
|
|
58c867885c | ||
|
|
ccb6e25914 | ||
|
|
965d2d4036 | ||
|
|
9f8d7c9e41 | ||
|
|
8d352c1f82 | ||
|
|
faead09174 | ||
|
|
43f77376b6 | ||
|
|
ae3ce1220b | ||
|
|
6e3673136a | ||
|
|
c0ba92f503 | ||
|
|
90cb0357c6 | ||
|
|
d55e55ebcc | ||
|
|
c2522329fd | ||
|
|
2470c1788e | ||
|
|
230d259551 | ||
|
|
cb533335c4 | ||
|
|
a8890e4b13 | ||
|
|
0281e0dba4 | ||
|
|
1c209f49fc | ||
|
|
28caa1d47d | ||
|
|
a4701866a4 | ||
|
|
12f72c8ca9 | ||
|
|
c1165bd12d | ||
|
|
215fb53c52 | ||
|
|
8df780b237 | ||
|
|
79679cbb16 | ||
|
|
fb2db4b918 | ||
|
|
05b66f1dcf | ||
|
|
0f1b2003be | ||
|
|
ea6f0abf7c | ||
|
|
6f91da9461 | ||
|
|
45cdfff128 | ||
|
|
8c38e41261 | ||
|
|
63666fca48 | ||
|
|
d279b6c232 | ||
|
|
17f7f920c4 | ||
|
|
3197dfddd9 | ||
|
|
b55c86544b | ||
|
|
af1d5b86e1 | ||
|
|
d900516302 | ||
|
|
fa68a25bea | ||
|
|
2cc2d34719 | ||
|
|
4640d043e3 | ||
|
|
137e8d042f | ||
|
|
bc957893f4 | ||
|
|
b8107ee497 | ||
|
|
6b3a988526 | ||
|
|
5cb39874a2 | ||
|
|
4ceaedcbe8 | ||
|
|
f375cc9a82 | ||
|
|
9fc671cc17 | ||
|
|
5937ed57ce | ||
|
|
2e3ed0c23f | ||
|
|
5d1d2ef289 | ||
|
|
480c224250 | ||
|
|
cd731c3ad2 | ||
|
|
3fb3b45fdc | ||
|
|
9bc49e3381 | ||
|
|
0816adbaec | ||
|
|
1d69941882 | ||
|
|
f961b75bba | ||
|
|
8f600f334f | ||
|
|
cf55d12991 | ||
|
|
78919df886 | ||
|
|
1e16fb8ca2 | ||
|
|
c332528185 | ||
|
|
387c5e67f3 | ||
|
|
2ed780e14d | ||
|
|
1b8714fe7f | ||
|
|
e28f8cae74 | ||
|
|
5d600c6715 | ||
|
|
ea031a2161 | ||
|
|
87ef98dad5 | ||
|
|
4d4a04e9aa | ||
|
|
e6e71e9278 | ||
|
|
02220d02ed | ||
|
|
ff7b77bda7 | ||
|
|
f8ffae75c4 | ||
|
|
3ec797f56e | ||
|
|
cb350b26a1 | ||
|
|
dccebb0bea | ||
|
|
4cefbb52e1 | ||
|
|
d757009b48 | ||
|
|
ca202711e1 | ||
|
|
74f11859e4 | ||
|
|
47f80cf3db | ||
|
|
f04229d8cb | ||
|
|
076d64df8f | ||
|
|
a20dd3f955 | ||
|
|
982118c942 | ||
|
|
a51feb8638 | ||
|
|
3d7479f9aa | ||
|
|
9663964790 | ||
|
|
76ffc2d268 | ||
|
|
d0d118b31e | ||
|
|
2c0e18ce1c | ||
|
|
89876ef96f | ||
|
|
895b5b2ee1 | ||
|
|
9fd0e9af66 | ||
|
|
648a8f9237 | ||
|
|
c259b8ed46 | ||
|
|
b1df0ac194 | ||
|
|
4e1ae1bc1a | ||
|
|
b6ac1dea4d | ||
|
|
219d9af885 | ||
|
|
c6bba54573 | ||
|
|
f53cae0faa | ||
|
|
c1853a71a9 | ||
|
|
6953a57333 | ||
|
|
a109f11926 | ||
|
|
45c9904e05 | ||
|
|
08fc32cdc6 | ||
|
|
6c10f1e364 | ||
|
|
e8549ffb79 | ||
|
|
8a8d89dfc0 | ||
|
|
b6c4376217 | ||
|
|
cbb3f6672f | ||
|
|
bd5e47f5fc | ||
|
|
2aa756af38 | ||
|
|
78c2840b22 | ||
|
|
af0a516a79 | ||
|
|
cc97a8ffcc | ||
|
|
535e8653a0 | ||
|
|
210e5b0023 | ||
|
|
651beb4b9c | ||
|
|
f4d04a3dcb | ||
|
|
d573da55b0 | ||
|
|
3c4be537d9 | ||
|
|
6a87d5eea9 | ||
|
|
9800331505 | ||
|
|
7728d5b317 | ||
|
|
8f47ed8b0a | ||
|
|
c137e682dc | ||
|
|
14c639a425 | ||
|
|
06bfb1ad26 | ||
|
|
33c3611345 | ||
|
|
e012046f62 | ||
|
|
237d9d067d | ||
|
|
6519db82d1 | ||
|
|
0a60cbedb5 | ||
|
|
ebf3730454 | ||
|
|
3cf823ffb3 | ||
|
|
6231a9f931 | ||
|
|
dd30e939ae | ||
|
|
6f2e1d3794 | ||
|
|
d7c709607d | ||
|
|
2e41bd7607 | ||
|
|
bb7301fb63 | ||
|
|
b9907ec401 | ||
|
|
416e9e8e1d | ||
|
|
2918c57fb8 | ||
|
|
f55e0bf6e3 | ||
|
|
83d41dba6f | ||
|
|
7284fb539f | ||
|
|
3d16fa6f19 | ||
|
|
c65c4270c3 | ||
|
|
0099c282b6 | ||
|
|
f932f3efb1 | ||
|
|
9115e1a3a3 | ||
|
|
60bc88a075 | ||
|
|
a7044c73ba | ||
|
|
dc84ab3e41 | ||
|
|
6eb686c06b | ||
|
|
e81adb241b | ||
|
|
065b50f5a2 | ||
|
|
a6133e9c48 | ||
|
|
85b24e1e8d | ||
|
|
7bc000517f | ||
|
|
95d9403790 | ||
|
|
9653e72e47 | ||
|
|
a80f114d66 | ||
|
|
74ae4743d8 | ||
|
|
d1d30e7eb5 | ||
|
|
e83be01475 | ||
|
|
5d66eedcc7 | ||
|
|
974d43cb08 | ||
|
|
22efd6574d | ||
|
|
f8a2291a55 | ||
|
|
8c302e314f | ||
|
|
752f74767e | ||
|
|
e5319e04c7 | ||
|
|
e8e9037ef1 | ||
|
|
c6bfae0d63 | ||
|
|
a830c27ceb | ||
|
|
93bf198073 | ||
|
|
4c12af957c | ||
|
|
9ea3c54b92 | ||
|
|
4be8d77a79 | ||
|
|
4620764111 | ||
|
|
ca86137d0f | ||
|
|
b299a732c0 | ||
|
|
9fb7456b38 | ||
|
|
b3a3cca9fe | ||
|
|
f98009ec54 | ||
|
|
7a4c9d9933 | ||
|
|
91d15ea221 | ||
|
|
b043fec0d5 | ||
|
|
0bab46eb5c | ||
|
|
82bff09373 | ||
|
|
329b2d30d0 | ||
|
|
0d65b652d4 | ||
|
|
9d3c19e86a | ||
|
|
56a7800519 | ||
|
|
ba0cebd713 | ||
|
|
e28628d148 | ||
|
|
40bc860dc6 | ||
|
|
decf32fdd5 | ||
|
|
d9d6ee9922 | ||
|
|
903609a38f | ||
|
|
4504903b4c | ||
|
|
4bf4972b6e | ||
|
|
47e4b41dd2 | ||
|
|
af413ff1c0 | ||
|
|
5fcf0808c6 | ||
|
|
fb956b3aa1 | ||
|
|
93986af181 | ||
|
|
37a8bf7bfc | ||
|
|
e60e36a0e2 | ||
|
|
199e182399 | ||
|
|
a9f4b29f32 | ||
|
|
22cd43b8a2 | ||
|
|
669533cae6 | ||
|
|
d6efd469e4 | ||
|
|
0d4a6fc75f | ||
|
|
e403194bba | ||
|
|
b8c5a10551 | ||
|
|
7fdb0873db | ||
|
|
2d61644b05 | ||
|
|
084816fb9f | ||
|
|
68a89556d6 | ||
|
|
b5ea90f740 | ||
|
|
1a77c1fe36 | ||
|
|
4aa1bb7246 | ||
|
|
3a80120bf6 | ||
|
|
d01eccf912 | ||
|
|
25621f8deb | ||
|
|
dc006bd7f2 | ||
|
|
629f09089b | ||
|
|
e6839e4983 | ||
|
|
344ec94a3f | ||
|
|
62e89258e4 | ||
|
|
b6bb93f0b6 | ||
|
|
39a1d5cc89 | ||
|
|
8fa24de3d4 | ||
|
|
1def5015fb | ||
|
|
1dbc924d31 | ||
|
|
95da007107 | ||
|
|
82d3e466be | ||
|
|
b0dacf6b11 | ||
|
|
64d090839d | ||
|
|
633a7eac4e | ||
|
|
357fba5d2b | ||
|
|
b727f2fe1f | ||
|
|
4453eec687 | ||
|
|
c169eef161 | ||
|
|
17af55d3c8 | ||
|
|
8df12e5e9c | ||
|
|
cd423f23c6 | ||
|
|
86c2c4cd41 | ||
|
|
d9c4fc3721 | ||
|
|
b91e72d8a1 | ||
|
|
6cc96ef679 | ||
|
|
28fe951c40 | ||
|
|
22f789e77c | ||
|
|
2e5138eddc | ||
|
|
731c21f082 | ||
|
|
99d7672284 | ||
|
|
567fdd9619 | ||
|
|
6067e438a3 | ||
|
|
fc17a1742a | ||
|
|
12d3994f45 | ||
|
|
f7f1598e71 | ||
|
|
8caf559a1a | ||
|
|
e927161ec1 | ||
|
|
ba387a892f | ||
|
|
18015bde4f | ||
|
|
8affefbbab | ||
|
|
0225919a45 | ||
|
|
0901807db8 | ||
|
|
625cbfc50a | ||
|
|
b2bc5aff68 | ||
|
|
337c61db2c | ||
|
|
5c2c96fc2e | ||
|
|
04c77a8532 | ||
|
|
ebcf5b3177 | ||
|
|
9d2117e2ac | ||
|
|
c1c22dc6c6 | ||
|
|
1e10e3818e | ||
|
|
802c67ace4 | ||
|
|
5c3709b5d8 | ||
|
|
626c65df02 | ||
|
|
f2f492bf3f | ||
|
|
40f69d320e | ||
|
|
1893cd306a | ||
|
|
096b685e2c | ||
|
|
1965490bee | ||
|
|
559dcabf0e | ||
|
|
a9e8d8f8f7 | ||
|
|
dba67eea91 | ||
|
|
70fe6fda83 | ||
|
|
5155cf2b23 | ||
|
|
5ca9b95cca | ||
|
|
fa0da3a695 | ||
|
|
fa9d8a9b15 | ||
|
|
e3b6b24c5f | ||
|
|
1eb7205c12 | ||
|
|
92f4bdae03 | ||
|
|
bd63a460eb | ||
|
|
7403f60366 | ||
|
|
66295b0adf | ||
|
|
eb6d2fb0eb | ||
|
|
a8c34ccc1a | ||
|
|
8c501db2fa | ||
|
|
d2fbb9dfa7 | ||
|
|
e4c9b736f7 | ||
|
|
f02f446015 | ||
|
|
05d6d2fcd4 | ||
|
|
d5492442fb | ||
|
|
02e975f594 | ||
|
|
61800d8945 | ||
|
|
f450c9dbe3 | ||
|
|
b46574bef6 | ||
|
|
e3b3fa3342 | ||
|
|
21b2c0c26a | ||
|
|
dcbf54fae1 | ||
|
|
2fe6fb28e4 | ||
|
|
86022a967c | ||
|
|
0efbebd94f | ||
|
|
2aee2ea79e | ||
|
|
60a20b6984 | ||
|
|
fd753fb262 | ||
|
|
0ae57a017e | ||
|
|
88772a9ced | ||
|
|
65647f4aae | ||
|
|
5e6ccc44fc | ||
|
|
b9df8ad038 | ||
|
|
474cd1e55a | ||
|
|
b52e8bc87c | ||
|
|
f35723e772 | ||
|
|
415d1a2668 | ||
|
|
b3feb9bffd | ||
|
|
fdeabc15ab | ||
|
|
dfbf0d34b6 | ||
|
|
0c3fd8a6d9 | ||
|
|
5b9dd96e02 | ||
|
|
46c89a1bcf | ||
|
|
721760d679 | ||
|
|
2cdb874484 | ||
|
|
e5bccc470b | ||
|
|
ba768f8744 | ||
|
|
a33828467f | ||
|
|
6fed0f3b58 | ||
|
|
5f3599eaa7 | ||
|
|
44ca01aa27 | ||
|
|
451306f719 | ||
|
|
29518f3ba5 | ||
|
|
d74bfd834d | ||
|
|
ac8b5a7bcc | ||
|
|
8363cb7449 | ||
|
|
390cf6b642 | ||
|
|
0dbf00a767 | ||
|
|
a361e0b990 | ||
|
|
3a8ba4cbee | ||
|
|
d5c9e68a3e | ||
|
|
253d94c176 | ||
|
|
fd941e4701 | ||
|
|
a99198de48 | ||
|
|
e729e85c42 | ||
|
|
7eb9325047 | ||
|
|
ba4554da79 | ||
|
|
97fb1e00c2 | ||
|
|
3eb332f3d0 | ||
|
|
0d11bafb57 | ||
|
|
e01dfd76e8 | ||
|
|
854145e435 | ||
|
|
707bfd4241 | ||
|
|
02bc6e62e7 | ||
|
|
9fde4e2121 | ||
|
|
66eb4fb5ad | ||
|
|
e362f423c0 | ||
|
|
dc08eb7899 | ||
|
|
a1e307b4ce | ||
|
|
a0f16fc038 | ||
|
|
7c36c76b0d | ||
|
|
8488be311e | ||
|
|
2297862d81 | ||
|
|
539fdfa016 | ||
|
|
a7f6797499 | ||
|
|
cc7f745a0a | ||
|
|
265675795e | ||
|
|
2c789e48f3 | ||
|
|
9da6c50cbe | ||
|
|
5c53de8e76 | ||
|
|
3cdc826fca | ||
|
|
02292f99ab | ||
|
|
0279e75c4b | ||
|
|
44f1798dce | ||
|
|
23468a9908 | ||
|
|
8eb2aaa0a8 | ||
|
|
aa468e2153 | ||
|
|
9e5de82967 | ||
|
|
856e2be1ca | ||
|
|
8d4b023d61 | ||
|
|
da43a4d3b4 | ||
|
|
f06f11ad7a | ||
|
|
c807e37525 | ||
|
|
c9a8d2bd23 | ||
|
|
65c6c821e7 | ||
|
|
6cccacaaab | ||
|
|
7da97d69b0 | ||
|
|
f7574009b5 | ||
|
|
0416e883ca | ||
|
|
c0ccb86e3a | ||
|
|
69d21e20c9 | ||
|
|
7885f413b8 | ||
|
|
dfd5c5b508 | ||
|
|
bffbccac50 | ||
|
|
12c2d73846 | ||
|
|
27d15763f8 | ||
|
|
0eaa43f36b | ||
|
|
e30834bb2d | ||
|
|
fefb946a25 | ||
|
|
2d857ecf2f | ||
|
|
2cf179ccf6 | ||
|
|
5ebfc603e6 | ||
|
|
a925d6710a | ||
|
|
80d5272248 | ||
|
|
b4f6177be7 | ||
|
|
3907a1783a | ||
|
|
b676ea7127 | ||
|
|
ca65f5ad9a | ||
|
|
3e89b73ca0 | ||
|
|
17e9e836f6 | ||
|
|
c48d862d0f | ||
|
|
052282cff9 | ||
|
|
6d40166de7 | ||
|
|
ab7781b4fa | ||
|
|
f5218e207b | ||
|
|
7fb6a095c6 | ||
|
|
88669d2e0f | ||
|
|
1187d64f69 | ||
|
|
3074fc9ab5 | ||
|
|
bcea19001e | ||
|
|
1c98c53805 | ||
|
|
4799f6fe0a | ||
|
|
7470fb709f | ||
|
|
8c1e18b1cd | ||
|
|
9143187efd | ||
|
|
b661f39422 | ||
|
|
3a764a9da6 | ||
|
|
2341dfb95d | ||
|
|
ff121d2af5 | ||
|
|
55d7bab0e6 | ||
|
|
4a3d951329 | ||
|
|
014c297458 | ||
|
|
280da72e63 | ||
|
|
6277db8d45 | ||
|
|
60530a80d9 | ||
|
|
d79fac7729 | ||
|
|
195127b099 | ||
|
|
a3e74c55f1 | ||
|
|
e408bc9113 | ||
|
|
89dc917cb9 | ||
|
|
f83cdf2f5d | ||
|
|
f3040fc10d | ||
|
|
44acd79081 | ||
|
|
a3e4da0b3d | ||
|
|
73a52e5395 | ||
|
|
6cb1aa24cd | ||
|
|
f5c913d26e | ||
|
|
c86f14e75d | ||
|
|
65a97167de | ||
|
|
0530c57d31 | ||
|
|
adbe29e3d1 | ||
|
|
5fae07af11 | ||
|
|
400e5f60f2 | ||
|
|
3268225941 | ||
|
|
91fa39705d | ||
|
|
c41f86f0a4 | ||
|
|
81b741478a | ||
|
|
f6fc2f8808 | ||
|
|
6a0bb2f452 | ||
|
|
d02f85315a | ||
|
|
aa180e9f3f | ||
|
|
6b773d6b74 | ||
|
|
6cbaacf1e0 | ||
|
|
2ffb77cb4d | ||
|
|
59a57d6c3f | ||
|
|
8a332c1402 | ||
|
|
37855bfe7f | ||
|
|
f68b0117c4 | ||
|
|
aa867adbd3 | ||
|
|
3fd429c72a | ||
|
|
39bbb4c2bd | ||
|
|
64e6d0b1f8 | ||
|
|
26a2ef8fb7 | ||
|
|
a6c46ee55c | ||
|
|
f8d58cb74e | ||
|
|
3ea52a0198 | ||
|
|
054efb3435 | ||
|
|
fbd5c404d2 | ||
|
|
32b65e8dbc | ||
|
|
773b4fe02e | ||
|
|
1a62e5e42d | ||
|
|
f812460158 | ||
|
|
654c070976 | ||
|
|
e776ba1b33 | ||
|
|
7497371b32 | ||
|
|
50afc4c507 | ||
|
|
064e51d24e | ||
|
|
a029ec18a6 | ||
|
|
b0a6e5c946 | ||
|
|
e8c85d13ff | ||
|
|
b228438127 | ||
|
|
1fb48a1e8a | ||
|
|
91b6d498fe | ||
|
|
efe204c346 | ||
|
|
4a4cd9492a | ||
|
|
5446464d7e | ||
|
|
baa5258a43 | ||
|
|
df6a679548 | ||
|
|
3692f17e55 | ||
|
|
2bac94ad5c | ||
|
|
d46d80bd5c | ||
|
|
f0e5196659 | ||
|
|
05a459ea19 | ||
|
|
8e85d5a02a | ||
|
|
81036b9330 | ||
|
|
c91b7606a0 | ||
|
|
d6e7a331d5 | ||
|
|
831b0baddd | ||
|
|
e4bf2bd9ad | ||
|
|
7a4d8b824e | ||
|
|
ff34d1d5f9 | ||
|
|
c1ce24e5f0 | ||
|
|
1f450c83ec | ||
|
|
769000da2d | ||
|
|
a53c37bc3c | ||
|
|
333cc5996c | ||
|
|
bccc90f5a2 | ||
|
|
5575a66b8d | ||
|
|
0fee8143dd | ||
|
|
b6e26d15e1 | ||
|
|
7e0b2702de | ||
|
|
e46fb9dba7 | ||
|
|
f4dee75a17 | ||
|
|
6d1ff0cb49 | ||
|
|
3dcabc9502 | ||
|
|
10174aa7bc | ||
|
|
cd87cbe44e | ||
|
|
4594c7bf0a | ||
|
|
bf153eb96b | ||
|
|
d1a1dafcad | ||
|
|
246463a3ec | ||
|
|
4b4541515c | ||
|
|
1bb337c3dd | ||
|
|
8b380a01e6 | ||
|
|
5c32ec8cfa | ||
|
|
9660307f00 | ||
|
|
a3a757f842 | ||
|
|
a2727a110e | ||
|
|
dd1aba0d05 | ||
|
|
6692f8dce2 | ||
|
|
372b3145ea | ||
|
|
8b7a102927 | ||
|
|
24e1cf0d7d | ||
|
|
2ce88283a2 | ||
|
|
2ae2e23b57 | ||
|
|
9c7bb96b1c | ||
|
|
0f49208040 | ||
|
|
8f11a925df | ||
|
|
f65f7d68e6 | ||
|
|
f16fcf25e2 | ||
|
|
0048f9725d | ||
|
|
4bb5592e75 | ||
|
|
f5b18f953d | ||
|
|
73ff1b4fe5 | ||
|
|
d7fbd3c3bc | ||
|
|
0b62bee90d | ||
|
|
0c275cf490 | ||
|
|
8b466ef0a3 | ||
|
|
a4a88769af | ||
|
|
2dce8d09b8 | ||
|
|
e6e6401702 | ||
|
|
e83ece392c | ||
|
|
33887711f7 | ||
|
|
947bd547aa | ||
|
|
86e0f8ad34 | ||
|
|
bb161f9da8 | ||
|
|
07360efd17 | ||
|
|
7c8eaaa4f9 | ||
|
|
48acc8715b | ||
|
|
9db76f9a15 | ||
|
|
df0909483e | ||
|
|
ec8109644b | ||
|
|
315b389a66 | ||
|
|
bce30eb522 | ||
|
|
d05d1c6a48 | ||
|
|
1803e32322 | ||
|
|
01e04843bf | ||
|
|
3907f39c29 | ||
|
|
24281b512e | ||
|
|
f4f0478975 | ||
|
|
c570c0929f | ||
|
|
f53473f9e9 | ||
|
|
39e6d28826 | ||
|
|
0b56d07a67 |
@@ -263,6 +263,12 @@ jobs:
|
||||
command: |
|
||||
cargo fmt --check
|
||||
|
||||
- run:
|
||||
name: "lint"
|
||||
working_directory: "./render-wasm"
|
||||
command: |
|
||||
./lint
|
||||
|
||||
- run:
|
||||
name: "cargo tests"
|
||||
working_directory: "./render-wasm"
|
||||
|
||||
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
36
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,29 +1,19 @@
|
||||
<!--
|
||||
### Related Ticket
|
||||
|
||||
Some key notes before you open a PR:
|
||||
<!-- Reference the related GitHub/Taiga ticket. -->
|
||||
|
||||
1. Select which branch should this PR be merged in? By default, you should always merge to the develop branch.
|
||||
2. PR name follows [convention](http://karma-runner.github.io/4.0/dev/git-commit-msg.html)
|
||||
3. All tests pass locally, UI and Unit tests
|
||||
4. All business logic and validations must be on the server-side
|
||||
5. Update necessary Documentation
|
||||
6. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes
|
||||
### Summary
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Also, if you're new here
|
||||
### Checklist
|
||||
|
||||
- Contribution Guide => https://github.com/uxbox/uxbox/blob/develop/CONTRIBUTING.md
|
||||
- [ ] Choose the correct target branch; use `develop` by default.
|
||||
- [ ] Provide a brief summary of the changes introduced.
|
||||
- [ ] Add a detailed explanation of how to reproduce the issue and/or verify the fix, if applicable.
|
||||
- [ ] Include screenshots or videos, if applicable.
|
||||
- [ ] Add or modify existing integration tests in case of bugs or new features, if applicable.
|
||||
- [ ] Check CI passes successfully.
|
||||
- [ ] Update the `CHANGES.md` file, referencing the related GitHub issue, if applicable.
|
||||
|
||||
-->
|
||||
|
||||
> Please provide enough information so that others can review your pull request:
|
||||
|
||||
<!-- You can skip this if you're fixing a typo or updating existing documentation -->
|
||||
|
||||
> Explain the **details** for making this change. What existing problem does the pull request solve?
|
||||
|
||||
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
|
||||
|
||||
> Screenshots/GIFs
|
||||
|
||||
<!-- Add images/recordings to better visualize the change: expected/current behviour -->
|
||||
<!-- For more details, check the contribution guidelines: https://github.com/penpot/penpot/blob/develop/CONTRIBUTING.md -->
|
||||
|
||||
200
CHANGES.md
200
CHANGES.md
@@ -1,6 +1,51 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.5.3
|
||||
## 2.8.0 (Next / Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
**Breaking changes on penpot library:**
|
||||
|
||||
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
|
||||
`name`, and `background` props (instead of the previous positional arguments)
|
||||
- Rename the `file.createRect` method to `file.addRect`
|
||||
- Rename the `file.createCircle` method to `file.addCircle`
|
||||
- Rename the `file.createPath` method to `file.addPath`
|
||||
- Rename the `file.createText` method to `file.addText`
|
||||
- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style)
|
||||
- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style)
|
||||
- Rename `file.lookupShape` to `file.getShape`
|
||||
- Rename `file.asMap` to `file.toMap`
|
||||
- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color)
|
||||
- Remove `file.deleteLibraryColor` (this library is intended to build files)
|
||||
- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography)
|
||||
- Remove `file.deleteLibraryTypography` (this library is intended to build files)
|
||||
- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
|
||||
- Remove `file.deleteObject` (this library is intended to build files)
|
||||
- Remove `file.updateObject` (this library is intended to build files)
|
||||
- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes)
|
||||
- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property
|
||||
- Add `file.currentFrameId` read-only property
|
||||
- Add `file.lastId` read-only property
|
||||
|
||||
There are also relevant semantic changes in how components should be created: this refactor removes
|
||||
all notions of the old components (v1). Since v2, the shapes that are part of a component live on a
|
||||
page. So, from now on, to create a component, you should first create a frame, then add shapes
|
||||
and/or groups to that frame, and then create a component by declaring that frame as the component
|
||||
root.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Optimize profile setup flow for better user experience [Taiga #10028](https://tree.taiga.io/project/penpot/us/10028)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
## 2.7.0 (Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -8,21 +53,148 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Design improvements to the Invitations page with an empty state [GitHub #2608](https://github.com/penpot/penpot/issues/2608) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Update board presets with a newer devices [Taiga #10610](https://tree.taiga.io/project/penpot/us/10610)
|
||||
- Propagate "sharing a prototype" to editors and viewers [Taiga #8853](https://tree.taiga.io/project/penpot/us/8853)
|
||||
- Design improvements to the Invitations page with an empty state [Taiga #4554](https://tree.taiga.io/project/penpot/us/4554)
|
||||
- Duplicate token sets [Taiga #10694](https://tree.taiga.io/project/penpot/issue/10694)
|
||||
- Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746)
|
||||
- Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668)
|
||||
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
|
||||
- Fix resize bar background on tokens panel [Taiga #10811](https://tree.taiga.io/project/penpot/issue/10811)
|
||||
- Fix shortcut for history version panel [Taiga #11006](https://tree.taiga.io/project/penpot/issue/11006)
|
||||
- Fix positioning of comment drafts when near the right / bottom edges of viewport [Taiga #10534](https://tree.taiga.io/project/penpot/issue/10534)
|
||||
- Fix path having a wrong selrect [Taiga #10257](https://tree.taiga.io/project/penpot/issue/10257)
|
||||
- Fix SVG `stroke-linecap` property when importing SVGs [Taiga #9489](https://tree.taiga.io/project/penpot/issue/9489)
|
||||
- Fix position problems cutting-pasting a component [Taiga #10677](https://tree.taiga.io/project/penpot/issue/10677)
|
||||
- Fix design tab has a horizontal scroll [Taiga #10660](https://tree.taiga.io/project/penpot/issue/10660)
|
||||
- Fix long file names being clipped when longer than allowed length [Taiga #10662](https://tree.taiga.io/project/penpot/issue/10662)
|
||||
- Fix problem with error detail in toast [Taiga #10519](https://tree.taiga.io/project/penpot/issue/10519)
|
||||
- Fix view mode error when an external user tries to export something from a prototype using a shared link [Taiga #10251](https://tree.taiga.io/project/penpot/issue/10251)
|
||||
- Fix merge path nodes with only one node selected [Taiga #9626](https://tree.taiga.io/project/penpot/issue/9626)
|
||||
- Fix problem with import errors [Taiga #10040](https://tree.taiga.io/project/penpot/issue/10040)
|
||||
- Fix color gradient on texts [Taiga Issue #7488](https://tree.taiga.io/project/penpot/issue/7488)
|
||||
- Add support for self mentions [Taiga #10809](https://tree.taiga.io/project/penpot/issue/10809)
|
||||
- Fix team info settings alignment [Taiga #10869](https://tree.taiga.io/project/penpot/issue/10869)
|
||||
- Fix left sidebar horizontal scroll on nested layers [Taiga #10791](https://tree.taiga.io/project/penpot/issue/10791)
|
||||
- Improve error message details importing tokens [Taiga Issue #10772](https://tree.taiga.io/project/penpot/issue/10772)
|
||||
- Fix no selected set after Drag & Drop [Github #71](https://github.com/tokens-studio/penpot/issues/71)
|
||||
- Styledictionary v5 Update [Github #6283](https://github.com/penpot/penpot/pull/6283)
|
||||
- Fix Rename a set throws an internal error [Github #78](https://github.com/tokens-studio/penpot/issues/78)
|
||||
- Fix Out of Sync Token Value & Color Picker [Github #102](https://github.com/tokens-studio/penpot/issues/102)
|
||||
- Fix Color should preserve color space [Github #69](https://github.com/tokens-studio/penpot/issues/69)
|
||||
- Fix cannot rename Design Token Sets when group of same name exists [Taiga Issue #10773](https://tree.taiga.io/project/penpot/issue/10773)
|
||||
- Fix problem when duplicating grid layout [Github #6391](https://github.com/penpot/penpot/issues/6391)
|
||||
- Fix issue that makes workspace shortcuts stop working [Taiga #11062](https://tree.taiga.io/project/penpot/issue/11062)
|
||||
|
||||
## 2.6.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Increase the height of the right sidebar dropdowns [Taiga #10615](https://tree.taiga.io/project/penpot/issue/10615)
|
||||
- Fix scroll on token themes modal [Taiga #10745](https://tree.taiga.io/project/penpot/issue/10745)
|
||||
- Fix collapsing grouped sets in "edit Theme" closes the dialog [Taiga #10771](https://tree.taiga.io/project/penpot/issue/10771)
|
||||
- Fix unexpected exception on path editor on merge segments when undo stack is empty
|
||||
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
|
||||
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
|
||||
- Fix several issues with internal srepl helpers
|
||||
- Fix unexpected exception on template import from libraries
|
||||
- Fix incorrect uuid parsing from different parts of code
|
||||
- Fix update layout on component restore [Taiga #10637](https://tree.taiga.io/project/penpot/issue/10637)
|
||||
- Fix horizontal scroll in viewer [Github #6290](https://github.com/penpot/penpot/issues/6290)
|
||||
- Fix detach component in a particular case [Taiga #10837](https://tree.taiga.io/project/penpot/issue/10837)
|
||||
|
||||
## 2.6.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix webhooks not shown in list [Taiga #10763](https://tree.taiga.io/project/penpot/issue/10763)
|
||||
- Fix colorpicker scroll when dropdown displayed [Taiga #10696](https://tree.taiga.io/project/penpot/issue/10696)
|
||||
- Clean internal workspace state on exit or url changed [Taiga #10619](https://tree.taiga.io/project/penpot/issue/10619)
|
||||
|
||||
## 2.6.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Design Tokens
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- [COMMENTS] "Mark All as Read" Functionality in Dashboard [Taiga #9235](https://tree.taiga.io/project/penpot/us/9235)
|
||||
- [COMMENTS] Bubble Groups [Taiga #9236](https://tree.taiga.io/project/penpot/us/9236)
|
||||
- Change templates carrousel [Taiga #9803](https://tree.taiga.io/project/penpot/us/9803)
|
||||
- [DESIGN TOKENS] Tokens CRUD. Types added: Color, Opacity, Border radius, Dimension, Sizing, Spacing, Rotation and Stroke.
|
||||
- [DESIGN TOKENS] Create references (alias) that point to other tokens.
|
||||
- [DESIGN TOKENS] Math operations in token values.
|
||||
- [DESIGN TOKENS] Sets CRUD, grouping and reordering.
|
||||
- [DESIGN TOKENS] Multidimensional Themes and Sets management.
|
||||
- [DESIGN TOKENS] Apply/Remove tokens to/from elements from the Tokens tab.
|
||||
- [DESIGN TOKENS] Integration with components.
|
||||
- [DESIGN TOKENS] Import and export tokens from a JSON file.
|
||||
- [DESIGN TOKENS] Apply Themes and Sets at document level.
|
||||
- Add more descriptive tooltip to boards for first time users [Taiga #9426](https://tree.taiga.io/project/penpot/us/9426)
|
||||
- First State of a Project Changes Consolidation [Taia #10605](https://tree.taiga.io/project/penpot/us/10605)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix opacity in frame containers [Github #5858](https://github.com/penpot/penpot/pull/5858)
|
||||
- Avoid resizing on click [Taiga #10213](https://tree.taiga.io/project/penpot/issue/10213)
|
||||
- Hide horizontal scroll from dashboard sidebar [Taiga #10422](https://tree.taiga.io/project/penpot/issue/10422)
|
||||
- Fix cut and paste a copy a cmponent inside its parent [Taiga #10365](https://tree.taiga.io/project/penpot/us/10365)
|
||||
- Fix duplicate page with component over frame [Taiga #8151](https://tree.taiga.io/project/penpot/issue/8151) and [Taiga #9698](https://tree.taiga.io/project/penpot/issue/9698)
|
||||
- The plugin list in the navigation menu lacks scrolling, some plugins are not visible when a large number are installed [Taiga #9360](https://tree.taiga.io/project/penpot/us/9360)
|
||||
- Fix hidden toolbar click event still available [Taiga #10437](https://tree.taiga.io/project/penpot/us/10437)
|
||||
- Fix hovering over templates [Taiga #10545](https://tree.taiga.io/project/penpot/issue/10545)
|
||||
- Fix problem with default shadows value in plugins [Plugins #191](https://github.com/penpot/penpot-plugins/issues/191)
|
||||
- Fix problem with constraints when creating group [Taiga #10455](https://tree.taiga.io/project/penpot/issue/10455)
|
||||
- Fix opening pen with shortcut multiple times breaks toolbar [Taiga #10566](https://tree.taiga.io/project/penpot/issue/10566)
|
||||
- Fix actions when workspace is visited first time [Taiga #10548](https://tree.taiga.io/project/penpot/issue/10548)
|
||||
- Chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
|
||||
- Fix assets name on inspect tab [Taiga #10630](https://tree.taiga.io/project/penpot/issue/10630)
|
||||
- Fix chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
|
||||
- Fix incorrect handling of background task result (now task rows are properly marked as completed)
|
||||
- Fix available size of resize handler [Taiga #10639](https://tree.taiga.io/project/penpot/issue/10639)
|
||||
- Internal error when install a plugin by penpothub - Try plugin [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
|
||||
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
|
||||
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix feature loading on workspace when opening a file in a background
|
||||
tab [Taiga #10377](https://tree.taiga.io/project/penpot/issue/10377)
|
||||
- Fix minor inconsistencies on RPC `get-file-libraries` and `get-file`
|
||||
methods (add missing team-id prop)
|
||||
- Fix problem with viewer role and inspect mode [Taiga #9751](https://tree.taiga.io/project/penpot/issue/9751)
|
||||
- Fix error when clicking on a comment at the viewer's sidebar [Taiga #10465](https://tree.taiga.io/project/penpot/issue/10465)
|
||||
|
||||
## 2.5.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Component sync issues with multiple tabs [Taiga #10471](https://tree.taiga.io/project/penpot/issue/10471)
|
||||
|
||||
## 2.5.2
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- When the workspace is empty, set default the board creation tool [Taiga #9425](https://tree.taiga.io/project/penpot/us/9425)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -34,22 +206,12 @@
|
||||
|
||||
## 2.5.1
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve Nginx entryponit to get the resolvers dinamically by default
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
Although this is not a breaking change, we believe it’s important to highlight it in this
|
||||
@@ -78,9 +240,6 @@ If you have a big database and many cores available, you can reduce the time of
|
||||
all files by increasing paralelizacion changing the `max-jobs` value from 1 to N (where N
|
||||
is a number of cores)
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- [GRADIENTS] New gradients UI with multi-stop support. [Taiga #3418](https://tree.taiga.io/project/penpot/epic/3418)
|
||||
@@ -97,6 +256,7 @@ is a number of cores)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix menu shadow color [Taiga #10102](https://tree.taiga.io/project/penpot/issue/10102)
|
||||
- Fix missing state refresh on notifications update [Taiga #10253](https://tree.taiga.io/project/penpot/issue/10253)
|
||||
- Fix icon visualization on select component [Taiga #8889](https://tree.taiga.io/project/penpot/issue/8889)
|
||||
- Fix typo on integration tests docs [Taiga #10112](https://tree.taiga.io/project/penpot/issue/10112)
|
||||
|
||||
39
README.md
39
README.md
@@ -16,18 +16,18 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a> •
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://penpot.app/learning-center"><b>Learning Center</b></a> •
|
||||
<a href="https://community.penpot.app/"><b>Community</b></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
||||
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
||||
|
||||
</p>
|
||||
@@ -40,12 +40,13 @@
|
||||
|
||||
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
||||
|
||||
Penpot is available on browser and [self host](https://penpot.app/self-host). It’s web-based and works with open standards (SVG, CSS and HTML). And last but not least, it’s free!
|
||||
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
||||
|
||||
Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. Plus, it's faster and more accessible.
|
||||
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
||||
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
||||
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us)
|
||||
|
||||
|
||||
🎇 **Penpot Fest** is our design, code & Open Source event. Check out the highlights from [Penpot Fest 2023 edition](https://www.youtube.com/watch?v=sOpLZaK5mDc)!
|
||||
🎇 Design, code, and Open Source meet at [Penpot Fest](https://penpot.app/penpotfest)! Be part of the 2025 edition in Madrid, Spain, on October 9-10.
|
||||
|
||||
## Table of contents ##
|
||||
|
||||
@@ -61,7 +62,7 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||
|
||||
### Plugin system ###
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
|
||||
### Designed for developers ###
|
||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||
@@ -78,6 +79,10 @@ Penpot offers integration into the development toolchain, thanks to its support
|
||||
### What’s great for design ###
|
||||
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
|
||||
|
||||
### Design Tokens ###
|
||||
With Penpot’s standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
@@ -125,13 +130,13 @@ You will find the following categories:
|
||||
|
||||
## Contributing ##
|
||||
|
||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||
|
||||
Choose your way:
|
||||
Choose your way:
|
||||
|
||||
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||
- Invite your [team to join](https://design.penpot.app/#/auth/register)
|
||||
- Star this repo and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app) and [X](https://twitter.com/penpotapp).
|
||||
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app)
|
||||
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
||||
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
|
||||
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -151,6 +151,78 @@ Debug Main Page
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="widget">
|
||||
<h2>Feature Flags</h2>
|
||||
<fieldset>
|
||||
<legend>Enable</legend>
|
||||
<desc>Add a feature flag to a team</desc>
|
||||
<form method="post" action="/dbg/actions/add-team-feature">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Disable</legend>
|
||||
<desc>Remove a feature flag from a team</desc>
|
||||
<form method="post" action="/dbg/actions/remove-team-feature">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
|
||||
@@ -35,40 +35,35 @@ def get_prepl_conninfo():
|
||||
|
||||
return host, port
|
||||
|
||||
def send_eval(expr):
|
||||
def send(data):
|
||||
host, port = get_prepl_conninfo()
|
||||
with socket.create_connection((host, port)) as s:
|
||||
f = s.makefile(mode="rw")
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
s.send(expr.encode("utf-8"))
|
||||
s.send(b":repl/quit\n\n")
|
||||
json.dump(data, f)
|
||||
f.write("\n")
|
||||
f.flush()
|
||||
|
||||
with s.makefile() as f:
|
||||
while True:
|
||||
line = f.readline()
|
||||
result = json.loads(line)
|
||||
tag = result.get("tag", None)
|
||||
if tag == "ret":
|
||||
return result.get("val", None), result.get("exception", None)
|
||||
elif tag == "out":
|
||||
print(result.get("val"), end="")
|
||||
else:
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
while True:
|
||||
line = f.readline()
|
||||
result = json.loads(line)
|
||||
tag = result.get("tag", None)
|
||||
|
||||
def encode(val):
|
||||
return json.dumps(json.dumps(val))
|
||||
if tag == "ret":
|
||||
return result.get("val", None), result.get("err", None)
|
||||
elif tag == "out":
|
||||
print(result.get("val"), end="")
|
||||
else:
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
|
||||
def print_error(res):
|
||||
for error in res["via"]:
|
||||
print("ERR:", error["message"])
|
||||
break
|
||||
def print_error(error):
|
||||
print("ERR:", error["hint"])
|
||||
|
||||
def run_cmd(params):
|
||||
try:
|
||||
expr = "(app.srepl.cli/exec {})".format(encode(params))
|
||||
res, failed = send_eval(expr)
|
||||
if failed:
|
||||
print_error(res)
|
||||
res, err = send(params)
|
||||
if err:
|
||||
print_error(err)
|
||||
sys.exit(-1)
|
||||
|
||||
return res
|
||||
@@ -96,7 +91,7 @@ def update_profile(email, fullname, password, is_active):
|
||||
"email": email,
|
||||
"fullname": fullname,
|
||||
"password": password,
|
||||
"is_active": is_active
|
||||
"isActive": is_active
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +133,7 @@ def derive_password(password):
|
||||
params = {
|
||||
"cmd": "derive-password",
|
||||
"params": {
|
||||
"password": password,
|
||||
"password": password
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptons \
|
||||
enable-subscriptons-old";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -70,15 +72,18 @@ export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||
|
||||
export JAVA_OPTS="--enable-preview \
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints";
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:jmx-remote -A:dev"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-preview $JVM_OPTS"
|
||||
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-native-access=ALL-UNNAMED --enable-preview $JVM_OPTS"
|
||||
|
||||
ENTRYPOINT=${1:-app.main};
|
||||
|
||||
|
||||
@@ -23,18 +23,9 @@ export PENPOT_FLAGS="\
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Djdk.attach.allowAttachSelf \
|
||||
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
|
||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-J-XX:+EnableDynamicAgentLoading \
|
||||
-J-XX:-OmitStackTraceInFastThrow \
|
||||
-J-XX:+UnlockDiagnosticVMOptions \
|
||||
-J-XX:+DebugNonSafepoints"
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptons \
|
||||
enable-subscriptons-old ";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -65,6 +56,20 @@ export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
|
||||
entrypoint=${1:-app.main};
|
||||
|
||||
set -ex
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
clojure $OPTIONS -A:dev -M -m $entrypoint;
|
||||
export OPTIONS="-A:jmx-remote -A:dev"
|
||||
|
||||
set -ex
|
||||
clojure $OPTIONS -M -m $entrypoint;
|
||||
|
||||
107
backend/src/app/binfile/cleaner.clj
Normal file
107
backend/src/app/binfile/cleaner.clj
Normal file
@@ -0,0 +1,107 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.binfile.cleaner
|
||||
"A collection of helpers for perform cleaning of artifacts; mainly
|
||||
for recently imported shapes."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PRE DECODE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn clean-shape-pre-decode
|
||||
"Applies a pre-decode phase migration to the shape"
|
||||
[shape]
|
||||
(if (= "bool" (:type shape))
|
||||
(if-let [content (get shape :bool-content)]
|
||||
(-> shape
|
||||
(assoc :content content)
|
||||
(dissoc :bool-content))
|
||||
shape)
|
||||
shape))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; POST DECODE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- fix-shape-shadow-color
|
||||
"Some shapes can come with invalid `id` property on shadow colors
|
||||
caused by incorrect uuid parsing bug that should be already fixed;
|
||||
this function removes the invalid id from the data structure."
|
||||
[shape]
|
||||
(let [fix-color
|
||||
(fn [{:keys [id] :as color}]
|
||||
(if (uuid? id)
|
||||
color
|
||||
(if (and (string? id)
|
||||
(re-matches uuid/regex id))
|
||||
(assoc color :id (uuid/uuid id))
|
||||
(dissoc color :id))))
|
||||
|
||||
fix-shadow
|
||||
(fn [shadow]
|
||||
(d/update-when shadow :color fix-color))
|
||||
|
||||
xform
|
||||
(map fix-shadow)]
|
||||
|
||||
(d/update-when shape :shadow
|
||||
(fn [shadows]
|
||||
(into [] xform shadows)))))
|
||||
|
||||
(defn- fix-root-shape
|
||||
"Ensure all root objects are well formed shapes"
|
||||
[shape]
|
||||
(if (= (:id shape) uuid/zero)
|
||||
(-> shape
|
||||
(assoc :parent-id uuid/zero)
|
||||
(assoc :frame-id uuid/zero)
|
||||
;; We explicitly dissoc them and let the shape-setup
|
||||
;; to regenerate it with valid values.
|
||||
(dissoc :selrect)
|
||||
(dissoc :points)
|
||||
(cts/setup-shape))
|
||||
shape))
|
||||
|
||||
(defn- fix-legacy-flex-dir
|
||||
"This operation is only relevant to old data and it is fixed just
|
||||
for convenience."
|
||||
[shape]
|
||||
(d/update-when shape :layout-flex-dir
|
||||
(fn [dir]
|
||||
(case dir
|
||||
:reverse-row :row-reverse
|
||||
:reverse-column :column-reverse
|
||||
dir))))
|
||||
|
||||
(defn clean-shape-post-decode
|
||||
"A shape procesor that expected to be executed after schema decoding
|
||||
process but before validation."
|
||||
[shape]
|
||||
(-> shape
|
||||
(fix-shape-shadow-color)
|
||||
(fix-root-shape)
|
||||
(fix-legacy-flex-dir)))
|
||||
|
||||
(defn- fix-container
|
||||
[container]
|
||||
(-> container
|
||||
;; Remove possible `nil` keys on objects
|
||||
(d/update-when :objects dissoc nil)
|
||||
(d/update-when :objects d/update-vals clean-shape-post-decode)))
|
||||
|
||||
(defn clean-file
|
||||
[file & {:as _opts}]
|
||||
(update file :data
|
||||
(fn [data]
|
||||
(-> data
|
||||
(d/update-when :pages-index d/update-vals fix-container)
|
||||
(d/update-when :components d/update-vals fix-container)
|
||||
(d/without-nils)))))
|
||||
@@ -431,15 +431,21 @@
|
||||
(update :components relink-shapes)
|
||||
(update :media relink-media)
|
||||
(update :colors relink-colors)
|
||||
(d/without-nils))))))
|
||||
(d/without-nils))))
|
||||
|
||||
;; NOTE: this is necessary because when we just creating a new
|
||||
;; file from imported artifact or cloned file there are no
|
||||
;; migrations registered on the database, so we need to persist
|
||||
;; all of them, not only the applied
|
||||
(vary-meta dissoc ::fmg/migrated)))
|
||||
|
||||
(defn encode-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [file (if (contains? (:features file) "fdata/objects-map")
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id features] :as file}]
|
||||
(let [file (if (contains? features "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
file (if (contains? features "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (feat.fdata/enable-pointer-map file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
;; There is no migration needed, but we don't want to allow
|
||||
;; copy paste nor import of variant files into no-variant teams
|
||||
"variants/v1"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
"A ZIP based binary file exportation"
|
||||
(:refer-clojure :exclude [read])
|
||||
(:require
|
||||
[app.binfile.cleaner :as bfl]
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.migrations :as bfm]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.migrations :as-alias fmg]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
@@ -594,16 +596,35 @@
|
||||
|
||||
(defn- read-file-components
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-component)
|
||||
(validate-component))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
(let [clean-component-post-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
(fn [objects]
|
||||
(reduce-kv (fn [objects id shape]
|
||||
(assoc objects id (bfl/clean-shape-post-decode shape)))
|
||||
objects
|
||||
objects))))
|
||||
clean-component-pre-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
(fn [objects]
|
||||
(reduce-kv (fn [objects id shape]
|
||||
(assoc objects id (bfl/clean-shape-pre-decode shape)))
|
||||
objects
|
||||
objects))))]
|
||||
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(clean-component-pre-decode)
|
||||
(decode-component)
|
||||
(clean-component-post-decode)
|
||||
(validate-component))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty))))
|
||||
|
||||
(defn- read-file-typographies
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
@@ -630,7 +651,9 @@
|
||||
(->> (keep (match-shape-entry-fn file-id page-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(bfl/clean-shape-pre-decode)
|
||||
(decode-shape)
|
||||
(bfl/clean-shape-post-decode)
|
||||
(validate-shape))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
@@ -735,7 +758,6 @@
|
||||
(dissoc :options)
|
||||
(bfc/process-file))]
|
||||
|
||||
|
||||
(bfm/register-pending-migrations! cfg file)
|
||||
(bfc/save-file! cfg file ::db/return-keys false)
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
[clojure.set :as set]
|
||||
[integrant.core :as ig]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.date-time :as jdbc-dt])
|
||||
[next.jdbc.date-time :as jdbc-dt]
|
||||
[next.jdbc.transaction])
|
||||
(:import
|
||||
com.zaxxer.hikari.HikariConfig
|
||||
com.zaxxer.hikari.HikariDataSource
|
||||
@@ -41,6 +42,8 @@
|
||||
org.postgresql.util.PGInterval
|
||||
org.postgresql.util.PGobject))
|
||||
|
||||
(def ^:dynamic *conn* nil)
|
||||
|
||||
(declare open)
|
||||
(declare create-pool)
|
||||
|
||||
@@ -223,16 +226,6 @@
|
||||
(let [^OutputStream os (.getOutputStream ^LargeObject lobj)]
|
||||
(io/make-output-stream os opts))))
|
||||
|
||||
(defmacro with-atomic
|
||||
[& args]
|
||||
(if (symbol? (first args))
|
||||
(let [cfgs (first args)
|
||||
body (rest args)]
|
||||
`(jdbc/with-transaction [conn# (::pool ~cfgs)]
|
||||
(let [~cfgs (assoc ~cfgs ::conn conn#)]
|
||||
~@body)))
|
||||
`(jdbc/with-transaction ~@args)))
|
||||
|
||||
(defn open
|
||||
[system-or-pool]
|
||||
(if (pool? system-or-pool)
|
||||
@@ -535,43 +528,31 @@
|
||||
(l/trc :hint "explicit rollback requested (savepoint)")
|
||||
(.rollback conn sp))))
|
||||
|
||||
(defn transact!
|
||||
"A lower-level function for executing function in a transaction"
|
||||
([transactable f] (transact! transactable f {}))
|
||||
([transactable f opts]
|
||||
(binding [next.jdbc.transaction/*nested-tx* :ignore]
|
||||
(jdbc/transact transactable f opts))))
|
||||
|
||||
(defn tx-run!
|
||||
"Run a function in a transaction."
|
||||
[system f & params]
|
||||
(cond
|
||||
(connection? system)
|
||||
(if (connection? system)
|
||||
(tx-run! {::conn system} f)
|
||||
|
||||
(pool? system)
|
||||
(tx-run! {::pool system} f)
|
||||
|
||||
(::conn system)
|
||||
(let [conn (::conn system)
|
||||
sp (savepoint conn)]
|
||||
(try
|
||||
(let [system' (-> system
|
||||
(assoc ::savepoint sp)
|
||||
(dissoc ::rollback))
|
||||
result (apply f system' params)]
|
||||
(if (::rollback system)
|
||||
(rollback! conn sp)
|
||||
(release! conn sp))
|
||||
result)
|
||||
(catch Throwable cause
|
||||
(.rollback ^Connection conn ^Savepoint sp)
|
||||
(throw cause))))
|
||||
|
||||
(::pool system)
|
||||
(with-atomic [conn (::pool system)]
|
||||
(let [system' (-> system
|
||||
(assoc ::conn conn)
|
||||
(dissoc ::rollback))
|
||||
result (apply f system' params)]
|
||||
(when (::rollback system)
|
||||
(rollback! conn))
|
||||
result))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid system/cfg provided"))))
|
||||
(if (pool? system)
|
||||
(tx-run! {::pool system} f)
|
||||
(if-let [conn (or (::conn system)
|
||||
(::pool system))]
|
||||
(transact! conn
|
||||
(fn [conn]
|
||||
(let [system' (-> system
|
||||
(dissoc ::rollback)
|
||||
(assoc ::conn conn))]
|
||||
(apply f system' params)))
|
||||
{:rollback-only (::rollback system)
|
||||
:read-only (::read-only system)})
|
||||
(throw (IllegalArgumentException. "invalid system/cfg provided"))))))
|
||||
|
||||
(defn run!
|
||||
[system f & params]
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gshp]
|
||||
[app.common.logging :as l]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.math :as mth]
|
||||
@@ -36,9 +35,9 @@
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.path :as ctsp]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -127,10 +126,10 @@
|
||||
(sm/lazy-validator ::ctsx/content))
|
||||
|
||||
(def valid-path-content?
|
||||
(sm/lazy-validator ::ctsp/content))
|
||||
(sm/lazy-validator ::path/segments))
|
||||
|
||||
(def valid-path-segment?
|
||||
(sm/lazy-validator ::ctsp/segment))
|
||||
(sm/lazy-validator ::path/segment))
|
||||
|
||||
(def valid-rgb-color-string?
|
||||
(sm/lazy-validator ::ctc/rgb-color))
|
||||
@@ -580,12 +579,10 @@
|
||||
(let [shape (update shape :content fix-path-content)]
|
||||
(if (not (valid-path-content? (:content shape)))
|
||||
shape
|
||||
(let [[points selrect] (gshp/content->points+selrect shape (:content shape))]
|
||||
(-> shape
|
||||
(dissoc :bool-content)
|
||||
(dissoc :bool-type)
|
||||
(assoc :points points)
|
||||
(assoc :selrect selrect)))))
|
||||
(-> shape
|
||||
(dissoc :bool-content)
|
||||
(dissoc :bool-type)
|
||||
(path/update-geometry))))
|
||||
|
||||
;; When we fount a bool shape with no content,
|
||||
;; we convert it to a simple rect
|
||||
@@ -1071,7 +1068,7 @@
|
||||
groups (d/group-by #(first (cfh/split-path (:path %))) assets)
|
||||
;; If there is a group called as the generic-name we have to preserve it
|
||||
unames (into #{} (keep str) (keys groups))
|
||||
groups (rename-keys groups {generic-name (cfh/generate-unique-name unames generic-name)})
|
||||
groups (rename-keys groups {generic-name (cfh/generate-unique-name generic-name unames)})
|
||||
|
||||
;; Split large groups in chunks of max-group-size elements
|
||||
groups (loop [groups (seq groups)
|
||||
@@ -1462,8 +1459,6 @@
|
||||
(:objects page)
|
||||
(:id page)
|
||||
file-id
|
||||
true
|
||||
nil
|
||||
cfsh/prepare-create-artboard-from-selection)]
|
||||
|
||||
(shape-cb shape)
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.types.path :as path]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
@@ -30,7 +33,7 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn enable-objects-map
|
||||
[file]
|
||||
[file & _opts]
|
||||
(let [update-page
|
||||
(fn [page]
|
||||
(if (and (pmap/pointer-map? page)
|
||||
@@ -136,10 +139,56 @@
|
||||
|
||||
(defn enable-pointer-map
|
||||
"Enable the fdata/pointer-map feature on the file."
|
||||
[file]
|
||||
[file & _opts]
|
||||
(-> file
|
||||
(update :data (fn [fdata]
|
||||
(-> fdata
|
||||
(update :pages-index d/update-vals pmap/wrap)
|
||||
(d/update-when :components pmap/wrap))))
|
||||
(update :features conj "fdata/pointer-map")))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PATH-DATA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn enable-path-data
|
||||
"Enable the fdata/path-data feature on the file."
|
||||
[file & _opts]
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/path-shape? object)
|
||||
(cfh/bool-shape? object))
|
||||
(update object :content path/content)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> file
|
||||
(update :data (fn [data]
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
(update :features conj "fdata/path-data"))))
|
||||
|
||||
(defn disable-path-data
|
||||
[file & _opts]
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/path-shape? object)
|
||||
(cfh/bool-shape? object))
|
||||
(update object :content vec)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(when-let [conn db/*conn*]
|
||||
(db/delete! conn :file-migration {:file-id (:id file)
|
||||
:name "0003-convert-path-content"}))
|
||||
(-> file
|
||||
(update :data (fn [data]
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
(update :features disj "fdata/path-data")
|
||||
(update :migrations disj "0003-convert-path-content")
|
||||
(vary-meta update ::fmg/migrated disj "0003-convert-path-content"))))
|
||||
|
||||
@@ -155,10 +155,10 @@
|
||||
[["" {:middleware [[mw/server-timing]
|
||||
[mw/params]
|
||||
[mw/format-response]
|
||||
[mw/parse-request]
|
||||
[mw/errors errors/handle]
|
||||
[session/soft-auth cfg]
|
||||
[actoken/soft-auth cfg]
|
||||
[mw/parse-request]
|
||||
[mw/errors errors/handle]
|
||||
[mw/restrict-methods]]}
|
||||
|
||||
(::mtx/routes cfg)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl.helpers :as srepl]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.blob :as blob]
|
||||
@@ -430,6 +430,50 @@
|
||||
::yres/body "OK"}))
|
||||
|
||||
|
||||
(defn- add-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
skip-check (contains? params :skip-check)]
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
|
||||
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
|
||||
(defn- remove-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
skip-check (contains? params :skip-check)]
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
|
||||
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OTHER SMALL VIEWS/HANDLERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -500,6 +544,10 @@
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/actions/reset-file-version"
|
||||
{:handler (partial reset-file-version cfg)}]
|
||||
["/actions/add-team-feature"
|
||||
{:handler (partial add-team-feature)}]
|
||||
["/actions/remove-team-feature"
|
||||
{:handler (partial remove-team-feature)}]
|
||||
["/file/export" {:handler (partial export-handler cfg)}]
|
||||
["/file/import" {:handler (partial import-handler cfg)}]
|
||||
["/file/data" {:handler (partial file-data-handler cfg)}]
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
@@ -62,7 +61,8 @@
|
||||
::yres/body data}
|
||||
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/err :hint "restriction error" :data data)
|
||||
(l/err :hint "restriction error"
|
||||
:cause err)
|
||||
{::yres/status 400
|
||||
::yres/body data}))))
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
(= code :invalid-image)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(let [cause (or parent-cause err)]
|
||||
(l/warn :hint "unexpected error on processing image" :cause cause)
|
||||
(l/warn :hint "image process error" :cause cause)
|
||||
{::yres/status 400 ::yres/body data}))
|
||||
|
||||
:else
|
||||
@@ -177,7 +177,7 @@
|
||||
(let [state (.getSQLState ^java.sql.SQLException error)
|
||||
cause (or parent-cause error)]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "PSQL error"
|
||||
(l/error :hint "postgresql error"
|
||||
:cause cause)
|
||||
(cond
|
||||
(= state "57014")
|
||||
|
||||
@@ -337,16 +337,17 @@
|
||||
or (updated_at is null and
|
||||
created_at < now() - ?::interval)")
|
||||
|
||||
(defmethod ig/init-key ::tasks/gc
|
||||
[_ {:keys [::db/pool ::tasks/max-age] :as cfg}]
|
||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||
(fn [_]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-expired interval interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :task "gc"
|
||||
:hint "clean http sessions"
|
||||
:deleted result)
|
||||
result))))
|
||||
(defn- collect-expired-tasks
|
||||
[{:keys [::db/conn ::tasks/max-age]}]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-expired interval interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :task "gc"
|
||||
:hint "clean http sessions"
|
||||
:deleted result)
|
||||
result))
|
||||
|
||||
(defmethod ig/init-key ::tasks/gc
|
||||
[_ {:keys [::tasks/max-age] :as cfg}]
|
||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||
(fn [_] (db/tx-run! cfg collect-expired-tasks)))
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(let [session-id (some-> params :session-id uuid/parse*)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
[::ip-addr {:optional true} ::sm/text]
|
||||
[::props {:optional true} [:map-of :keyword :any]]
|
||||
[::context {:optional true} [:map-of :keyword :any]]
|
||||
[::tracked-at {:optional true} ::sm/inst]
|
||||
[::webhooks/event? {:optional true} ::sm/boolean]
|
||||
[::webhooks/batch-timeout {:optional true} ::dt/duration]
|
||||
[::webhooks/batch-key {:optional true}
|
||||
@@ -118,12 +119,12 @@
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
request (-> params meta ::http/request)
|
||||
profile-id (or (::profile-id resultm)
|
||||
(:profile-id result)
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
(let [resultm (meta result)
|
||||
request (-> params meta ::http/request)
|
||||
profile-id (or (::profile-id resultm)
|
||||
(:profile-id result)
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
|
||||
session-id (get params ::rpc/external-session-id)
|
||||
event-origin (get params ::rpc/external-event-origin)
|
||||
@@ -135,14 +136,14 @@
|
||||
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
ip-addr (inet/parse-request request)]
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
(::rpc/type cfg))
|
||||
|
||||
@@ -53,11 +53,16 @@
|
||||
(assoc :logger/name logger)
|
||||
(assoc :logger/level level)
|
||||
(dissoc :request/params :value :params :data))]
|
||||
|
||||
(merge
|
||||
{:context (-> (into (sorted-map) ctx)
|
||||
(pp/pprint-str :length 50))
|
||||
:props (pp/pprint-str props :length 50)
|
||||
:hint (or (ex-message cause) @message)
|
||||
:hint (or (when-let [message (ex-message cause)]
|
||||
(if-let [props-hint (:hint props)]
|
||||
(str props-hint ": " message)
|
||||
message))
|
||||
@message)
|
||||
:trace (or (::trace record)
|
||||
(some-> cause (ex/format-throwable :data? false :explain? false :header? false :summary? false)))}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.data.json :as json]
|
||||
@@ -67,18 +68,27 @@
|
||||
(defmethod ig/init-key ::process-event-handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(l/dbg :hint "process webhook event" :name (:name props))
|
||||
|
||||
(when-let [items (lookup-webhooks cfg props)]
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event props
|
||||
:config item})))))))))
|
||||
(let [items (lookup-webhooks cfg props)
|
||||
event {::audit/profile-id (:profile-id props)
|
||||
::audit/name "webhook"
|
||||
::audit/type "trigger"
|
||||
::audit/props {:name (get props :name)
|
||||
:event-id (get props :id)
|
||||
:total-affected (count items)}}]
|
||||
|
||||
(audit/insert! cfg event)
|
||||
|
||||
(when items
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event props
|
||||
:config item}))))))))))
|
||||
;; --- RUN
|
||||
|
||||
(declare interpret-exception)
|
||||
|
||||
@@ -43,13 +43,8 @@
|
||||
(decode-row token)))
|
||||
|
||||
(defn repl:create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [props (:app.setup/props system)]
|
||||
(create-access-token {::db/conn conn ::setup/props props}
|
||||
profile-id
|
||||
name
|
||||
expiration))))
|
||||
[cfg profile-id name expiration]
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
(contains? cf/flags :login-with-password))
|
||||
(ex/raise :type :restriction
|
||||
:code :login-disabled
|
||||
:hint "login is disabled in this instance"))
|
||||
:hint "login is disabled"))
|
||||
|
||||
(letfn [(check-password [cfg profile password]
|
||||
(if (= (:password profile) "!")
|
||||
@@ -79,7 +79,8 @@
|
||||
:code :wrong-credentials))
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
:code :profile-blocked
|
||||
:hint "profile is marked as blocked"))
|
||||
(when-not (check-password cfg profile password)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
@@ -149,7 +150,7 @@
|
||||
;; ---- COMMAND: Recover Profile
|
||||
|
||||
(defn recover-profile
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token password]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token password]}]
|
||||
(letfn [(validate-token [token]
|
||||
(let [tdata (tokens/verify (::setup/props cfg) {:token token :iss :password-recovery})]
|
||||
(:profile-id tdata)))
|
||||
@@ -159,10 +160,10 @@
|
||||
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
|
||||
nil))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(->> (validate-token token)
|
||||
(update-password conn))
|
||||
nil)))
|
||||
(->> (validate-token token)
|
||||
(update-password conn))
|
||||
|
||||
nil))
|
||||
|
||||
(def schema:recover-profile
|
||||
[:map {:title "recover-profile"}
|
||||
@@ -173,7 +174,8 @@
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::sm/params schema:recover-profile
|
||||
::climit/id :auth/global}
|
||||
::climit/id :auth/global
|
||||
::db/transaction true}
|
||||
[cfg params]
|
||||
(recover-profile cfg params))
|
||||
|
||||
@@ -182,11 +184,11 @@
|
||||
(defn- validate-register-attempt!
|
||||
[cfg params]
|
||||
|
||||
(when (or
|
||||
(not (contains? cf/flags :registration))
|
||||
(not (contains? cf/flags :login-with-password)))
|
||||
(when (or (not (contains? cf/flags :registration))
|
||||
(not (contains? cf/flags :login-with-password)))
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
:code :registration-disabled
|
||||
:hint "registration disabled"))
|
||||
|
||||
(when (contains? params :invitation-token)
|
||||
(let [invitation (tokens/verify (::setup/props cfg)
|
||||
@@ -200,12 +202,14 @@
|
||||
(when (and (email.blacklist/enabled? cfg)
|
||||
(email.blacklist/contains? cfg (:email params)))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-domain-is-not-allowed))
|
||||
:code :email-domain-is-not-allowed
|
||||
:hint "email domain in blacklist"))
|
||||
|
||||
(when (and (email.whitelist/enabled? cfg)
|
||||
(not (email.whitelist/contains? cfg (:email params))))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-domain-is-not-allowed))
|
||||
:code :email-domain-is-not-allowed
|
||||
:hint "email domain not in whitelist"))
|
||||
|
||||
;; Perform a basic validation of email & password
|
||||
(when (= (str/lower (:email params))
|
||||
@@ -218,16 +222,16 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-permanent-bounces
|
||||
:email (:email params)
|
||||
:hint "looks like the email has bounce reports"))
|
||||
:hint "email has bounce reports"))
|
||||
|
||||
(when (eml/has-complaint-reports? cfg (:email params))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-complaints
|
||||
:email (:email params)
|
||||
:hint "looks like the email has complaint reports")))
|
||||
:hint "email has complaint reports")))
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email accept-newsletter-updates] :as params}]
|
||||
|
||||
(validate-register-attempt! cfg params)
|
||||
|
||||
@@ -239,7 +243,8 @@
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 7})}
|
||||
:exp (dt/in-future {:days 7})
|
||||
:props {:newsletter-updates (or accept-newsletter-updates false)}}
|
||||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
(def ^:private
|
||||
schema:export-binfile
|
||||
[:map {:title "export-binfile"}
|
||||
[:name [:string {:max 250}]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:version {:optional true} ::sm/int]
|
||||
[:include-libraries ::sm/boolean]
|
||||
@@ -78,7 +77,7 @@
|
||||
"Export a penpot file in a binary format."
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true
|
||||
::sm/result schema:export-binfile}
|
||||
::sm/params schema:export-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
|
||||
(files/check-read-permissions! pool profile-id file-id)
|
||||
(fn [_]
|
||||
|
||||
@@ -797,3 +797,18 @@
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
(def ^:private
|
||||
schema:mark-all-threads-as-read
|
||||
[:map {:title "mark-all-threads-as-read"}
|
||||
[:threads [:vector ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::mark-all-threads-as-read
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:mark-all-threads-as-read}
|
||||
[cfg {:keys [::rpc/profile-id threads] :as params}]
|
||||
(db/tx-run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [thread-id threads]
|
||||
(upsert-comment-thread-status! conn profile-id thread-id)))))
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
|
||||
[{:keys [::db/pool] :as cfg} _]
|
||||
[cfg _]
|
||||
|
||||
(when-not (contains? cf/flags :demo-users)
|
||||
(ex/raise :type :validation
|
||||
@@ -49,9 +49,11 @@
|
||||
:password (profile/derive-password cfg password)
|
||||
:props {}}]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))))
|
||||
|
||||
(let [profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)}))))
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
[:project-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- migrate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} {:keys [read-only?]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [;; For avoid unnecesary overhead of creating multiple pointers and
|
||||
@@ -219,43 +219,45 @@
|
||||
file (-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
(fmg/migrate-file))]
|
||||
|
||||
;; When file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
;;
|
||||
;; WARN: he following code will not work on read-only mode,
|
||||
;; it is a known issue; we keep is not implemented until we
|
||||
;; really need this.
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map file)
|
||||
file)]
|
||||
(if (or read-only? (db/read-only? conn))
|
||||
file
|
||||
(let [;; When file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map file)
|
||||
file)]
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:version (:version file)
|
||||
:features (db/create-array conn "text" (:features file))}
|
||||
{:id id})
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:version (:version file)
|
||||
:features (db/create-array conn "text" (:features file))}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(when (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/persist-pointers! cfg id))
|
||||
(when (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/persist-pointers! cfg id))
|
||||
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||
& {:keys [project-id
|
||||
migrate?
|
||||
include-deleted?
|
||||
lock-for-update?]
|
||||
lock-for-update?
|
||||
preload-pointers?]
|
||||
:or {include-deleted? false
|
||||
lock-for-update? false
|
||||
migrate? true}}]
|
||||
migrate? true
|
||||
preload-pointers? false}
|
||||
:as options}]
|
||||
|
||||
(assert (db/connection? conn) "expected cfg with valid connection")
|
||||
|
||||
@@ -273,10 +275,16 @@
|
||||
;; because it has heavy and synchronous operations for
|
||||
;; decoding file body that are not very friendly with virtual
|
||||
;; threads.
|
||||
file (px/invoke! executor #(decode-row file))]
|
||||
file (px/invoke! executor #(decode-row file))
|
||||
|
||||
file (if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file options)
|
||||
file)]
|
||||
|
||||
(if preload-pointers?
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
|
||||
(if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file)
|
||||
file)))
|
||||
|
||||
(defn get-minimal-file
|
||||
@@ -292,7 +300,7 @@
|
||||
|
||||
(defn get-file-etag
|
||||
[{:keys [::rpc/profile-id]} {:keys [modified-at revn vern permissions]}]
|
||||
(str profile-id "/" revn "/" vern "/"
|
||||
(str profile-id "/" revn "/" vern "/" (hash fmg/available-migrations) "/"
|
||||
(dt/format-instant modified-at :iso)
|
||||
"/"
|
||||
(uri/map->query-string permissions)))
|
||||
@@ -323,11 +331,12 @@
|
||||
|
||||
file (-> (get-file cfg id :project-id project-id)
|
||||
(assoc :permissions perms)
|
||||
(assoc :team-id (:id team))
|
||||
(check-version!))]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
;; This operation is needed for backward comapatibility with frontends that
|
||||
;; does not support pointer-map resolution mechanism; this just resolves the
|
||||
@@ -473,7 +482,7 @@
|
||||
(update page :objects update-vals #(dissoc % :thumbnail)))
|
||||
|
||||
(defn get-page
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id] :as params}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id page-id object-id share-id] :as params}]
|
||||
|
||||
(when (and (uuid? object-id)
|
||||
(not (uuid? page-id)))
|
||||
@@ -481,22 +490,30 @@
|
||||
:code :params-validation
|
||||
:hint "page-id is required when object-id is provided"))
|
||||
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
||||
|
||||
file (get-file cfg file-id)
|
||||
file (get-file cfg file-id :read-only? true)
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
proj (db/get conn :project {:id (:project-id file)})
|
||||
|
||||
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
(if (pmap/pointer-map? page)
|
||||
(deref page)
|
||||
page)))]
|
||||
team (-> (db/get conn :team {:id (:team-id proj)})
|
||||
(teams/decode-row))
|
||||
|
||||
_ (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
(if (pmap/pointer-map? page)
|
||||
(deref page)
|
||||
page)))]
|
||||
|
||||
(when-not perms
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "object not found"))
|
||||
|
||||
(cond-> (prune-thumbnails page)
|
||||
(some? object-id)
|
||||
@@ -613,6 +630,7 @@
|
||||
SELECT l.id,
|
||||
l.features,
|
||||
l.project_id,
|
||||
p.team_id,
|
||||
l.created_at,
|
||||
l.modified_at,
|
||||
l.deleted_at,
|
||||
@@ -622,6 +640,7 @@
|
||||
l.synced_at,
|
||||
l.is_shared
|
||||
FROM libs AS l
|
||||
INNER JOIN project AS p ON (p.id = l.project_id)
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn get-file-libraries
|
||||
@@ -730,11 +749,13 @@
|
||||
:project-id project-id
|
||||
:file-id id)
|
||||
|
||||
file (get-file cfg id :project-id project-id)]
|
||||
file (get-file cfg id
|
||||
:project-id project-id
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
{:name (:name file)
|
||||
@@ -803,17 +824,17 @@
|
||||
[:id ::sm/uuid]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]}
|
||||
[:modified-at ::dt/instant]]
|
||||
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [file (rename-file conn params)]
|
||||
(rph/with-meta
|
||||
(select-keys file [:id :name :created-at :modified-at])
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)}}))))
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [file (rename-file conn params)]
|
||||
(rph/with-meta
|
||||
(select-keys file [:id :name :created-at :modified-at])
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)}})))
|
||||
|
||||
;; --- MUTATION COMMAND: set-file-shared
|
||||
|
||||
@@ -1005,15 +1026,17 @@
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:link-file-to-library}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
(when (= file-id library-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-library
|
||||
:hint "A file cannot be linked to itself"))
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params)))
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params))))
|
||||
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
@@ -1031,12 +1054,12 @@
|
||||
(sv/defmethod ::unlink-file-from-library
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:unlink-file-to-library}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(unlink-file-from-library conn params)
|
||||
nil))
|
||||
::sm/params schema:unlink-file-to-library
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(unlink-file-from-library conn params)
|
||||
nil)
|
||||
|
||||
;; --- MUTATION COMMAND: update-sync
|
||||
|
||||
@@ -1056,12 +1079,11 @@
|
||||
(sv/defmethod ::update-file-library-sync-status
|
||||
"Update the synchronization status of a file->library link"
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-file-library-sync-status}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(update-sync conn params)))
|
||||
|
||||
::sm/params schema:update-file-library-sync-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(update-sync conn params))
|
||||
|
||||
;; --- MUTATION COMMAND: ignore-sync
|
||||
|
||||
@@ -1082,9 +1104,9 @@
|
||||
(sv/defmethod ::ignore-file-library-sync-status
|
||||
"Ignore updates in linked files"
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:ignore-file-library-sync-status}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{}))))
|
||||
::sm/params schema:ignore-file-library-sync-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{})))
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
:features features
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at
|
||||
:create-page create-page
|
||||
:deleted-at deleted-at}
|
||||
{:create-page create-page
|
||||
:page-id page-id})
|
||||
file (-> (bfc/insert-file! cfg file)
|
||||
(bfc/decode-row))]
|
||||
@@ -91,9 +91,6 @@
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
@@ -107,25 +104,28 @@
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features)))]
|
||||
(assoc :features features))]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id})
|
||||
|
||||
;; FIXME: IMPORTANT: this code can have race
|
||||
;; conditions, because we have no locks for updating
|
||||
;; team so, creating two files concurrently can lead
|
||||
;; to lost team features updating
|
||||
;; FIXME: IMPORTANT: this code can have race conditions, because
|
||||
;; we have no locks for updating team so, creating two files
|
||||
;; concurrently can lead to lost team features updating
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
pages of a file with specific permissions (who-comment and who-inspect)."
|
||||
{::doc/added "1.18"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn (assoc params :profile-id profile-id))))
|
||||
::sm/params schema:create-share-link
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn create-share-link
|
||||
[conn {:keys [profile-id file-id pages who-comment who-inspect]}]
|
||||
@@ -61,10 +61,10 @@
|
||||
(sv/defmethod ::delete-share-link
|
||||
{::doc/added "1.18"
|
||||
::doc/module ::files
|
||||
::sm/params schema:delete-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||
(db/delete! conn :share-link {:id id})
|
||||
nil)))
|
||||
::sm/params schema:delete-share-link
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||
(db/delete! conn :share-link {:id id})
|
||||
nil))
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as thc]
|
||||
@@ -18,7 +17,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
@@ -180,8 +178,7 @@
|
||||
(def ^:private
|
||||
schema:get-file-data-for-thumbnail
|
||||
[:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
[:file-id ::sm/uuid]])
|
||||
|
||||
(def ^:private
|
||||
schema:partial-file
|
||||
@@ -201,18 +198,16 @@
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
|
||||
file (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(-> (files/get-file cfg file-id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(fmg/migrate-file)))]
|
||||
file (files/get-file cfg file-id
|
||||
:preload-pointers? true
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
@@ -177,12 +177,19 @@
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
;; the features defined on team row, we update it
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)})))
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
|
||||
@@ -396,45 +396,49 @@
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn clone-template
|
||||
[cfg {:keys [project-id profile-id] :as params} template]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||
;; NOTE: the importation process performs some operations
|
||||
;; that are not very friendly with virtual threads, and for
|
||||
;; avoid unexpected blocking of other concurrent operations
|
||||
;; we dispatch that operation to a dedicated executor.
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
:min-age "30m")
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
|
||||
|
||||
format (bfc/parse-file-format template)
|
||||
team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
;; NOTE: the importation process performs some operations
|
||||
;; that are not very friendly with virtual threads, and for
|
||||
;; avoid unexpected blocking of other concurrent operations
|
||||
;; we dispatch that operation to a dedicated executor.
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
:min-age "30m")
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
format (bfc/parse-file-format template)
|
||||
team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
|
||||
(let [props (audit/clean-props params)]
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
|
||||
result))))
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(let [props (audit/clean-props params)]
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))))
|
||||
|
||||
result))
|
||||
|
||||
(def ^:private
|
||||
schema:clone-template
|
||||
|
||||
@@ -273,15 +273,14 @@
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:clone-file-media-object}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-file-media-object params))))
|
||||
::sm/params schema:clone-file-media-object
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(clone-file-media-object cfg params))
|
||||
|
||||
(defn clone-file-media-object
|
||||
[{:keys [conn]} {:keys [id file-id is-local]}]
|
||||
[{:keys [::db/conn]} {:keys [id file-id is-local]}]
|
||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
|
||||
@@ -125,32 +125,32 @@
|
||||
(sv/defmethod ::update-profile
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile
|
||||
::sm/result schema:profile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
;; NOTE: we need to retrieve the profile independently if we use
|
||||
;; it or not for explicit locking and avoid concurrent updates of
|
||||
;; the same row/object.
|
||||
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
|
||||
(decode-row))
|
||||
::sm/result schema:profile
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||
;; NOTE: we need to retrieve the profile independently if we use
|
||||
;; it or not for explicit locking and avoid concurrent updates of
|
||||
;; the same row/object.
|
||||
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
|
||||
(decode-row))
|
||||
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
(assoc :lang lang)
|
||||
(assoc :theme theme))]
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
(assoc :lang lang)
|
||||
(assoc :theme theme))]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
(d/without-nils)
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
(d/without-nils)
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)}))))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Password
|
||||
@@ -169,21 +169,20 @@
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-password
|
||||
::climit/id :auth/global}
|
||||
::climit/id :auth/global
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id password] :as params}]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
(when (= (:email profile) (str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(when (= (:email profile) (str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))))
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))
|
||||
|
||||
(defn- invalidate-profile-session!
|
||||
"Removes all sessions except the current one."
|
||||
@@ -441,37 +440,36 @@
|
||||
(declare ^:private get-owned-teams)
|
||||
|
||||
(sv/defmethod ::delete-profile
|
||||
{::doc/added "1.0"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
{::doc/added "1.0"
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
;; explicitly removes all participants from the team
|
||||
(when (some pos? (map :participants teams))
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
;; explicitly removes all participants from the team
|
||||
(when (some pos? (map :participants teams))
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
|
||||
;; Mark profile deleted immediatelly
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
;; Mark profile deleted immediatelly
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
|
||||
;; Schedule cascade deletion to a worker
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :delete-object
|
||||
::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id profile-id}})
|
||||
;; Schedule cascade deletion to a worker
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :delete-object
|
||||
::wrk/params {:object :profile
|
||||
:deleted-at deleted-at
|
||||
:id profile-id}})
|
||||
|
||||
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg))))))
|
||||
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg)))))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
|
||||
@@ -219,12 +219,12 @@
|
||||
::sm/params schema:update-project-pin
|
||||
::webhooks/batch-timeout (dt/duration "5s")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil)
|
||||
|
||||
;; --- MUTATION: Rename Project
|
||||
|
||||
@@ -238,17 +238,17 @@
|
||||
(sv/defmethod ::rename-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:rename-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
|
||||
(db/update! conn :project
|
||||
{:name name}
|
||||
{:id id})
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:prev-name (:name project)}}))))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
|
||||
(db/update! conn :project
|
||||
{:name name}
|
||||
{:id id})
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:prev-name (:name project)}})))
|
||||
|
||||
;; --- MUTATION: Delete Project
|
||||
|
||||
@@ -280,13 +280,13 @@
|
||||
(sv/defmethod ::delete-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:delete-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (delete-project conn id)]
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:name (:name project)
|
||||
:created-at (:created-at project)
|
||||
:modified-at (:modified-at project)}}))))
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id] :as params}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(let [project (delete-project conn id)]
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:team-id (:team-id project)
|
||||
:name (:name project)
|
||||
:created-at (:created-at project)
|
||||
:modified-at (:modified-at project)}})))
|
||||
|
||||
@@ -76,9 +76,10 @@
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [features] :as row}]
|
||||
[{:keys [features subscription] :as row}]
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))
|
||||
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
|
||||
|
||||
;; FIXME: move
|
||||
|
||||
@@ -126,16 +127,40 @@
|
||||
(get-teams conn profile-id)))
|
||||
|
||||
(def sql:get-teams-with-permissions
|
||||
"select t.*,
|
||||
"SELECT t.*,
|
||||
tp.is_owner,
|
||||
tp.is_admin,
|
||||
tp.can_edit,
|
||||
(t.id = ?) as is_default
|
||||
from team_profile_rel as tp
|
||||
join team as t on (t.id = tp.team_id)
|
||||
where t.deleted_at is null
|
||||
and tp.profile_id = ?
|
||||
order by tp.created_at asc")
|
||||
(t.id = ?) AS is_default
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN team AS t ON (t.id = tp.team_id)
|
||||
WHERE t.deleted_at IS null
|
||||
AND tp.profile_id = ?
|
||||
ORDER BY tp.created_at ASC")
|
||||
|
||||
(def sql:get-teams-with-permissions-and-subscription
|
||||
"SELECT t.*,
|
||||
tp.is_owner,
|
||||
tp.is_admin,
|
||||
tp.can_edit,
|
||||
(t.id = ?) AS is_default,
|
||||
|
||||
jsonb_build_object(
|
||||
'~:type', COALESCE(p.props->'~:subscription'->>'~:type', 'professional'),
|
||||
'~:status', CASE COALESCE(p.props->'~:subscription'->>'~:type', 'professional')
|
||||
WHEN 'professional' THEN 'active'
|
||||
ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete')
|
||||
END
|
||||
) AS subscription
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN team AS t ON (t.id = tp.team_id)
|
||||
JOIN team_profile_rel AS tpr
|
||||
ON (tpr.team_id = t.id AND tpr.is_owner IS true)
|
||||
JOIN profile AS p
|
||||
ON (tpr.profile_id = p.id)
|
||||
WHERE t.deleted_at IS null
|
||||
AND tp.profile_id = ?
|
||||
ORDER BY tp.created_at ASC;")
|
||||
|
||||
(defn process-permissions
|
||||
[team]
|
||||
@@ -150,13 +175,21 @@
|
||||
(dissoc :is-owner :is-admin :can-edit)
|
||||
(assoc :permissions permissions))))
|
||||
|
||||
(def ^:private
|
||||
xform:process-teams
|
||||
(comp
|
||||
(map decode-row)
|
||||
(map process-permissions)))
|
||||
|
||||
(defn get-teams
|
||||
[conn profile-id]
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(->> (db/exec! conn [sql:get-teams-with-permissions (:default-team-id profile) profile-id])
|
||||
(map decode-row)
|
||||
(map process-permissions)
|
||||
(vec))))
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
sql (if (contains? cf/flags :subscriptions)
|
||||
sql:get-teams-with-permissions-and-subscription
|
||||
sql:get-teams-with-permissions)]
|
||||
|
||||
(->> (db/exec! conn [sql (:default-team-id profile) profile-id])
|
||||
(into [] xform:process-teams))))
|
||||
|
||||
;; --- Query: Team (by ID)
|
||||
|
||||
@@ -527,14 +560,14 @@
|
||||
|
||||
(sv/defmethod ::update-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :team
|
||||
{:name name}
|
||||
{:id id})
|
||||
nil))
|
||||
::sm/params schema:update-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id name]}]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :team
|
||||
{:name name}
|
||||
{:id id})
|
||||
nil)
|
||||
|
||||
|
||||
;; --- Mutation: Leave Team
|
||||
@@ -592,10 +625,10 @@
|
||||
|
||||
(sv/defmethod ::leave-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:leave-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(leave-team conn (assoc params :profile-id profile-id))))
|
||||
::sm/params schema:leave-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(leave-team conn (assoc params :profile-id profile-id)))
|
||||
|
||||
;; --- Mutation: Delete Team
|
||||
|
||||
@@ -627,16 +660,16 @@
|
||||
|
||||
(sv/defmethod ::delete-team
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
::sm/params schema:delete-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
(delete-team conn id)
|
||||
nil)))
|
||||
(delete-team conn id)
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Team Update Role
|
||||
|
||||
@@ -714,31 +747,30 @@
|
||||
|
||||
(sv/defmethod ::delete-team-member
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team-member}
|
||||
[{:keys [::db/pool ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (get-team pool :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
::sm/params schema:delete-team-member
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
|
||||
(let [team (get-team conn :profile-id profile-id :team-id team-id)
|
||||
perms (get-permissions conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when (= member-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
(when (= member-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
(mbus/pub! msgbus
|
||||
:topic member-id
|
||||
:message {:type :team-membership-change
|
||||
:change :removed
|
||||
:team-id team-id
|
||||
:team-name (:name team)})
|
||||
|
||||
(mbus/pub! msgbus
|
||||
:topic member-id
|
||||
:message {:type :team-membership-change
|
||||
:change :removed
|
||||
:team-id team-id
|
||||
:team-name (:name team)})
|
||||
|
||||
nil)))
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Update Team Photo
|
||||
|
||||
@@ -764,16 +796,16 @@
|
||||
(let [team (get-team pool :profile-id profile-id :team-id team-id)
|
||||
photo (profile/upload-photo cfg params)]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(check-admin-permissions! conn profile-id team-id)
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
(check-admin-permissions! pool profile-id team-id)
|
||||
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
;; Mark object as touched for make it ellegible for tentative
|
||||
;; garbage collection.
|
||||
(when-let [id (:photo-id team)]
|
||||
(sto/touch-object! storage id))
|
||||
|
||||
(assoc team :photo-id (:id photo)))))
|
||||
;; Save new photo
|
||||
(db/update! pool :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
|
||||
(assoc team :photo-id (:id photo))))
|
||||
|
||||
@@ -408,20 +408,20 @@
|
||||
(sv/defmethod ::update-team-invitation-role
|
||||
{::doc/added "1.17"
|
||||
::doc/module :teams
|
||||
::sm/params schema:update-team-invitation-role}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
::sm/params schema:update-team-invitation-role
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(db/update! conn :team-invitation
|
||||
{:role (name role) :updated-at (dt/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
(db/update! conn :team-invitation
|
||||
{:role (name role) :updated-at (dt/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
|
||||
nil)))
|
||||
nil))
|
||||
|
||||
;; --- Mutation: Delete invitation
|
||||
|
||||
@@ -432,20 +432,20 @@
|
||||
|
||||
(sv/defmethod ::delete-team-invitation
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-team-invition}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
::sm/params schema:delete-team-invition
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(let [invitation (db/delete! conn :team-invitation
|
||||
{:team-id team-id
|
||||
:email-to (profile/clean-email email)}
|
||||
{::db/return-keys true})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))
|
||||
(let [invitation (db/delete! conn :team-invitation
|
||||
{:team-id team-id
|
||||
:email-to (profile/clean-email email)}
|
||||
{::db/return-keys true})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))
|
||||
|
||||
|
||||
;; --- Mutation: Request Team Invitation
|
||||
|
||||
@@ -78,7 +78,10 @@
|
||||
:always
|
||||
(update :data select-keys [:id :options :pages :pages-index :components]))
|
||||
|
||||
libs (files/get-file-libraries conn file-id)
|
||||
libs (->> (files/get-file-libraries conn file-id)
|
||||
(mapv (fn [{:keys [id] :as lib}]
|
||||
(merge lib (files/get-file cfg id)))))
|
||||
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
(-> row
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
|
||||
(sv/defmethod ::delete-webhook
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-webhook}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(check-webhook-edition-permissions! conn profile-id (:team-id whook) (:profile-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil)))
|
||||
::sm/params schema:delete-webhook
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(check-webhook-edition-permissions! conn profile-id (:team-id whook) (:profile-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil))
|
||||
|
||||
;; --- Query: Webhooks
|
||||
|
||||
(def sql:get-webhooks
|
||||
"SELECT id, uri, mtype, is_active, error_code, error_count, profile_id
|
||||
FROM webhook
|
||||
WHERE team_id = ?
|
||||
"SELECT id, uri, mtype, is_active, error_code, error_count, profile_id
|
||||
FROM webhook
|
||||
WHERE team_id = ?
|
||||
ORDER BY uri")
|
||||
|
||||
(def ^:private schema:get-webhooks
|
||||
|
||||
@@ -78,19 +78,19 @@
|
||||
|
||||
(defmethod ig/init-key ::props
|
||||
[_ {:keys [::db/pool ::key] :as cfg}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/xact-lock! conn 0)
|
||||
(when-not key
|
||||
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool))))))
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/xact-lock! conn 0)
|
||||
(when-not key
|
||||
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
|
||||
"all sessions on each restart, it is highly recommended setting up the "
|
||||
"PENPOT_SECRET_KEY environment variable")))
|
||||
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (get-all-props conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||
|
||||
;; FIXME
|
||||
(sm/register! ::props :any)
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
|
||||
(ns app.srepl
|
||||
"Server Repl."
|
||||
(:refer-clojure :exclude [read-line])
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.srepl.cli]
|
||||
[app.srepl.cli :as cli]
|
||||
[app.srepl.main]
|
||||
[app.util.json :as json]
|
||||
[app.util.locks :as locks]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.core.server :as ccs]
|
||||
[clojure.main :as cm]
|
||||
[integrant.core :as ig]))
|
||||
@@ -28,17 +32,80 @@
|
||||
:init repl-init
|
||||
:read ccs/repl-read))
|
||||
|
||||
(defn- ex->data
|
||||
[cause phase]
|
||||
(let [data (ex-data cause)
|
||||
explain (ex/explain data)]
|
||||
(cond-> {:phase phase
|
||||
:code (get data :code :unknown)
|
||||
:type (get data :type :unknown)
|
||||
:hint (or (get data :hint) (ex-message cause))}
|
||||
(some? explain)
|
||||
(assoc :explain explain))))
|
||||
|
||||
(defn read-line
|
||||
[]
|
||||
(if-let [line (c/read-line)]
|
||||
(try
|
||||
(l/dbg :hint "decode" :data line)
|
||||
(json/decode line :key-fn json/read-kebab-key)
|
||||
(catch Throwable _cause
|
||||
(l/warn :hint "unable to decode data" :data line)
|
||||
nil))
|
||||
::eof))
|
||||
|
||||
(defn json-repl
|
||||
[]
|
||||
(let [out *out*
|
||||
lock (locks/create)]
|
||||
(ccs/prepl *in*
|
||||
(fn [m]
|
||||
(binding [*out* out,
|
||||
*flush-on-newline* true,
|
||||
*print-readably* true]
|
||||
(locks/locking lock
|
||||
(println (json/encode-str m))))))))
|
||||
(let [lock (locks/create)
|
||||
out *out*
|
||||
|
||||
out-fn
|
||||
(fn [m]
|
||||
(locks/locking lock
|
||||
(binding [*out* out]
|
||||
(l/warn :hint "write" :data m)
|
||||
(println (json/encode m :key-fn json/write-camel-key)))))
|
||||
|
||||
tapfn
|
||||
(fn [val]
|
||||
(out-fn {:tag :tap :val val}))]
|
||||
|
||||
(binding [*out* (PrintWriter-on #(out-fn {:tag :out :val %1}) nil true)
|
||||
*err* (PrintWriter-on #(out-fn {:tag :err :val %1}) nil true)]
|
||||
(try
|
||||
(add-tap tapfn)
|
||||
(loop []
|
||||
(when (try
|
||||
(let [data (read-line)
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
(l/dbg :hint "received" :data (if (= data ::eof) "EOF" data))
|
||||
|
||||
(try
|
||||
(when-not (= data ::eof)
|
||||
(when-not (nil? data)
|
||||
(let [result (cli/exec data)
|
||||
elapsed (tpoint)]
|
||||
(l/warn :hint "result" :data result)
|
||||
(out-fn {:tag :ret
|
||||
:val (if (instance? Throwable result)
|
||||
(Throwable->map result)
|
||||
result)
|
||||
:elapsed (inst-ms elapsed)})))
|
||||
true)
|
||||
(catch Throwable cause
|
||||
(let [elapsed (tpoint)]
|
||||
(out-fn {:tag :ret
|
||||
:err (ex->data cause :eval)
|
||||
:elapsed (inst-ms elapsed)})
|
||||
true))))
|
||||
(catch Throwable cause
|
||||
(out-fn {:tag :ret
|
||||
:err (ex->data cause :read)})
|
||||
true))
|
||||
(recur)))
|
||||
(finally
|
||||
(remove-tap tapfn))))))
|
||||
|
||||
;; --- State initialization
|
||||
|
||||
|
||||
@@ -9,14 +9,23 @@
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.util.json :as json]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||
check-fn (sm/check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
(defn- get-current-system
|
||||
[]
|
||||
(or (deref (requiring-resolve 'app.main/system))
|
||||
@@ -24,88 +33,218 @@
|
||||
|
||||
(defmulti ^:private exec-command ::cmd)
|
||||
|
||||
(defmethod exec-command :default
|
||||
[{:keys [::cmd]}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/ffmt "command '%' not implemented" cmd)))
|
||||
|
||||
(defn exec
|
||||
"Entry point with external tools integrations that uses PREPL
|
||||
interface for interacting with running penpot backend."
|
||||
[data]
|
||||
(let [data (json/decode data)]
|
||||
(-> {::cmd (keyword (:cmd data "default"))}
|
||||
(merge (:params data))
|
||||
(exec-command))))
|
||||
(-> {::cmd (get data :cmd)}
|
||||
(merge (:params data))
|
||||
(exec-command)))
|
||||
|
||||
(defmethod exec-command :create-profile
|
||||
(defmethod exec-command "create-profile"
|
||||
[{:keys [fullname email password is-active]
|
||||
:or {is-active true}}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn))))))
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn)))))))
|
||||
|
||||
(defmethod exec-command :update-profile
|
||||
(defmethod exec-command "update-profile"
|
||||
[{:keys [fullname email password is-active]}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [params (cond-> {}
|
||||
(some? fullname)
|
||||
(assoc :fullname fullname)
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [params (cond-> {}
|
||||
(some? fullname)
|
||||
(assoc :fullname fullname)
|
||||
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
(when (seq params)
|
||||
(let [res (db/update! conn :profile
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil})]
|
||||
(pos? (db/get-update-count res))))))))
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
(when (seq params)
|
||||
(let [res (db/update! conn :profile
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil})]
|
||||
(pos? (db/get-update-count res)))))))))
|
||||
|
||||
(defmethod exec-command :delete-profile
|
||||
(defmethod exec-command "echo"
|
||||
[params]
|
||||
params)
|
||||
|
||||
|
||||
(defmethod exec-command "delete-profile"
|
||||
[{:keys [email soft]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:email email :deleted-at nil})
|
||||
(db/delete! conn :profile
|
||||
{:email email}))]
|
||||
(pos? (db/get-update-count res)))))))
|
||||
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:email email :deleted-at nil})
|
||||
(db/delete! conn :profile
|
||||
{:email email}))]
|
||||
(pos? (db/get-update-count res))))))
|
||||
|
||||
(defmethod exec-command :search-profile
|
||||
(defmethod exec-command "search-profile"
|
||||
[{:keys [email]}]
|
||||
(when-not email
|
||||
(ex/raise :type :assertion
|
||||
:code :invalid-arguments
|
||||
:hint "email should be provided"))
|
||||
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [sql (str "select email, fullname, created_at, deleted_at from profile "
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email]))))))
|
||||
|
||||
(let [sql (str "select email, fullname, created_at, deleted_at from profile "
|
||||
" where email similar to ? order by created_at desc limit 100")]
|
||||
(db/exec! conn [sql email])))))
|
||||
|
||||
(defmethod exec-command :derive-password
|
||||
(defmethod exec-command "derive-password"
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
|
||||
(defmethod exec-command :default
|
||||
[{:keys [::cmd]}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/ffmt "command '%' not implemented" (name cmd))))
|
||||
(defmethod exec-command "authenticate"
|
||||
[{:keys [token]}]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [props (get system ::setup/props)]
|
||||
(tokens/verify props {:token token :iss "authentication"}))))
|
||||
|
||||
(def ^:private schema:get-customer
|
||||
[:map [:id ::sm/uuid]])
|
||||
|
||||
(def coerce-get-customer-params
|
||||
(coercer schema:get-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `get-customer` rpc call"))
|
||||
|
||||
(def sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[system profile-id]
|
||||
(let [result (db/exec-one! system [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(defmethod exec-command "get-customer"
|
||||
[params]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [{:keys [id] :as params} (coerce-get-customer-params params)
|
||||
{:keys [props] :as profile} (cmd.profile/get-profile system id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots system id)
|
||||
:subscription (get props :subscription)})))
|
||||
|
||||
(def ^:private schema:customer-subscription
|
||||
[:map {:title "CustomerSubscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"pass_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at ::sm/timestamp]
|
||||
[:start-date [:maybe ::sm/timestamp]]
|
||||
[:ended-at [:maybe ::sm/timestamp]]
|
||||
[:trial-end [:maybe ::sm/timestamp]]
|
||||
[:trial-start [:maybe ::sm/timestamp]]
|
||||
[:cancel-at [:maybe ::sm/timestamp]]
|
||||
[:canceled-at [:maybe ::sm/timestamp]]
|
||||
|
||||
[:current-period-end ::sm/timestamp]
|
||||
[:current-period-start ::sm/timestamp]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private schema:update-customer-subscription
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:subscription [:maybe schema:customer-subscription]]])
|
||||
|
||||
(def coerce-update-customer-subscription-params
|
||||
(coercer schema:update-customer-subscription
|
||||
:type :validation
|
||||
:hint "invalid data provided for `update-customer-subscription` rpc call"))
|
||||
|
||||
(defmethod exec-command "update-customer-subscription"
|
||||
[params]
|
||||
(when-let [system (get-current-system)]
|
||||
(let [{:keys [id subscription]} (coerce-update-customer-subscription-params params)
|
||||
;; FIXME: locking
|
||||
{:keys [props] :as profile} (cmd.profile/get-profile system id)
|
||||
props (assoc props :subscription subscription)]
|
||||
|
||||
(db/update! system :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
true)))
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
component-child (first component-children)]
|
||||
(if (or (nil? child) (nil? component-child))
|
||||
container
|
||||
(let [container (if (and (not (ctk/is-main-of? component-child child true))
|
||||
(let [container (if (and (not (ctk/is-main-of? component-child child))
|
||||
(nil? (ctk/get-swap-slot child))
|
||||
(ctk/instance-head? child))
|
||||
(let [slot (guess-swap-slot component-child component-container)]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.db :as db]
|
||||
[app.features.components-v2 :as feat.comp-v2]
|
||||
@@ -142,7 +143,9 @@
|
||||
(update-fn file opts)))]
|
||||
|
||||
(when (and (some? file')
|
||||
(not (identical? file file')))
|
||||
(or (fmg/migrated? file)
|
||||
(not (identical? file file'))))
|
||||
|
||||
(when validate?
|
||||
(cfv/validate-file-schema! file'))
|
||||
|
||||
|
||||
@@ -101,38 +101,46 @@
|
||||
"Mark the profile blocked and removes all the http sessiones
|
||||
associated with the profile-id."
|
||||
[email]
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-active true} {:id (:id profile)})
|
||||
:activated))))
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-active true} {:id (:id profile)})
|
||||
:activated))))))
|
||||
|
||||
(defn mark-profile-as-blocked!
|
||||
"Mark the profile blocked and removes all the http sessiones
|
||||
associated with the profile-id."
|
||||
[email]
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
:blocked))))
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [profile (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
:blocked))))))
|
||||
|
||||
(defn reset-password!
|
||||
"Reset a password to a specific one for a concrete user or all users
|
||||
if email is `:all` keyword."
|
||||
[& {:keys [email password] :or {password "123123"} :as params}]
|
||||
(us/verify! (contains? params :email) "`email` parameter is mandatory")
|
||||
(db/with-atomic [conn (:app.db/pool main/system)]
|
||||
(let [password (derive-password password)]
|
||||
(if (= email :all)
|
||||
(db/exec! conn ["update profile set password=?" password])
|
||||
(let [email (str/lower email)]
|
||||
(db/exec! conn ["update profile set password=? where email=?" password email]))))))
|
||||
(when-not email
|
||||
(throw (IllegalArgumentException. "email is mandatory")))
|
||||
|
||||
(some-> main/system
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [password (derive-password password)]
|
||||
(if (= email :all)
|
||||
(db/exec! conn ["update profile set password=?" password])
|
||||
(let [email (str/lower email)]
|
||||
(db/exec! conn ["update profile set password=? where email=?" password email]))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FEATURES
|
||||
@@ -148,14 +156,18 @@
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id feat.fdata/enable-pointer-map opts))
|
||||
|
||||
(defn enable-path-data-feature-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id feat.fdata/enable-path-data opts))
|
||||
|
||||
(defn enable-storage-features-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(enable-objects-map-feature-on-file! file-id opts)
|
||||
(enable-pointer-map-feature-on-file! file-id opts))
|
||||
|
||||
(defn enable-team-feature!
|
||||
[team-id feature]
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
[team-id feature & {:keys [skip-check] :or {skip-check false}}]
|
||||
(when (and (not skip-check) (not (contains? cfeat/supported-features feature)))
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
@@ -173,9 +185,8 @@
|
||||
:enabled))))))
|
||||
|
||||
(defn disable-team-feature!
|
||||
[team-id feature]
|
||||
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
[team-id feature & {:keys [skip-check] :or {skip-check false}}]
|
||||
(when (and (not skip-check) (not (contains? cfeat/supported-features feature)))
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
@@ -202,100 +213,116 @@
|
||||
This method allows send flash notifications to specified target destinations.
|
||||
The message can be a free text or a preconfigured one.
|
||||
|
||||
The destination can be: all, profile-id, team-id, or a coll of them."
|
||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
The destination can be: all, profile-id, team-id, or a coll of them.
|
||||
It also can be:
|
||||
|
||||
{:email \"some@example.com\"}
|
||||
[[:email \"some@example.com\"], ...]
|
||||
|
||||
Command examples:
|
||||
|
||||
(notify! :dest :all :code :maintenance)
|
||||
(notify! :dest :all :code :upgrade-version)
|
||||
"
|
||||
[& {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
|
||||
(when-not (contains? #{:success :error :info :warning} level)
|
||||
(ex/raise :type :assertion
|
||||
:code :incorrect-level
|
||||
:hint (str "level '" level "' not supported")))
|
||||
|
||||
(letfn [(send [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
(let [message {:type :notification
|
||||
:code code
|
||||
:level level
|
||||
:version (:full cf/version)
|
||||
:subs-id dest
|
||||
:message message}
|
||||
message (->> (dissoc params :dest :code :message :level)
|
||||
(merge message))]
|
||||
(mbus/pub! msgbus
|
||||
:topic (str dest)
|
||||
:message message)))
|
||||
(let [{:keys [::mbus/msgbus ::db/pool]} main/system
|
||||
|
||||
(resolve-profile [email]
|
||||
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
|
||||
send
|
||||
(fn [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
(let [message {:type :notification
|
||||
:code code
|
||||
:level level
|
||||
:version (:full cf/version)
|
||||
:subs-id dest
|
||||
:message message}
|
||||
message (->> (dissoc params :dest :code :message :level)
|
||||
(merge message))]
|
||||
(mbus/pub! msgbus
|
||||
:topic dest
|
||||
:message message)))
|
||||
|
||||
(resolve-team [team-id]
|
||||
(->> (db/query pool :team-profile-rel
|
||||
{:team-id team-id}
|
||||
{:columns [:profile-id]})
|
||||
(map :profile-id)))
|
||||
resolve-profile
|
||||
(fn [email]
|
||||
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
|
||||
|
||||
(resolve-dest [dest]
|
||||
(cond
|
||||
(= :all dest)
|
||||
[uuid/zero]
|
||||
resolve-team
|
||||
(fn [team-id]
|
||||
(->> (db/query pool :team-profile-rel
|
||||
{:team-id team-id}
|
||||
{:columns [:profile-id]})
|
||||
(map :profile-id)))
|
||||
|
||||
(uuid? dest)
|
||||
[dest]
|
||||
resolve-dest
|
||||
(fn resolve-dest [dest]
|
||||
(cond
|
||||
(= :all dest)
|
||||
[uuid/zero]
|
||||
|
||||
(string? dest)
|
||||
(some-> dest h/parse-uuid resolve-dest)
|
||||
(uuid? dest)
|
||||
[dest]
|
||||
|
||||
(nil? dest)
|
||||
(resolve-dest uuid/zero)
|
||||
(string? dest)
|
||||
(some-> dest h/parse-uuid resolve-dest)
|
||||
|
||||
(map? dest)
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
(nil? dest)
|
||||
[uuid/zero]
|
||||
|
||||
(and (vector? dest)
|
||||
(every? vector? dest))
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
(map? dest)
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(and (vector? dest)
|
||||
(keyword? (first dest)))
|
||||
(let [[op param] dest]
|
||||
(and (vector? dest)
|
||||
(every? vector? dest))
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(and (vector? dest)
|
||||
(keyword? (first dest)))
|
||||
(let [[op param] dest]
|
||||
(cond
|
||||
(= op :email)
|
||||
(cond
|
||||
(= op :email)
|
||||
(cond
|
||||
(and (coll? param)
|
||||
(every? string? param))
|
||||
(sequence (comp
|
||||
(keep resolve-profile)
|
||||
(mapcat identity))
|
||||
param)
|
||||
(and (coll? param)
|
||||
(every? string? param))
|
||||
(sequence (comp
|
||||
(keep resolve-profile)
|
||||
(mapcat identity))
|
||||
param)
|
||||
|
||||
(string? param)
|
||||
(resolve-profile param))
|
||||
(string? param)
|
||||
(resolve-profile param))
|
||||
|
||||
(= op :team-id)
|
||||
(cond
|
||||
(coll? param)
|
||||
(sequence (comp
|
||||
(mapcat resolve-team)
|
||||
(keep h/parse-uuid))
|
||||
param)
|
||||
(= op :team-id)
|
||||
(cond
|
||||
(coll? param)
|
||||
(sequence (comp
|
||||
(mapcat resolve-team)
|
||||
(keep h/parse-uuid))
|
||||
param)
|
||||
|
||||
(uuid? param)
|
||||
(resolve-team param)
|
||||
(uuid? param)
|
||||
(resolve-team param)
|
||||
|
||||
(string? param)
|
||||
(some-> param h/parse-uuid resolve-team))
|
||||
(string? param)
|
||||
(some-> param h/parse-uuid resolve-team))
|
||||
|
||||
(= op :profile-id)
|
||||
(if (coll? param)
|
||||
(sequence (keep h/parse-uuid) param)
|
||||
(resolve-dest param))))))]
|
||||
(= op :profile-id)
|
||||
(if (coll? param)
|
||||
(sequence (keep h/parse-uuid) param)
|
||||
(resolve-dest param))))))]
|
||||
|
||||
(->> (resolve-dest dest)
|
||||
(filter some?)
|
||||
@@ -314,14 +341,23 @@
|
||||
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[file-id label]
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
[file-id & {:keys [label id]}]
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
snapshot-id (some-> id h/parse-uuid)]
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(when-let [snapshot (->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first))]
|
||||
(fsnap/restore-file-snapshot! system file-id (:id snapshot)))))))
|
||||
(cond
|
||||
(uuid? snapshot-id)
|
||||
(fsnap/restore-file-snapshot! system file-id snapshot-id)
|
||||
|
||||
(string? label)
|
||||
(->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first)
|
||||
(fsnap/restore-file-snapshot! system file-id))
|
||||
|
||||
:else
|
||||
(throw (ex-info "snapshot id or label should be provided" {})))))))
|
||||
|
||||
(defn list-file-snapshots!
|
||||
[file-id & {:as _}]
|
||||
@@ -384,10 +420,12 @@
|
||||
"Apply a function to the file. Optionally save the changes or not.
|
||||
The function receives the decoded and migrated file data."
|
||||
[file-id update-fn & {:keys [rollback?] :or {rollback? true} :as opts}]
|
||||
(db/tx-run! (assoc main/system ::db/rollback rollback?)
|
||||
(fn [system]
|
||||
(binding [h/*system* system]
|
||||
(h/process-file! system file-id update-fn opts)))))
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
(db/tx-run! (assoc main/system ::db/rollback rollback?)
|
||||
(fn [system]
|
||||
(binding [h/*system* system
|
||||
db/*conn* (db/get-connection system)]
|
||||
(h/process-file! system file-id update-fn opts))))))
|
||||
|
||||
(defn process-team-files!
|
||||
"Apply a function to each file of the specified team."
|
||||
@@ -399,7 +437,8 @@
|
||||
(when (string? label)
|
||||
(h/take-team-snapshot! system team-id label))
|
||||
|
||||
(binding [h/*system* system]
|
||||
(binding [h/*system* system
|
||||
db/*conn* (db/get-connection system)]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(reduce (fn [result file-id]
|
||||
(if (h/process-file! system file-id update-fn opts)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
file is eligible to be garbage collected after some period of
|
||||
inactivity (the default threshold is 72h)."
|
||||
(:require
|
||||
[app.binfile.cleaner :as bfl]
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.validate :as cfv]
|
||||
@@ -258,6 +259,7 @@
|
||||
(if-let [file (get-file cfg file-id)]
|
||||
(let [file (->> file
|
||||
(bfc/decode-file cfg)
|
||||
(bfl/clean-file)
|
||||
(clean-media! cfg)
|
||||
(clean-fragments! cfg))
|
||||
file (assoc file :has-media-trimmed true)]
|
||||
|
||||
@@ -26,18 +26,14 @@
|
||||
{k (assoc v ::min-age (cf/get-deletion-delay))})
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::db/pool ::min-age] :as cfg}]
|
||||
[_ {:keys [::min-age] :as cfg}]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (or (:min-age props) min-age)]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval min-age)
|
||||
result (db/exec-one! conn [sql:delete-completed-tasks interval])
|
||||
result (db/get-update-count result)]
|
||||
|
||||
(l/debug :hint "task finished" :total result)
|
||||
|
||||
(when (:rollback? props)
|
||||
(db/rollback! conn))
|
||||
|
||||
result)))))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(db/tx-run! (fn [{:keys [::db/conn]}]
|
||||
(let [interval (db/interval min-age)
|
||||
result (db/exec-one! conn [sql:delete-completed-tasks interval])
|
||||
result (db/get-update-count result)]
|
||||
(l/debug :hint "task finished" :total result)
|
||||
result)))))))
|
||||
|
||||
@@ -71,11 +71,12 @@
|
||||
|
||||
(run-batch! [rconn]
|
||||
(try
|
||||
(db/with-atomic [conn pool]
|
||||
(if-let [tasks (get-tasks conn)]
|
||||
(->> (group-by :queue tasks)
|
||||
(run! (partial push-tasks! conn rconn)))
|
||||
(px/sleep (::wait-duration cfg))))
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(if-let [tasks (get-tasks conn)]
|
||||
(->> (group-by :queue tasks)
|
||||
(run! (partial push-tasks! conn rconn)))
|
||||
;; FIXME: this sleep should be outside the transaction
|
||||
(px/sleep (::wait-duration cfg)))))
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Exception cause
|
||||
|
||||
@@ -24,6 +24,33 @@
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(def schema:task
|
||||
[:map {:title "Task"}
|
||||
[:id ::sm/uuid]
|
||||
[:queue :string]
|
||||
[:name :string]
|
||||
[:created-at ::sm/inst]
|
||||
[:modified-at ::sm/inst]
|
||||
[:scheduled-at {:optional true} ::sm/inst]
|
||||
[:completed-at {:optional true} ::sm/inst]
|
||||
[:error {:optional true} :string]
|
||||
[:max-retries :int]
|
||||
[:retry-num :int]
|
||||
[:priority :int]
|
||||
[:status [:enum "scheduled" "completed" "new" "retry" "failed"]]
|
||||
[:label {:optional true} :string]
|
||||
[:props :map]])
|
||||
|
||||
(def schema:result
|
||||
[:map {:title "TaskResult"}
|
||||
[:status [:enum "retry" "failed" "completed"]]
|
||||
[:error {:optional true} [:fn ex/exception?]]
|
||||
[:inc-by {:optional true} :int]
|
||||
[:delay {:optional true} :int]])
|
||||
|
||||
(def valid-task-result?
|
||||
(sm/validator schema:result))
|
||||
|
||||
(defn- decode-task-row
|
||||
[{:keys [props] :as row}]
|
||||
(cond-> row
|
||||
@@ -51,10 +78,11 @@
|
||||
:retry (:retry-num task))
|
||||
(let [tpoint (dt/tpoint)
|
||||
task-fn (wrk/get-task registry (:name task))
|
||||
result (if task-fn
|
||||
(task-fn task)
|
||||
{:status :completed :task task})
|
||||
elapsed (dt/format-duration (tpoint))]
|
||||
result (when task-fn (task-fn task))
|
||||
elapsed (dt/format-duration (tpoint))
|
||||
result (if (valid-task-result? result)
|
||||
result
|
||||
{:status "completed"})]
|
||||
|
||||
(when-not task-fn
|
||||
(l/wrn :hint "no task handler found" :name (:name task)))
|
||||
@@ -76,7 +104,7 @@
|
||||
(if (and (< (:retry-num task)
|
||||
(:max-retries task))
|
||||
(= ::retry (:type edata)))
|
||||
(cond-> {:status :retry :task task :error cause}
|
||||
(cond-> {:status "retry" :error cause}
|
||||
(dt/duration? (:delay edata))
|
||||
(assoc :delay (:delay edata))
|
||||
|
||||
@@ -87,8 +115,8 @@
|
||||
::l/context (get-error-context cause task)
|
||||
:cause cause)
|
||||
(if (>= (:retry-num task) (:max-retries task))
|
||||
{:status :failed :task task :error cause}
|
||||
{:status :retry :task task :error cause})))))))
|
||||
{:status "failed" :error cause}
|
||||
{:status "retry" :error cause})))))))
|
||||
|
||||
(defn- run-task!
|
||||
[{:keys [::id ::timeout] :as cfg} task-id]
|
||||
@@ -116,12 +144,17 @@
|
||||
:task-id task-id)
|
||||
|
||||
:else
|
||||
(run-task cfg task))))
|
||||
(let [result (run-task cfg task)]
|
||||
(with-meta result
|
||||
{::task task})))))
|
||||
|
||||
(defn- run-worker-loop!
|
||||
[{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}]
|
||||
(letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}]
|
||||
(let [explain (ex-message error)
|
||||
(letfn [(handle-task-retry [{:keys [error inc-by delay] :or {inc-by 1 delay 1000} :as result}]
|
||||
(let [explain (if (ex/exception? error)
|
||||
(ex-message error)
|
||||
(str error))
|
||||
task (-> result meta ::task)
|
||||
nretry (+ (:retry-num task) inc-by)
|
||||
now (dt/now)
|
||||
delay (->> (iterate #(* % 2) delay) (take nretry) (last))]
|
||||
@@ -134,8 +167,9 @@
|
||||
{:id (:id task)})
|
||||
nil))
|
||||
|
||||
(handle-task-failure [{:keys [task error]}]
|
||||
(let [explain (ex-message error)]
|
||||
(handle-task-failure [{:keys [error] :as result}]
|
||||
(let [task (-> result meta ::task)
|
||||
explain (ex-message error)]
|
||||
(db/update! pool :task
|
||||
{:error explain
|
||||
:modified-at (dt/now)
|
||||
@@ -143,8 +177,9 @@
|
||||
{:id (:id task)})
|
||||
nil))
|
||||
|
||||
(handle-task-completion [{:keys [task]}]
|
||||
(let [now (dt/now)]
|
||||
(handle-task-completion [result]
|
||||
(let [task (-> result meta ::task)
|
||||
now (dt/now)]
|
||||
(db/update! pool :task
|
||||
{:completed-at now
|
||||
:modified-at now
|
||||
@@ -168,10 +203,11 @@
|
||||
(process-result [{:keys [status] :as result}]
|
||||
(ex/try!
|
||||
(case status
|
||||
:retry (handle-task-retry result)
|
||||
:failed (handle-task-failure result)
|
||||
:completed (handle-task-completion result)
|
||||
nil)))
|
||||
"retry" (handle-task-retry result)
|
||||
"failed" (handle-task-failure result)
|
||||
"completed" (handle-task-completion result)
|
||||
(throw (IllegalArgumentException.
|
||||
(str "invalid status received: " status))))))
|
||||
|
||||
(run-task-loop [task-id]
|
||||
(loop [result (run-task! cfg task-id)]
|
||||
|
||||
@@ -138,14 +138,13 @@
|
||||
" FROM information_schema.tables "
|
||||
" WHERE table_schema = 'public' "
|
||||
" AND table_name != 'migrations';")]
|
||||
(db/with-atomic [conn *pool*]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(let [result (->> (db/exec! conn [sql])
|
||||
(map :table-name))]
|
||||
(doseq [table result]
|
||||
(db/exec! conn [(str "delete from " table ";")]))))
|
||||
|
||||
(db/transact! *pool* (fn [conn]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(let [result (->> (db/exec! conn [sql])
|
||||
(map :table-name))]
|
||||
(doseq [table result]
|
||||
(db/exec! conn [(str "delete from " table ";")])))))
|
||||
(next)))
|
||||
|
||||
(defn clean-storage
|
||||
|
||||
@@ -20,45 +20,33 @@
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest soft-auth-middleware
|
||||
(db/with-atomic [conn (::db/pool th/*system*)]
|
||||
(let [profile (th/create-profile* 1)
|
||||
system (-> th/*system*
|
||||
(assoc ::db/conn conn)
|
||||
(assoc ::main/props (:app.setup/props th/*system*)))
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
|
||||
token (app.rpc.commands.access-token/create-access-token
|
||||
system (:id profile) "test" nil)
|
||||
request (volatile! nil)
|
||||
handler (#'app.http.access-token/wrap-soft-auth
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
|
||||
request (volatile! nil)
|
||||
handler (#'app.http.access-token/wrap-soft-auth
|
||||
(fn [req] (vreset! request req))
|
||||
system)]
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return nil}]
|
||||
(handler {})
|
||||
(t/is (= {} @request)))
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return nil}]
|
||||
(handler {})
|
||||
(t/is (= {} @request)))
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return (:token token)}]
|
||||
(handler {})
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return (:token token)}]
|
||||
(handler {})
|
||||
|
||||
(let [token-id (get @request :app.http.access-token/id)]
|
||||
(t/is (= token-id (:id token))))))))
|
||||
(let [token-id (get @request :app.http.access-token/id)]
|
||||
(t/is (= token-id (:id token)))))))
|
||||
|
||||
(t/deftest authz-middleware
|
||||
(let [profile (th/create-profile* 1)
|
||||
system (assoc th/*system* ::main/props (:app.setup/props th/*system*))
|
||||
|
||||
token (db/with-atomic [conn (::db/pool th/*system*)]
|
||||
(let [system (assoc system ::db/conn conn)]
|
||||
(app.rpc.commands.access-token/create-access-token
|
||||
system (:id profile) "test" nil)))
|
||||
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
request (volatile! {})
|
||||
handler (#'app.http.access-token/wrap-authz
|
||||
(fn [req] (vreset! request req))
|
||||
system)]
|
||||
th/*system*)]
|
||||
|
||||
(handler nil)
|
||||
(t/is (nil? @request))
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 122785 (:size mobj1)))
|
||||
(t/is (= 3302 (:size mobj2)))))))
|
||||
(t/is (= 3299 (:size mobj2)))))))
|
||||
|
||||
(t/deftest media-object-upload
|
||||
(let [prof (th/create-profile* 1)
|
||||
@@ -85,7 +85,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 312043 (:size mobj1)))
|
||||
(t/is (= 3887 (:size mobj2)))))))
|
||||
(t/is (= 3901 (:size mobj2)))))))
|
||||
|
||||
|
||||
(t/deftest media-object-upload-idempotency
|
||||
@@ -163,7 +163,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 122785 (:size mobj1)))
|
||||
(t/is (= 3302 (:size mobj2)))))))
|
||||
(t/is (= 3299 (:size mobj2)))))))
|
||||
|
||||
(t/deftest media-object-upload-command
|
||||
(let [prof (th/create-profile* 1)
|
||||
@@ -200,7 +200,7 @@
|
||||
(t/is (sto/object? mobj1))
|
||||
(t/is (sto/object? mobj2))
|
||||
(t/is (= 312043 (:size mobj1)))
|
||||
(t/is (= 3887 (:size mobj2)))))))
|
||||
(t/is (= 3901 (:size mobj2)))))))
|
||||
|
||||
|
||||
(t/deftest media-object-upload-idempotency-command
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{org.clojure/clojure {:mvn/version "1.12.0"}
|
||||
org.clojure/data.json {:mvn/version "2.5.1"}
|
||||
org.clojure/tools.cli {:mvn/version "1.1.230"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.132"}
|
||||
org.clojure/clojurescript {:mvn/version "1.12.38"}
|
||||
org.clojure/test.check {:mvn/version "1.1.1"}
|
||||
org.clojure/data.fressian {:mvn/version "1.1.0"}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
thheller/shadow-cljs {:mvn/version "2.28.20"}
|
||||
thheller/shadow-cljs {:mvn/version "3.0.3"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"shadow-cljs": "2.28.20",
|
||||
"shadow-cljs": "3.0.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
|
||||
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
|
||||
|
||||
export OPTIONS="
|
||||
-A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Djdk.attach.allowAttachSelf \
|
||||
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
|
||||
-J-XX:+EnableDynamicAgentLoading \
|
||||
-J-XX:-OmitStackTraceInFastThrow \
|
||||
-J-XX:+UnlockDiagnosticVMOptions \
|
||||
-J-XX:+DebugNonSafepoints \
|
||||
-J-Djdk.tracePinnedThreads=full"
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
export OPTIONS_EVAL="nil"
|
||||
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
|
||||
|
||||
@@ -342,13 +342,20 @@
|
||||
(-> (hex->hsl data)
|
||||
(conj opacity)))
|
||||
|
||||
#?(:cljs
|
||||
(defn format-hsla
|
||||
[[h s l a]]
|
||||
(let [precision 2
|
||||
rounded-s (* 100 (parse-double (d/format-precision s precision)))
|
||||
rounded-l (* 100 (parse-double (d/format-precision l precision)))]
|
||||
(str/concat "" h ", " rounded-s "%, " rounded-l "%, " a))))
|
||||
(defn format-hsla
|
||||
[[h s l a]]
|
||||
(let [precision 2
|
||||
rounded-h (int h)
|
||||
rounded-s (d/format-number (* 100 s) precision)
|
||||
rounded-l (d/format-number (* 100 l) precision)
|
||||
rounded-a (d/format-number a precision)]
|
||||
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
|
||||
|
||||
(defn format-rgba
|
||||
[[r g b a]]
|
||||
(let [precision 2
|
||||
rounded-a (d/format-number a precision)]
|
||||
(str/ffmt "%, %, %, %" r g b rounded-a)))
|
||||
|
||||
(defn- hue->rgb
|
||||
"Helper for hsl->rgb"
|
||||
|
||||
@@ -82,15 +82,22 @@
|
||||
"Assoc a k v pair, in the order position just before the other key."
|
||||
[o ks k v before-k]
|
||||
(let [f (fn [o']
|
||||
(cond-> (reduce
|
||||
(fn [acc [k' v']]
|
||||
(cond
|
||||
(and before-k (= k' before-k)) (assoc acc k v k' v')
|
||||
(= k k') acc
|
||||
:else (assoc acc k' v')))
|
||||
(ordered-map)
|
||||
o')
|
||||
(not before-k) (assoc k v)))]
|
||||
(let [found (volatile! false)
|
||||
result (reduce
|
||||
(fn [acc [k' v']]
|
||||
(cond
|
||||
(and before-k (= k' before-k))
|
||||
(do
|
||||
(vreset! found true)
|
||||
(assoc acc k v k' v'))
|
||||
|
||||
(= k k') acc
|
||||
:else (assoc acc k' v')))
|
||||
(ordered-map)
|
||||
o')]
|
||||
(if (or (not before-k) (not @found))
|
||||
(assoc result k v)
|
||||
result)))]
|
||||
(if (seq ks)
|
||||
(oupdate-in o ks f)
|
||||
(f o))))
|
||||
@@ -1007,27 +1014,35 @@
|
||||
(def ^:const trail-zeros-regex-1 #"\.0+$")
|
||||
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
|
||||
|
||||
#?(:cljs
|
||||
(defn format-precision
|
||||
"Creates a number with predetermined precision and then removes the trailing 0.
|
||||
(defn format-precision
|
||||
"Creates a number with predetermined precision and then removes the trailing 0.
|
||||
Examples:
|
||||
12.0123, 0 => 12
|
||||
12.0123, 1 => 12
|
||||
12.0123, 2 => 12.01"
|
||||
[num precision]
|
||||
[num precision]
|
||||
|
||||
(if (number? num)
|
||||
(try
|
||||
(let [num-str (mth/to-fixed num precision)
|
||||
(if (number? num)
|
||||
(try
|
||||
(let [num-str (mth/to-fixed num precision)
|
||||
;; Remove all trailing zeros after the comma 100.00000
|
||||
num-str (str/replace num-str trail-zeros-regex-1 "")]
|
||||
num-str (str/replace num-str trail-zeros-regex-1 "")]
|
||||
;; Remove trailing zeros after a decimal number: 0.001|00|
|
||||
(if-let [m (re-find trail-zeros-regex-2 num-str)]
|
||||
(str/replace num-str (first m) (second m))
|
||||
num-str))
|
||||
(catch :default _
|
||||
(str num)))
|
||||
(str num))))
|
||||
(if-let [m (re-find trail-zeros-regex-2 num-str)]
|
||||
(str/replace num-str (first m) (second m))
|
||||
num-str))
|
||||
(catch #?(:clj Throwable :cljs :default) _
|
||||
(str num)))
|
||||
(str num)))
|
||||
|
||||
(defn format-number
|
||||
([value]
|
||||
(format-number value nil))
|
||||
([value {:keys [precision] :or {precision 2}}]
|
||||
(let [value (if (string? value) (parse-double value) value)]
|
||||
(when (num? value)
|
||||
(let [value (format-precision value precision)]
|
||||
(str value))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Util protocols
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.common.data.macros
|
||||
"Data retrieval & manipulation specific macros."
|
||||
(:refer-clojure :exclude [get-in select-keys str with-open min max])
|
||||
(:refer-clojure :exclude [get-in select-keys str with-open max])
|
||||
#?(:cljs (:require-macros [app.common.data.macros]))
|
||||
(:require
|
||||
#?(:clj [clojure.core :as c]
|
||||
@@ -144,3 +144,8 @@
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
(when *assert*
|
||||
`(runtime-assert ~hint (fn [] ~expr))))))
|
||||
|
||||
(defn truncate
|
||||
"Truncates a string to a certain length"
|
||||
[s max-length]
|
||||
(subs s 0 (min max-length (count s))))
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
(defn undo
|
||||
[stack]
|
||||
(update stack :index dec))
|
||||
(update stack :index #(max 0 (dec %))))
|
||||
|
||||
(defn redo
|
||||
[{index :index items :items :as stack}]
|
||||
|
||||
@@ -46,21 +46,30 @@
|
||||
#{"fdata/objects-map"
|
||||
"fdata/pointer-map"
|
||||
"fdata/shape-data-type"
|
||||
"fdata/path-data"
|
||||
"components/v2"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"
|
||||
"text-editor/v2"
|
||||
"render-wasm/v1"})
|
||||
"render-wasm/v1"
|
||||
"variants/v1"})
|
||||
|
||||
;; A set of features enabled by default
|
||||
(def default-features
|
||||
#{"fdata/shape-data-type"
|
||||
"fdata/path-data"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"components/v2"
|
||||
"plugins/runtime"})
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"})
|
||||
|
||||
;; A set of features that should not be propagated to team on creating
|
||||
;; or modifying a file
|
||||
(def no-team-inheritable-features
|
||||
#{"fdata/path-data"})
|
||||
|
||||
;; A set of features which only affects on frontend and can be enabled
|
||||
;; and disabled freely by the user any time. This features does not
|
||||
@@ -83,12 +92,12 @@
|
||||
;; be applied (per example backend can operate in both modes with or
|
||||
;; without migration applied)
|
||||
(def no-migration-features
|
||||
(-> #{"fdata/objects-map"
|
||||
"fdata/pointer-map"
|
||||
"layout/grid"
|
||||
(-> #{"layout/grid"
|
||||
"design-tokens/v1"
|
||||
"fdata/shape-data-type"
|
||||
"design-tokens/v1"}
|
||||
(into frontend-only-features)))
|
||||
"fdata/path-data"}
|
||||
(into frontend-only-features)
|
||||
(into backend-only-features)))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::features}
|
||||
@@ -102,15 +111,14 @@
|
||||
"Translate a flag to a feature name"
|
||||
[flag]
|
||||
(case flag
|
||||
:feature-components-v2 "components/v2"
|
||||
:feature-styles-v2 "styles/v2"
|
||||
:feature-grid-layout "layout/grid"
|
||||
:feature-fdata-objects-map "fdata/objects-map"
|
||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||
:feature-plugins "plugins/runtime"
|
||||
:feature-design-tokens "design-tokens/v1"
|
||||
:feature-text-editor-v2 "text-editor/v2"
|
||||
:feature-render-wasm "render-wasm/v1"
|
||||
:feature-variants "variants/v1"
|
||||
nil))
|
||||
|
||||
(defn migrate-legacy-features
|
||||
@@ -155,7 +163,6 @@
|
||||
team-features (into #{} xf-remove-ephimeral (:features team))]
|
||||
(-> enabled-features
|
||||
(set/intersection no-migration-features)
|
||||
(set/difference frontend-only-features)
|
||||
(set/union team-features))))
|
||||
|
||||
(defn check-client-features!
|
||||
@@ -164,6 +171,8 @@
|
||||
frontend client"
|
||||
[enabled-features client-features]
|
||||
(when (set? client-features)
|
||||
;; Check if client declares support for features enabled on
|
||||
;; backend side
|
||||
(let [not-supported (-> enabled-features
|
||||
(set/difference client-features)
|
||||
(set/difference frontend-only-features)
|
||||
@@ -173,14 +182,6 @@
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "client declares no support for '%' features"
|
||||
(str/join "," not-supported)))))
|
||||
|
||||
(let [not-supported (set/difference client-features supported-features)]
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "backend does not support '%' features requested by client"
|
||||
(str/join "," not-supported))))))
|
||||
|
||||
enabled-features)
|
||||
@@ -191,57 +192,55 @@
|
||||
supported by the current backend"
|
||||
[enabled-features]
|
||||
(let [not-supported (set/difference enabled-features supported-features)]
|
||||
(when (seq not-supported)
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-not-supported
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "features '%' not supported"
|
||||
(str/join "," not-supported)))))
|
||||
enabled-features)
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "feature '%' not supported on this backend" not-supported)))
|
||||
enabled-features))
|
||||
|
||||
(defn check-file-features!
|
||||
"Function used for check feature compability between currently
|
||||
enabled features set on backend with the provided featured set by
|
||||
the penpot file"
|
||||
([enabled-features file-features]
|
||||
(check-file-features! enabled-features file-features #{}))
|
||||
([enabled-features file-features client-features]
|
||||
(let [file-features (into #{} xf-remove-ephimeral file-features)
|
||||
;; We should ignore all features that does not match with the
|
||||
;; `no-migration-features` set because we can't enable them
|
||||
;; as-is, because they probably need migrations
|
||||
client-features (set/intersection client-features no-migration-features)]
|
||||
(let [not-supported (-> enabled-features
|
||||
(set/union client-features)
|
||||
(set/difference file-features)
|
||||
;; NOTE: we don't want to raise a feature-mismatch
|
||||
;; exception for features which don't require an
|
||||
;; explicit file migration process or has no real
|
||||
;; effect on file data structure
|
||||
(set/difference no-migration-features))]
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "enabled features '%' not present in file (missing migration)"
|
||||
(str/join "," not-supported)))))
|
||||
[enabled-features file-features]
|
||||
(let [file-features (into #{} xf-remove-ephimeral file-features)
|
||||
not-supported (-> enabled-features
|
||||
(set/difference file-features)
|
||||
;; NOTE: we don't want to raise a feature-mismatch
|
||||
;; exception for features which don't require an
|
||||
;; explicit file migration process or has no real
|
||||
;; effect on file data structure
|
||||
(set/difference no-migration-features))]
|
||||
|
||||
(check-supported-features! file-features)
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
|
||||
not-supported)))
|
||||
|
||||
(let [not-supported (-> file-features
|
||||
(set/difference enabled-features)
|
||||
(set/difference client-features)
|
||||
(set/difference backend-only-features)
|
||||
(set/difference frontend-only-features))]
|
||||
(check-supported-features! file-features)
|
||||
|
||||
(when (seq not-supported)
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature (first not-supported)
|
||||
:hint (str/ffmt "file features '%' not enabled"
|
||||
(str/join "," not-supported))))))
|
||||
;; Components v1 is deprecated
|
||||
(when-not (contains? file-features "components/v2")
|
||||
(ex/raise :type :restriction
|
||||
:code :file-in-components-v1
|
||||
:hint "components v1 is deprecated"))
|
||||
|
||||
enabled-features))
|
||||
(let [not-supported (-> file-features
|
||||
(set/difference enabled-features)
|
||||
(set/difference backend-only-features)
|
||||
(set/difference frontend-only-features))]
|
||||
|
||||
;; Check if file has a feature but that feature is not enabled
|
||||
(when-let [not-supported (first not-supported)]
|
||||
(ex/raise :type :restriction
|
||||
:code :file-feature-mismatch
|
||||
:feature not-supported
|
||||
:hint (str/ffmt "file feature '%' not enabled" not-supported))))
|
||||
|
||||
enabled-features))
|
||||
|
||||
(defn check-teams-compatibility!
|
||||
[{source-features :features} {destination-features :features}]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,11 +26,10 @@
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.token-theme :as ctot]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
@@ -336,13 +335,17 @@
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
|
||||
[:name {:optional true} :string]]]
|
||||
[:name {:optional true} :string]
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
|
||||
|
||||
[:del-component
|
||||
[:map {:title "DelComponentChange"}
|
||||
[:type [:= :del-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:main-instance {:optional true} :any]
|
||||
;; when it is an undo of a cut-paste, we need to undo the movement
|
||||
;; of the shapes so we need to move them delta
|
||||
[:delta {:optional true} ::gpt/point]
|
||||
[:skip-undelete? {:optional true} :boolean]]]
|
||||
|
||||
[:restore-component
|
||||
@@ -371,111 +374,58 @@
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:add-temporary-token-theme
|
||||
[:map {:title "AddTemporaryTokenThemeChange"}
|
||||
[:type [:= :add-temporary-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[:theme-ids [:set :string]]]]
|
||||
|
||||
[:delete-temporary-token-theme
|
||||
[:map {:title "DeleteTemporaryTokenThemeChange"}
|
||||
[:type [:= :delete-temporary-token-theme]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-theme
|
||||
[:map {:title "AddTokenThemeChange"}
|
||||
[:type [:= :add-token-theme]]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:mod-token-theme
|
||||
[:map {:title "ModTokenThemeChange"}
|
||||
[:type [:= :mod-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]
|
||||
[:token-theme ::ctot/token-theme]]]
|
||||
|
||||
[:del-token-theme
|
||||
[:map {:title "DelTokenThemeChange"}
|
||||
[:type [:= :del-token-theme]]
|
||||
[:group :string]
|
||||
[:name :string]]]
|
||||
|
||||
[:add-token-set
|
||||
[:map {:title "AddTokenSetChange"}
|
||||
[:type [:= :add-token-set]]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:add-token-sets
|
||||
[:map {:title "AddTokenSetsChange"}
|
||||
[:type [:= :add-token-sets]]
|
||||
[:token-sets [:sequential ::ctot/token-set]]]]
|
||||
|
||||
[:rename-token-set-group
|
||||
[:map {:title "RenameTokenSetGroup"}
|
||||
[:type [:= :rename-token-set-group]]
|
||||
[:set-group-path [:vector :string]]
|
||||
[:set-group-fname :string]]]
|
||||
|
||||
[:mod-token-set
|
||||
[:map {:title "ModTokenSetChange"}
|
||||
[:type [:= :mod-token-set]]
|
||||
[:name :string]
|
||||
[:token-set ::ctot/token-set]]]
|
||||
|
||||
[:move-token-set-before
|
||||
[:map {:title "MoveTokenSetBefore"}
|
||||
[:type [:= :move-token-set-before]]
|
||||
[:move-token-set
|
||||
[:map {:title "MoveTokenSet"}
|
||||
[:type [:= :move-token-set]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group? [:maybe :boolean]]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
|
||||
[:move-token-set-group-before
|
||||
[:map {:title "MoveTokenSetGroupBefore"}
|
||||
[:type [:= :move-token-set-group-before]]
|
||||
[:move-token-set-group
|
||||
[:map {:title "MoveTokenSetGroup"}
|
||||
[:type [:= :move-token-set-group]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group? [:maybe :boolean]]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
|
||||
[:del-token-set
|
||||
[:map {:title "DelTokenSetChange"}
|
||||
[:type [:= :del-token-set]]
|
||||
[:name :string]]]
|
||||
|
||||
[:del-token-set-path
|
||||
[:map {:title "DelTokenSetPathChange"}
|
||||
[:type [:= :del-token-set-path]]
|
||||
[:path :string]]]
|
||||
[:set-token-theme
|
||||
[:map {:title "SetTokenThemeChange"}
|
||||
[:type [:= :set-token-theme]]
|
||||
[:theme-name :string]
|
||||
[:group :string]
|
||||
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib :any]]]
|
||||
|
||||
[:add-token
|
||||
[:map {:title "AddTokenChange"}
|
||||
[:type [:= :add-token]]
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:set-name :string]
|
||||
[:token ::cto/token]]]
|
||||
[:group? :boolean]
|
||||
[:token-set [:maybe ctob/schema:token-set-attrs]]]]
|
||||
|
||||
[:mod-token
|
||||
[:map {:title "ModTokenChange"}
|
||||
[:type [:= :mod-token]]
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]
|
||||
[:token ::cto/token]]]
|
||||
|
||||
[:del-token
|
||||
[:map {:title "DelTokenChange"}
|
||||
[:type [:= :del-token]]
|
||||
[:set-name :string]
|
||||
[:name :string]]]]])
|
||||
[:token-name :string]
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@@ -782,20 +732,22 @@
|
||||
|
||||
(update-group [group objects]
|
||||
(let [lookup (d/getf objects)
|
||||
children (->> group :shapes (map lookup))]
|
||||
children (get group :shapes)]
|
||||
(cond
|
||||
;; If the group is empty we don't make any changes. Will be removed by a later process
|
||||
(empty? children)
|
||||
group
|
||||
|
||||
(= :bool (:type group))
|
||||
(gsh/update-bool-selrect group children objects)
|
||||
(gsh/update-bool group objects)
|
||||
|
||||
(:masked-group group)
|
||||
(set-mask-selrect group children)
|
||||
(->> (map lookup children)
|
||||
(set-mask-selrect group))
|
||||
|
||||
:else
|
||||
(gsh/update-group-selrect group children))))]
|
||||
(->> (map lookup children)
|
||||
(gsh/update-group-selrect group)))))]
|
||||
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id :objects] reg-objects)
|
||||
@@ -1009,8 +961,8 @@
|
||||
(ctkl/mod-component data params))
|
||||
|
||||
(defmethod process-change :del-component
|
||||
[data {:keys [id skip-undelete? main-instance]}]
|
||||
(ctf/delete-component data id skip-undelete? main-instance))
|
||||
[data {:keys [id skip-undelete? delta]}]
|
||||
(ctf/delete-component data id skip-undelete? delta))
|
||||
|
||||
(defmethod process-change :restore-component
|
||||
[data {:keys [id page-id]}]
|
||||
@@ -1040,80 +992,63 @@
|
||||
[data {:keys [tokens-lib]}]
|
||||
(assoc data :tokens-lib tokens-lib))
|
||||
|
||||
(defmethod process-change :add-token
|
||||
[data {:keys [set-name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-token-in-set set-name (ctob/make-token token)))))
|
||||
(defmethod process-change :set-token
|
||||
[data {:keys [set-name token-name token]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token)
|
||||
(ctob/delete-token-from-set lib' set-name token-name)
|
||||
|
||||
(defmethod process-change :mod-token
|
||||
[data {:keys [set-name name token]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-token-in-set
|
||||
set-name
|
||||
name
|
||||
(fn [old-token]
|
||||
(ctob/make-token (merge old-token token)))))))
|
||||
(not (ctob/get-token-in-set lib' set-name token-name))
|
||||
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
|
||||
|
||||
(defmethod process-change :del-token
|
||||
[data {:keys [set-name name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-token-from-set
|
||||
set-name
|
||||
name))))
|
||||
:else
|
||||
(ctob/update-token-in-set lib' set-name token-name (fn [prev-token]
|
||||
(ctob/make-token (merge prev-token token)))))))))
|
||||
|
||||
(defmethod process-change :add-temporary-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme token-theme)))))
|
||||
(defmethod process-change :set-token-set
|
||||
[data {:keys [set-name group? token-set]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token-set)
|
||||
(if group?
|
||||
(ctob/delete-set-group lib' set-name)
|
||||
(ctob/delete-set lib' set-name))
|
||||
|
||||
(not (ctob/get-set lib' set-name))
|
||||
(ctob/add-set lib' (ctob/make-token-set token-set))
|
||||
|
||||
:else
|
||||
(ctob/update-set lib' set-name (fn [prev-token-set]
|
||||
(ctob/make-token-set (merge prev-token-set token-set)))))))))
|
||||
|
||||
(defmethod process-change :set-token-theme
|
||||
[data {:keys [group theme-name theme]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not theme)
|
||||
(ctob/delete-theme lib' group theme-name)
|
||||
|
||||
(not (ctob/get-theme lib' group theme-name))
|
||||
(ctob/add-theme lib' (ctob/make-token-theme theme))
|
||||
|
||||
:else
|
||||
(ctob/update-theme lib'
|
||||
group theme-name
|
||||
(fn [prev-token-theme]
|
||||
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
|
||||
|
||||
(defmethod process-change :update-active-token-themes
|
||||
[data {:keys [theme-ids]}]
|
||||
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
|
||||
(ctob/set-active-themes theme-ids))))
|
||||
|
||||
(defmethod process-change :delete-temporary-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-theme
|
||||
[data {:keys [token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-theme (-> token-theme
|
||||
(ctob/make-token-theme))))))
|
||||
|
||||
(defmethod process-change :mod-token-theme
|
||||
[data {:keys [name group token-theme]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-theme group name
|
||||
(fn [prev-theme]
|
||||
(merge prev-theme token-theme))))))
|
||||
|
||||
(defmethod process-change :del-token-theme
|
||||
[data {:keys [group name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-theme group name))))
|
||||
|
||||
(defmethod process-change :add-token-set
|
||||
[data {:keys [token-set]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set token-set)))))
|
||||
|
||||
(defmethod process-change :add-token-sets
|
||||
[data {:keys [token-sets]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/add-sets (map ctob/make-token-set token-sets)))))
|
||||
|
||||
(defmethod process-change :rename-token-set-group
|
||||
[data {:keys [set-group-path set-group-fname]}]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
@@ -1121,37 +1056,17 @@
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/rename-set-group set-group-path set-group-fname)))))
|
||||
|
||||
(defmethod process-change :mod-token-set
|
||||
[data {:keys [name token-set]}]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
(-> lib
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/update-set name (fn [prev-set]
|
||||
(merge prev-set (dissoc token-set :tokens))))))))
|
||||
|
||||
(defmethod process-change :move-token-set-before
|
||||
[data {:keys [from-path to-path before-path before-group?] :as changes}]
|
||||
(defmethod process-change :move-token-set
|
||||
[data {:keys [from-path to-path before-path before-group] :as changes}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set from-path to-path before-path before-group?))))
|
||||
(ctob/move-set from-path to-path before-path before-group))))
|
||||
|
||||
(defmethod process-change :move-token-set-group-before
|
||||
[data {:keys [from-path to-path before-path before-group?]}]
|
||||
(defmethod process-change :move-token-set-group
|
||||
[data {:keys [from-path to-path before-path before-group]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-group from-path to-path before-path before-group?))))
|
||||
|
||||
(defmethod process-change :del-token-set
|
||||
[data {:keys [name]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-set-path name))))
|
||||
|
||||
(defmethod process-change :del-token-set-path
|
||||
[data {:keys [path]}]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/delete-set-path path))))
|
||||
(ctob/move-set-group from-path to-path before-path before-group))))
|
||||
|
||||
;; === Operations
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.changes :as cfc]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
@@ -84,8 +83,7 @@
|
||||
|
||||
(defn with-objects
|
||||
[changes objects]
|
||||
(let [fdata (binding [cfeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data (uuid/next) uuid/zero))
|
||||
(let [fdata (ctf/make-file-data (uuid/next) uuid/zero)
|
||||
fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)]
|
||||
(vary-meta changes assoc
|
||||
::file-data fdata
|
||||
@@ -155,13 +153,14 @@
|
||||
(dm/get-in data [:pages-index uuid/zero :objects])))
|
||||
|
||||
(defn apply-changes-local
|
||||
[changes]
|
||||
[changes & {:keys [apply-to-library?]}]
|
||||
(dm/assert!
|
||||
"expected valid changes"
|
||||
(check-changes! changes))
|
||||
|
||||
(if-let [file-data (::file-data (meta changes))]
|
||||
(let [index (::applied-changes-count (meta changes))
|
||||
(let [library-data (::library-data (meta changes))
|
||||
index (::applied-changes-count (meta changes))
|
||||
redo-changes (:redo-changes changes)
|
||||
new-changes (if (< index (count redo-changes))
|
||||
(->> (subvec (:redo-changes changes) index)
|
||||
@@ -169,8 +168,12 @@
|
||||
(assoc :page-id uuid/zero)
|
||||
(dissoc :component-id))))
|
||||
[])
|
||||
new-file-data (cfc/process-changes file-data new-changes)]
|
||||
new-file-data (cfc/process-changes file-data new-changes)
|
||||
new-library-data (if apply-to-library?
|
||||
(cfc/process-changes library-data new-changes)
|
||||
library-data)]
|
||||
(vary-meta changes assoc ::file-data new-file-data
|
||||
::library-data new-library-data
|
||||
::applied-changes-count (count redo-changes)))
|
||||
changes))
|
||||
|
||||
@@ -475,9 +478,12 @@
|
||||
(let [old-val (get old attr)
|
||||
new-val (get new attr)]
|
||||
(not= old-val new-val)))
|
||||
new-obj (if with-objects?
|
||||
(update-fn object objects)
|
||||
(update-fn object))]
|
||||
|
||||
new-obj
|
||||
(if with-objects?
|
||||
(update-fn object objects)
|
||||
(update-fn object))]
|
||||
|
||||
(when-not (= object new-obj)
|
||||
(let [attrs (or attrs (d/concat-set (keys object) (keys new-obj)))]
|
||||
(filter (partial changed? object new-obj) attrs)))))
|
||||
@@ -654,9 +660,13 @@
|
||||
nil ;; so it does not need resize
|
||||
|
||||
(= (:type parent) :bool)
|
||||
(gsh/update-bool-selrect parent children objects)
|
||||
(gsh/update-bool parent objects)
|
||||
|
||||
(= (:type parent) :group)
|
||||
;; FIXME: this functions should be
|
||||
;; normalized in the same way as
|
||||
;; update-bool in order to make all
|
||||
;; this code consistent
|
||||
(if (:masked-group parent)
|
||||
(gsh/update-mask-selrect parent children)
|
||||
(gsh/update-group-selrect parent children)))]
|
||||
@@ -762,13 +772,6 @@
|
||||
(update :undo-changes conj {:type :add-typography :typography prev-typography})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-temporary-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-temporary-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-active-token-themes
|
||||
[changes token-active-theme-ids prev-token-active-theme-ids]
|
||||
(-> changes
|
||||
@@ -776,42 +779,32 @@
|
||||
(update :undo-changes conj {:type :update-active-token-themes :theme-ids prev-token-active-theme-ids})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn add-token-theme
|
||||
[changes token-theme]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-theme :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :del-token-theme :group (:group token-theme) :name (:name token-theme)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token-theme
|
||||
[changes token-theme prev-token-theme]
|
||||
(let [name (or (:name prev-token-theme)
|
||||
(:name token-theme))
|
||||
group (or (:group prev-token-theme)
|
||||
(:group token-theme))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-theme :group group :name name :token-theme token-theme})
|
||||
(update :undo-changes conj {:type :mod-token-theme :group group :name name :token-theme (or prev-token-theme token-theme)})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn delete-token-theme
|
||||
[changes group name]
|
||||
(defn set-token-theme [changes group theme-name theme]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-theme group name))]
|
||||
prev-theme (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-theme group theme-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-theme :group group :name name})
|
||||
(update :undo-changes conj {:type :add-token-theme :token-theme prev-token-theme})
|
||||
(update :redo-changes conj {:type :set-token-theme
|
||||
:theme-name theme-name
|
||||
:group group
|
||||
:theme theme})
|
||||
(update :undo-changes conj (if prev-theme
|
||||
{:type :set-token-theme
|
||||
:group group
|
||||
:theme-name (or
|
||||
;; Undo of edit
|
||||
(:name theme)
|
||||
;; Undo of delete
|
||||
theme-name)
|
||||
:theme prev-theme}
|
||||
;; Undo of create
|
||||
{:type :set-token-theme
|
||||
:group group
|
||||
:theme-name theme-name
|
||||
:theme nil}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token-set
|
||||
[changes token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token-set :token-set token-set})
|
||||
(update :undo-changes conj {:type :del-token-set :name (:name token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn rename-token-set-group
|
||||
[changes set-group-path set-group-fname]
|
||||
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
|
||||
@@ -821,58 +814,34 @@
|
||||
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn update-token-set
|
||||
[changes token-set prev-token-set]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token-set :name (:name prev-token-set) :token-set token-set})
|
||||
(update :undo-changes conj {:type :mod-token-set :name (:name token-set) :token-set (or prev-token-set token-set)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token-set-path
|
||||
[changes group? path]
|
||||
(assert-library! changes)
|
||||
(let [;; TODO Move leaking prefix to library
|
||||
prefixed-path (if group?
|
||||
(ctob/set-group-path->set-group-prefixed-path path)
|
||||
(ctob/set-full-path->set-prefixed-full-path path))
|
||||
prefixed-path-str (ctob/join-set-path prefixed-path)
|
||||
library-data (::library-data (meta changes))
|
||||
prev-token-sets (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-path-sets prefixed-path-str))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token-set-path :path prefixed-path-str})
|
||||
(update :undo-changes conj {:type :add-token-sets :token-sets prev-token-sets})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn move-token-set-before
|
||||
(defn move-token-set
|
||||
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :move-token-set-before
|
||||
(update :redo-changes conj {:type :move-token-set
|
||||
:from-path from-path
|
||||
:to-path to-path
|
||||
:before-path before-path
|
||||
:before-group? before-group?})
|
||||
(update :undo-changes conj {:type :move-token-set-before
|
||||
:before-group before-group?})
|
||||
(update :undo-changes conj {:type :move-token-set
|
||||
:from-path to-path
|
||||
:to-path from-path
|
||||
:before-path prev-before-path
|
||||
:before-group? prev-before-group?})
|
||||
:before-group prev-before-group?})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn move-token-set-group-before
|
||||
(defn move-token-set-group
|
||||
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
|
||||
(prn prev-before-path prev-before-group?)
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :move-token-set-group-before
|
||||
(update :redo-changes conj {:type :move-token-set-group
|
||||
:from-path from-path
|
||||
:to-path to-path
|
||||
:before-path before-path
|
||||
:before-group? before-group?})
|
||||
(update :undo-changes conj {:type :move-token-set-group-before
|
||||
:before-group before-group?})
|
||||
(update :undo-changes conj {:type :move-token-set-group
|
||||
:from-path to-path
|
||||
:to-path from-path
|
||||
:before-path prev-before-path
|
||||
:before-group? prev-before-group?})
|
||||
:before-group prev-before-group?})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn set-tokens-lib
|
||||
@@ -884,36 +853,84 @@
|
||||
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-token
|
||||
[changes set-name token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :add-token :set-name set-name :token token})
|
||||
(update :undo-changes conj {:type :del-token :set-name set-name :name (:name token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn update-token
|
||||
[changes set-name token prev-token]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-token :set-name set-name :name (:name prev-token) :token token})
|
||||
(update :undo-changes conj {:type :mod-token :set-name set-name :name (:name token) :token (or prev-token token)})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn delete-token
|
||||
[changes set-name token-name]
|
||||
(defn set-token [changes set-name token-name token]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set set-name)
|
||||
(ctob/get-token token-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :del-token :set-name set-name :name token-name})
|
||||
(update :undo-changes conj {:type :add-token :set-name set-name :token prev-token})
|
||||
(update :redo-changes conj {:type :set-token
|
||||
:set-name set-name
|
||||
:token-name token-name
|
||||
:token token})
|
||||
(update :undo-changes conj (if prev-token
|
||||
{:type :set-token
|
||||
:set-name set-name
|
||||
:token-name (or
|
||||
;; Undo of edit
|
||||
(:name token)
|
||||
;; Undo of delete
|
||||
token-name)
|
||||
:token prev-token}
|
||||
;; Undo of create token
|
||||
{:type :set-token
|
||||
:set-name set-name
|
||||
:token-name token-name
|
||||
:token nil}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn rename-token-set
|
||||
[changes name new-name]
|
||||
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-set (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-token-set
|
||||
:set-name name
|
||||
:token-set (assoc prev-token-set :name new-name)
|
||||
:group? false})
|
||||
(update :undo-changes conj {:type :set-token-set
|
||||
:set-name new-name
|
||||
:token-set prev-token-set
|
||||
:group? false})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn set-token-set
|
||||
[changes set-name group? token-set]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-token-set (some-> (get library-data :tokens-lib)
|
||||
(ctob/get-set set-name))]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-token-set
|
||||
:set-name set-name
|
||||
:token-set token-set
|
||||
:group? group?})
|
||||
(update :undo-changes conj (if prev-token-set
|
||||
{:type :set-token-set
|
||||
:set-name (or
|
||||
;; Undo of edit
|
||||
(:name token-set)
|
||||
;; Undo of delete
|
||||
set-name)
|
||||
:token-set prev-token-set
|
||||
:group? group?}
|
||||
;; Undo of create
|
||||
{:type :set-token-set
|
||||
:set-name set-name
|
||||
:token-set nil
|
||||
:group? group?}))
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn add-component
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil))
|
||||
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation]
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page]
|
||||
(add-component changes id path name updated-shapes main-instance-id main-instance-page nil nil nil))
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page annotation]
|
||||
(add-component changes id path name updated-shapes main-instance-id main-instance-page annotation nil nil))
|
||||
([changes id path name updated-shapes main-instance-id main-instance-page annotation variant-id variant-properties & {:keys [apply-changes-local-library?]}]
|
||||
(assert-page-id! changes)
|
||||
(assert-objects! changes)
|
||||
(let [page-id (::page-id (meta changes))
|
||||
@@ -953,8 +970,10 @@
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:annotation annotation}
|
||||
(some? new-shapes) ;; this will be null in components-v2
|
||||
(assoc :shapes (vec new-shapes))))
|
||||
(some? variant-id)
|
||||
(assoc :variant-id variant-id)
|
||||
(seq variant-properties)
|
||||
(assoc :variant-properties variant-properties)))
|
||||
(into (map mk-change) updated-shapes))))
|
||||
(update :undo-changes
|
||||
(fn [undo-changes]
|
||||
@@ -967,33 +986,51 @@
|
||||
(map mk-change))
|
||||
updated-shapes))))
|
||||
|
||||
(apply-changes-local)))))
|
||||
(apply-changes-local {:apply-to-library? apply-changes-local-library?})))))
|
||||
|
||||
(defn update-component
|
||||
[changes id update-fn]
|
||||
[changes id update-fn & {:keys [apply-changes-local-library?]}]
|
||||
(assert-library! changes)
|
||||
(let [library-data (::library-data (meta changes))
|
||||
prev-component (get-in library-data [:components id])
|
||||
new-component (update-fn prev-component)]
|
||||
(if prev-component
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-component
|
||||
:id id
|
||||
:name (:name new-component)
|
||||
:path (:path new-component)
|
||||
:main-instance-id (:main-instance-id new-component)
|
||||
:main-instance-page (:main-instance-page new-component)
|
||||
:annotation (:annotation new-component)
|
||||
:objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components)
|
||||
:modified-at (:modified-at new-component)})
|
||||
(update :undo-changes conj {:type :mod-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:annotation (:annotation prev-component)
|
||||
:objects (:objects prev-component)}))
|
||||
(update :redo-changes conj (cond-> {:type :mod-component
|
||||
:id id
|
||||
:name (:name new-component)
|
||||
:path (:path new-component)
|
||||
:main-instance-id (:main-instance-id new-component)
|
||||
:main-instance-page (:main-instance-page new-component)
|
||||
:annotation (:annotation new-component)
|
||||
:objects (:objects new-component) ;; for deleted components
|
||||
:modified-at (:modified-at new-component)}
|
||||
(some? (:variant-id new-component))
|
||||
(assoc :variant-id (:variant-id new-component))
|
||||
(nil? (:variant-id new-component))
|
||||
(dissoc :variant-id)
|
||||
(seq (:variant-properties new-component))
|
||||
(assoc :variant-properties (:variant-properties new-component))
|
||||
(not (seq (:variant-properties new-component)))
|
||||
(dissoc :variant-properties)))
|
||||
(update :undo-changes conj (cond-> {:type :mod-component
|
||||
:id id
|
||||
:name (:name prev-component)
|
||||
:path (:path prev-component)
|
||||
:main-instance-id (:main-instance-id prev-component)
|
||||
:main-instance-page (:main-instance-page prev-component)
|
||||
:annotation (:annotation prev-component)
|
||||
:objects (:objects prev-component)}
|
||||
(some? (:variant-id prev-component))
|
||||
(assoc :variant-id (:variant-id prev-component))
|
||||
(nil? (:variant-id prev-component))
|
||||
(dissoc :variant-id)
|
||||
(seq (:variant-properties prev-component))
|
||||
(assoc :variant-properties (:variant-properties prev-component))
|
||||
(not (seq (:variant-properties prev-component)))
|
||||
(dissoc :variant-properties)))
|
||||
(cond-> apply-changes-local-library?
|
||||
(apply-changes-local {:apply-to-library? true})))
|
||||
changes)))
|
||||
|
||||
(defn delete-component
|
||||
@@ -1007,7 +1044,7 @@
|
||||
:page-id page-id})))
|
||||
|
||||
(defn restore-component
|
||||
[changes id page-id main-instance]
|
||||
[changes id page-id delta]
|
||||
(assert-library! changes)
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :restore-component
|
||||
@@ -1015,7 +1052,34 @@
|
||||
:page-id page-id})
|
||||
(update :undo-changes conj {:type :del-component
|
||||
:id id
|
||||
:main-instance main-instance})))
|
||||
:delta delta})))
|
||||
|
||||
(defn reorder-children
|
||||
[changes id children]
|
||||
(assert-page-id! changes)
|
||||
(assert-objects! changes)
|
||||
|
||||
(let [page-id (::page-id (meta changes))
|
||||
objects (lookup-objects changes)
|
||||
shape (get objects id)
|
||||
old-children (:shapes shape)
|
||||
|
||||
redo-change
|
||||
{:type :reorder-children
|
||||
:parent-id (:id shape)
|
||||
:page-id page-id
|
||||
:shapes children}
|
||||
|
||||
undo-change
|
||||
{:type :reorder-children
|
||||
:parent-id (:id shape)
|
||||
:page-id page-id
|
||||
:shapes old-children}]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj redo-change)
|
||||
(update :undo-changes conj undo-change)
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn reorder-grid-children
|
||||
[changes ids]
|
||||
@@ -1055,3 +1119,19 @@
|
||||
(reduce reorder-grid changes))]
|
||||
|
||||
changes))
|
||||
|
||||
(defn get-library-data
|
||||
[changes]
|
||||
(::library-data (meta changes)))
|
||||
|
||||
(defn get-objects
|
||||
[changes]
|
||||
(dm/get-in (::file-data (meta changes)) [:pages-index uuid/zero :objects]))
|
||||
|
||||
(defn get-page
|
||||
[changes]
|
||||
(::page (meta changes)))
|
||||
|
||||
(defn get-page-id
|
||||
[changes]
|
||||
(::page-id (meta changes)))
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
[clojure.walk :as walk]
|
||||
@@ -201,7 +200,7 @@
|
||||
result))))
|
||||
|
||||
(defn get-parent-seq
|
||||
"Returns a vector of parents of the specified shape."
|
||||
"Returns a lazy seq of parents of the specified shape."
|
||||
([objects shape-id]
|
||||
(get-parent-seq objects (get objects shape-id) shape-id))
|
||||
|
||||
@@ -284,6 +283,22 @@
|
||||
:else
|
||||
(get-root-frame objects (:frame-id frame)))))
|
||||
|
||||
(defn get-parent-frame
|
||||
"Similar to `get-frame, but always return the parent frame. When root
|
||||
frame is provided, then itself is returned."
|
||||
[objects shape-or-id]
|
||||
(cond
|
||||
(map? shape-or-id)
|
||||
(get objects (dm/get-prop shape-or-id :frame-id))
|
||||
|
||||
(= uuid/zero shape-or-id)
|
||||
(get objects uuid/zero)
|
||||
|
||||
:else
|
||||
(some->> shape-or-id
|
||||
(get objects)
|
||||
(get-frame objects))))
|
||||
|
||||
(defn valid-frame-target?
|
||||
[objects parent-id shape-id]
|
||||
(let [shape (get objects shape-id)]
|
||||
@@ -400,31 +415,57 @@
|
||||
elements)]
|
||||
(into #{} (keep :name) elements)))
|
||||
|
||||
(defn- extract-numeric-suffix
|
||||
[basename]
|
||||
(if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)]
|
||||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
(defn- name-seq
|
||||
"Creates a lazy, infinite sequence of names starting with `base-name`,
|
||||
followed by variants with suffixes applied. The sequence follows this pattern:
|
||||
- `base-name`
|
||||
- `(str base-name (suffix-fn 1))`
|
||||
- `(str base-name (suffix-fn 2))`
|
||||
- `(str base-name (suffix-fn 3))`, etc."
|
||||
[base-name suffix-fn]
|
||||
(cons base-name
|
||||
(map #(str/concat base-name (suffix-fn %))
|
||||
(iterate inc 1))))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
"Generates a unique name by selecting the first available name from a generated sequence.
|
||||
The sequence consists of `base-name` and its variants, avoiding conflicts with `existing-names`.
|
||||
|
||||
Parameters:
|
||||
- `base-name` - string used as the base for name generation.
|
||||
- `existing-names` - a collection of existing names to check for uniqueness.
|
||||
- Options:
|
||||
- `:suffix-fn` - a function that generates suffixes, given an integer (default: `get-suffix`).
|
||||
- `:immediate-suffix?` - if `true`, the base name is considered taken, and suffixing starts immediately.
|
||||
|
||||
Returns:
|
||||
- A unique name not present in `existing-names`."
|
||||
[base-name existing-names & {:keys [suffix-fn immediate-suffix? suffix]}]
|
||||
(dm/assert!
|
||||
"expected a set of strings"
|
||||
(sm/check-set-of-strings! used))
|
||||
(coll? existing-names))
|
||||
|
||||
(dm/assert!
|
||||
"expected a string for `basename`."
|
||||
(string? basename))
|
||||
(string? base-name))
|
||||
(let [suffix-fn (if suffix-fn
|
||||
suffix-fn
|
||||
(if suffix
|
||||
(fn [copy-count]
|
||||
(str/concat "-"
|
||||
suffix
|
||||
(when (> copy-count 1)
|
||||
(str "-" copy-count))))
|
||||
(fn [copy-count]
|
||||
(str/concat " " copy-count))))
|
||||
|
||||
(if-not (contains? used basename)
|
||||
basename
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (str prefix " " counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate))))))
|
||||
existing-name-set (cond-> (set existing-names)
|
||||
immediate-suffix? (conj base-name))
|
||||
names (name-seq base-name suffix-fn)]
|
||||
|
||||
(->> names
|
||||
(remove #(contains? existing-name-set %))
|
||||
first)))
|
||||
|
||||
(defn walk-pages
|
||||
"Go through all pages of a file and apply a function to each one"
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.text :as gsht]
|
||||
[app.common.logging :as l]
|
||||
[app.common.math :as mth]
|
||||
@@ -27,7 +26,10 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.segment :as path.segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
@@ -35,9 +37,7 @@
|
||||
|
||||
#?(:cljs (l/set-level! :info))
|
||||
|
||||
(declare ^:private available-migrations)
|
||||
(declare ^:private migration-up-index)
|
||||
(declare ^:private migration-down-index)
|
||||
(declare available-migrations)
|
||||
|
||||
(def version cfd/version)
|
||||
|
||||
@@ -49,7 +49,10 @@
|
||||
[file]
|
||||
(or (nil? (:version file))
|
||||
(not= cfd/version (:version file))
|
||||
(not= available-migrations (:migrations file))))
|
||||
(boolean
|
||||
(->> (:migrations file #{})
|
||||
(set/difference available-migrations)
|
||||
(not-empty)))))
|
||||
|
||||
(def xf:map-name
|
||||
(map :name))
|
||||
@@ -96,13 +99,13 @@
|
||||
(if (nil? migrations)
|
||||
(generate-migrations-from-version version)
|
||||
migrations)))
|
||||
(migrate)
|
||||
(update :features (fnil into #{}) (deref cfeat/*new*))
|
||||
;; NOTE: in some future we can consider to apply
|
||||
;; a migration to the whole database and remove
|
||||
;; this code from this function that executes on
|
||||
;; each file migration operation
|
||||
(update :features cfeat/migrate-legacy-features)))))
|
||||
(update :features cfeat/migrate-legacy-features)
|
||||
(migrate)
|
||||
(update :features (fnil into #{}) (deref cfeat/*new*))))))
|
||||
|
||||
(defn migrated?
|
||||
[file]
|
||||
@@ -119,16 +122,16 @@
|
||||
(into [] shapes)
|
||||
shapes))))
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-3"
|
||||
[data _]
|
||||
(letfn [(migrate-path [shape]
|
||||
(if-not (contains? shape :content)
|
||||
(let [content (gsp/segments->content (:segments shape) (:close? shape))
|
||||
selrect (gsh/content->selrect content)
|
||||
(let [content (path.segment/points->content (:segments shape) :close (:close? shape))
|
||||
selrect (path.segment/content->selrect content)
|
||||
points (grc/rect->points selrect)]
|
||||
(-> shape
|
||||
(dissoc :segments)
|
||||
@@ -172,9 +175,9 @@
|
||||
(fix-empty-points)))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Put the id of the local file in :component-file in instances of
|
||||
;; local components
|
||||
@@ -187,9 +190,9 @@
|
||||
object))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Fixes issues with selrect/points for shapes with width/height =
|
||||
;; 0 (line-like paths)
|
||||
@@ -199,7 +202,7 @@
|
||||
(if (= (:type shape) :path)
|
||||
(let [{:keys [width height]} (grc/points->rect (:points shape))]
|
||||
(if (or (mth/almost-zero? width) (mth/almost-zero? height))
|
||||
(let [selrect (gsh/content->selrect (:content shape))
|
||||
(let [selrect (path.segment/content->selrect (:content shape))
|
||||
points (grc/rect->points selrect)
|
||||
transform (gmt/matrix)
|
||||
transform-inv (gmt/matrix)]
|
||||
@@ -212,11 +215,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(update container :objects update-vals fix-line-paths))]
|
||||
(update container :objects d/update-vals fix-line-paths))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; Remove interactions pointing to deleted frames
|
||||
(defmethod migrate-data "legacy-7"
|
||||
@@ -227,9 +230,9 @@
|
||||
(filterv #(get-in page [:objects (:destination %)]) interactions))))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals (partial update-object page)))]
|
||||
(update page :objects d/update-vals (partial update-object page)))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Remove groups without any shape, both in pages and components
|
||||
(defmethod migrate-data "legacy-8"
|
||||
@@ -269,8 +272,8 @@
|
||||
(assoc container :objects objects)))))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals clean-container)
|
||||
(update :components update-vals clean-container))))
|
||||
(update :pages-index d/update-vals clean-container)
|
||||
(d/update-when :components d/update-vals clean-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-9"
|
||||
[data _]
|
||||
@@ -304,7 +307,7 @@
|
||||
[data _]
|
||||
(letfn [(update-page [page]
|
||||
(d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))]
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-11"
|
||||
[data _]
|
||||
@@ -318,7 +321,7 @@
|
||||
(update page :objects (fn [objects]
|
||||
(update-vals objects (partial update-object objects)))))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-12"
|
||||
[data _]
|
||||
@@ -328,9 +331,9 @@
|
||||
(assoc :size nil)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-in-when page [:options :saved-grids] update-vals update-grid))]
|
||||
(d/update-in-when page [:options :saved-grids] d/update-vals update-grid))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; Add rx and ry to images
|
||||
(defmethod migrate-data "legacy-13"
|
||||
@@ -348,9 +351,9 @@
|
||||
(fix-radius)))
|
||||
|
||||
(update-page [page]
|
||||
(update page :objects update-vals update-object))]
|
||||
(update page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "legacy-14"
|
||||
[data _]
|
||||
@@ -380,8 +383,8 @@
|
||||
container))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-16"
|
||||
[data _]
|
||||
@@ -423,11 +426,11 @@
|
||||
(assign-fills)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-17"
|
||||
[data _]
|
||||
@@ -452,11 +455,11 @@
|
||||
(assoc :fills [])))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; Remove position-data to solve a bug with the text positioning
|
||||
(defmethod migrate-data "legacy-18"
|
||||
@@ -467,11 +470,11 @@
|
||||
(dissoc :position-data)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-19"
|
||||
[data _]
|
||||
@@ -483,11 +486,11 @@
|
||||
(dissoc :position-data)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-25"
|
||||
[data _]
|
||||
@@ -499,10 +502,10 @@
|
||||
(update :selrect grc/make-rect)
|
||||
(cts/create-shape))))
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-26"
|
||||
[data _]
|
||||
@@ -515,11 +518,11 @@
|
||||
(assoc :transform-inverse (gmt/matrix))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-27"
|
||||
[data _]
|
||||
@@ -546,11 +549,11 @@
|
||||
(dissoc :saved-component-root?))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-28"
|
||||
[data _]
|
||||
@@ -575,8 +578,8 @@
|
||||
(d/update-when container :objects #(update-vals % (partial update-object %))))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-29"
|
||||
[data _]
|
||||
@@ -607,11 +610,11 @@
|
||||
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-31"
|
||||
[data _]
|
||||
@@ -622,10 +625,10 @@
|
||||
(dissoc :use-for-thumbnail?))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-32"
|
||||
[data _]
|
||||
@@ -640,11 +643,11 @@
|
||||
object)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-33"
|
||||
[data _]
|
||||
@@ -662,9 +665,9 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-34"
|
||||
[data _]
|
||||
@@ -674,10 +677,10 @@
|
||||
(dissoc object :x :y :width :height)
|
||||
object))
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-36"
|
||||
[data _]
|
||||
@@ -687,8 +690,8 @@
|
||||
(dissoc objects nil)
|
||||
objects))))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-37"
|
||||
[data _]
|
||||
@@ -716,11 +719,11 @@
|
||||
shape)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-39"
|
||||
[data _]
|
||||
@@ -738,11 +741,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-40"
|
||||
[data _]
|
||||
@@ -762,11 +765,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-41"
|
||||
[data _]
|
||||
@@ -795,11 +798,11 @@
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-42"
|
||||
[data _]
|
||||
@@ -812,11 +815,11 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def ^:private valid-fill?
|
||||
(sm/lazy-validator ::cts/fill))
|
||||
@@ -841,14 +844,11 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(def ^:private valid-shadow?
|
||||
(sm/lazy-validator ::ctss/shadow))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-44"
|
||||
[data _]
|
||||
@@ -861,14 +861,14 @@
|
||||
|
||||
(update-object [object]
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(filter ctss/valid-shadow?))]
|
||||
(d/update-when object :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-45"
|
||||
[data _]
|
||||
@@ -881,9 +881,9 @@
|
||||
:parent-id parent-id)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals fix-shape))]
|
||||
(d/update-when container :objects d/update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-46"
|
||||
[data _]
|
||||
@@ -891,10 +891,10 @@
|
||||
(dissoc object :thumbnail))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-47"
|
||||
[data _]
|
||||
@@ -915,9 +915,9 @@
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals (partial fix-shape page)))]
|
||||
(d/update-when page :objects d/update-vals (partial fix-shape page)))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "legacy-48"
|
||||
[data _]
|
||||
@@ -929,9 +929,9 @@
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals fix-shape))]
|
||||
(d/update-when page :objects d/update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
;; Remove hide-in-viewer for shapes that are origin or destination of an interaction
|
||||
(defmethod migrate-data "legacy-49"
|
||||
@@ -949,9 +949,9 @@
|
||||
(mapcat :interactions)
|
||||
(map :destination)
|
||||
(set))]
|
||||
(update page :objects update-vals (partial update-object destinations))))]
|
||||
(update page :objects d/update-vals (partial update-object destinations))))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
;; This migration mainly fixes paths with curve-to segments
|
||||
;; without :c1x :c1y :c2x :c2y properties. Additionally, we found a
|
||||
@@ -994,11 +994,11 @@
|
||||
|
||||
update-container
|
||||
(fn [page]
|
||||
(d/update-when page :objects update-vals update-shape))]
|
||||
(d/update-when page :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def ^:private valid-color?
|
||||
(sm/lazy-validator ::ctc/color))
|
||||
@@ -1018,9 +1018,9 @@
|
||||
shape))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals update-shape))]
|
||||
(d/update-when page :objects d/update-vals update-shape))]
|
||||
|
||||
(update data :pages-index update-vals update-page)))
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
|
||||
(defmethod migrate-data "legacy-53"
|
||||
@@ -1036,15 +1036,15 @@
|
||||
|
||||
(update-shape [shape]
|
||||
(let [xform (comp (map fix-shadow)
|
||||
(filter valid-shadow?))]
|
||||
(filter ctss/valid-shadow?))]
|
||||
(d/update-when shape :shadow #(into [] xform %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; This migration moves page options to the page level
|
||||
(defmethod migrate-data "legacy-55"
|
||||
@@ -1096,11 +1096,11 @@
|
||||
(update :content (partial txt/transform-nodes identity fix-fills)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
|
||||
(defmethod migrate-data "legacy-57"
|
||||
@@ -1127,7 +1127,7 @@
|
||||
(-> data
|
||||
(update :pages (fn [pages] (into [] (remove nil?) pages)))
|
||||
(update :pages-index dissoc nil)
|
||||
(update :pages-index update-vals update-page))))
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "legacy-59"
|
||||
[data _]
|
||||
@@ -1138,11 +1138,11 @@
|
||||
(d/update-when shape :touched #(into #{} (map fix-touched) %)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-shape))]
|
||||
(d/update-when container :objects d/update-vals update-shape))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-62"
|
||||
[data _]
|
||||
@@ -1175,7 +1175,7 @@
|
||||
;; so the relevant objects are inside the component
|
||||
(d/update-when component :objects remove-cycles))]
|
||||
|
||||
(update data :components update-vals update-component)))
|
||||
(d/update-when data :components d/update-vals update-component)))
|
||||
|
||||
(defmethod migrate-data "legacy-65"
|
||||
[data _]
|
||||
@@ -1186,14 +1186,14 @@
|
||||
update-page
|
||||
(fn [page]
|
||||
(-> (update-object page)
|
||||
(update :objects update-vals update-object)))]
|
||||
(update :objects d/update-vals update-object)))]
|
||||
|
||||
(-> data
|
||||
(update-object)
|
||||
(d/update-when :pages-index update-vals update-page)
|
||||
(d/update-when :colors update-vals update-object)
|
||||
(d/update-when :typographies update-vals update-object)
|
||||
(d/update-when :components update-vals update-object))))
|
||||
(update :pages-index d/update-vals update-page)
|
||||
(d/update-when :colors d/update-vals update-object)
|
||||
(d/update-when :typographies d/update-vals update-object)
|
||||
(d/update-when :components d/update-vals update-object))))
|
||||
|
||||
(defmethod migrate-data "legacy-66"
|
||||
[data _]
|
||||
@@ -1207,23 +1207,122 @@
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "legacy-67"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
(d/update-when object :shadow #(into [] (reverse %))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0001-remove-tokens-from-groups"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
(cond-> object
|
||||
(and (= :group (:type object))
|
||||
(contains? (:applied-tokens object) :fill))
|
||||
(assoc :fills [])
|
||||
(and (= :group (:type object))
|
||||
(contains? object :applied-tokens))
|
||||
(dissoc :applied-tokens)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects d/update-vals update-object))]
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defmethod migrate-data "0002-clean-shape-interactions"
|
||||
[data _]
|
||||
(let [decode-fn (sm/decoder ctsi/schema:interaction sm/json-transformer)
|
||||
validate-fn (sm/validator ctsi/schema:interaction)
|
||||
|
||||
xform
|
||||
(comp
|
||||
(map decode-fn)
|
||||
(filter validate-fn))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :interactions
|
||||
(fn [interactions]
|
||||
(into [] xform interactions))))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0002-normalize-bool-content"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
;; NOTE: we still preserve the previous value for possible
|
||||
;; rollback, we still need to perform an other migration
|
||||
;; for properly delete the bool-content prop from shapes
|
||||
;; once the know the migration was OK
|
||||
(if (cfh/bool-shape? object)
|
||||
(if-let [content (:bool-content object)]
|
||||
(assoc object :content content)
|
||||
object)
|
||||
(dissoc object :bool-content :bool-type)))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0003-fix-root-shape"
|
||||
[data _]
|
||||
(letfn [(update-object [shape]
|
||||
(if (= (:id shape) uuid/zero)
|
||||
(-> shape
|
||||
(assoc :parent-id uuid/zero)
|
||||
(assoc :frame-id uuid/zero)
|
||||
;; We explicitly dissoc them and let the shape-setup
|
||||
;; to regenerate it with valid values.
|
||||
(dissoc :selrect)
|
||||
(dissoc :points)
|
||||
(cts/setup-shape))
|
||||
shape))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container)
|
||||
(d/without-nils))))
|
||||
|
||||
(defmethod migrate-data "0003-convert-path-content"
|
||||
[data _]
|
||||
(some-> cfeat/*new* (swap! conj "fdata/path-data"))
|
||||
|
||||
(letfn [(update-object [object]
|
||||
(if (or (cfh/bool-shape? object)
|
||||
(cfh/path-shape? object))
|
||||
(update object :content path/content)
|
||||
object))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
@@ -1278,4 +1377,9 @@
|
||||
"legacy-62"
|
||||
"legacy-65"
|
||||
"legacy-66"
|
||||
"legacy-67"]))
|
||||
"legacy-67"
|
||||
"0001-remove-tokens-from-groups"
|
||||
"0002-normalize-bool-content"
|
||||
"0002-clean-shape-interactions"
|
||||
"0003-fix-root-shape"
|
||||
"0003-convert-path-content"]))
|
||||
|
||||
@@ -572,6 +572,51 @@
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :not-a-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :invalid-variant-id
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :invalid-variant-properties
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-not-main
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :parent-not-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-no-properties
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-bad-variant-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :variant-component-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
|
||||
(defmethod repair-error :default
|
||||
[_ error file _]
|
||||
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;; FIXME: move to logic?
|
||||
|
||||
(defn prepare-add-shape
|
||||
[changes shape objects]
|
||||
(let [index (:index (meta shape))
|
||||
@@ -35,18 +37,21 @@
|
||||
(pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column)))
|
||||
(cond-> (ctl/grid-layout? objects (:parent-id shape))
|
||||
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))]
|
||||
|
||||
[shape changes]))
|
||||
|
||||
(defn prepare-move-shapes-into-frame
|
||||
[changes frame-id shapes objects]
|
||||
[changes frame-id shapes objects remove-layout-data?]
|
||||
(let [parent-id (dm/get-in objects [frame-id :parent-id])
|
||||
shapes (remove #(= % parent-id) shapes)
|
||||
to-move (->> shapes
|
||||
(map (d/getf objects))
|
||||
(not-empty))]
|
||||
|
||||
(if to-move
|
||||
(-> changes
|
||||
(cond-> (not (ctl/any-layout? objects frame-id))
|
||||
(cond-> (and remove-layout-data?
|
||||
(not (ctl/any-layout? objects frame-id)))
|
||||
(pcb/update-shapes shapes ctl/remove-layout-item-data))
|
||||
(pcb/update-shapes shapes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))
|
||||
(pcb/change-parent frame-id to-move 0)
|
||||
@@ -61,6 +66,10 @@
|
||||
changes id parent-id objects selected index frame-name without-fill? nil))
|
||||
|
||||
([changes id parent-id objects selected index frame-name without-fill? target-cell-id]
|
||||
(prepare-create-artboard-from-selection
|
||||
changes id parent-id objects selected index frame-name without-fill? target-cell-id nil))
|
||||
|
||||
([changes id parent-id objects selected index frame-name without-fill? target-cell-id delta]
|
||||
(when-let [selected-objs (->> selected
|
||||
(map (d/getf objects))
|
||||
(not-empty))]
|
||||
@@ -98,10 +107,11 @@
|
||||
:id))
|
||||
target-cell-id)
|
||||
|
||||
|
||||
attrs
|
||||
{:type :frame
|
||||
:x (:x srect)
|
||||
:y (:y srect)
|
||||
:x (cond-> (:x srect) delta (+ (:x delta)))
|
||||
:y (cond-> (:y srect) delta (+ (:y delta)))
|
||||
:width (:width srect)
|
||||
:height (:height srect)}
|
||||
|
||||
@@ -133,7 +143,7 @@
|
||||
(prepare-add-shape changes shape objects)
|
||||
|
||||
changes
|
||||
(prepare-move-shapes-into-frame changes (:id shape) selected' objects)
|
||||
(prepare-move-shapes-into-frame changes (:id shape) selected' objects false)
|
||||
|
||||
changes
|
||||
(cond-> changes
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
(ns app.main.ui.workspace.tokens.token
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.files.tokens
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
|
||||
[app.common.data.macros :as dm]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -21,7 +27,9 @@
|
||||
{:value parsed-value
|
||||
:unit unit}))))
|
||||
|
||||
(defn token-identifier [{:keys [name] :as _token}]
|
||||
;; FIXME: looks very redundant function
|
||||
(defn token-identifier
|
||||
[{:keys [name] :as _token}]
|
||||
name)
|
||||
|
||||
(defn attributes-map
|
||||
@@ -43,7 +51,7 @@
|
||||
(defn token-attribute-applied?
|
||||
"Test if `token` is applied to a `shape` on single `token-attribute`."
|
||||
[token shape token-attribute]
|
||||
(when-let [id (get-in shape [:applied-tokens token-attribute])]
|
||||
(when-let [id (dm/get-in shape [:applied-tokens token-attribute])]
|
||||
(= (token-identifier token) id)))
|
||||
|
||||
(defn token-applied?
|
||||
@@ -56,15 +64,18 @@
|
||||
[token shapes token-attributes]
|
||||
(some #(token-applied? token % token-attributes) shapes))
|
||||
|
||||
(defn shapes-ids-by-applied-attributes [token shapes token-attributes]
|
||||
(reduce (fn [acc shape]
|
||||
(let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %)
|
||||
[% #{(:id shape)}])
|
||||
token-attributes)
|
||||
(filter some?)
|
||||
(into {}))]
|
||||
(merge-with into acc applied-ids-by-attribute)))
|
||||
{} shapes))
|
||||
(defn shapes-ids-by-applied-attributes
|
||||
[token shapes token-attributes]
|
||||
(let [conj* (fnil conj #{})]
|
||||
(reduce (fn [result shape]
|
||||
(let [shape-id (dm/get-prop shape :id)]
|
||||
(->> token-attributes
|
||||
(filter #(token-attribute-applied? token shape %))
|
||||
(reduce (fn [result attr]
|
||||
(update result attr conj* shape-id))
|
||||
result))))
|
||||
{}
|
||||
shapes)))
|
||||
|
||||
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
|
||||
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
|
||||
@@ -122,13 +133,6 @@
|
||||
(defn color-token? [token]
|
||||
(= (:type token) :color))
|
||||
|
||||
(defn color-bullet-color [token-color-value]
|
||||
(when-let [tc (tinycolor/valid-color token-color-value)]
|
||||
(if (tinycolor/alpha tc)
|
||||
{:color (tinycolor/->hex-string tc)
|
||||
:opacity (tinycolor/alpha tc)}
|
||||
(tinycolor/->hex-string tc))))
|
||||
|
||||
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
|
||||
(when (and resolved-value (color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
;; FIXME: this should be precalculated ?
|
||||
(defn is-reference? [token]
|
||||
(str/includes? (:value token) "{"))
|
||||
@@ -10,12 +10,15 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -56,7 +59,17 @@
|
||||
:instance-head-not-frame
|
||||
:misplaced-slot
|
||||
:missing-slot
|
||||
:shape-ref-cycle})
|
||||
:shape-ref-cycle
|
||||
:not-a-variant
|
||||
:invalid-variant-id
|
||||
:invalid-variant-properties
|
||||
:variant-not-main
|
||||
:parent-not-variant
|
||||
:variant-bad-name
|
||||
:variant-bad-variant-name
|
||||
:variant-component-bad-name
|
||||
:variant-no-properties
|
||||
:variant-component-bad-id})
|
||||
|
||||
(def ^:private schema:error
|
||||
[:map {:title "ValidationError"}
|
||||
@@ -401,6 +414,68 @@
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
|
||||
|
||||
(defn- check-variant-container
|
||||
"Shape is a variant container, so:
|
||||
-all its children should be variants with variant-id equals to the shape-id
|
||||
-all the components should have the same properties
|
||||
"
|
||||
[shape file page]
|
||||
(let [shape-id (:id shape)
|
||||
shapes (:shapes shape)
|
||||
children (map #(ctst/get-shape page %) shapes)
|
||||
prop-names (cfv/extract-properties-names (first children) (:data file))]
|
||||
(doseq [child children]
|
||||
(if (not (ctk/is-variant? child))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id child))
|
||||
child file page)
|
||||
(do
|
||||
(when (not= (:variant-id child) shape-id)
|
||||
(report-error :invalid-variant-id
|
||||
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
|
||||
child file page))
|
||||
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page)))))))
|
||||
|
||||
(defn- check-variant
|
||||
"Shape is a variant, so
|
||||
-it should be a main component
|
||||
-its parent should be a variant-container
|
||||
-its variant-name is derived from the properties
|
||||
-its name should be tha same as its parent's
|
||||
"
|
||||
[shape file page]
|
||||
(let [parent (ctst/get-shape page (:parent-id shape))
|
||||
component (ctkl/get-component (:data file) (:component-id shape) true)
|
||||
name (ctv/properties-to-name (:variant-properties component))]
|
||||
(when-not (ctk/main-instance? shape)
|
||||
(report-error :variant-not-main
|
||||
(str/ffmt "Variant % is not a main instance" (:id shape))
|
||||
shape file page))
|
||||
(when-not (ctk/is-variant-container? parent)
|
||||
(report-error :parent-not-variant
|
||||
(str/ffmt "Variant % has an invalid parent" (:id shape))
|
||||
shape file page))
|
||||
|
||||
(when-not (= name (:variant-name shape))
|
||||
(report-error :variant-bad-variant-name
|
||||
(str/ffmt "Variant % has an invalid variant-name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:name parent) (:name shape))
|
||||
(report-error :variant-bad-name
|
||||
(str/ffmt "Variant % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
|
||||
(report-error :variant-component-bad-name
|
||||
(str/ffmt "Component % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:variant-id component) (:variant-id shape))
|
||||
(report-error :variant-component-bad-id
|
||||
(str/ffmt "Variant % has adifferent variant-id than its component" (:id shape))
|
||||
shape file page))))
|
||||
|
||||
(defn- check-shape
|
||||
"Validate referential integrity and semantic coherence of
|
||||
a shape and all its children. Report all errors found.
|
||||
@@ -421,6 +496,12 @@
|
||||
(check-parent-children shape file page)
|
||||
(check-frame shape file page)
|
||||
|
||||
(when (ctk/is-variant-container? shape)
|
||||
(check-variant-container shape file page))
|
||||
|
||||
(when (ctk/is-variant? shape)
|
||||
(check-variant shape file page))
|
||||
|
||||
(if (ctk/instance-head? shape)
|
||||
(if (not= :frame (:type shape))
|
||||
(report-error :instance-head-not-frame
|
||||
@@ -496,6 +577,24 @@
|
||||
"This deleted component has shapes with shape-ref pointing to self"
|
||||
component file nil :cycles-ids cycles-ids))))
|
||||
|
||||
(defn- check-variant-component
|
||||
"Component is a variant, so:
|
||||
-Its main should be a variant
|
||||
-It should have at least one variant property"
|
||||
[component file]
|
||||
(let [component-page (ctf/get-component-page (:data file) component)
|
||||
main-component (if (:deleted component)
|
||||
(dm/get-in component [:objects (:main-instance-id component)])
|
||||
(ctst/get-shape component-page (:main-instance-id component)))]
|
||||
(when-not (ctk/is-variant? main-component)
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id main-component))
|
||||
main-component file component-page))
|
||||
(when (< (count (:variant-properties component)) 1)
|
||||
(report-error :variant-no-properties
|
||||
(str/ffmt "Component variant % should have properties" (:id main-component))
|
||||
main-component file nil))))
|
||||
|
||||
(defn- check-component
|
||||
"Validate semantic coherence of a component. Report all errors found."
|
||||
[component file]
|
||||
@@ -505,7 +604,10 @@
|
||||
component file nil))
|
||||
(when (:deleted component)
|
||||
(check-component-duplicate-swap-slot component file)
|
||||
(check-ref-cycles component file)))
|
||||
(check-ref-cycles component file))
|
||||
|
||||
(when (ctk/is-variant? component)
|
||||
(check-variant-component component file)))
|
||||
|
||||
(defn- get-orphan-shapes
|
||||
[{:keys [objects] :as page}]
|
||||
|
||||
84
common/src/app/common/files/variant.cljc
Normal file
84
common/src/app/common/files/variant.cljc
Normal file
@@ -0,0 +1,84 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
(ns app.common.files.variant
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[app.common.types.variant :as ctv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
|
||||
(defn find-variant-components
|
||||
"Find a list of the components thet belongs to this variant-id"
|
||||
[data objects variant-id]
|
||||
;; We can't simply filter components, because we need to maintain the order
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
(map #(dm/get-in objects [% :component-id]))
|
||||
(map #(ctcl/get-component data % true))
|
||||
reverse))
|
||||
|
||||
(defn- dashes-to-end
|
||||
[property-values]
|
||||
(let [dashes (if (some #(= % "--") property-values) ["--"] [])]
|
||||
(concat (remove #(= % "--") property-values) dashes)))
|
||||
|
||||
|
||||
(defn extract-properties-names
|
||||
[shape data]
|
||||
(->> shape
|
||||
(#(ctcl/get-component data (:component-id %) true))
|
||||
:variant-properties
|
||||
(map :name)))
|
||||
|
||||
|
||||
(defn extract-properties-values
|
||||
[data objects variant-id]
|
||||
(->> (find-variant-components data objects variant-id)
|
||||
(mapcat :variant-properties)
|
||||
(group-by :name)
|
||||
(map (fn [[k v]]
|
||||
{:name k
|
||||
:value (->> v
|
||||
(map #(if (str/empty? (:value %)) "--" (:value %)))
|
||||
distinct
|
||||
dashes-to-end)}))))
|
||||
|
||||
(defn get-variant-mains
|
||||
[component data]
|
||||
(assert (ctv/valid-variant-component? component) "expected valid component variant")
|
||||
(when-let [variant-id (:variant-id component)]
|
||||
(let [page-id (:main-instance-page component)
|
||||
objects (-> (dm/get-in data [:pages-index page-id])
|
||||
(get :objects))]
|
||||
(dm/get-in objects [variant-id :shapes]))))
|
||||
|
||||
|
||||
(defn is-secondary-variant?
|
||||
[component data]
|
||||
(let [shapes (get-variant-mains component data)]
|
||||
(and (seq shapes)
|
||||
(not= (:main-instance-id component) (last shapes)))))
|
||||
|
||||
(defn get-primary-variant
|
||||
[data component]
|
||||
(let [page-id (:main-instance-page component)
|
||||
objects (-> (dm/get-in data [:pages-index page-id])
|
||||
(get :objects))
|
||||
variant-id (:variant-id component)]
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
peek
|
||||
(get objects))))
|
||||
|
||||
(defn get-primary-component
|
||||
[data component-id]
|
||||
(when-let [component (ctcl/get-component data component-id)]
|
||||
(if (ctc/is-variant? component)
|
||||
(->> component
|
||||
(get-primary-variant data)
|
||||
:component-id
|
||||
(ctcl/get-component data))
|
||||
component)))
|
||||
@@ -79,7 +79,7 @@
|
||||
:file-schema-validation
|
||||
;; Reports the schema validation errors internally.
|
||||
:soft-file-schema-validation
|
||||
;; Activates the referential integrity validation during update file; related to components-v2.
|
||||
;; Activates the referential integrity validation during update file.
|
||||
:file-validation
|
||||
;; Reports the referential integrity validation errors internally.
|
||||
:soft-file-validation
|
||||
@@ -124,7 +124,9 @@
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:export-file-v3
|
||||
:render-wasm-dpr
|
||||
:hide-release-modal})
|
||||
:hide-release-modal
|
||||
:subscriptions
|
||||
:subscriptions-old})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -126,21 +126,20 @@
|
||||
o)))
|
||||
|
||||
(def schema:matrix
|
||||
{:type :map
|
||||
:pred valid-matrix?
|
||||
:type-properties
|
||||
{:title "matrix"
|
||||
:description "Matrix instance"
|
||||
:error/message "expected a valid matrix instance"
|
||||
:gen/gen (matrix-generator)
|
||||
:decode/json decode-matrix
|
||||
:decode/string decode-matrix
|
||||
:encode/json matrix->json
|
||||
:encode/string matrix->str
|
||||
::oapi/type "string"
|
||||
::oapi/format "matrix"}})
|
||||
|
||||
(sm/register! ::matrix schema:matrix)
|
||||
(sm/register!
|
||||
{:type ::matrix
|
||||
:pred valid-matrix?
|
||||
:type-properties
|
||||
{:title "matrix"
|
||||
:description "Matrix instance"
|
||||
:error/message "expected a valid matrix instance"
|
||||
:gen/gen (matrix-generator)
|
||||
:decode/json decode-matrix
|
||||
:decode/string decode-matrix
|
||||
:encode/json matrix->json
|
||||
:encode/string matrix->str
|
||||
::oapi/type "string"
|
||||
::oapi/format "matrix"}}))
|
||||
|
||||
;; FIXME: deprecated
|
||||
(s/def ::a ::us/safe-float)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.geom.point
|
||||
(:refer-clojure :exclude [divide min max abs])
|
||||
(:refer-clojure :exclude [divide min max abs zero?])
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
#?(:cljs [cljs.core :as c]
|
||||
@@ -85,24 +85,22 @@
|
||||
(into {} p)
|
||||
p))
|
||||
|
||||
;; FIXME: make like matrix
|
||||
(def schema:point
|
||||
{:type ::point
|
||||
:pred valid-point?
|
||||
:type-properties
|
||||
{:title "point"
|
||||
:description "Point"
|
||||
:error/message "expected a valid point"
|
||||
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
|
||||
(sg/fmap #(apply pos->Point %)))
|
||||
::oapi/type "string"
|
||||
::oapi/format "point"
|
||||
:decode/json decode-point
|
||||
:decode/string decode-point
|
||||
:encode/json point->json
|
||||
:encode/string point->str}})
|
||||
|
||||
(sm/register! schema:point)
|
||||
(sm/register!
|
||||
{:type ::point
|
||||
:pred valid-point?
|
||||
:type-properties
|
||||
{:title "point"
|
||||
:description "Point"
|
||||
:error/message "expected a valid point"
|
||||
:gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int))
|
||||
(sg/fmap #(apply pos->Point %)))
|
||||
::oapi/type "string"
|
||||
::oapi/format "point"
|
||||
:decode/json decode-point
|
||||
:decode/string decode-point
|
||||
:encode/json point->json
|
||||
:encode/string point->str}}))
|
||||
|
||||
(defn point-like?
|
||||
[{:keys [x y] :as v}]
|
||||
@@ -470,6 +468,13 @@
|
||||
(and ^boolean (mth/almost-zero? (dm/get-prop p :x))
|
||||
^boolean (mth/almost-zero? (dm/get-prop p :y))))
|
||||
|
||||
(defn zero?
|
||||
[p]
|
||||
(let [x (dm/get-prop p :x)
|
||||
y (dm/get-prop p :y)]
|
||||
(and ^boolean (== 0 x)
|
||||
^boolean (== 0 y))))
|
||||
|
||||
(defn lerp
|
||||
"Calculates a linear interpolation between two points given a tvalue"
|
||||
[p1 p2 t]
|
||||
|
||||
@@ -10,13 +10,11 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.bool :as gsb]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.constraints :as gct]
|
||||
[app.common.geom.shapes.corners :as gsc]
|
||||
[app.common.geom.shapes.fit-frame :as gsff]
|
||||
[app.common.geom.shapes.intersect :as gsi]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.transforms :as gtr]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
@@ -166,7 +164,7 @@
|
||||
(dm/export gtr/calculate-geometry)
|
||||
(dm/export gtr/update-group-selrect)
|
||||
(dm/export gtr/update-mask-selrect)
|
||||
(dm/export gtr/update-bool-selrect)
|
||||
(dm/export gtr/update-bool)
|
||||
(dm/export gtr/apply-transform)
|
||||
(dm/export gtr/transform-shape)
|
||||
(dm/export gtr/transform-selrect)
|
||||
@@ -180,12 +178,6 @@
|
||||
;; Constratins
|
||||
(dm/export gct/calc-child-modifiers)
|
||||
|
||||
;; PATHS
|
||||
;; FIXME: rename
|
||||
(dm/export gsp/content->selrect)
|
||||
(dm/export gsp/transform-content)
|
||||
(dm/export gsp/open-path?)
|
||||
|
||||
;; Intersection
|
||||
(dm/export gsi/overlaps?)
|
||||
(dm/export gsi/overlaps-path?)
|
||||
@@ -193,9 +185,6 @@
|
||||
(dm/export gsi/has-point-rect?)
|
||||
(dm/export gsi/rect-contains-shape?)
|
||||
|
||||
;; Bool
|
||||
(dm/export gsb/calc-bool-content)
|
||||
|
||||
;; Constraints
|
||||
(dm/export gct/default-constraints-h)
|
||||
(dm/export gct/default-constraints-v)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.geom.shapes.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cpf]
|
||||
[app.common.svg.path.bool :as pb]
|
||||
[app.common.svg.path.shapes-to-path :as stp]))
|
||||
|
||||
(defn calc-bool-content
|
||||
[shape objects]
|
||||
|
||||
(let [extract-content-xf
|
||||
(comp (map (d/getf objects))
|
||||
(filter (comp not :hidden))
|
||||
(remove cpf/svg-raw-shape?)
|
||||
(map #(stp/convert-to-path % objects))
|
||||
(map :content))
|
||||
|
||||
shapes-content
|
||||
(into [] extract-content-xf (:shapes shape))]
|
||||
(pb/content-bool (:bool-type shape) shapes-content)))
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.math :as mth]))
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.path :as path]))
|
||||
|
||||
(defn shape-stroke-margin
|
||||
[shape stroke-width]
|
||||
@@ -104,7 +104,7 @@
|
||||
(let [strokes (:strokes shape)
|
||||
|
||||
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||
^boolean (gsp/open-path? shape))
|
||||
^boolean (path/shape-with-open-path? shape))
|
||||
|
||||
stroke-width
|
||||
(->> strokes
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
;;
|
||||
;; 5. If any track still has an infinite growth limit set its growth limit to its base size.
|
||||
|
||||
;; - Distribute extra space accross spaned tracks
|
||||
;; - Distribute extra space accross spaned tracks
|
||||
;; - Maximize tracks
|
||||
;;
|
||||
;; - Expand flexible tracks
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
track-list))
|
||||
|
||||
(defn add-auto-size
|
||||
(defn stretch-tracks
|
||||
[track-list add-size]
|
||||
(->> track-list
|
||||
(mapv (fn [{:keys [type size max-size] :as track}]
|
||||
@@ -357,7 +357,8 @@
|
||||
to-idx (+ (dec (get cell prop)) (get cell prop-span))
|
||||
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
|
||||
|
||||
to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects)
|
||||
to-allocate
|
||||
(size-to-allocate type parent (get children-map shape-id) cell bounds objects)
|
||||
|
||||
;; Remove the size and the tracks that are not allocated
|
||||
[to-allocate total-frs indexed-tracks]
|
||||
@@ -493,11 +494,11 @@
|
||||
|
||||
column-tracks (cond-> column-tracks
|
||||
(= :stretch (:layout-justify-content parent))
|
||||
(add-auto-size column-add-auto))
|
||||
(stretch-tracks column-add-auto))
|
||||
|
||||
row-tracks (cond-> row-tracks
|
||||
(= :stretch (:layout-align-content parent))
|
||||
(add-auto-size row-add-auto))
|
||||
(stretch-tracks row-add-auto))
|
||||
|
||||
column-total-size (tracks-total-size column-tracks)
|
||||
row-total-size (tracks-total-size row-tracks)
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpp]
|
||||
[app.common.geom.shapes.text :as gte]
|
||||
[app.common.math :as mth]))
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.path.segment :as path.segm]))
|
||||
|
||||
(defn orientation
|
||||
"Given three ordered points gives the orientation
|
||||
@@ -186,7 +186,7 @@
|
||||
rect-lines (points->lines rect-points)
|
||||
path-lines (if simple?
|
||||
(points->lines (:points shape))
|
||||
(gpp/path->lines shape))
|
||||
(path.segm/path->lines shape))
|
||||
start-point (-> shape :content (first) :params (gpt/point))]
|
||||
|
||||
(or (intersects-lines? rect-lines path-lines)
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.bool :as gshb]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpa]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.modifiers :as ctm]))
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.path :as path]))
|
||||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
@@ -77,7 +76,11 @@
|
||||
position-data)
|
||||
position-data))))
|
||||
|
||||
;; FIXME: revist usage of mutability
|
||||
;; FIXME: review performance of this; this function is executing too
|
||||
;; many times, including when the point vector is 0,0. This function
|
||||
;; can be implemented in function of transform which is already mor
|
||||
;; performant
|
||||
|
||||
(defn move
|
||||
"Move the shape relatively to its current
|
||||
position applying the provided delta."
|
||||
@@ -95,8 +98,8 @@
|
||||
(d/update-when :x d/safe+ dx)
|
||||
(d/update-when :y d/safe+ dy)
|
||||
(d/update-when :position-data move-position-data mvec)
|
||||
(cond-> (= :bool type) (update :bool-content gpa/move-content mvec))
|
||||
(cond-> (= :path type) (update :content gpa/move-content mvec)))))
|
||||
(cond-> (or (= :bool type) (= :path type))
|
||||
(update :content path/move-content mvec)))))
|
||||
|
||||
;; --- Absolute Movement
|
||||
|
||||
@@ -317,14 +320,11 @@
|
||||
points (gco/transform-points (dm/get-prop shape :points) transform-mtx)
|
||||
selrect (gco/transform-selrect (dm/get-prop shape :selrect) transform-mtx)
|
||||
|
||||
shape (if (= type :bool)
|
||||
(update shape :bool-content gpa/transform-content transform-mtx)
|
||||
shape)
|
||||
shape (if (= type :text)
|
||||
(update shape :position-data transform-position-data transform-mtx)
|
||||
shape)
|
||||
shape (if (= type :path)
|
||||
(update shape :content gpa/transform-content transform-mtx)
|
||||
shape (if (or (= type :path) (= type :bool))
|
||||
(update shape :content path/transform-content transform-mtx)
|
||||
(assoc shape
|
||||
:x (dm/get-prop selrect :x)
|
||||
:y (dm/get-prop selrect :y)
|
||||
@@ -355,12 +355,9 @@
|
||||
rotation (mod (+ (d/nilv (:rotation shape) 0)
|
||||
(d/nilv (dm/get-in shape [:modifiers :rotation]) 0))
|
||||
360)
|
||||
shape (if (= type :bool)
|
||||
(update shape :bool-content gpa/transform-content transform-mtx)
|
||||
shape)
|
||||
|
||||
shape (if (= type :path)
|
||||
(update shape :content gpa/transform-content transform-mtx)
|
||||
shape (if (or (= type :path) (= type :bool))
|
||||
(update shape :content path/transform-content transform-mtx)
|
||||
(assoc shape
|
||||
:x (dm/get-prop selrect :x)
|
||||
:y (dm/get-prop selrect :y)
|
||||
@@ -377,8 +374,14 @@
|
||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||
its properties. We adjust de x,y,width,height and create a custom transform"
|
||||
[shape transform-mtx]
|
||||
(if ^boolean (gmt/move? transform-mtx)
|
||||
(cond
|
||||
(nil? transform-mtx)
|
||||
shape
|
||||
|
||||
^boolean (gmt/move? transform-mtx)
|
||||
(apply-transform-move shape transform-mtx)
|
||||
|
||||
:else
|
||||
(apply-transform-generic shape transform-mtx)))
|
||||
|
||||
(defn- update-group-viewbox
|
||||
@@ -450,20 +453,14 @@
|
||||
(assoc :flip-x (-> mask :flip-x))
|
||||
(assoc :flip-y (-> mask :flip-y)))))
|
||||
|
||||
(defn update-bool-selrect
|
||||
(defn update-bool
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape children objects]
|
||||
|
||||
(let [bool-content (gshb/calc-bool-content shape objects)
|
||||
shape (assoc shape :bool-content bool-content)
|
||||
[points selrect] (gpa/content->points+selrect shape bool-content)]
|
||||
|
||||
(if (and (some? selrect) (d/not-empty? points))
|
||||
(-> shape
|
||||
(assoc :selrect selrect)
|
||||
(assoc :points points))
|
||||
(update-group-selrect shape children))))
|
||||
[shape objects]
|
||||
(let [content (path/calc-bool-content shape objects)
|
||||
shape (assoc shape :content content)]
|
||||
(path/update-geometry shape)))
|
||||
|
||||
;; FIXME: revisit
|
||||
(defn update-shapes-geometry
|
||||
[objects ids]
|
||||
(->> ids
|
||||
@@ -477,7 +474,7 @@
|
||||
(update-mask-selrect shape children)
|
||||
|
||||
(cfh/bool-shape? shape)
|
||||
(update-bool-selrect shape children objects)
|
||||
(update-bool shape objects)
|
||||
|
||||
(cfh/group-shape? shape)
|
||||
(update-group-selrect shape children)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,10 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as cto]
|
||||
@@ -79,181 +81,185 @@
|
||||
(pcb/update-shapes ids update-fn {:attrs #{:blocked :hidden}}))))
|
||||
|
||||
(defn generate-delete-shapes
|
||||
[changes file page objects ids {:keys [components-v2 ignore-touched component-swap]}]
|
||||
(let [ids (cfh/clean-loops objects ids)
|
||||
([changes file page objects ids options]
|
||||
(generate-delete-shapes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
ids
|
||||
options))
|
||||
([changes ids {:keys [ignore-touched component-swap]}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
data (pcb/get-library-data changes)
|
||||
page-id (pcb/get-page-id changes)
|
||||
page (or (pcb/get-page changes)
|
||||
(ctpl/get-page data page-id))
|
||||
|
||||
in-component-copy?
|
||||
(fn [shape-id]
|
||||
ids (cfh/clean-loops objects ids)
|
||||
in-component-copy?
|
||||
(fn [shape-id]
|
||||
;; Look for shapes that are inside a component copy, but are
|
||||
;; not the root. In this case, they must not be deleted,
|
||||
;; but hidden (to be able to recover them more easily).
|
||||
;; Unless we are doing a component swap, in which case we want
|
||||
;; to delete the old shape
|
||||
(let [shape (get objects shape-id)]
|
||||
(and (ctn/has-any-copy-parent? objects shape)
|
||||
(not component-swap))))
|
||||
(let [shape (get objects shape-id)]
|
||||
(and (ctn/has-any-copy-parent? objects shape)
|
||||
(not component-swap))))
|
||||
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if components-v2
|
||||
(loop [ids-seq (seq ids)
|
||||
ids-to-delete []
|
||||
ids-to-hide []]
|
||||
(let [id (first ids-seq)]
|
||||
(if (nil? id)
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if (in-component-copy? id)
|
||||
(recur (rest ids-seq)
|
||||
ids-to-delete
|
||||
(conj ids-to-hide id))
|
||||
(recur (rest ids-seq)
|
||||
(conj ids-to-delete id)
|
||||
ids-to-hide)))))
|
||||
[ids []])
|
||||
[ids-to-delete ids-to-hide]
|
||||
(loop [ids-seq (seq ids)
|
||||
ids-to-delete []
|
||||
ids-to-hide []]
|
||||
(let [id (first ids-seq)]
|
||||
(if (nil? id)
|
||||
[ids-to-delete ids-to-hide]
|
||||
(if (in-component-copy? id)
|
||||
(recur (rest ids-seq)
|
||||
ids-to-delete
|
||||
(conj ids-to-hide id))
|
||||
(recur (rest ids-seq)
|
||||
(conj ids-to-delete id)
|
||||
ids-to-hide)))))
|
||||
|
||||
changes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
lookup (d/getf objects)
|
||||
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
;; If any of the deleted shapes is the destination of
|
||||
;; some interaction, this must be deleted, too.
|
||||
(let [interactions (:interactions shape)]
|
||||
(some #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %)))
|
||||
interactions)))
|
||||
(vals objects))
|
||||
(let [interactions (:interactions shape)]
|
||||
(some #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %)))
|
||||
interactions)))
|
||||
(vals objects))
|
||||
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(pcb/set-flow changes id nil)
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(pcb/set-flow changes id nil)
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
|
||||
|
||||
all-parents
|
||||
(reduce (fn [res id]
|
||||
all-parents
|
||||
(reduce (fn [res id]
|
||||
;; All parents of any deleted shape must be resized.
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
(reduce (fn [res id]
|
||||
(into res (cfh/get-children-ids objects id)))
|
||||
[])
|
||||
(reverse)
|
||||
(into (d/ordered-set)))
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
(reduce (fn [res id]
|
||||
(into res (cfh/get-children-ids objects id)))
|
||||
[])
|
||||
(reverse)
|
||||
(into (d/ordered-set)))
|
||||
|
||||
find-all-empty-parents
|
||||
(fn recursive-find-empty-parents [empty-parents]
|
||||
(let [all-ids (into empty-parents ids-to-delete)
|
||||
contains? (partial contains? all-ids)
|
||||
xform (comp (map lookup)
|
||||
(filter #(or (cfh/group-shape? %) (cfh/bool-shape? %)))
|
||||
(remove #(->> (:shapes %) (remove contains?) seq))
|
||||
(map :id))
|
||||
parents (into #{} xform all-parents)]
|
||||
(if (= empty-parents parents)
|
||||
empty-parents
|
||||
(recursive-find-empty-parents parents))))
|
||||
find-all-empty-parents
|
||||
(fn recursive-find-empty-parents [empty-parents]
|
||||
(let [all-ids (into empty-parents ids-to-delete)
|
||||
contains? (partial contains? all-ids)
|
||||
xform (comp (map lookup)
|
||||
(filter #(or (cfh/group-shape? %) (cfh/bool-shape? %) (ctk/is-variant-container? %)))
|
||||
(remove #(->> (:shapes %) (remove contains?) seq))
|
||||
(map :id))
|
||||
parents (into #{} xform all-parents)]
|
||||
(if (= empty-parents parents)
|
||||
empty-parents
|
||||
(recursive-find-empty-parents parents))))
|
||||
|
||||
empty-parents
|
||||
empty-parents
|
||||
;; Any parent whose children are all deleted, must be deleted too.
|
||||
;; Unless we are during a component swap: in this case we are replacing a shape by
|
||||
;; other one, so must not delete empty parents.
|
||||
(if-not component-swap
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
#{})
|
||||
(if-not component-swap
|
||||
(into (d/ordered-set) (find-all-empty-parents #{}))
|
||||
#{})
|
||||
|
||||
components-to-delete
|
||||
(if components-v2
|
||||
(reduce (fn [components id]
|
||||
(let [shape (get objects id)]
|
||||
(if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file
|
||||
(:main-instance shape)) ;; but check anyway
|
||||
(conj components (:component-id shape))
|
||||
components)))
|
||||
[]
|
||||
(into ids-to-delete all-children))
|
||||
[])
|
||||
components-to-delete
|
||||
(reduce (fn [components id]
|
||||
(let [shape (get objects id)]
|
||||
(if (and (= (:component-file shape) (:id data)) ;; Main instances should exist only in local file
|
||||
(:main-instance shape)) ;; but check anyway
|
||||
(conj components (:component-id shape))
|
||||
components)))
|
||||
[]
|
||||
(into ids-to-delete all-children))
|
||||
|
||||
ids-set (set ids-to-delete)
|
||||
|
||||
guides-to-delete
|
||||
(->> (:guides page)
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
ids-set (set ids-to-delete)
|
||||
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(pcb/set-flow changes guide-id nil))
|
||||
changes
|
||||
guides-to-delete)
|
||||
guides-to-delete
|
||||
(->> (:guides page)
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
|
||||
changes (reduce (fn [changes component-id]
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(pcb/set-flow changes guide-id nil))
|
||||
changes
|
||||
guides-to-delete)
|
||||
|
||||
changes (reduce (fn [changes component-id]
|
||||
;; It's important to delete the component before the main instance, because we
|
||||
;; need to store the instance position if we want to restore it later.
|
||||
(pcb/delete-component changes component-id (:id page)))
|
||||
changes
|
||||
components-to-delete)
|
||||
(pcb/delete-component changes component-id (:id page)))
|
||||
changes
|
||||
components-to-delete)
|
||||
|
||||
changes (-> changes
|
||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||
(pcb/remove-objects all-children {:ignore-touched true})
|
||||
(pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched})
|
||||
(pcb/remove-objects empty-parents)
|
||||
(pcb/resize-parents all-parents)
|
||||
(pcb/update-shapes groups-to-unmask
|
||||
(fn [shape]
|
||||
(assoc shape :masked-group false)))
|
||||
(pcb/update-shapes (map :id interacting-shapes)
|
||||
(fn [shape]
|
||||
(d/update-when shape :interactions
|
||||
(fn [interactions]
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %))))
|
||||
interactions))))))]
|
||||
[all-parents changes]))
|
||||
changes (-> changes
|
||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||
(pcb/remove-objects all-children {:ignore-touched true})
|
||||
(pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched})
|
||||
(pcb/remove-objects empty-parents)
|
||||
(pcb/resize-parents all-parents)
|
||||
(pcb/update-shapes groups-to-unmask
|
||||
(fn [shape]
|
||||
(assoc shape :masked-group false)))
|
||||
(pcb/update-shapes (map :id interacting-shapes)
|
||||
(fn [shape]
|
||||
(d/update-when shape :interactions
|
||||
(fn [interactions]
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %))))
|
||||
interactions))))))]
|
||||
[all-parents changes])))
|
||||
|
||||
|
||||
(defn generate-relocate
|
||||
[changes objects parent-id page-id to-index ids & {:keys [cell ignore-parents?]}]
|
||||
(let [ids (cfh/order-by-indexed-shapes objects ids)
|
||||
shapes (map (d/getf objects) ids)
|
||||
parent (get objects parent-id)
|
||||
[changes parent-id to-index ids & {:keys [cell ignore-parents?]}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
ids (cfh/order-by-indexed-shapes objects ids)
|
||||
shapes (map (d/getf objects) ids)
|
||||
parent (get objects parent-id)
|
||||
all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids)
|
||||
parents (if ignore-parents? #{parent-id} all-parents)
|
||||
parents (if ignore-parents? #{parent-id} all-parents)
|
||||
|
||||
children-ids
|
||||
(->> ids
|
||||
(mapcat #(cfh/get-children-ids-with-self objects %)))
|
||||
children-ids (mapcat #(cfh/get-children-ids-with-self objects %) ids)
|
||||
|
||||
child-heads
|
||||
(->> ids
|
||||
(mapcat #(ctn/get-child-heads objects %))
|
||||
(map :id))
|
||||
child-heads (mapcat #(ctn/get-child-heads objects %) ids)
|
||||
|
||||
child-heads-ids (map :id child-heads)
|
||||
|
||||
variant-shapes (filter ctk/is-variant? shapes)
|
||||
|
||||
component-main-parent
|
||||
(ctn/find-component-main objects parent false)
|
||||
@@ -337,12 +343,21 @@
|
||||
(map :id)))
|
||||
|
||||
index-cell-data (when to-index (ctl/get-cell-by-index parent to-index))
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))]
|
||||
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))
|
||||
|
||||
|
||||
;; Parents that are a variant-container that becomes empty
|
||||
empty-variant-cont (reduce
|
||||
(fn [to-delete parent-id]
|
||||
(let [parent (get objects parent-id)]
|
||||
(if (and (ctk/is-variant-container? parent)
|
||||
(empty? (remove (set ids) (:shapes parent))))
|
||||
(conj to-delete (:id parent))
|
||||
to-delete)))
|
||||
#{}
|
||||
(remove #(= % parent-id) all-parents))]
|
||||
|
||||
(-> changes
|
||||
(pcb/with-page-id page-id)
|
||||
(pcb/with-objects objects)
|
||||
|
||||
;; Remove layout-item properties when moving a shape outside a layout
|
||||
(cond-> (not (ctl/any-layout? parent))
|
||||
(pcb/update-shapes ids ctl/remove-layout-item-data))
|
||||
@@ -353,7 +368,7 @@
|
||||
|
||||
;; Remove the swap slots if it is moving to a different component
|
||||
(pcb/update-shapes
|
||||
child-heads
|
||||
child-heads-ids
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(not= component-main-parent (ctn/find-component-main objects shape false))
|
||||
@@ -365,7 +380,15 @@
|
||||
|
||||
;; Add component-root property when moving a component outside a component
|
||||
(cond-> (not (ctn/get-instance-root objects parent))
|
||||
(pcb/update-shapes child-heads #(assoc % :component-root true)))
|
||||
(pcb/update-shapes child-heads-ids #(assoc % :component-root true)))
|
||||
|
||||
;; Remove variant info and rename when moving outside a variant-container
|
||||
(cond-> (not (ctk/is-variant-container? parent))
|
||||
(clvp/generate-make-shapes-no-variant variant-shapes))
|
||||
|
||||
;; Add variant info and rename when moving into a different variant-container
|
||||
(cond-> (ctk/is-variant-container? parent)
|
||||
(clvp/generate-make-shapes-variant child-heads parent))
|
||||
|
||||
;; Move the shapes
|
||||
(pcb/change-parent parent-id
|
||||
@@ -440,7 +463,11 @@
|
||||
(pcb/update-shapes ids #(assoc % :blocked true)))
|
||||
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
(pcb/resize-parents parents)
|
||||
|
||||
;; Remove parents when are a variant-container that becomes empty
|
||||
(cond-> (seq empty-variant-cont)
|
||||
(#(second (generate-delete-shapes % empty-variant-cont {})))))))
|
||||
|
||||
(defn change-show-in-viewer
|
||||
[shape hide?]
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.logic.tokens
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.types.tokens-lib :as ctob]))
|
||||
|
||||
(defn generate-update-active-sets
|
||||
"Copy the active sets from the currently active themes and move them to the hidden token theme and update the theme with `update-hidden-theme-fn`.
|
||||
"Copy the active sets from the currently active themes and move them
|
||||
to the hidden token theme and update the theme with
|
||||
`update-theme-fn`.
|
||||
|
||||
Use this for managing sets active state without having to modify a user created theme (\"no themes selected\" state in the ui)."
|
||||
[changes tokens-lib update-hidden-theme-fn]
|
||||
Use this for managing sets active state without having to modify a
|
||||
user created theme (\"no themes selected\" state in the ui)."
|
||||
[changes tokens-lib update-theme-fn]
|
||||
(let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
|
||||
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
|
||||
|
||||
prev-hidden-token-theme (ctob/get-hidden-theme tokens-lib)
|
||||
hidden-token-theme (-> (or (some-> prev-hidden-token-theme (ctob/set-sets active-token-set-names))
|
||||
(ctob/make-hidden-token-theme :sets active-token-set-names))
|
||||
(update-hidden-theme-fn))
|
||||
prev-hidden-token-theme (ctob/get-hidden-theme tokens-lib)
|
||||
|
||||
changes (-> changes
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} prev-active-token-themes))
|
||||
|
||||
changes (if prev-hidden-token-theme
|
||||
(pcb/update-token-theme changes hidden-token-theme prev-hidden-token-theme)
|
||||
(pcb/add-token-theme changes hidden-token-theme))]
|
||||
changes))
|
||||
hidden-token-theme (-> (some-> prev-hidden-token-theme (ctob/set-sets active-token-set-names))
|
||||
(update-theme-fn))]
|
||||
(-> changes
|
||||
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} prev-active-token-themes)
|
||||
(pcb/set-token-theme (:group prev-hidden-token-theme)
|
||||
(:name prev-hidden-token-theme)
|
||||
hidden-token-theme))))
|
||||
|
||||
(defn generate-toggle-token-set
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
|
||||
user theme."
|
||||
[changes tokens-lib set-name]
|
||||
(generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name)))
|
||||
|
||||
@@ -48,17 +55,19 @@
|
||||
(defn vec-starts-with? [v1 v2]
|
||||
(= (subvec v1 0 (min (count v1) (count v2))) v2))
|
||||
|
||||
(defn calculate-move-token-set-or-set-group
|
||||
(defn- calculate-move-token-set-or-set-group
|
||||
[tokens-lib {:keys [from-index to-index position collapsed-paths]
|
||||
:or {collapsed-paths #{}}}]
|
||||
(let [tree (-> (ctob/get-set-tree tokens-lib)
|
||||
(ctob/walk-sets-tree-seq :walk-children? #(contains? collapsed-paths %)))
|
||||
(ctob/walk-sets-tree-seq :skip-children-pred #(contains? collapsed-paths %)))
|
||||
|
||||
from (nth tree from-index)
|
||||
to (nth tree to-index)
|
||||
before (case position
|
||||
:top to
|
||||
:bot (nth tree (inc to-index) nil)
|
||||
:center nil)
|
||||
|
||||
prev-before (if (:group? from)
|
||||
(->> (drop (inc from-index) tree)
|
||||
(filter (fn [element]
|
||||
@@ -72,6 +81,7 @@
|
||||
(= :bot position)
|
||||
(:group? to)
|
||||
(not (get collapsed-paths (:path to)))))
|
||||
|
||||
from-path (:path from)
|
||||
to-parent-path (if drop-as-direct-group-child?
|
||||
(:path to)
|
||||
@@ -117,15 +127,15 @@
|
||||
(defn generate-move-token-set
|
||||
"Create changes for dropping a token set or token set.
|
||||
Throws for impossible moves."
|
||||
[changes tokens-lib drop-opts]
|
||||
(if-let [drop-opts' (calculate-move-token-set-or-set-group tokens-lib drop-opts)]
|
||||
(pcb/move-token-set-before changes drop-opts')
|
||||
[changes tokens-lib params]
|
||||
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
|
||||
(pcb/move-token-set changes params)
|
||||
changes))
|
||||
|
||||
(defn generate-move-token-set-group
|
||||
"Create changes for dropping a token set or token set group.
|
||||
Throws for impossible moves"
|
||||
[changes tokens-lib drop-opts]
|
||||
(if-let [drop-opts' (calculate-move-token-set-or-set-group tokens-lib drop-opts)]
|
||||
(pcb/move-token-set-group-before changes drop-opts')
|
||||
[changes tokens-lib params]
|
||||
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
|
||||
(pcb/move-token-set-group changes params)
|
||||
changes))
|
||||
|
||||
201
common/src/app/common/logic/variant_properties.cljc
Normal file
201
common/src/app/common/logic/variant_properties.cljc
Normal file
@@ -0,0 +1,201 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
(ns app.common.logic.variant-properties
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[app.common.types.variant :as ctv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn generate-update-property-name
|
||||
[changes variant-id pos new-name]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(pcb/update-component
|
||||
changes (:id component)
|
||||
#(assoc-in % [:variant-properties pos :name] new-name)
|
||||
{:apply-changes-local-library? true}))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-remove-property
|
||||
[changes variant-id pos]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(let [props (:variant-properties component)
|
||||
props (d/remove-at-index props pos)
|
||||
main-id (:main-instance-id component)
|
||||
name (ctv/properties-to-name props)]
|
||||
(-> changes
|
||||
(pcb/update-component (:id component) #(assoc % :variant-properties props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
changes
|
||||
related-components)))
|
||||
|
||||
|
||||
(defn generate-update-property-value
|
||||
[changes component-id pos value]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
component (ctcl/get-component data component-id true)
|
||||
main-id (:main-instance-id component)
|
||||
name (-> (:variant-properties component)
|
||||
(update pos assoc :value value)
|
||||
ctv/properties-to-name)]
|
||||
(-> changes
|
||||
(pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
|
||||
|
||||
|
||||
(defn generate-add-new-property
|
||||
[changes variant-id & {:keys [fill-values? property-name]}]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
next-prop-num (ctv/next-property-number props)
|
||||
property-name (or property-name (str ctv/property-prefix next-prop-num))
|
||||
|
||||
[_ changes]
|
||||
(reduce (fn [[num changes] component]
|
||||
(let [main-id (:main-instance-id component)
|
||||
|
||||
update-props #(-> (d/nilv % [])
|
||||
(conj {:name property-name
|
||||
:value (if fill-values? (str ctv/value-prefix num) "")}))
|
||||
|
||||
update-name #(if fill-values?
|
||||
(if (str/empty? %)
|
||||
(str ctv/value-prefix num)
|
||||
(str % ", " ctv/value-prefix num))
|
||||
%)]
|
||||
[(inc num)
|
||||
(-> changes
|
||||
(pcb/update-component (:id component)
|
||||
#(update % :variant-properties update-props)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [main-id] #(update % :variant-name update-name)))]))
|
||||
[1 changes]
|
||||
related-components)]
|
||||
changes))
|
||||
|
||||
(defn- generate-make-shape-no-variant
|
||||
[changes shape]
|
||||
(let [new-name (ctv/variant-name-to-name shape)
|
||||
[cpath cname] (cfh/parse-path-name new-name)]
|
||||
(-> changes
|
||||
(pcb/update-component (:component-id shape)
|
||||
#(-> (dissoc % :variant-id :variant-properties)
|
||||
(assoc :name cname
|
||||
:path cpath))
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(-> (dissoc % :variant-id :variant-name)
|
||||
(assoc :name new-name))))))
|
||||
|
||||
(defn generate-make-shapes-no-variant
|
||||
[changes shapes]
|
||||
(reduce generate-make-shape-no-variant changes shapes))
|
||||
|
||||
|
||||
(defn- create-new-properties-from-variant
|
||||
[shape min-props data container-name base-properties]
|
||||
(let [component (ctcl/get-component data (:component-id shape) true)
|
||||
|
||||
add-name? (not= (:name component) container-name)
|
||||
props (ctv/merge-properties base-properties
|
||||
(:variant-properties component))
|
||||
new-props (- min-props
|
||||
(+ (count props)
|
||||
(if add-name? 1 0)))
|
||||
props (ctv/add-new-props props (repeat new-props ""))]
|
||||
|
||||
(if add-name?
|
||||
(ctv/add-new-prop props (:name component))
|
||||
props)))
|
||||
|
||||
(defn- create-new-properties-from-non-variant
|
||||
[shape min-props container-name base-properties]
|
||||
(let [;; Remove container name from shape name if present
|
||||
shape-name (ctv/remove-prefix (:name shape) container-name)]
|
||||
(ctv/path-to-properties shape-name base-properties min-props)))
|
||||
|
||||
|
||||
(defn generate-make-shapes-variant
|
||||
[changes shapes variant-container]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
variant-id (:id variant-container)
|
||||
|
||||
;; If we are cut-pasting a variant-container, this will be null
|
||||
;; because it hasn't any shapes yet
|
||||
first-comp-id (->> variant-container
|
||||
:shapes
|
||||
first
|
||||
(get objects)
|
||||
:component-id)
|
||||
|
||||
base-props (->> (get-in data [:components first-comp-id :variant-properties])
|
||||
(map #(assoc % :value "")))
|
||||
num-base-props (count base-props)
|
||||
|
||||
[cpath cname] (cfh/parse-path-name (:name variant-container))
|
||||
container-name (:name variant-container)
|
||||
|
||||
create-new-properties
|
||||
(fn [shape min-props]
|
||||
(if (ctk/is-variant? shape)
|
||||
(create-new-properties-from-variant shape min-props data container-name base-props)
|
||||
(create-new-properties-from-non-variant shape min-props container-name base-props)))
|
||||
|
||||
total-props (reduce (fn [m shape]
|
||||
(max m (count (create-new-properties shape num-base-props))))
|
||||
0
|
||||
shapes)
|
||||
|
||||
num-new-props (if (or (zero? num-base-props)
|
||||
(< total-props num-base-props))
|
||||
0
|
||||
(- total-props num-base-props))
|
||||
|
||||
changes (nth
|
||||
(iterate #(generate-add-new-property % variant-id) changes)
|
||||
num-new-props)
|
||||
|
||||
changes (pcb/update-shapes changes (map :id shapes)
|
||||
#(assoc % :variant-id variant-id
|
||||
:name (:name variant-container)))]
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(let [component (ctcl/get-component data (:component-id shape) true)]
|
||||
(if (or (zero? num-base-props) ;; do nothing if there are no base props
|
||||
(and (= variant-id (:variant-id shape)) ;; or we are only moving the shape inside its parent (it is
|
||||
(not (:deleted component)))) ;; the same parent and the component isn't deleted)
|
||||
changes
|
||||
(let [props (create-new-properties shape total-props)
|
||||
variant-name (ctv/properties-to-name props)]
|
||||
(-> (pcb/update-component changes
|
||||
(:component-id shape)
|
||||
#(assoc % :variant-id variant-id
|
||||
:variant-properties props
|
||||
:name cname
|
||||
:path cpath)
|
||||
{:apply-changes-local-library? true})
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(assoc % :variant-name variant-name)))))))
|
||||
changes
|
||||
shapes)))
|
||||
70
common/src/app/common/logic/variants.cljc
Normal file
70
common/src/app/common/logic/variants.cljc
Normal file
@@ -0,0 +1,70 @@
|
||||
(ns app.common.logic.variants
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.variant :as ctv]))
|
||||
|
||||
|
||||
(defn- generate-path
|
||||
[path objects base-id shape]
|
||||
(let [get-type #(case %
|
||||
:frame :container
|
||||
:group :container
|
||||
:rect :shape
|
||||
:circle :shape
|
||||
:bool :shape
|
||||
:path :shape
|
||||
%)]
|
||||
(if (= base-id (:id shape))
|
||||
path
|
||||
(generate-path (str path " " (:name shape) (get-type (:type shape))) objects base-id (get objects (:parent-id shape))))))
|
||||
|
||||
(defn generate-add-new-variant
|
||||
[changes shape variant-id new-component-id new-shape-id prop-num]
|
||||
(let [data (pcb/get-library-data changes)
|
||||
objects (pcb/get-objects changes)
|
||||
component-id (:component-id shape)
|
||||
value (str ctv/value-prefix
|
||||
(-> (cfv/extract-properties-values data objects variant-id)
|
||||
last
|
||||
:value
|
||||
count
|
||||
inc))
|
||||
|
||||
[new-shape changes] (-> changes
|
||||
(cll/generate-duplicate-component
|
||||
{:data data}
|
||||
component-id
|
||||
new-component-id
|
||||
{:new-shape-id new-shape-id :apply-changes-local-library? true}))]
|
||||
(-> changes
|
||||
(clvp/generate-update-property-value new-component-id prop-num value)
|
||||
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
|
||||
|
||||
(defn generate-keep-touched
|
||||
[changes new-shape original-shape original-shapes page libraries]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
new-path-map (into {}
|
||||
(map (fn [shape] {(generate-path "" objects (:id new-shape) shape) shape}))
|
||||
(cfh/get-children-with-self objects (:id new-shape)))
|
||||
|
||||
orig-touched (filter (comp seq :touched) original-shapes)
|
||||
orig-objects (into {} (map (juxt :id identity) original-shapes))
|
||||
container (ctn/make-container page :page)]
|
||||
(reduce
|
||||
(fn [changes touched-shape]
|
||||
(let [path (generate-path "" orig-objects (:id original-shape) touched-shape)
|
||||
related-shape (get new-path-map path)
|
||||
orig-ref-shape (ctf/find-ref-shape nil container libraries touched-shape)]
|
||||
(if related-shape
|
||||
(cll/update-attrs-on-switch
|
||||
changes related-shape touched-shape new-shape original-shape orig-ref-shape container)
|
||||
changes)))
|
||||
changes
|
||||
orig-touched)))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
@@ -27,10 +28,6 @@
|
||||
[malli.transform :as mt]
|
||||
[malli.util :as mu]))
|
||||
|
||||
(defprotocol ILazySchema
|
||||
(-validate [_ o])
|
||||
(-explain [_ o]))
|
||||
|
||||
(def default-options
|
||||
{:registry sr/default-registry})
|
||||
|
||||
@@ -50,10 +47,6 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn- lazy-schema?
|
||||
[s]
|
||||
(satisfies? ILazySchema s))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (schema? s)
|
||||
@@ -110,8 +103,16 @@
|
||||
(malli.error/error-value exp {:malli.error/mask-valid-values '...}))
|
||||
|
||||
(defn optional-keys
|
||||
[schema]
|
||||
(mu/optional-keys schema default-options))
|
||||
([schema]
|
||||
(mu/optional-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/optional-keys schema keys default-options)))
|
||||
|
||||
(defn required-keys
|
||||
([schema]
|
||||
(mu/required-keys schema nil default-options))
|
||||
([schema keys]
|
||||
(mu/required-keys schema keys default-options)))
|
||||
|
||||
(defn transformer
|
||||
[& transformers]
|
||||
@@ -145,11 +146,30 @@
|
||||
;; :else
|
||||
;; o))
|
||||
|
||||
(defn -transform-map-keys
|
||||
([f]
|
||||
(let [xform (map (fn [[k v]] [(f k) v]))]
|
||||
#(cond->> % (map? %) (into (empty %) xform))))
|
||||
([ks f]
|
||||
(let [xform (map (fn [[k v]] [(cond-> k (contains? ks k) f) v]))]
|
||||
#(cond->> % (map? %) (into (empty %) xform)))))
|
||||
|
||||
(defn json-transformer
|
||||
[]
|
||||
(mt/transformer
|
||||
(mt/json-transformer)
|
||||
(mt/collection-transformer)))
|
||||
(let [map-of-key-decoders (mt/-string-decoders)]
|
||||
(mt/transformer
|
||||
{:name :json
|
||||
:decoders (-> (mt/-json-decoders)
|
||||
(assoc :map-of {:compile (fn [schema _]
|
||||
(let [key-schema (some-> schema (m/children) (first))]
|
||||
(or (some-> key-schema (m/type) map-of-key-decoders
|
||||
(mt/-interceptor schema {}) (m/-intercepting)
|
||||
(m/-comp m/-keyword->string)
|
||||
(mt/-transform-if-valid key-schema)
|
||||
(-transform-map-keys))
|
||||
(-transform-map-keys m/-keyword->string))))}))
|
||||
:encoders (mt/-json-encoders)}
|
||||
(mt/collection-transformer))))
|
||||
|
||||
(defn string-transformer
|
||||
[]
|
||||
@@ -205,6 +225,11 @@
|
||||
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn decode-fn
|
||||
[s transformer]
|
||||
(let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))]
|
||||
(fn [v] (@vfn v))))
|
||||
|
||||
(defn humanize-explain
|
||||
"Returns a string representation of the explain data structure"
|
||||
[{:keys [errors value]} & {:keys [length level]}]
|
||||
@@ -250,38 +275,36 @@
|
||||
([s] (lookup sr/default-registry s))
|
||||
([registry s] (schema (mr/schema registry s))))
|
||||
|
||||
(defn- fast-check
|
||||
"A fast path for checking process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
[s type code hint value]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [explain (-explain s value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
value)
|
||||
|
||||
(declare ^:private lazy-schema)
|
||||
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(partial fast-check schema type code hint)))
|
||||
(let [s (schema s)
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
|
||||
(fn [value]
|
||||
(let [validate-fn @validator*]
|
||||
(when-not ^boolean (validate-fn value)
|
||||
(let [explain-fn @explainer*
|
||||
explain (explain-fn value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
value))))
|
||||
|
||||
(defn check
|
||||
"A helper intended to be used on assertions for validate/check the
|
||||
schema over provided data. Raises an assertion exception."
|
||||
[s value & {:keys [hint type code]}]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(fast-check s type code hint value)))
|
||||
schema over provided data. Raises an assertion exception.
|
||||
|
||||
Use only on non-performance sensitive code, because it creates the
|
||||
check-fn instance all the time it is invoked."
|
||||
[s value & {:as opts}]
|
||||
(let [check-fn (check-fn s opts)]
|
||||
(check-fn value)))
|
||||
|
||||
(defn type-schema
|
||||
[& {:as params}]
|
||||
@@ -295,11 +318,14 @@
|
||||
([params]
|
||||
(cond
|
||||
(map? params)
|
||||
(let [type (get params :type)]
|
||||
(let [mdata (meta params)
|
||||
type (or (get mdata ::id)
|
||||
(get mdata ::type)
|
||||
(get params :type))]
|
||||
(assert (qualified-keyword? type) "expected qualified keyword for `type`")
|
||||
(let [s (m/-simple-schema params)]
|
||||
(swap! sr/registry assoc type s)
|
||||
nil))
|
||||
s))
|
||||
|
||||
(vector? params)
|
||||
(let [mdata (meta params)
|
||||
@@ -307,97 +333,41 @@
|
||||
(get mdata ::type))]
|
||||
(assert (qualified-keyword? type) "expected qualified keyword to be on metadata")
|
||||
(swap! sr/registry assoc type params)
|
||||
nil)
|
||||
params)
|
||||
|
||||
(m/into-schema? params)
|
||||
(let [type (m/-type params)]
|
||||
(swap! sr/registry assoc type params))
|
||||
(swap! sr/registry assoc type params)
|
||||
params)
|
||||
|
||||
:else
|
||||
(throw (ex-info "Invalid Arguments" {}))))
|
||||
|
||||
([type params]
|
||||
(let [s (if (map? params)
|
||||
(cond
|
||||
(= :set (:type params))
|
||||
(m/-collection-schema params)
|
||||
|
||||
(= :vector (:type params))
|
||||
(m/-collection-schema params)
|
||||
|
||||
:else
|
||||
(m/-simple-schema params))
|
||||
params)]
|
||||
|
||||
(swap! sr/registry assoc type s)
|
||||
nil)))
|
||||
|
||||
(defn- lazy-schema
|
||||
"Create ans instance of ILazySchema"
|
||||
[s]
|
||||
(let [schema (schema s)
|
||||
validator (delay (m/validator schema))
|
||||
explainer (delay (m/explainer schema))]
|
||||
|
||||
(reify
|
||||
m/AST
|
||||
(-to-ast [_ options] (m/-to-ast schema options))
|
||||
|
||||
m/EntrySchema
|
||||
(-entries [_] (m/-entries schema))
|
||||
(-entry-parser [_] (m/-entry-parser schema))
|
||||
|
||||
m/Cached
|
||||
(-cache [_] (m/-cache schema))
|
||||
|
||||
m/LensSchema
|
||||
(-keep [_] (m/-keep schema))
|
||||
(-get [_ key default] (m/-get schema key default))
|
||||
(-set [_ key value] (m/-set schema key value))
|
||||
|
||||
m/Schema
|
||||
(-validator [_]
|
||||
(m/-validator schema))
|
||||
(-explainer [_ path]
|
||||
(m/-explainer schema path))
|
||||
(-parser [_]
|
||||
(m/-parser schema))
|
||||
(-unparser [_]
|
||||
(m/-unparser schema))
|
||||
(-transformer [_ transformer method options]
|
||||
(m/-transformer schema transformer method options))
|
||||
(-walk [_ walker path options]
|
||||
(m/-walk schema walker path options))
|
||||
(-properties [_]
|
||||
(m/-properties schema))
|
||||
(-options [_]
|
||||
(m/-options schema))
|
||||
(-children [_]
|
||||
(m/-children schema))
|
||||
(-parent [_]
|
||||
(m/-parent schema))
|
||||
(-form [_]
|
||||
(m/-form schema))
|
||||
|
||||
ILazySchema
|
||||
(-validate [_ o]
|
||||
(@validator o))
|
||||
(-explain [_ o]
|
||||
(@explainer o)))))
|
||||
(swap! sr/registry assoc type params)
|
||||
params))
|
||||
|
||||
;; --- BUILTIN SCHEMAS
|
||||
|
||||
(register! :merge (mu/-merge))
|
||||
(register! :union (mu/-union))
|
||||
|
||||
(def uuid-rx
|
||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
(defn parse-uuid
|
||||
(defn- parse-uuid
|
||||
[s]
|
||||
(if (string? s)
|
||||
(some->> (re-matches uuid-rx s) uuid/uuid)
|
||||
s))
|
||||
(if (uuid? s)
|
||||
s
|
||||
(if (str/empty? s)
|
||||
nil
|
||||
(try
|
||||
(uuid/parse s)
|
||||
(catch #?(:clj Exception :cljs :default) _cause
|
||||
s)))))
|
||||
|
||||
(defn- encode-uuid
|
||||
[v]
|
||||
(if (uuid? v)
|
||||
(str v)
|
||||
v))
|
||||
|
||||
(register!
|
||||
{:type ::uuid
|
||||
@@ -409,8 +379,8 @@
|
||||
:gen/gen (sg/uuid)
|
||||
:decode/string parse-uuid
|
||||
:decode/json parse-uuid
|
||||
:encode/string str
|
||||
:encode/json str
|
||||
:encode/string encode-uuid
|
||||
:encode/json encode-uuid
|
||||
::oapi/type "string"
|
||||
::oapi/format "uuid"}})
|
||||
|
||||
@@ -801,7 +771,8 @@
|
||||
|
||||
gen (sg/one-of
|
||||
(sg/small-int :max max :min min)
|
||||
(sg/small-double :max max :min min))]
|
||||
(->> (sg/small-double :max max :min min)
|
||||
(sg/fmap #(mth/precision % 2))))]
|
||||
|
||||
{:pred pred
|
||||
:type-properties
|
||||
@@ -856,7 +827,7 @@
|
||||
choices))]
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "contains"
|
||||
{:title "contains any"
|
||||
:description "contains predicate"}}))})
|
||||
|
||||
(register!
|
||||
@@ -866,7 +837,7 @@
|
||||
{:title "inst"
|
||||
:description "Satisfies Inst protocol"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
:gen/gen (->> (sg/small-int :min 0 :max 100000)
|
||||
(sg/fmap (fn [v] (tm/parse-instant v))))
|
||||
|
||||
:decode/string tm/parse-instant
|
||||
@@ -876,6 +847,22 @@
|
||||
::oapi/type "string"
|
||||
::oapi/format "iso"}})
|
||||
|
||||
(register!
|
||||
{:type ::timestamp
|
||||
:pred inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "Satisfies Inst protocol"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (tm/parse-instant v))))
|
||||
:decode/string tm/parse-instant
|
||||
:encode/string inst-ms
|
||||
:decode/json tm/parse-instant
|
||||
:encode/json inst-ms
|
||||
::oapi/type "string"
|
||||
::oapi/format "number"}})
|
||||
|
||||
(register!
|
||||
{:type ::fn
|
||||
:pred fn?})
|
||||
@@ -1019,26 +1006,26 @@
|
||||
(def valid-text?
|
||||
(validator ::text))
|
||||
|
||||
(def check-safe-int!
|
||||
(def check-safe-int
|
||||
(check-fn ::safe-int))
|
||||
|
||||
(def check-set-of-strings!
|
||||
(def check-set-of-strings
|
||||
(check-fn ::set-of-strings))
|
||||
|
||||
(def check-email!
|
||||
(def check-email
|
||||
(check-fn ::email))
|
||||
|
||||
(def check-uuid!
|
||||
(def check-uuid
|
||||
(check-fn ::uuid :hint "expected valid uuid instance"))
|
||||
|
||||
(def check-string!
|
||||
(def check-string
|
||||
(check-fn :string :hint "expected string"))
|
||||
|
||||
(def check-coll-of-uuid!
|
||||
(def check-coll-of-uuid
|
||||
(check-fn ::coll-of-uuid))
|
||||
|
||||
(def check-set-of-uuid!
|
||||
(def check-set-of-uuid
|
||||
(check-fn ::set-of-uuid))
|
||||
|
||||
(def check-set-of-emails!
|
||||
(def check-set-of-emails
|
||||
(check-fn [::set ::email]))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.generators
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.schema.registry :as sr]
|
||||
@@ -126,3 +126,7 @@
|
||||
(defn tuple
|
||||
[& opts]
|
||||
(apply tg/tuple opts))
|
||||
|
||||
(defn vector
|
||||
[& opts]
|
||||
(apply tg/vector opts))
|
||||
|
||||
@@ -56,12 +56,8 @@
|
||||
(str "(pass=TRUE, tests=" (:num-tests params) ", seed=" (:seed params) ", elapsed=" time "ms)"))))
|
||||
|
||||
(defmethod ct/report #?(:clj ::thrunk :cljs [:cljs.test/default ::thrunk])
|
||||
[{:keys [::params] :as m}]
|
||||
(let [smallest (-> params :shrunk :smallest vec)]
|
||||
(println)
|
||||
(println "Condition failed with the following params:")
|
||||
(println)
|
||||
(pp/pprint smallest)))
|
||||
[_]
|
||||
nil)
|
||||
|
||||
(defmethod ct/report #?(:clj ::trial :cljs [:cljs.test/default ::trial])
|
||||
[_]
|
||||
@@ -75,9 +71,12 @@
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
res (:result params)]
|
||||
(println)
|
||||
|
||||
(println "---------------------------------------------------------")
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=FALSE, tests=" (:num-tests params) ", seed=" (:seed params) ")"))
|
||||
(pp/pprint (:fail params))
|
||||
(println "---------------------------------------------------------")
|
||||
|
||||
(when (ex/exception? res)
|
||||
#?(:clj (ex/print-throwable res)
|
||||
|
||||
@@ -40,76 +40,3 @@
|
||||
(map (fn [segment]
|
||||
(.toPersistentMap ^js segment)))
|
||||
(parser/parse path-str)))))
|
||||
|
||||
#?(:cljs
|
||||
(defn content->buffer
|
||||
"Converts the path content into binary format."
|
||||
[content]
|
||||
(let [total (count content)
|
||||
ssize 28
|
||||
buffer (new js/ArrayBuffer (* total ssize))
|
||||
dview (new js/DataView buffer)]
|
||||
(loop [index 0]
|
||||
(when (< index total)
|
||||
(let [segment (nth content index)
|
||||
offset (* index ssize)]
|
||||
(case (:command segment)
|
||||
:move-to
|
||||
(let [{:keys [x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 1)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
:line-to
|
||||
(let [{:keys [x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 2)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
:curve-to
|
||||
(let [{:keys [c1x c1y c2x c2y x y]} (:params segment)]
|
||||
(.setInt16 dview (+ offset 0) 3)
|
||||
(.setFloat32 dview (+ offset 4) c1x)
|
||||
(.setFloat32 dview (+ offset 8) c1y)
|
||||
(.setFloat32 dview (+ offset 12) c2x)
|
||||
(.setFloat32 dview (+ offset 16) c2y)
|
||||
(.setFloat32 dview (+ offset 20) x)
|
||||
(.setFloat32 dview (+ offset 24) y))
|
||||
|
||||
:close-path
|
||||
(.setInt16 dview (+ offset 0) 4))
|
||||
(recur (inc index)))))
|
||||
buffer)))
|
||||
|
||||
#?(:cljs
|
||||
(defn buffer->content
|
||||
"Converts the a buffer to a path content vector"
|
||||
[buffer]
|
||||
(assert (instance? js/ArrayBuffer buffer) "expected ArrayBuffer instance")
|
||||
(let [ssize 28
|
||||
total (/ (.-byteLength buffer) ssize)
|
||||
dview (new js/DataView buffer)]
|
||||
(loop [index 0
|
||||
result []]
|
||||
(if (< index total)
|
||||
(let [offset (* index ssize)
|
||||
type (.getInt16 dview (+ offset 0))
|
||||
command (case type
|
||||
1 :move-to
|
||||
2 :line-to
|
||||
3 :curve-to
|
||||
4 :close-path)
|
||||
params (case type
|
||||
1 {:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
2 {:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
3 {:c1x (.getFloat32 dview (+ offset 4))
|
||||
:c1y (.getFloat32 dview (+ offset 8))
|
||||
:c2x (.getFloat32 dview (+ offset 12))
|
||||
:c2y (.getFloat32 dview (+ offset 16))
|
||||
:x (.getFloat32 dview (+ offset 20))
|
||||
:y (.getFloat32 dview (+ offset 24))}
|
||||
4 {})]
|
||||
(recur (inc index)
|
||||
(conj result {:command command
|
||||
:params params})))
|
||||
result)))))
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.bool
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[app.common.svg.path.subpath :as ups]))
|
||||
|
||||
(defn add-previous
|
||||
([content]
|
||||
(add-previous content nil))
|
||||
([content first]
|
||||
(->> (d/with-prev content)
|
||||
(mapv (fn [[cmd prev]]
|
||||
(cond-> cmd
|
||||
(and (nil? prev) (some? first))
|
||||
(assoc :prev first)
|
||||
|
||||
(some? prev)
|
||||
(assoc :prev (gsp/command->point prev))))))))
|
||||
|
||||
(defn close-paths
|
||||
"Removes the :close-path commands and replace them for line-to so we can calculate
|
||||
the intersections"
|
||||
[content]
|
||||
|
||||
(loop [head (first content)
|
||||
content (rest content)
|
||||
result []
|
||||
last-move nil
|
||||
last-p nil]
|
||||
|
||||
(if (nil? head)
|
||||
result
|
||||
(let [head-p (gsp/command->point head)
|
||||
head (cond
|
||||
(and (= :close-path (:command head))
|
||||
(or (nil? last-p) ;; Ignore consecutive close-paths
|
||||
(< (gpt/distance last-p last-move) 0.01)))
|
||||
nil
|
||||
|
||||
(= :close-path (:command head))
|
||||
(upc/make-line-to last-move)
|
||||
|
||||
:else
|
||||
head)]
|
||||
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(cond-> result (some? head) (conj head))
|
||||
(if (= :move-to (:command head))
|
||||
head-p
|
||||
last-move)
|
||||
head-p)))))
|
||||
|
||||
(defn- split-command
|
||||
[cmd values]
|
||||
(case (:command cmd)
|
||||
:line-to (gsp/split-line-to-ranges (:prev cmd) cmd values)
|
||||
:curve-to (gsp/split-curve-to-ranges (:prev cmd) cmd values)
|
||||
[cmd]))
|
||||
|
||||
(defn split-ts [seg-1 seg-2]
|
||||
(cond
|
||||
(and (= :line-to (:command seg-1))
|
||||
(= :line-to (:command seg-2)))
|
||||
(gsp/line-line-intersect (gsp/command->line seg-1) (gsp/command->line seg-2))
|
||||
|
||||
(and (= :line-to (:command seg-1))
|
||||
(= :curve-to (:command seg-2)))
|
||||
(gsp/line-curve-intersect (gsp/command->line seg-1) (gsp/command->bezier seg-2))
|
||||
|
||||
(and (= :curve-to (:command seg-1))
|
||||
(= :line-to (:command seg-2)))
|
||||
(let [[seg-2' seg-1']
|
||||
(gsp/line-curve-intersect (gsp/command->line seg-2) (gsp/command->bezier seg-1))]
|
||||
;; Need to reverse because we send the arguments reversed
|
||||
[seg-1' seg-2'])
|
||||
|
||||
(and (= :curve-to (:command seg-1))
|
||||
(= :curve-to (:command seg-2)))
|
||||
(gsp/curve-curve-intersect (gsp/command->bezier seg-1) (gsp/command->bezier seg-2))
|
||||
|
||||
:else
|
||||
[[] []]))
|
||||
|
||||
(defn content-intersect-split
|
||||
[content-a content-b sr-a sr-b]
|
||||
|
||||
(let [command->selrect (memoize gsp/command->selrect)]
|
||||
|
||||
(letfn [(overlap-segment-selrect?
|
||||
[segment selrect]
|
||||
(if (= :move-to (:command segment))
|
||||
false
|
||||
(let [r1 (command->selrect segment)]
|
||||
(grc/overlaps-rects? r1 selrect))))
|
||||
|
||||
(overlap-segments?
|
||||
[seg-1 seg-2]
|
||||
(if (or (= :move-to (:command seg-1))
|
||||
(= :move-to (:command seg-2)))
|
||||
false
|
||||
(let [r1 (command->selrect seg-1)
|
||||
r2 (command->selrect seg-2)]
|
||||
(grc/overlaps-rects? r1 r2))))
|
||||
|
||||
(split
|
||||
[seg-1 seg-2]
|
||||
(if (not (overlap-segments? seg-1 seg-2))
|
||||
[seg-1]
|
||||
(let [[ts-seg-1 _] (split-ts seg-1 seg-2)]
|
||||
(-> (split-command seg-1 ts-seg-1)
|
||||
(add-previous (:prev seg-1))))))
|
||||
|
||||
(split-segment-on-content
|
||||
[segment content content-sr]
|
||||
|
||||
(if (overlap-segment-selrect? segment content-sr)
|
||||
(->> content
|
||||
(filter #(overlap-segments? segment %))
|
||||
(reduce
|
||||
(fn [result current]
|
||||
(into [] (mapcat #(split % current)) result))
|
||||
[segment]))
|
||||
[segment]))
|
||||
|
||||
(split-content
|
||||
[content-a content-b sr-b]
|
||||
(into []
|
||||
(mapcat #(split-segment-on-content % content-b sr-b))
|
||||
content-a))]
|
||||
|
||||
[(split-content content-a content-b sr-b)
|
||||
(split-content content-b content-a sr-a)])))
|
||||
|
||||
(defn is-segment?
|
||||
[cmd]
|
||||
(and (contains? cmd :prev)
|
||||
(contains? #{:line-to :curve-to} (:command cmd))))
|
||||
|
||||
(defn contains-segment?
|
||||
[segment content content-sr content-geom]
|
||||
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (gsp/command->line segment)
|
||||
(gsp/line-values 0.5))
|
||||
|
||||
:curve-to (-> (gsp/command->bezier segment)
|
||||
(gsp/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(or
|
||||
(gsp/is-point-in-geom-data? point content-geom)
|
||||
(gsp/is-point-in-border? point content)))))
|
||||
|
||||
(defn inside-segment?
|
||||
[segment content-sr content-geom]
|
||||
(let [point (case (:command segment)
|
||||
:line-to (-> (gsp/command->line segment)
|
||||
(gsp/line-values 0.5))
|
||||
|
||||
:curve-to (-> (gsp/command->bezier segment)
|
||||
(gsp/curve-values 0.5)))]
|
||||
|
||||
(and (grc/contains-point? content-sr point)
|
||||
(gsp/is-point-in-geom-data? point content-geom))))
|
||||
|
||||
(defn overlap-segment?
|
||||
"Finds if the current segment is overlapping against other
|
||||
segment meaning they have the same coordinates"
|
||||
[segment content]
|
||||
|
||||
(let [overlap-single?
|
||||
(fn [other]
|
||||
(when (and (= (:command segment) (:command other))
|
||||
(contains? #{:line-to :curve-to} (:command segment)))
|
||||
|
||||
(case (:command segment)
|
||||
:line-to (let [[p1 q1] (gsp/command->line segment)
|
||||
[p2 q2] (gsp/command->line other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1))
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)))
|
||||
[segment other]))
|
||||
|
||||
:curve-to (let [[p1 q1 h11 h21] (gsp/command->bezier segment)
|
||||
[p2 q2 h12 h22] (gsp/command->bezier other)]
|
||||
|
||||
(when (or (and (< (gpt/distance p1 p2) 0.1)
|
||||
(< (gpt/distance q1 q2) 0.1)
|
||||
(< (gpt/distance h11 h12) 0.1)
|
||||
(< (gpt/distance h21 h22) 0.1))
|
||||
|
||||
(and (< (gpt/distance p1 q2) 0.1)
|
||||
(< (gpt/distance q1 p2) 0.1)
|
||||
(< (gpt/distance h11 h22) 0.1)
|
||||
(< (gpt/distance h21 h12) 0.1)))
|
||||
|
||||
[segment other])))))]
|
||||
|
||||
(->> content
|
||||
(d/seek overlap-single?)
|
||||
(some?))))
|
||||
|
||||
(defn fix-move-to
|
||||
[content]
|
||||
;; Remove the field `:prev` and makes the necessaries `move-to`
|
||||
;; then clean the subpaths
|
||||
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
prev nil
|
||||
result []]
|
||||
|
||||
(if (nil? current)
|
||||
result
|
||||
|
||||
(let [result (if (not= (:prev current) prev)
|
||||
(conj result (upc/make-move-to (:prev current)))
|
||||
result)]
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(gsp/command->point current)
|
||||
(conj result (dissoc current :prev)))))))
|
||||
|
||||
(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content-b that are not inside content-a
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)
|
||||
|
||||
content
|
||||
(concat
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
(->> content-b-split (filter #(not (contains-segment? % content-a sr-a content-a-geom)))))
|
||||
|
||||
content-geom (gsp/content->geom-data content)
|
||||
|
||||
content-sr (gsp/content->selrect (fix-move-to content))
|
||||
|
||||
;; Overlapping segments should be added when they are part of the border
|
||||
border-content
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(overlap-segment? % content-a-split)
|
||||
(not (inside-segment? % content-sr content-geom)))))]
|
||||
|
||||
;; Ensure that the output is always a vector
|
||||
(d/concat-vec content border-content)))
|
||||
|
||||
(defn create-difference [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are not inside content-b
|
||||
;; Pick all segments in content b that are inside content-a
|
||||
;; removing overlapping
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom))))
|
||||
|
||||
;; Reverse second content so we can have holes inside other shapes
|
||||
(->> content-b-split
|
||||
(filter #(and (contains-segment? % content-a sr-a content-a-geom)
|
||||
(not (overlap-segment? % content-a-split))))))))
|
||||
|
||||
(defn create-intersection [content-a content-a-split content-b content-b-split sr-a sr-b]
|
||||
;; Pick all segments in content-a that are inside content-b
|
||||
;; Pick all segments in content-b that are inside content-a
|
||||
(let [content-a-geom (gsp/content->geom-data content-a)
|
||||
content-b-geom (gsp/content->geom-data content-b)]
|
||||
(d/concat-vec
|
||||
(->> content-a-split (filter #(contains-segment? % content-b sr-b content-b-geom)))
|
||||
(->> content-b-split (filter #(contains-segment? % content-a sr-a content-a-geom))))))
|
||||
|
||||
|
||||
(defn create-exclusion [content-a content-b]
|
||||
;; Pick all segments
|
||||
(d/concat-vec content-a content-b))
|
||||
|
||||
(defn content-bool-pair
|
||||
[bool-type content-a content-b]
|
||||
|
||||
(let [;; We need to reverse the second path when making a difference/intersection/exclude
|
||||
;; and both shapes are in the same direction
|
||||
should-reverse? (and (not= :union bool-type)
|
||||
(= (ups/clockwise? content-b)
|
||||
(ups/clockwise? content-a)))
|
||||
|
||||
content-a (-> content-a
|
||||
(close-paths)
|
||||
(add-previous))
|
||||
|
||||
content-b (-> content-b
|
||||
(close-paths)
|
||||
(cond-> should-reverse? (ups/reverse-content))
|
||||
(add-previous))
|
||||
|
||||
sr-a (gsp/content->selrect content-a)
|
||||
sr-b (gsp/content->selrect content-b)
|
||||
|
||||
;; Split content in new segments in the intersection with the other path
|
||||
[content-a-split content-b-split] (content-intersect-split content-a content-b sr-a sr-b)
|
||||
content-a-split (->> content-a-split add-previous (filter is-segment?))
|
||||
content-b-split (->> content-b-split add-previous (filter is-segment?))
|
||||
|
||||
bool-content
|
||||
(case bool-type
|
||||
:union (create-union content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:difference (create-difference content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:intersection (create-intersection content-a content-a-split content-b content-b-split sr-a sr-b)
|
||||
:exclude (create-exclusion content-a-split content-b-split))]
|
||||
|
||||
(->> (fix-move-to bool-content)
|
||||
(ups/close-subpaths))))
|
||||
|
||||
(defn content-bool
|
||||
[bool-type contents]
|
||||
;; We apply the boolean operation in to each pair and the result to the next
|
||||
;; element
|
||||
(if (seq contents)
|
||||
(->> contents
|
||||
(reduce (partial content-bool-pair bool-type))
|
||||
(into []))
|
||||
[]))
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.command
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]))
|
||||
|
||||
(defn command->point
|
||||
([prev-pos {:keys [relative params] :as command}]
|
||||
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||
(if relative
|
||||
(-> prev-pos (update :x + x) (update :y + y))
|
||||
(command->point command))))
|
||||
|
||||
([command]
|
||||
(when command
|
||||
(let [{:keys [x y]} (:params command)]
|
||||
(gpt/point x y)))))
|
||||
|
||||
|
||||
(defn make-move-to [to]
|
||||
{:command :move-to
|
||||
:relative false
|
||||
:params {:x (:x to)
|
||||
:y (:y to)}})
|
||||
|
||||
(defn make-line-to [to]
|
||||
{:command :line-to
|
||||
:relative false
|
||||
:params {:x (:x to)
|
||||
:y (:y to)}})
|
||||
|
||||
(defn make-curve-params
|
||||
([point]
|
||||
(make-curve-params point point point))
|
||||
|
||||
([point handler] (make-curve-params point handler point))
|
||||
|
||||
([point h1 h2]
|
||||
{:x (:x point)
|
||||
:y (:y point)
|
||||
:c1x (:x h1)
|
||||
:c1y (:y h1)
|
||||
:c2x (:x h2)
|
||||
:c2y (:y h2)}))
|
||||
|
||||
(defn update-curve-to
|
||||
[command h1 h2]
|
||||
(let [params {:x (-> command :params :x)
|
||||
:y (-> command :params :y)
|
||||
:c1x (:x h1)
|
||||
:c1y (:y h1)
|
||||
:c2x (:x h2)
|
||||
:c2y (:y h2)}]
|
||||
(-> command
|
||||
(assoc :command :curve-to)
|
||||
(assoc :params params))))
|
||||
|
||||
(defn make-curve-to
|
||||
[to h1 h2]
|
||||
{:command :curve-to
|
||||
:relative false
|
||||
:params (make-curve-params to h1 h2)})
|
||||
|
||||
(defn update-handler
|
||||
[command prefix point]
|
||||
(let [[cox coy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y])]
|
||||
(-> command
|
||||
(assoc-in [:params cox] (:x point))
|
||||
(assoc-in [:params coy] (:y point)))))
|
||||
|
||||
(defn apply-content-modifiers
|
||||
"Apply to content a map with point translations"
|
||||
[content modifiers]
|
||||
(letfn [(apply-to-index [content [index params]]
|
||||
(if (contains? content index)
|
||||
(cond-> content
|
||||
(and
|
||||
(or (:c1x params) (:c1y params) (:c2x params) (:c2y params))
|
||||
(= :line-to (get-in content [index :command])))
|
||||
|
||||
(-> (assoc-in [index :command] :curve-to)
|
||||
(assoc-in [index :params]
|
||||
(make-curve-params
|
||||
(get-in content [index :params])
|
||||
(get-in content [(dec index) :params]))))
|
||||
|
||||
(:x params) (update-in [index :params :x] + (:x params))
|
||||
(:y params) (update-in [index :params :y] + (:y params))
|
||||
|
||||
(:c1x params) (update-in [index :params :c1x] + (:c1x params))
|
||||
(:c1y params) (update-in [index :params :c1y] + (:c1y params))
|
||||
|
||||
(:c2x params) (update-in [index :params :c2x] + (:c2x params))
|
||||
(:c2y params) (update-in [index :params :c2y] + (:c2y params)))
|
||||
content))]
|
||||
(let [content (if (vector? content) content (into [] content))]
|
||||
(reduce apply-to-index content modifiers))))
|
||||
|
||||
(defn get-handler [{:keys [params] :as command} prefix]
|
||||
(let [cx (d/prefix-keyword prefix :x)
|
||||
cy (d/prefix-keyword prefix :y)]
|
||||
(when (and command
|
||||
(contains? params cx)
|
||||
(contains? params cy))
|
||||
(gpt/point (get params cx)
|
||||
(get params cy)))))
|
||||
|
||||
(defn content->handlers
|
||||
"Retrieve a map where for every point will retrieve a list of
|
||||
the handlers that are associated with that point.
|
||||
point -> [[index, prefix]]"
|
||||
[content]
|
||||
(->> (d/with-prev content)
|
||||
(d/enumerate)
|
||||
(mapcat (fn [[index [cur-cmd pre-cmd]]]
|
||||
(if (and pre-cmd (= :curve-to (:command cur-cmd)))
|
||||
(let [cur-pos (command->point cur-cmd)
|
||||
pre-pos (command->point pre-cmd)]
|
||||
(-> [[pre-pos [index :c1]]
|
||||
[cur-pos [index :c2]]]))
|
||||
[])))
|
||||
|
||||
(group-by first)
|
||||
(d/mapm #(mapv second %2))))
|
||||
|
||||
(defn point-indices
|
||||
[content point]
|
||||
(->> (d/enumerate content)
|
||||
(filter (fn [[_ cmd]] (= point (command->point cmd))))
|
||||
(mapv (fn [[index _]] index))))
|
||||
|
||||
(defn handler-indices
|
||||
"Return an index where the key is the positions and the values the handlers"
|
||||
[content point]
|
||||
(->> (d/with-prev content)
|
||||
(d/enumerate)
|
||||
(mapcat (fn [[index [cur-cmd pre-cmd]]]
|
||||
(if (and (some? pre-cmd) (= :curve-to (:command cur-cmd)))
|
||||
(let [cur-pos (command->point cur-cmd)
|
||||
pre-pos (command->point pre-cmd)]
|
||||
(cond-> []
|
||||
(= pre-pos point) (conj [index :c1])
|
||||
(= cur-pos point) (conj [index :c2])))
|
||||
[])))))
|
||||
|
||||
(defn opposite-index
|
||||
"Calculates the opposite index given a prefix and an index"
|
||||
[content index prefix]
|
||||
|
||||
(let [point (if (= prefix :c2)
|
||||
(command->point (nth content index))
|
||||
(command->point (nth content (dec index))))
|
||||
|
||||
point->handlers (content->handlers content)
|
||||
|
||||
handlers (->> point
|
||||
(point->handlers)
|
||||
(filter (fn [[ci cp]] (and (not= index ci) (not= prefix cp)))))]
|
||||
|
||||
(cond
|
||||
(= (count handlers) 1)
|
||||
(->> handlers first)
|
||||
|
||||
(and (= :c1 prefix) (= (count content) index))
|
||||
[(dec index) :c2]
|
||||
|
||||
:else nil)))
|
||||
|
||||
|
||||
(defn get-commands
|
||||
"Returns the commands involving a point with its indices"
|
||||
[content point]
|
||||
(->> (d/enumerate content)
|
||||
(filterv (fn [[_ cmd]] (= (command->point cmd) point)))))
|
||||
|
||||
|
||||
(defn prefix->coords [prefix]
|
||||
(case prefix
|
||||
:c1 [:c1x :c1y]
|
||||
:c2 [:c2x :c2y]
|
||||
nil))
|
||||
|
||||
(defn handler->point [content index prefix]
|
||||
(when (and (some? index)
|
||||
(some? prefix)
|
||||
(contains? content index))
|
||||
(let [[cx cy] (prefix->coords prefix)]
|
||||
(if (= :curve-to (get-in content [index :command]))
|
||||
(gpt/point (get-in content [index :params cx])
|
||||
(get-in content [index :params cy]))
|
||||
|
||||
(gpt/point (get-in content [index :params :x])
|
||||
(get-in content [index :params :y]))))))
|
||||
|
||||
(defn handler->node [content index prefix]
|
||||
(if (= prefix :c1)
|
||||
(command->point (get content (dec index)))
|
||||
(command->point (get content index))))
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.svg.path.legacy-parser1
|
||||
"The first SVG Path parser implementation.
|
||||
|
||||
Written in a mix of CLJS and JS code and used in production until
|
||||
1.19, used mainly for tests."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path.arc-to-bezier :as a2b]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
||||
|
||||
;; Matches numbers for path values allows values like... -.01, 10, +12.22
|
||||
;; 0 and 1 are special because can refer to flags
|
||||
(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
|
||||
|
||||
(def flag-regex #"[01]")
|
||||
|
||||
(defn extract-params [cmd-str extract-commands]
|
||||
(loop [result []
|
||||
extract-idx 0
|
||||
current {}
|
||||
remain (-> cmd-str (subs 1) (str/trim))]
|
||||
|
||||
(let [[param type] (nth extract-commands extract-idx)
|
||||
regex (case type
|
||||
:flag flag-regex
|
||||
#_:number num-regex)
|
||||
match (re-find regex remain)]
|
||||
|
||||
(if match
|
||||
(let [value (-> match first csvg/fix-dot-number d/read-string)
|
||||
remain (str/replace-first remain regex "")
|
||||
current (assoc current param value)
|
||||
extract-idx (inc extract-idx)
|
||||
[result current extract-idx]
|
||||
(if (>= extract-idx (count extract-commands))
|
||||
[(conj result current) {} 0]
|
||||
[result current extract-idx])]
|
||||
(recur result
|
||||
extract-idx
|
||||
current
|
||||
remain))
|
||||
(cond-> result
|
||||
(seq current) (conj current))))))
|
||||
|
||||
;; Path specification
|
||||
;; https://www.w3.org/TR/SVG11/paths.html
|
||||
(defmulti parse-command (comp str/upper first))
|
||||
|
||||
(defmethod parse-command "M" [cmd]
|
||||
(let [relative (str/starts-with? cmd "m")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
|
||||
(into [{:command :move-to
|
||||
:relative relative
|
||||
:params (first param-list)}]
|
||||
|
||||
(for [params (rest param-list)]
|
||||
{:command :line-to
|
||||
:relative relative
|
||||
:params params}))))
|
||||
|
||||
(defmethod parse-command "Z" [_]
|
||||
[{:command :close-path}])
|
||||
|
||||
(defmethod parse-command "L" [cmd]
|
||||
(let [relative (str/starts-with? cmd "l")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "H" [cmd]
|
||||
(let [relative (str/starts-with? cmd "h")
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-horizontal
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "V" [cmd]
|
||||
(let [relative (str/starts-with? cmd "v")
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-vertical
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "C" [cmd]
|
||||
(let [relative (str/starts-with? cmd "c")
|
||||
param-list (extract-params cmd [[:c1x :number]
|
||||
[:c1y :number]
|
||||
[:c2x :number]
|
||||
[:c2y :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "S" [cmd]
|
||||
(let [relative (str/starts-with? cmd "s")
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "Q" [cmd]
|
||||
(let [relative (str/starts-with? cmd "q")
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "T" [cmd]
|
||||
(let [relative (str/starts-with? cmd "t")
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "A" [cmd]
|
||||
(let [relative (str/starts-with? cmd "a")
|
||||
param-list (extract-params cmd [[:rx :number]
|
||||
[:ry :number]
|
||||
[:x-axis-rotation :number]
|
||||
[:large-arc-flag :flag]
|
||||
[:sweep-flag :flag]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :elliptical-arc
|
||||
:relative relative
|
||||
:params params})))
|
||||
|
||||
(defn smooth->curve
|
||||
[{:keys [params]} pos handler]
|
||||
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
|
||||
{:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x (:cx params)
|
||||
:c2y (:cy params)}))
|
||||
|
||||
(defn quadratic->curve
|
||||
[sp ep cp]
|
||||
(let [cp1 (-> (gpt/to-vec sp cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add sp))
|
||||
|
||||
cp2 (-> (gpt/to-vec ep cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add ep))]
|
||||
|
||||
{:c1x (:x cp1)
|
||||
:c1y (:y cp1)
|
||||
:c2x (:x cp2)
|
||||
:c2y (:y cp2)}))
|
||||
|
||||
(defn arc->beziers*
|
||||
[from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation]
|
||||
(a2b/calculateBeziers from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation))
|
||||
|
||||
(defn arc->beziers [from-p command]
|
||||
(let [to-command
|
||||
(fn [[_ _ c1x c1y c2x c2y x y]]
|
||||
{:command :curve-to
|
||||
:relative (:relative command)
|
||||
:params {:c1x c1x :c1y c1y
|
||||
:c2x c2x :c2y c2y
|
||||
:x x :y y}})
|
||||
|
||||
{from-x :x from-y :y} from-p
|
||||
{:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command)
|
||||
result (arc->beziers* from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
|
||||
(mapv to-command result)))
|
||||
|
||||
(defn simplify-commands
|
||||
"Removes some commands and convert relative to absolute coordinates"
|
||||
[commands]
|
||||
(let [simplify-command
|
||||
;; prev-pos : previous position for the current path. Necessary for relative commands
|
||||
;; prev-start : previous move-to necessary for Z commands
|
||||
;; prev-cc : previous command control point for cubic beziers
|
||||
;; prev-qc : previous command control point for quadratic curves
|
||||
(fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]]
|
||||
(let [command (assoc command :prev-pos prev-pos)
|
||||
|
||||
command
|
||||
(cond-> command
|
||||
(:relative command)
|
||||
(-> (assoc :relative false)
|
||||
(d/update-in-when [:params :c1x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :c1y] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :c2x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :c2y] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :cx] + (:x prev-pos))
|
||||
(d/update-in-when [:params :cy] + (:y prev-pos))
|
||||
|
||||
(d/update-in-when [:params :x] + (:x prev-pos))
|
||||
(d/update-in-when [:params :y] + (:y prev-pos))
|
||||
|
||||
(cond->
|
||||
(= :line-to-horizontal (:command command))
|
||||
(d/update-in-when [:params :value] + (:x prev-pos))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(d/update-in-when [:params :value] + (:y prev-pos)))))
|
||||
|
||||
params (:params command)
|
||||
orig-command command
|
||||
|
||||
command
|
||||
(cond-> command
|
||||
(= :line-to-horizontal (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :x] (:value params))
|
||||
(assoc-in [:params :y] (:y prev-pos)))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :y] (:value params))
|
||||
(assoc-in [:params :x] (:x prev-pos)))
|
||||
|
||||
(= :smooth-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (smooth->curve command prev-pos prev-cc)))
|
||||
|
||||
(= :quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
|
||||
|
||||
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(into result (arc->beziers prev-pos command))
|
||||
(conj result command))
|
||||
|
||||
next-cc (case (:command orig-command)
|
||||
:smooth-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:curve-to
|
||||
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
|
||||
|
||||
(:line-to-horizontal :line-to-vertical)
|
||||
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-qc (case (:command orig-command)
|
||||
:quadratic-bezier-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:smooth-quadratic-bezier-curve-to
|
||||
(upg/calculate-opposite-handler prev-pos prev-qc)
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-pos (if (= :close-path (:command command))
|
||||
prev-start
|
||||
(upc/command->point prev-pos command))
|
||||
|
||||
next-start (if (= :move-to (:command command)) next-pos prev-start)]
|
||||
|
||||
[result next-pos next-start next-cc next-qc]))
|
||||
|
||||
start (first commands)
|
||||
start (cond-> start
|
||||
(:relative start)
|
||||
(assoc :relative false))
|
||||
|
||||
start-pos (gpt/point (:params start))]
|
||||
|
||||
(->> (map vector (rest commands) commands)
|
||||
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
|
||||
(first))))
|
||||
|
||||
(defn parse [path-str]
|
||||
(if (empty? path-str)
|
||||
path-str
|
||||
(let [clean-path-str
|
||||
(-> path-str
|
||||
(str/trim)
|
||||
;; Change "commas" for spaces
|
||||
(str/replace #"," " ")
|
||||
;; Remove all consecutive spaces
|
||||
(str/replace #"\s+" " "))
|
||||
commands (re-seq commands-regex clean-path-str)]
|
||||
(-> (mapcat parse-command commands)
|
||||
(simplify-commands)))))
|
||||
|
||||
@@ -12,15 +12,23 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.path :as upg]
|
||||
[app.common.math :as mth]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path.command :as upc]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
[app.common.types.path.segment :as path.segment]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
|
||||
(def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
|
||||
|
||||
(defn- get-point
|
||||
"Get a point for a segment"
|
||||
[prev-pos {:keys [relative params] :as segment}]
|
||||
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||
(if relative
|
||||
(-> prev-pos (update :x + x) (update :y + y))
|
||||
(path.helpers/segment->point segment))))
|
||||
|
||||
(defn extract-params
|
||||
[data pattern]
|
||||
(loop [result []
|
||||
@@ -185,7 +193,7 @@
|
||||
|
||||
(defn smooth->curve
|
||||
[{:keys [params]} pos handler]
|
||||
(let [{c1x :x c1y :y} (upg/calculate-opposite-handler pos handler)]
|
||||
(let [{c1x :x c1y :y} (path.segment/calculate-opposite-handler pos handler)]
|
||||
{:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x (:cx params)
|
||||
@@ -413,7 +421,7 @@
|
||||
|
||||
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (upg/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
(update :params merge (quadratic->curve prev-pos (gpt/point params) (path.segment/calculate-opposite-handler prev-pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(into result (arc->beziers prev-pos command))
|
||||
@@ -436,13 +444,13 @@
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:smooth-quadratic-bezier-curve-to
|
||||
(upg/calculate-opposite-handler prev-pos prev-qc)
|
||||
(path.segment/calculate-opposite-handler prev-pos prev-qc)
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
next-pos (if (= :close-path (:command command))
|
||||
prev-start
|
||||
(upc/command->point prev-pos command))
|
||||
(get-point prev-pos command))
|
||||
|
||||
next-start (if (= :move-to (:command command)) next-pos prev-start)]
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.svg.path :as path]
|
||||
[app.common.types.path.segment :as path.segm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -220,9 +221,9 @@
|
||||
(let [transform (csvg/parse-transform (:transform attrs))
|
||||
content (cond-> (path/parse (:d attrs))
|
||||
(some? transform)
|
||||
(gsh/transform-content transform))
|
||||
(path.segm/transform-content transform))
|
||||
|
||||
selrect (gsh/content->selrect content)
|
||||
selrect (path.segm/content->selrect content)
|
||||
points (grc/rect->points selrect)
|
||||
origin (gpt/negate (gpt/point svg-data))
|
||||
attrs (-> (dissoc attrs :d :transform)
|
||||
@@ -435,16 +436,12 @@
|
||||
|
||||
attrs
|
||||
(-> attrs
|
||||
(cond-> linecap
|
||||
(dissoc :strokeLinecap))
|
||||
(cond-> (some? color)
|
||||
(dissoc :stroke :strokeWidth :strokeOpacity))
|
||||
(update
|
||||
:style
|
||||
(fn [style]
|
||||
(-> style
|
||||
(cond-> linecap
|
||||
(dissoc :strokeLinecap))
|
||||
(cond-> (some? color)
|
||||
(dissoc :stroke :strokeWidth :strokeOpacity)))))
|
||||
(d/without-nils))]
|
||||
@@ -461,12 +458,14 @@
|
||||
|
||||
(and (some? linecap) (cfh/path-shape? shape)
|
||||
(or (= linecap :round) (= linecap :square)))
|
||||
|
||||
(assoc :stroke-cap-start linecap
|
||||
:stroke-cap-end linecap)
|
||||
:stroke-cap-end linecap
|
||||
:stroke-linecap linecap)
|
||||
|
||||
(d/any-key? (dm/get-in shape [:strokes 0])
|
||||
:strokeColor :strokeOpacity :strokeWidth
|
||||
:strokeCapStart :strokeCapEnd)
|
||||
:strokeLinecap :strokeCapStart :strokeCapEnd)
|
||||
(assoc-in [:strokes 0 :stroke-style] :svg))))
|
||||
|
||||
(defn setup-opacity [shape]
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Need that root is already a frame"
|
||||
(cfh/frame-shape? root))
|
||||
|
||||
(let [[_new-root _new-shapes updated-shapes]
|
||||
(let [[_new-root updated-shapes]
|
||||
(ctn/convert-shape-in-component root (:objects page) (:id file))
|
||||
|
||||
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
|
||||
@@ -54,8 +54,15 @@
|
||||
:name name
|
||||
:path path
|
||||
:main-instance-id (:id updated-root)
|
||||
:main-instance-page (:id page)
|
||||
:shapes updated-shapes))))))))
|
||||
:main-instance-page (:id page)))))))))
|
||||
|
||||
(defn update-component
|
||||
[file component-label & {:keys [] :as params}]
|
||||
(let [component-id (thi/id component-label)]
|
||||
(ctf/update-file-data
|
||||
file
|
||||
(fn [file-data]
|
||||
(ctkl/update-component file-data component-id #(merge % params))))))
|
||||
|
||||
(defn get-component
|
||||
[file label & {:keys [include-deleted?] :or {include-deleted? false}}]
|
||||
@@ -90,7 +97,6 @@
|
||||
component
|
||||
(:data library)
|
||||
(gpt/point 100 100)
|
||||
true
|
||||
{:force-id (thi/new-id! copy-root-label)
|
||||
:force-frame-id frame-id})
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
& {:keys [component-params root-params child-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child-label [:name Rect1]
|
||||
;; :child-label [:name Rect1]
|
||||
(-> file
|
||||
(add-frame-with-child root-label child-label :frame-params root-params :child-params child-params)
|
||||
(thc/make-component component-label root-label component-params)))
|
||||
@@ -95,7 +95,7 @@
|
||||
& {:keys [component-params main-root-params main-child-params copy-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main-root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :main-child-label [:name Rect1]
|
||||
;; :main-child-label [:name Rect1]
|
||||
;;
|
||||
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label
|
||||
;; <no-label> [:name Rect1] ---> :main-child-label
|
||||
@@ -113,9 +113,9 @@
|
||||
& {:keys [component-params root-params child-params-list]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
(as-> file $
|
||||
(add-frame $ root-label root-params)
|
||||
(reduce (fn [file [index [label params]]]
|
||||
@@ -134,9 +134,9 @@
|
||||
& {:keys [component-params main-root-params main-child-params-list copy-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;; :child1-label [:name Rect1]
|
||||
;; :child2-label [:name Rect2]
|
||||
;; :child3-label [:name Rect3]
|
||||
;;
|
||||
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :root-label
|
||||
;; <no-label> [:name Rect1] ---> :child1-label
|
||||
@@ -156,7 +156,7 @@
|
||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;;
|
||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||
@@ -183,7 +183,7 @@
|
||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}]
|
||||
;; Generated shape tree:
|
||||
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;; :main1-child-label [:name Rect1]
|
||||
;;
|
||||
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||
@@ -336,8 +336,7 @@
|
||||
file
|
||||
{file-id file}
|
||||
(ctn/make-container container :page)
|
||||
(:id shape)
|
||||
true))
|
||||
(:id shape)))
|
||||
file' (thf/apply-changes file changes)]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
@@ -361,7 +360,7 @@
|
||||
(:objects page)
|
||||
#{(-> (ths/get-shape file shape-tag :page-label page-label)
|
||||
:id)}
|
||||
{:components-v2 true})
|
||||
{})
|
||||
file' (thf/apply-changes file changes)]
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
@@ -380,7 +379,7 @@
|
||||
(gpt/point 0 0) ;; delta
|
||||
{(:id file) file} ;; libraries
|
||||
(:data file) ;; library-data
|
||||
(:id file)) ;; file-id
|
||||
(:id file)) ;; file-id
|
||||
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
||||
#{(:id shape)}))
|
||||
file' (thf/apply-changes file changes)]
|
||||
|
||||
@@ -23,28 +23,32 @@
|
||||
|
||||
(defn sample-file
|
||||
[label & {:keys [page-label name view-only?] :as params}]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(let [params (cond-> params
|
||||
label
|
||||
(assoc :id (thi/new-id! label))
|
||||
(let [params
|
||||
(cond-> params
|
||||
label
|
||||
(assoc :id (thi/new-id! label))
|
||||
|
||||
page-label
|
||||
(assoc :page-id (thi/new-id! page-label))
|
||||
(nil? name)
|
||||
(assoc :name "Test file")
|
||||
|
||||
(nil? name)
|
||||
(assoc :name "Test file"))
|
||||
:always
|
||||
(assoc :features ffeat/default-features))
|
||||
|
||||
file (-> (ctf/make-file (dissoc params :page-label))
|
||||
(assoc :features #{"components/v2"})
|
||||
(assoc :permissions {:can-edit (not (true? view-only?))}))
|
||||
opts
|
||||
(cond-> {}
|
||||
page-label
|
||||
(assoc :page-id (thi/new-id! page-label)))
|
||||
|
||||
page (-> file
|
||||
:data
|
||||
(ctpl/pages-seq)
|
||||
(first))]
|
||||
file (-> (ctf/make-file params opts)
|
||||
(assoc :permissions {:can-edit (not (true? view-only?))}))
|
||||
|
||||
(with-meta file
|
||||
{:current-page-id (:id page)}))))
|
||||
page (-> file
|
||||
:data
|
||||
(ctpl/pages-seq)
|
||||
(first))]
|
||||
|
||||
(with-meta file
|
||||
{:current-page-id (:id page)})))
|
||||
|
||||
(defn validate-file!
|
||||
([file] (validate-file! file {}))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user