Compare commits
221 Commits
hiru-sync-
...
1.17.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24fa4f71ad | ||
|
|
fa21dc4cf9 | ||
|
|
2460f36bab | ||
|
|
4d627f8993 | ||
|
|
7771467aa0 | ||
|
|
0e97182ef0 | ||
|
|
f0c0e5e43a | ||
|
|
475b6ff6e0 | ||
|
|
a1f41c80a2 | ||
|
|
4297b6fda8 | ||
|
|
28dce3cc8b | ||
|
|
3c650ae47e | ||
|
|
1806200613 | ||
|
|
ed22e2c6d1 | ||
|
|
0487539b23 | ||
|
|
fd15ff940f | ||
|
|
ece6193260 | ||
|
|
813a188e24 | ||
|
|
0f07def536 | ||
|
|
490f5f19f1 | ||
|
|
b3216000fd | ||
|
|
2ef3e4b325 | ||
|
|
70edd2c290 | ||
|
|
02543b1a4f | ||
|
|
094556926e | ||
|
|
1ed3b3cf75 | ||
|
|
1637e82018 | ||
|
|
c467d04d50 | ||
|
|
8d19c067e8 | ||
|
|
a99fb7ada3 | ||
|
|
2f1d1a6c41 | ||
|
|
7f963edf9e | ||
|
|
9c99d86e08 | ||
|
|
6a5bfdd7fb | ||
|
|
a98ba72c12 | ||
|
|
ee42dd8b01 | ||
|
|
da209b7507 | ||
|
|
d49e1f1641 | ||
|
|
8e35ad0f7f | ||
|
|
be3a973d09 | ||
|
|
78aea0f24e | ||
|
|
6e1ce62aad | ||
|
|
070ea135e5 | ||
|
|
5ae1fe5867 | ||
|
|
eef2cba976 | ||
|
|
1c4dcf1574 | ||
|
|
220b80799d | ||
|
|
22b6d4241d | ||
|
|
fa02df7106 | ||
|
|
5d6462b2a7 | ||
|
|
3464842c1e | ||
|
|
d74af6ddc1 | ||
|
|
8cb33dc19c | ||
|
|
4912107fcc | ||
|
|
d5c7a6e547 | ||
|
|
f1085aadd1 | ||
|
|
ca5b59f102 | ||
|
|
a0898fbabd | ||
|
|
aaf332ed18 | ||
|
|
b05ca4bb82 | ||
|
|
b46b23b027 | ||
|
|
29c0190b7a | ||
|
|
f1b09e763e | ||
|
|
2e5e772392 | ||
|
|
ecd4bb54c9 | ||
|
|
3cfc432c23 | ||
|
|
e426425cb5 | ||
|
|
3a0cc63fa7 | ||
|
|
88a8370e8d | ||
|
|
e8972dd802 | ||
|
|
3e52bef6d4 | ||
|
|
7c215dc11b | ||
|
|
48c3e3e00b | ||
|
|
412dcae01a | ||
|
|
cc5f245209 | ||
|
|
dc4aabe263 | ||
|
|
708a8ce27b | ||
|
|
7c1d9ce06f | ||
|
|
b0cbf09950 | ||
|
|
f31bc7457f | ||
|
|
e47ce3235e | ||
|
|
fe76e0fab6 | ||
|
|
297ba10e9d | ||
|
|
dd2321a37b | ||
|
|
f98630a46b | ||
|
|
82d6ba790c | ||
|
|
575aec209c | ||
|
|
00e265695c | ||
|
|
071ac0366c | ||
|
|
1a2a90f829 | ||
|
|
028c084b22 | ||
|
|
e7e80e99bd | ||
|
|
70fa169d0d | ||
|
|
6be83fc6d6 | ||
|
|
1e9ece43d0 | ||
|
|
965c0d6fa2 | ||
|
|
950d5dcc2f | ||
|
|
43d034798c | ||
|
|
86712f977d | ||
|
|
707e6c2a33 | ||
|
|
3dfd87eee1 | ||
|
|
037ba19e87 | ||
|
|
cdbab2c098 | ||
|
|
e8ea61ee78 | ||
|
|
7ab91f68af | ||
|
|
91ececa59e | ||
|
|
8758723200 | ||
|
|
8a968dc081 | ||
|
|
f8cb505196 | ||
|
|
14e3439cae | ||
|
|
7dd55c7f9d | ||
|
|
e8e3398a74 | ||
|
|
95cad24c18 | ||
|
|
d31138db72 | ||
|
|
2c5f35e192 | ||
|
|
5a8f8ba349 | ||
|
|
3fe5cd3752 | ||
|
|
da60911d81 | ||
|
|
f4f1f80050 | ||
|
|
18445ea5f4 | ||
|
|
2d28e02742 | ||
|
|
b0b963fb7c | ||
|
|
5cfee13956 | ||
|
|
7271e98df3 | ||
|
|
f0386ef7b0 | ||
|
|
185cabb2fa | ||
|
|
3a19223264 | ||
|
|
2c38f31aa9 | ||
|
|
a1dcb11261 | ||
|
|
9f8d86a80e | ||
|
|
c59fc87fc4 | ||
|
|
3421e6ef57 | ||
|
|
40349c8ece | ||
|
|
5a53376b01 | ||
|
|
d4dfdaff57 | ||
|
|
c7f87d0f26 | ||
|
|
c7954990f0 | ||
|
|
fe118819ce | ||
|
|
073ec9ea2b | ||
|
|
f85a731969 | ||
|
|
a3a88d7a0a | ||
|
|
1660dd634e | ||
|
|
6e698110d6 | ||
|
|
951c67a2d5 | ||
|
|
50b7337b8c | ||
|
|
15e62ff649 | ||
|
|
e7ddd6055f | ||
|
|
aa3438f800 | ||
|
|
a45380a91c | ||
|
|
86b68aeca4 | ||
|
|
d69d392362 | ||
|
|
506c2b8d7b | ||
|
|
b463ebc17b | ||
|
|
f90fda2c90 | ||
|
|
87c5aa71a3 | ||
|
|
4f82f6bde4 | ||
|
|
545b3860b4 | ||
|
|
d4921c8eb9 | ||
|
|
18652d0b6f | ||
|
|
2dbeda1d8f | ||
|
|
9422d1e9e2 | ||
|
|
e0441bc16a | ||
|
|
d7d6166232 | ||
|
|
6fd6205634 | ||
|
|
7cd6f5ba70 | ||
|
|
9cc3cceb06 | ||
|
|
6f6bcd2f7e | ||
|
|
f9f3b3951f | ||
|
|
22ded62000 | ||
|
|
71d104f768 | ||
|
|
5a36cbceb7 | ||
|
|
f2033c46f3 | ||
|
|
6b225a10b5 | ||
|
|
38fe6e856a | ||
|
|
1984109436 | ||
|
|
9f9d9277a6 | ||
|
|
e041f93680 | ||
|
|
2d779a4414 | ||
|
|
21fc9289a6 | ||
|
|
b40ea3fb2a | ||
|
|
444e9a3081 | ||
|
|
f93d305545 | ||
|
|
09a91c87be | ||
|
|
e71d569cda | ||
|
|
a56a9868dc | ||
|
|
a09198b46e | ||
|
|
c7e9c658cd | ||
|
|
58d7bc5c14 | ||
|
|
e939db927e | ||
|
|
efe50479de | ||
|
|
ea1b3bd058 | ||
|
|
4751d7d385 | ||
|
|
bc88e30efa | ||
|
|
9623dbfbd6 | ||
|
|
f177de6661 | ||
|
|
43043e2dc1 | ||
|
|
05d21d7d07 | ||
|
|
02aab37ee7 | ||
|
|
d3aee1afa3 | ||
|
|
ac361cdb36 | ||
|
|
7ac6f49c08 | ||
|
|
d3e11433bf | ||
|
|
771d1d9194 | ||
|
|
4a3a53182b | ||
|
|
c25cf043fa | ||
|
|
7440d38c94 | ||
|
|
a8c0d437ce | ||
|
|
8d683beae4 | ||
|
|
4007d8713c | ||
|
|
ead64a1820 | ||
|
|
88e2a5c56e | ||
|
|
9782d9077f | ||
|
|
b4c4511d9d | ||
|
|
316b3d4539 | ||
|
|
1c54e9fa4d | ||
|
|
3d064b804b | ||
|
|
77cd645e25 | ||
|
|
04dc9f7881 | ||
|
|
0863a96f93 | ||
|
|
216a43cc43 | ||
|
|
05431cc757 |
54
CHANGES.md
@@ -1,4 +1,36 @@
|
||||
# CHANGELOG
|
||||
## 1.17.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix invite members button text [Taiga #4794](https://tree.taiga.io/project/penpot/issue/4794)
|
||||
- Fix problem with opacity in frames [Taiga #4795](https://tree.taiga.io/project/penpot/issue/4795)
|
||||
- Fix correct behaviour for space-around and added space-evenly option
|
||||
- Fix duplicate with alt and undo only undo one step [Taiga #4746](https://tree.taiga.io/project/penpot/issue/4746)
|
||||
- Fix problem creating frames inside layout [Taiga #4844](https://tree.taiga.io/project/penpot/issue/4844)
|
||||
|
||||
## 1.17.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
- Fix paste board inside itself [Taiga #4775](https://tree.taiga.io/project/penpot/issue/4775)
|
||||
- Fix middle button panning can drag guides [Taiga #4266](https://tree.taiga.io/project/penpot/issue/4266)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @ondrejkonec: for some code contributions on this release.
|
||||
|
||||
## 1.17.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
|
||||
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
|
||||
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
|
||||
- Fix button spacing on delete acount modal [Taiga #4762](https://tree.taiga.io/project/penpot/issue/4762)
|
||||
- Fix invitations input on team management and onboarding modal [Taiga #4760](https://tree.taiga.io/project/penpot/issue/4760)
|
||||
- Fix weird numeration creating new elements in dashboard [Taiga #4755](https://tree.taiga.io/project/penpot/issue/4755)
|
||||
- Fix can move shape with lens zoom active [Taiga #4787](https://tree.taiga.io/project/penpot/issue/4787)
|
||||
- Fix social links broken [Taiga #4759](https://tree.taiga.io/project/penpot/issue/4759)
|
||||
- Fix tooltips on left toolbar [Taiga #4793](https://tree.taiga.io/project/penpot/issue/4793)
|
||||
|
||||
## 1.17.0
|
||||
|
||||
@@ -10,6 +42,16 @@
|
||||
- Handoff visual improvements [Taiga #3124](https://tree.taiga.io/project/penpot/us/3124)
|
||||
- Dynamic alignment only in sight [Github 1971](https://github.com/penpot/penpot/issues/1971)
|
||||
- Add some accessibility to shortcut panel [Taiga #4713](https://tree.taiga.io/project/penpot/issue/4713)
|
||||
- Add shortcuts for text editing [Taiga #2052](https://tree.taiga.io/project/penpot/us/2052)
|
||||
- Second level boards treated as groups in terms of selection [Taiga #4269](https://tree.taiga.io/project/penpot/us/4269)
|
||||
- Performance improvements both for backend and frontend
|
||||
- Accessibility improvements for login area [Taiga #4353](https://tree.taiga.io/project/penpot/us/4353)
|
||||
- Outbound webhooks [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
|
||||
- Add copy invitation link to the invitation options [Taiga #4213](https://tree.taiga.io/project/penpot/us/4213)
|
||||
- Dynamic alignment only in sight [Taiga #3537](https://tree.taiga.io/project/penpot/us/3537)
|
||||
- Improve naming of layers [Taiga #4036](https://tree.taiga.io/project/penpot/us/4036)
|
||||
- Add zoom lense [Taiga #4691](https://tree.taiga.io/project/penpot/us/4691)
|
||||
- Detect potential problems with custom font vertical metrics [Taiga #4697](https://tree.taiga.io/project/penpot/us/4697)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -50,6 +92,18 @@
|
||||
- Fix hidden layers inside groups become visible after the group visibility is changed[Taiga #4710](https://tree.taiga.io/project/penpot/issue/4710)
|
||||
- Fix format of HSLA color on viewer [Taiga #4393](https://tree.taiga.io/project/penpot/issue/4393)
|
||||
- Fix some typos [Taiga #4724](https://tree.taiga.io/project/penpot/issue/4724)
|
||||
- Fix ctrl+c for inspect code [Taiga #4739](https://tree.taiga.io/project/penpot/issue/4739)
|
||||
- Fix text in custom font is not at the expected position at export [Taiga #4394](https://tree.taiga.io/project/penpot/issue/4394)
|
||||
- Fix unneeded popup when updating local components [Taiga #4430](https://tree.taiga.io/project/penpot/issue/4430)
|
||||
- Fix multiuser - "Shadow" element is not updating immediately [Taiga #4709](https://tree.taiga.io/project/penpot/issue/4709)
|
||||
- Fix paths not flagged as modified when resized [Taiga #4742](https://tree.taiga.io/project/penpot/issue/4742)
|
||||
- Fix resend invitation doesn't reset the expiration date [Taiga #4741](https://tree.taiga.io/project/penpot/issue/4741)
|
||||
- Fix incorrect state after undo page creation [Taiga #4690](https://tree.taiga.io/project/penpot/issue/4690)
|
||||
- Fix copy paste texts with typography assets linked [Taiga #4750](https://tree.taiga.io/project/penpot/issue/4750)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @iprithvitharun: let's make UX Writing contributions in Open Source a trend!
|
||||
|
||||
## 1.16.2-beta
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Being web based, Penpot is not dependent on operating systems or local installat
|
||||
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
|
||||
<img src="https://penpot.app/images/readme/open-source.png" alt="Open Source">
|
||||
</p>
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ You will find the following categories:
|
||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/cross-teams.webp" alt="Community">
|
||||
<img src="https://penpot.app/images/readme/cross-teams.webp" alt="Community">
|
||||
</p>
|
||||
|
||||
## Contributing ##
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v9.11"
|
||||
:git/sha "6f9197a"
|
||||
{:git/tag "v9.12"
|
||||
:git/sha "51646d8"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -211,9 +211,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -225,7 +225,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -239,9 +239,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
@@ -257,9 +257,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -271,7 +271,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -285,9 +285,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -301,7 +301,7 @@
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -321,7 +321,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -341,7 +341,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -361,7 +361,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -370,7 +370,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -381,7 +381,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -390,7 +390,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -401,7 +401,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -411,9 +411,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -425,7 +425,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -439,9 +439,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -457,9 +457,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -201,9 +201,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -215,7 +215,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -229,9 +229,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
@@ -247,9 +247,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -261,7 +261,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -275,9 +275,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -311,7 +311,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -331,7 +331,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -351,7 +351,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -360,7 +360,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -371,7 +371,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -380,7 +380,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -391,7 +391,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -401,9 +401,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -415,7 +415,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -429,9 +429,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -447,9 +447,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -206,9 +206,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -220,7 +220,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -234,9 +234,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
@@ -252,9 +252,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -266,7 +266,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -280,9 +280,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -296,7 +296,7 @@
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -316,7 +316,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -336,7 +336,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -356,7 +356,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -365,7 +365,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -376,7 +376,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -385,7 +385,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -396,7 +396,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -406,9 +406,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -420,7 +420,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -434,9 +434,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -452,9 +452,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -201,9 +201,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -215,7 +215,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -229,9 +229,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
@@ -247,9 +247,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -261,7 +261,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -275,9 +275,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -311,7 +311,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -331,7 +331,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -351,7 +351,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -360,7 +360,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -371,7 +371,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -380,7 +380,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -391,7 +391,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -401,9 +401,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -415,7 +415,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -429,9 +429,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
|
||||
<tr>
|
||||
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
@@ -447,9 +447,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -12,6 +12,7 @@ cp ../CHANGES.md target/classes/changelog.md;
|
||||
|
||||
clojure -T:build jar;
|
||||
mv target/penpot.jar target/dist/penpot.jar
|
||||
cp resources/log4j2.xml target/dist/log4j2.xml
|
||||
cp scripts/run.template.sh target/dist/run.sh;
|
||||
cp scripts/manage.py target/dist/manage.py
|
||||
chmod +x target/dist/run.sh;
|
||||
|
||||
@@ -19,10 +19,10 @@ PREPL_URI = "tcp://localhost:6063"
|
||||
def get_prepl_conninfo():
|
||||
uri_data = urlparse(PREPL_URI)
|
||||
if uri_data.scheme != "tcp":
|
||||
raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
|
||||
if not isinstance(uri_data.netloc, str):
|
||||
raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
|
||||
host, port = uri_data.netloc.split(":", 2)
|
||||
|
||||
@@ -46,7 +46,7 @@ def send_eval(expr):
|
||||
result = json.load(f)
|
||||
tag = result.get("tag", None)
|
||||
if tag != "ret":
|
||||
raise RuntimeException("unexpected response from PREPL")
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
return result.get("val", None), result.get("exception", None)
|
||||
|
||||
def encode(val):
|
||||
|
||||
@@ -18,5 +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 $JVM_OPTS"
|
||||
|
||||
set -x
|
||||
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- discover-oidc-config
|
||||
[cfg {:keys [::base-uri] :as opts}]
|
||||
[cfg {:keys [base-uri] :as opts}]
|
||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
||||
response (ex/try! (http/req! cfg
|
||||
{:method :get :uri (str discovery-uri)}
|
||||
@@ -64,10 +64,17 @@
|
||||
nil)
|
||||
|
||||
(= 200 (:status response))
|
||||
(let [data (json/decode (:body response))]
|
||||
{:token-uri (get data :token_endpoint)
|
||||
:auth-uri (get data :authorization_endpoint)
|
||||
:user-uri (get data :userinfo_endpoint)})
|
||||
(let [data (json/decode (:body response))
|
||||
token-uri (get data :token_endpoint)
|
||||
auth-uri (get data :authorization_endpoint)
|
||||
user-uri (get data :userinfo_endpoint)]
|
||||
(l/debug :hint "oidc uris discovered"
|
||||
:token-uri token-uri
|
||||
:auth-uri auth-uri
|
||||
:user-uri user-uri)
|
||||
{:token-uri token-uri
|
||||
:auth-uri auth-uri
|
||||
:user-uri user-uri})
|
||||
|
||||
:else
|
||||
(do
|
||||
@@ -110,7 +117,7 @@
|
||||
(if-let [opts (prepare-oidc-opts cfg)]
|
||||
(do
|
||||
(l/info :hint "provider initialized"
|
||||
:provider :oidc
|
||||
:provider "oidc"
|
||||
:method (if (:discover? opts) "discover" "manual")
|
||||
:client-id (:client-id opts)
|
||||
:client-secret (obfuscate-string (:client-secret opts))
|
||||
@@ -122,7 +129,7 @@
|
||||
:roles (:roles opts))
|
||||
opts)
|
||||
(do
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc)
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
|
||||
nil))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -144,13 +151,13 @@
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :hint "provider initialized"
|
||||
:provider :google
|
||||
:provider "google"
|
||||
:client-id (:client-id opts)
|
||||
:client-secret (obfuscate-string (:client-secret opts)))
|
||||
opts)
|
||||
|
||||
(do
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google)
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "google")
|
||||
nil)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -196,13 +203,13 @@
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :hint "provider initialized"
|
||||
:provider :github
|
||||
:provider "github"
|
||||
:client-id (:client-id opts)
|
||||
:client-secret (obfuscate-string (:client-secret opts)))
|
||||
opts)
|
||||
|
||||
(do
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github)
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "github")
|
||||
nil)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -225,14 +232,14 @@
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :hint "provider initialized"
|
||||
:provider :gitlab
|
||||
:provider "gitlab"
|
||||
:base-uri base
|
||||
:client-id (:client-id opts)
|
||||
:client-secret (obfuscate-string (:client-secret opts)))
|
||||
opts)
|
||||
|
||||
(do
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab)
|
||||
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "gitlab")
|
||||
nil)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -275,8 +282,19 @@
|
||||
"accept" "application/json"}
|
||||
:uri (:token-uri provider)
|
||||
:body (u/map->query-string params)}]
|
||||
|
||||
(l/trace :hint "request access token"
|
||||
:provider (:name provider)
|
||||
:client-id (:client-id provider)
|
||||
:client-secret (obfuscate-string (:client-secret provider))
|
||||
:grant-type (:grant_type params)
|
||||
:redirect-uri (:redirect_uri params))
|
||||
|
||||
(->> (http/req! cfg req)
|
||||
(p/map (fn [{:keys [status body] :as res}]
|
||||
(l/trace :hint "access token response"
|
||||
:status status
|
||||
:body body)
|
||||
(if (= status 200)
|
||||
(let [data (json/decode body)]
|
||||
{:token (get data :access_token)
|
||||
@@ -289,12 +307,19 @@
|
||||
(defn- retrieve-user-info
|
||||
[{:keys [provider] :as cfg} tdata]
|
||||
(letfn [(retrieve []
|
||||
(l/trace :hint "request user info"
|
||||
:uri (:user-uri provider)
|
||||
:token (obfuscate-string (:token tdata))
|
||||
:token-type (:type tdata))
|
||||
(http/req! cfg
|
||||
{:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get}))
|
||||
(validate-response [response]
|
||||
(l/trace :hint "user info response"
|
||||
:status (:status response)
|
||||
:body (:body response))
|
||||
(when-not (s/int-in-range? 200 300 (:status response))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
@@ -309,7 +334,7 @@
|
||||
(if-let [get-email-fn (:get-email-fn provider)]
|
||||
(get-email-fn tdata info)
|
||||
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
||||
(get info attr-kw))))
|
||||
(p/resolved (get info attr-kw)))))
|
||||
|
||||
(get-name [info]
|
||||
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
||||
@@ -325,6 +350,7 @@
|
||||
(qualify-props provider))}))
|
||||
|
||||
(validate-info [info]
|
||||
(l/trace :hint "authentication info" :info info)
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
||||
:info (pr-str info))
|
||||
@@ -334,10 +360,10 @@
|
||||
:info info))
|
||||
info)]
|
||||
|
||||
(-> (retrieve)
|
||||
(p/then validate-response)
|
||||
(p/then process-response)
|
||||
(p/then validate-info))))
|
||||
(->> (retrieve)
|
||||
(p/fmap validate-response)
|
||||
(p/mcat process-response)
|
||||
(p/fmap validate-info))))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(s/def ::email ::us/not-empty-string)
|
||||
@@ -434,12 +460,11 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(when-let [collector (::audit/collector cfg)]
|
||||
(audit/submit! collector {:type "command"
|
||||
:name "login"
|
||||
:profile-id (:id profile)
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props (audit/profile->props profile)}))
|
||||
(audit/submit! cfg {:type "command"
|
||||
:name "login-with-password"
|
||||
:profile-id (:id profile)
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props (audit/profile->props profile)})
|
||||
|
||||
(->> (redirect-response uri)
|
||||
(sxf request)))
|
||||
|
||||
@@ -167,7 +167,11 @@
|
||||
(instance? javax.sql.DataSource v))
|
||||
|
||||
(s/def ::pool pool?)
|
||||
(s/def ::conn some?)
|
||||
|
||||
;; DEPRECATED: to be removed in 1.18
|
||||
(s/def ::conn-or-pool some?)
|
||||
(s/def ::pool-or-conn some?)
|
||||
|
||||
(defn closed?
|
||||
[pool]
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.loggers.audit.tasks :as-alias tasks]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.retry :as rtry]
|
||||
@@ -30,7 +29,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[yetti.request :as yrq]))
|
||||
|
||||
@@ -77,28 +75,20 @@
|
||||
(merge (:props profile))
|
||||
(d/without-nils)))
|
||||
|
||||
(defn clean-props
|
||||
[{:keys [profile-id] :as event}]
|
||||
(let [invalid-keys #{:session-id
|
||||
:password
|
||||
:old-password
|
||||
:token}
|
||||
xform (comp
|
||||
(remove (fn [kv]
|
||||
(qualified-keyword? (first kv))))
|
||||
(remove (fn [kv]
|
||||
(contains? invalid-keys (first kv))))
|
||||
(remove (fn [[k v]]
|
||||
(and (= k :profile-id)
|
||||
(= v profile-id))))
|
||||
(filter (fn [[_ v]]
|
||||
(or (string? v)
|
||||
(keyword? v)
|
||||
(uuid? v)
|
||||
(boolean? v)
|
||||
(number? v)))))]
|
||||
(def reserved-props
|
||||
#{:session-id
|
||||
:password
|
||||
:old-password
|
||||
:token})
|
||||
|
||||
(update event :props #(into {} xform %))))
|
||||
(defn clean-props
|
||||
[props]
|
||||
(into {}
|
||||
(comp
|
||||
(d/without-nils)
|
||||
(d/without-qualified)
|
||||
(remove #(contains? reserved-props (key %))))
|
||||
props))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -132,7 +122,7 @@
|
||||
(s/keys :req [::wrk/executor ::db/pool]))
|
||||
|
||||
(defmethod ig/pre-init-spec ::collector [_]
|
||||
(s/keys :req [::db/pool ::wrk/executor ::mtx/metrics]))
|
||||
(s/keys :req [::db/pool ::wrk/executor]))
|
||||
|
||||
(defmethod ig/init-key ::collector
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
@@ -143,8 +133,8 @@
|
||||
:else
|
||||
cfg))
|
||||
|
||||
(defn- persist-event!
|
||||
[pool event]
|
||||
(defn- handle-event!
|
||||
[conn-or-pool event]
|
||||
(us/verify! ::event event)
|
||||
(let [params {:id (uuid/next)
|
||||
:name (:name event)
|
||||
@@ -161,7 +151,7 @@
|
||||
::rtry/max-retries 6
|
||||
::rtry/label "persist-audit-log-event"}
|
||||
(let [now (dt/now)]
|
||||
(db/insert! pool :audit-log
|
||||
(db/insert! conn-or-pool :audit-log
|
||||
(-> params
|
||||
(update :props db/tjson)
|
||||
(update :ip-addr db/inet)
|
||||
@@ -180,7 +170,7 @@
|
||||
:else label)
|
||||
dedupe? (boolean (and batch-key batch-timeout))]
|
||||
|
||||
(wrk/submit! ::wrk/conn pool
|
||||
(wrk/submit! ::wrk/conn conn-or-pool
|
||||
::wrk/task :process-webhook-event
|
||||
::wrk/queue :webhooks
|
||||
::wrk/max-retries 0
|
||||
@@ -191,16 +181,19 @@
|
||||
::webhooks/event
|
||||
(-> params
|
||||
(dissoc :ip-addr)
|
||||
(dissoc :type)))))))
|
||||
(dissoc :type)))))
|
||||
params))
|
||||
|
||||
(defn submit!
|
||||
"Submit audit event to the collector."
|
||||
[{:keys [::wrk/executor ::db/pool] :as collector} params]
|
||||
(us/assert! ::collector collector)
|
||||
(->> (px/submit! executor (partial persist-event! pool (d/without-nils params)))
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "audit: unexpected error processing event" :cause cause)
|
||||
(p/resolved nil)))))
|
||||
[{:keys [::wrk/executor] :as cfg} params]
|
||||
(let [conn (or (::db/conn cfg) (::db/pool cfg))]
|
||||
(us/assert! ::wrk/executor executor)
|
||||
(us/assert! ::db/pool-or-conn conn)
|
||||
(try
|
||||
(handle-event! conn (d/without-nils params))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "audit: unexpected error processing event" :cause cause)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TASK: ARCHIVE
|
||||
@@ -247,7 +240,7 @@
|
||||
from audit_log
|
||||
where archived_at is null
|
||||
order by created_at asc
|
||||
limit 256
|
||||
limit 128
|
||||
for update skip locked;")
|
||||
|
||||
(defn archive-events
|
||||
@@ -323,7 +316,7 @@
|
||||
where archived_at is not null")
|
||||
|
||||
(defn- clean-archived
|
||||
[{:keys [pool]}]
|
||||
[{:keys [::db/pool]}]
|
||||
(let [result (db/exec-one! pool [sql:clean-archived])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :hint "delete archived audit log entries" :deleted result)
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.db :as-alias db]
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.session :as-alias http.session]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.audit.tasks :as-alias audit.tasks]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.loggers.zmq :as-alias lzmq]
|
||||
@@ -268,10 +267,8 @@
|
||||
:github (ig/ref ::oidc.providers/github)
|
||||
:gitlab (ig/ref ::oidc.providers/gitlab)
|
||||
:oidc (ig/ref ::oidc.providers/generic)}
|
||||
::audit/collector (ig/ref ::audit/collector)
|
||||
::http.session/session (ig/ref :app.http.session/manager)}
|
||||
|
||||
|
||||
;; TODO: revisit the dependencies of this service, looks they are too much unused of them
|
||||
:app.http/router
|
||||
{:assets (ig/ref :app.http.assets/handlers)
|
||||
@@ -324,8 +321,7 @@
|
||||
:scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
||||
|
||||
:app.rpc/methods
|
||||
{::audit/collector (ig/ref ::audit/collector)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::props (ig/ref :app.setup/props)
|
||||
@@ -423,11 +419,6 @@
|
||||
::lzmq/receiver
|
||||
{}
|
||||
|
||||
::audit/collector
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
::audit.tasks/archive
|
||||
{::props (ig/ref :app.setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
(ns app.rpc
|
||||
(:require
|
||||
[app.auth.ldap :as-alias ldap]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.client :as-alias http.client]
|
||||
@@ -164,7 +164,8 @@
|
||||
|
||||
(defn- wrap-audit
|
||||
[cfg f mdata]
|
||||
(if-let [collector (::audit/collector cfg)]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
(contains? cf/flags :audit-log))
|
||||
(letfn [(handle-audit [params result]
|
||||
(let [resultm (meta result)
|
||||
request (::http/request params)
|
||||
@@ -181,8 +182,7 @@
|
||||
(merge (::audit/props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
(d/without-qualified)
|
||||
(d/without-nils))
|
||||
(audit/clean-props))
|
||||
|
||||
event {:type (or (::audit/type resultm)
|
||||
(::type cfg))
|
||||
@@ -210,13 +210,14 @@
|
||||
(::webhooks/event? resultm)
|
||||
false)}]
|
||||
|
||||
(audit/submit! collector event)))
|
||||
(audit/submit! cfg event)))
|
||||
|
||||
(handle-request [cfg params]
|
||||
(->> (f cfg params)
|
||||
(p/mcat (fn [result]
|
||||
(->> (handle-audit params result)
|
||||
(p/map (constantly result)))))))]
|
||||
(p/fnly (fn [result cause]
|
||||
(when-not cause
|
||||
(handle-audit params result))))))]
|
||||
|
||||
(if-not (::audit/skip mdata)
|
||||
(with-meta handle-request mdata)
|
||||
f))
|
||||
@@ -316,8 +317,7 @@
|
||||
(s/def ::sprops map?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::methods [_]
|
||||
(s/keys :req [::audit/collector
|
||||
::http.client/client
|
||||
(s/keys :req [::http.client/client
|
||||
::db/pool
|
||||
::ldap/provider
|
||||
::wrk/executor]
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [conn session] :as cfg} {:keys [token] :as params}]
|
||||
[{:keys [::db/conn session] :as cfg} {:keys [token] :as params}]
|
||||
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
|
||||
params (merge params claims)
|
||||
|
||||
@@ -372,11 +372,10 @@
|
||||
;; accordingly.
|
||||
(when-let [id (:profile-id claims)]
|
||||
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||
(when-let [collector (::audit/collector cfg)]
|
||||
(audit/submit! collector
|
||||
{:type "fact"
|
||||
:name "register-profile-retry"
|
||||
:profile-id id})))
|
||||
(audit/submit! cfg
|
||||
{:type "fact"
|
||||
:name "register-profile-retry"
|
||||
:profile-id id}))
|
||||
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
@@ -428,7 +427,7 @@
|
||||
::doc/added "1.15"}
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg :conn conn)
|
||||
(-> (assoc cfg ::db/conn conn)
|
||||
(register-profile params))))
|
||||
|
||||
;; ---- COMMAND: Request Profile Recovery
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
(library-summary [{:keys [id data] :as file}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
{:components (assets-sample (:components data) 4)
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
:typographies (assets-sample (:typographies data) 3)}))]
|
||||
|
||||
@@ -995,7 +996,8 @@
|
||||
:opt-un [::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
@@ -1025,7 +1027,8 @@
|
||||
(sv/defmethod ::upsert-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -473,7 +473,7 @@
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::member-id ::us/uuid)
|
||||
;; Temporarily disabled viewer role
|
||||
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
||||
;; https://tree.taiga.io/project/penpot/issue/1083
|
||||
;; (s/def ::role #{:owner :admin :editor :viewer})
|
||||
(s/def ::role #{:owner :admin :editor})
|
||||
|
||||
@@ -638,10 +638,11 @@
|
||||
;; --- Mutation: Create Team Invitation
|
||||
|
||||
(def sql:upsert-team-invitation
|
||||
"insert into team_invitation(team_id, email_to, role, valid_until)
|
||||
values (?, ?, ?, ?)
|
||||
"insert into team_invitation(id, team_id, email_to, role, valid_until)
|
||||
values (?, ?, ?, ?, ?)
|
||||
on conflict(team_id, email_to) do
|
||||
update set role = ?, updated_at = now();")
|
||||
update set role = ?, valid_until = ?, updated_at = now()
|
||||
returning *")
|
||||
|
||||
(defn- create-invitation-token
|
||||
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
||||
@@ -662,16 +663,8 @@
|
||||
:exp (dt/in-future {:days 30})}))
|
||||
|
||||
(defn- create-invitation
|
||||
[{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||
(let [member (profile/retrieve-profile-data-by-email conn email)
|
||||
expire (dt/in-future "168h") ;; 7 days
|
||||
itoken (create-invitation-token cfg {:profile-id (:id profile)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
:member-email (or (:email member) email)
|
||||
:member-id (:id member)
|
||||
:role role})
|
||||
ptoken (create-profile-identity-token cfg profile)]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||
(let [member (profile/retrieve-profile-data-by-email conn email)]
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
@@ -686,9 +679,6 @@
|
||||
:email email
|
||||
:hint "the email you invite has been repeatedly reported as spam or bounce"))
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/trace :hint "invitation token" :token itoken))
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
;; already present in the database, we proceed to add it to the
|
||||
;; team as-is, without email roundtrip.
|
||||
@@ -709,10 +699,38 @@
|
||||
(when-not (:is-active member)
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id member)})))
|
||||
(do
|
||||
(db/exec-one! conn [sql:upsert-team-invitation
|
||||
(:id team) (str/lower email) (name role) expire (name role)])
|
||||
{:id (:id member)}))
|
||||
|
||||
nil)
|
||||
(let [id (uuid/next)
|
||||
expire (dt/in-future "168h") ;; 7 days
|
||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||
(:id team) (str/lower email)
|
||||
(name role) expire
|
||||
(name role) expire])
|
||||
updated? (not= id (:id invitation))
|
||||
tprops {:profile-id (:id profile)
|
||||
:invitation-id (:id invitation)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
:member-email (:email-to invitation)
|
||||
:member-id (:id member)
|
||||
:role role}
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile)]
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
|
||||
(audit/submit! cfg
|
||||
{:type "action"
|
||||
:name (if updated?
|
||||
"update-team-invitation"
|
||||
"create-team-invitation")
|
||||
:profile-id (:id profile)
|
||||
:props (-> (dissoc tprops :profile-id)
|
||||
(d/without-nils))})
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (cf/get :public-uri)
|
||||
@@ -720,9 +738,9 @@
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})))
|
||||
:extra-data ptoken})
|
||||
|
||||
itoken))
|
||||
itoken))))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::emails ::us/set-of-valid-emails)
|
||||
@@ -763,14 +781,14 @@
|
||||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(let [cfg (assoc cfg ::conn conn)
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
invitations (->> emails
|
||||
(map (fn [email]
|
||||
{:email (str/lower email)
|
||||
:team team
|
||||
:profile profile
|
||||
:role role}))
|
||||
(map (partial create-invitation cfg)))]
|
||||
(keep (partial create-invitation cfg)))]
|
||||
(with-meta (vec invitations)
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
|
||||
@@ -786,9 +804,10 @@
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
(let [params (assoc params :profile-id profile-id)
|
||||
team (create-team conn params)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
cfg (assoc cfg ::conn conn)]
|
||||
cfg (assoc cfg ::db/conn conn)]
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(->> emails
|
||||
@@ -811,18 +830,16 @@
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(-> team
|
||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
||||
(rph/with-defer
|
||||
#(when-let [collector (::audit/collector cfg)]
|
||||
(audit/submit! collector
|
||||
{:type "command"
|
||||
:name "create-team-invitations"
|
||||
:profile-id profile-id
|
||||
:props {:emails emails
|
||||
:role role
|
||||
:profile-id profile-id
|
||||
:invitations (count emails)}})))))))
|
||||
(audit/submit! cfg
|
||||
{:type "command"
|
||||
:name "create-team-invitations"
|
||||
:profile-id profile-id
|
||||
:props {:emails emails
|
||||
:role role
|
||||
:profile-id profile-id
|
||||
:invitations (count emails)}})
|
||||
|
||||
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
|
||||
|
||||
;; --- Query: get-team-invitation-token
|
||||
|
||||
@@ -838,7 +855,7 @@
|
||||
{:team-id team-id
|
||||
:email-to (str/lower email)})
|
||||
(update :role keyword))
|
||||
member (profile/retrieve-profile-data-by-email pool (:email invit))
|
||||
member (profile/retrieve-profile-data-by-email pool (:email-to invit))
|
||||
token (create-invitation-token cfg {:team-id (:team-id invit)
|
||||
:profile-id profile-id
|
||||
:valid-until (:valid-until invit)
|
||||
@@ -884,6 +901,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(db/delete! conn :team-invitation
|
||||
{:team-id team-id :email-to (str/lower email)})
|
||||
nil)))
|
||||
(let [invitation (db/delete! conn :team-invitation
|
||||
{:team-id team-id
|
||||
:email-to (str/lower email)})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
:opt-un [::spec.team-invitation/member-id]))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [conn session] :as cfg}
|
||||
[{:keys [conn] :as cfg}
|
||||
{:keys [::rpc/profile-id token]}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
@@ -152,45 +152,30 @@
|
||||
(if (some? profile)
|
||||
(if (or (= member-id profile-id)
|
||||
(= member-email (:email profile)))
|
||||
;; if we have logged-in user and it matches the invitation we
|
||||
;; proceed with accepting the invitation and joining the
|
||||
;; current profile to the invited team.
|
||||
|
||||
;; if we have logged-in user and it matches the invitation we proceed
|
||||
;; with accepting the invitation and joining the current profile to the
|
||||
;; invited team.
|
||||
(let [profile (accept-invitation cfg claims invitation profile)]
|
||||
(-> (assoc claims :state :created)
|
||||
(rph/with-meta {::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id profile-id})))
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/props {:team-id (:team-id claims)
|
||||
:role (:role claims)
|
||||
:invitation-id (:id invitation)}})))
|
||||
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token
|
||||
:hint "logged-in user does not matches the invitation"))
|
||||
|
||||
;; If we have not logged-in user, we try find the invited
|
||||
;; profile by member-id or member-email props of the invitation
|
||||
;; token; If profile is found, we accept the invitation and
|
||||
;; leave the user logged-in.
|
||||
(if-let [member (db/get* conn :profile
|
||||
(if member-id
|
||||
{:id member-id}
|
||||
{:email member-email})
|
||||
{:columns [:id :email]})]
|
||||
(let [profile (accept-invitation cfg claims invitation member)]
|
||||
(-> (assoc claims :state :created)
|
||||
(rph/with-transform (session/create-fn session (:id profile)))
|
||||
(rph/with-meta {::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id member-id})))
|
||||
;; If we have not logged-in user, and invitation comes with member-id we
|
||||
;; redirect user to login, if no memeber-id is present in the invitation
|
||||
;; token, we redirect user the the register page.
|
||||
|
||||
{:invitation-token token
|
||||
:iss :team-invitation
|
||||
:redirect-to :auth-register
|
||||
:state :pending}))))
|
||||
{:invitation-token token
|
||||
:iss :team-invitation
|
||||
:redirect-to (if member-id :auth-login :auth-register)
|
||||
:state :pending})))
|
||||
|
||||
;; --- Default
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
@@ -22,10 +23,15 @@
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [uri] :as row}]
|
||||
(cond-> row
|
||||
(string? uri) (assoc :uri (u/uri uri))))
|
||||
|
||||
;; --- Mutation: Create Webhook
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::is-active ::us/boolean)
|
||||
(s/def ::mtype
|
||||
#{"application/json"
|
||||
@@ -59,7 +65,7 @@
|
||||
|
||||
(if (not= (:uri whook) (:uri params))
|
||||
(->> (http/req! cfg {:method :head
|
||||
:uri (:uri params)
|
||||
:uri (str (:uri params))
|
||||
:timeout (dt/duration "3s")})
|
||||
(p/hmap (fn [response exception]
|
||||
(if exception
|
||||
@@ -79,22 +85,24 @@
|
||||
|
||||
(defn- insert-webhook!
|
||||
[{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}]
|
||||
(db/insert! pool :webhook
|
||||
{:id (uuid/next)
|
||||
:team-id team-id
|
||||
:uri uri
|
||||
:is-active is-active
|
||||
:mtype mtype}))
|
||||
(-> (db/insert! pool :webhook
|
||||
{:id (uuid/next)
|
||||
:team-id team-id
|
||||
:uri (str uri)
|
||||
:is-active is-active
|
||||
:mtype mtype})
|
||||
(decode-row)))
|
||||
|
||||
(defn- update-webhook!
|
||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as wook} {:keys [uri mtype is-active] :as params}]
|
||||
(db/update! pool :webhook
|
||||
{:uri uri
|
||||
:is-active is-active
|
||||
:mtype mtype
|
||||
:error-code nil
|
||||
:error-count 0}
|
||||
{:id id}))
|
||||
(-> (db/update! pool :webhook
|
||||
{:uri (str uri)
|
||||
:is-active is-active
|
||||
:mtype mtype
|
||||
:error-code nil
|
||||
:error-count 0}
|
||||
{:id id})
|
||||
(decode-row)))
|
||||
|
||||
(sv/defmethod ::create-webhook
|
||||
{::doc/added "1.17"}
|
||||
@@ -110,7 +118,7 @@
|
||||
(sv/defmethod ::update-webhook
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [whook (db/get pool :webhook {:id id})]
|
||||
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
||||
(check-edition-permissions! pool profile-id (:team-id whook))
|
||||
(->> (validate-webhook! cfg whook params)
|
||||
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
|
||||
@@ -123,7 +131,7 @@
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [whook (db/get conn :webhook {:id id})]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(check-edition-permissions! conn profile-id (:team-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil)))
|
||||
@@ -143,4 +151,5 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(db/exec! conn [sql:get-webhooks team-id])))
|
||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||
(mapv decode-row))))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files :as cmd.files]
|
||||
[app.rpc.commands.files.create :as cmd.files.create]
|
||||
@@ -176,7 +176,8 @@
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.13"
|
||||
::doc/deprecated "1.17"}
|
||||
::doc/deprecated "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
||||
@@ -192,7 +193,8 @@
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.13"
|
||||
::doc/deprecated "1.17"}
|
||||
::doc/deprecated "1.17"
|
||||
::audit/skip true}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -41,3 +41,35 @@
|
||||
([file state]
|
||||
(repair-orphaned-shapes (:data file))
|
||||
(update state :total (fnil inc 0))))
|
||||
|
||||
(defn rename-layout-attrs
|
||||
([file]
|
||||
(let [found? (volatile! false)]
|
||||
(letfn [(update-shape
|
||||
[shape]
|
||||
(when (or (= (:layout-flex-dir shape) :reverse-row)
|
||||
(= (:layout-flex-dir shape) :reverse-column)
|
||||
(= (:layout-wrap-type shape) :no-wrap))
|
||||
(vreset! found? true))
|
||||
(cond-> shape
|
||||
(= (:layout-flex-dir shape) :reverse-row)
|
||||
(assoc :layout-flex-dir :row-reverse)
|
||||
(= (:layout-flex-dir shape) :reverse-column)
|
||||
(assoc :layout-flex-dir :column-reverse)
|
||||
(= (:layout-wrap-type shape) :no-wrap)
|
||||
(assoc :layout-wrap-type :nowrap)))
|
||||
|
||||
(update-page
|
||||
[page]
|
||||
(h/update-shapes page update-shape))]
|
||||
|
||||
(let [new-file (update file :data h/update-pages update-page)]
|
||||
(when @found?
|
||||
(l/info :hint "Found attrs to rename in file"
|
||||
:id (:id file)
|
||||
:name (:name file)))
|
||||
new-file))))
|
||||
|
||||
([file state]
|
||||
(rename-layout-attrs file)
|
||||
(update state :total (fnil inc 0))))
|
||||
@@ -154,7 +154,7 @@
|
||||
(t/is (th/success? out))
|
||||
(let [[thread :as result] (:result out)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= "Page-1" (:page-name thread)))
|
||||
(t/is (= "Page 1" (:page-name thread)))
|
||||
(t/is (= "hello world" (:content thread)))
|
||||
(t/is (= 2 (:count-comments thread)))
|
||||
(t/is (true? (:is-resolved thread))))))
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
(t/deftest accept-invitation-tokens
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
profile3 (th/create-profile* 3 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
@@ -181,25 +182,29 @@
|
||||
:member-email (:email profile2)
|
||||
:member-id (:id profile2)})]
|
||||
|
||||
;; --- Verify token as anonymous user
|
||||
(t/testing "Verify token as anonymous user"
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
(let [data {::th/type :verify-token :token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
|
||||
(let [data {::th/type :verify-token :token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= :created (:state result)))
|
||||
(t/is (= (:email profile2) (:member-email result)))
|
||||
(t/is (= (:id profile2) (:member-id result))))
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :invitation-token))
|
||||
(t/is (contains? result :iss))
|
||||
(t/is (contains? result :redirect-to))
|
||||
(t/is (contains? result :state))
|
||||
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 2 (count rows)))))
|
||||
(t/is (= :pending (:state result)))
|
||||
(t/is (= :auth-login (:redirect-to result))))
|
||||
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 1 (count rows))))))
|
||||
|
||||
;; Clean members
|
||||
(db/delete! pool :team-profile-rel
|
||||
@@ -207,46 +212,37 @@
|
||||
:profile-id (:id profile2)})
|
||||
|
||||
|
||||
;; --- Verify token as logged-in user
|
||||
(t/testing "Verify token as logged-in user"
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile2)
|
||||
:token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= :created (:state result)))
|
||||
(t/is (= (:email profile2) (:member-email result)))
|
||||
(t/is (= (:id profile2) (:member-id result))))
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 2 (count rows))))))
|
||||
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile2)
|
||||
:token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= :created (:state result)))
|
||||
(t/is (= (:email profile2) (:member-email result)))
|
||||
(t/is (= (:id profile2) (:member-id result))))
|
||||
(t/testing "Verify token as logged-in wrong user"
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile3)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 2 (count rows)))))
|
||||
|
||||
|
||||
;; --- Verify token as logged-in wrong user
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile1)
|
||||
:token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :invalid-token (:code edata)))))
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile1)
|
||||
:token token}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :invalid-token (:code edata))))))
|
||||
|
||||
)))
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
(ns backend-tests.rpc-webhooks-test
|
||||
(:require
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.storage :as sto]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
@@ -31,7 +32,7 @@
|
||||
(let [params {::th/type :create-webhook
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:uri "http://example.com"
|
||||
:uri (u/uri "http://example.com")
|
||||
:mtype "application/json"}
|
||||
out (th/command! params)]
|
||||
|
||||
|
||||
@@ -203,19 +203,22 @@
|
||||
([coll value]
|
||||
(sequence (replace-by-id value) coll)))
|
||||
|
||||
(defn without-nils
|
||||
"Given a map, return a map removing key-value
|
||||
pairs when value is `nil`."
|
||||
[data]
|
||||
(into {} (remove (comp nil? second)) data))
|
||||
|
||||
(defn vec-without-nils
|
||||
[coll]
|
||||
(into [] (remove nil?) coll))
|
||||
|
||||
(defn without-nils
|
||||
"Given a map, return a map removing key-value
|
||||
pairs when value is `nil`."
|
||||
([] (remove (comp nil? val)))
|
||||
([data]
|
||||
(into {} (without-nils) data)))
|
||||
|
||||
(defn without-qualified
|
||||
[data]
|
||||
(into {} (remove (comp qualified-keyword? first)) data))
|
||||
([]
|
||||
(remove (comp qualified-keyword? key)))
|
||||
([data]
|
||||
(into {} (without-qualified) data)))
|
||||
|
||||
(defn without-keys
|
||||
"Return a map without the keys provided
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
|
||||
(assert (nil? (:current-component-id file)))
|
||||
(let [page-id (or (:id data) (uuid/next))
|
||||
page (-> (ctp/make-empty-page page-id "Page-1")
|
||||
page (-> (ctp/make-empty-page page-id "Page 1")
|
||||
(d/deep-merge data))]
|
||||
(-> file
|
||||
(commit-change
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
(defn bounding-box
|
||||
"Returns a rect that wraps the shape after all transformations applied."
|
||||
[shape]
|
||||
; TODO: perhaps we need to store this calculation in a shape attribute
|
||||
;; TODO: perhaps we need to store this calculation in a shape attribute
|
||||
(gpr/points->rect (:points shape)))
|
||||
|
||||
(defn left-bound
|
||||
"Returns the lowest x coord of the shape BEFORE applying transformations."
|
||||
; TODO: perhaps some day we want after transformations, but for the
|
||||
; moment it's enough as is now.
|
||||
;; TODO: perhaps some day we want after transformations, but for the
|
||||
;; moment it's enough as is now.
|
||||
[shape]
|
||||
(or (:x shape) (:x (:selrect shape)))) ; Paths don't have :x attribute
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
|
||||
([attr val1 val2 precision]
|
||||
(let [close-val? (fn [num1 num2]
|
||||
(when (and (number? num1) (number? num2))
|
||||
(< (mth/abs (- num1 num2)) precision)))]
|
||||
(when (and (number? num1) (number? num2))
|
||||
(< (mth/abs (- num1 num2)) precision)))]
|
||||
(cond
|
||||
(and (number? val1) (number? val2))
|
||||
(close-val? val1 val2)
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
;; after-vec will contain the side length of the grown side
|
||||
;; we scale the shape by the diference and translate it by the start
|
||||
;; displacement (so its left+top position is constant)
|
||||
scale (/ (gpt/length after-vec) (gpt/length before-vec))
|
||||
scale (/ (gpt/length after-vec) (max 0.01 (gpt/length before-vec)))
|
||||
|
||||
resize-origin (gpo/origin child-points-after)
|
||||
|
||||
@@ -268,11 +268,11 @@
|
||||
|
||||
scale-x (if (= :scale constraints-h)
|
||||
1
|
||||
(/ (gpo/width-points child-bb-before) (gpo/width-points child-bb-after)))
|
||||
(/ (gpo/width-points child-bb-before) (max 0.01 (gpo/width-points child-bb-after))))
|
||||
|
||||
scale-y (if (= :scale constraints-v)
|
||||
1
|
||||
(/ (gpo/height-points child-bb-before) (gpo/height-points child-bb-after)))
|
||||
(/ (gpo/height-points child-bb-before) (max 0.01 (gpo/height-points child-bb-after))))
|
||||
|
||||
resize-vector (gpt/point scale-x scale-y)
|
||||
resize-origin (gpo/origin transformed-child-bounds)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.geom.shapes.flex-layout.modifiers :as fmo]))
|
||||
|
||||
(dm/export fbo/layout-content-bounds)
|
||||
(dm/export fbo/layout-content-points)
|
||||
(dm/export fbo/child-layout-bound-points)
|
||||
(dm/export fdr/get-drop-index)
|
||||
(dm/export fdr/get-drop-areas)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.geom.shapes.flex-layout.bounds
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.points :as gpo]
|
||||
[app.common.types.shape.layout :as ctl]))
|
||||
@@ -27,16 +28,19 @@
|
||||
h-center? (ctl/h-center? parent)
|
||||
h-end? (ctl/h-end? parent)
|
||||
|
||||
fill-w? (ctl/fill-width? child)
|
||||
fill-h? (ctl/fill-height? child)
|
||||
|
||||
base-p (gpo/origin child-bounds)
|
||||
|
||||
width (gpo/width-points child-bounds)
|
||||
height (gpo/height-points child-bounds)
|
||||
|
||||
min-width (if (ctl/fill-width? child)
|
||||
min-width (if fill-w?
|
||||
(ctl/child-min-width child)
|
||||
width)
|
||||
|
||||
min-height (if (ctl/fill-height? child)
|
||||
min-height (if fill-h?
|
||||
(ctl/child-min-height child)
|
||||
height)
|
||||
|
||||
@@ -60,26 +64,60 @@
|
||||
min-width (max min-width 0.01)
|
||||
min-height (max min-height 0.01)]
|
||||
|
||||
(-> [base-p]
|
||||
(conj (cond-> base-p
|
||||
(or row? h-start?)
|
||||
(gpt/add (hv min-width))
|
||||
(cond-> [base-p
|
||||
(gpt/add base-p (hv 0.01))
|
||||
(gpt/add base-p (vv 0.01))]
|
||||
|
||||
(and col? h-center?)
|
||||
(gpt/add (hv (/ min-width 2)))
|
||||
col?
|
||||
(conj (gpt/add base-p (vv min-height)))
|
||||
|
||||
(and col? h-center?)
|
||||
(gpt/subtract (hv min-width))))
|
||||
row?
|
||||
(conj (gpt/add base-p (hv min-width)))
|
||||
|
||||
(conj (cond-> base-p
|
||||
(or col? v-start?)
|
||||
(gpt/add (vv min-height))
|
||||
(and col? h-start?)
|
||||
(conj (gpt/add base-p (hv min-width)))
|
||||
|
||||
(and row? v-center?)
|
||||
(gpt/add (vv (/ min-height 2)))
|
||||
(and col? h-center?)
|
||||
(conj (gpt/add base-p (hv (/ min-width 2)))
|
||||
(gpt/subtract base-p (hv (/ min-width 2))))
|
||||
|
||||
(and row? v-end?)
|
||||
(gpt/subtract (vv min-height)))))))
|
||||
(and col? h-end?)
|
||||
(conj (gpt/subtract base-p (hv min-width)))
|
||||
|
||||
(and row? v-start?)
|
||||
(conj (gpt/add base-p (vv min-height)))
|
||||
|
||||
(and row? v-center?)
|
||||
(conj (gpt/add base-p (vv (/ min-height 2)))
|
||||
(gpt/subtract base-p (vv (/ min-height 2))))
|
||||
|
||||
(and row? v-end?)
|
||||
(conj (gpt/subtract base-p (vv min-height))))))
|
||||
|
||||
(defn layout-content-points
|
||||
[bounds parent children]
|
||||
|
||||
(let [parent-id (:id parent)
|
||||
parent-bounds @(get bounds parent-id)
|
||||
get-child-bounds
|
||||
(fn [child]
|
||||
(let [child-id (:id child)
|
||||
child-bounds @(get bounds child-id)
|
||||
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)
|
||||
|
||||
child-bounds
|
||||
(if (or (ctl/fill-width? child) (ctl/fill-height? child))
|
||||
(child-layout-bound-points parent child parent-bounds child-bounds)
|
||||
child-bounds)
|
||||
|
||||
child-bounds
|
||||
(when (d/not-empty? child-bounds)
|
||||
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
|
||||
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left))))]
|
||||
|
||||
child-bounds))]
|
||||
|
||||
(->> children (map get-child-bounds))))
|
||||
|
||||
(defn layout-content-bounds
|
||||
[bounds {:keys [layout-padding] :as parent} children]
|
||||
@@ -87,27 +125,37 @@
|
||||
(let [parent-id (:id parent)
|
||||
parent-bounds @(get bounds parent-id)
|
||||
|
||||
row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
space-around? (ctl/space-around? parent)
|
||||
space-evenly? (ctl/space-evenly? parent)
|
||||
content-evenly? (ctl/content-evenly? parent)
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
row-pad (if (or (and col? space-evenly?)
|
||||
(and col? space-around?)
|
||||
(and row? content-evenly?))
|
||||
layout-gap-row
|
||||
0)
|
||||
|
||||
col-pad (if (or(and row? space-evenly?)
|
||||
(and row? space-around?)
|
||||
(and col? content-evenly?))
|
||||
layout-gap-col
|
||||
0)
|
||||
|
||||
{pad-top :p1 pad-right :p2 pad-bottom :p3 pad-left :p4} layout-padding
|
||||
pad-top (or pad-top 0)
|
||||
pad-right (or pad-right 0)
|
||||
pad-bottom (or pad-bottom 0)
|
||||
pad-left (or pad-left 0)
|
||||
pad-top (+ (or pad-top 0) row-pad)
|
||||
pad-right (+ (or pad-right 0) col-pad)
|
||||
pad-bottom (+ (or pad-bottom 0) row-pad)
|
||||
pad-left (+ (or pad-left 0) col-pad)
|
||||
|
||||
child-bounds
|
||||
(fn [child]
|
||||
(let [child-id (:id child)
|
||||
child-bounds @(get bounds child-id)
|
||||
child-bounds
|
||||
(if (or (ctl/fill-height? child) (ctl/fill-height? child))
|
||||
(child-layout-bound-points parent child parent-bounds child-bounds)
|
||||
child-bounds)
|
||||
|
||||
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)]
|
||||
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
|
||||
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left)))))]
|
||||
|
||||
(as-> children $
|
||||
(map child-bounds $)
|
||||
(gpo/merge-parent-coords-bounds $ parent-bounds)
|
||||
(gpo/pad-points $ (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))))
|
||||
layout-points
|
||||
(layout-content-points bounds parent children)]
|
||||
|
||||
(if (d/not-empty? layout-points)
|
||||
(-> layout-points
|
||||
(gpo/merge-parent-coords-bounds parent-bounds)
|
||||
(gpo/pad-points (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))
|
||||
;; Cannot create some bounds from the children so we return the parent's
|
||||
parent-bounds)))
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
"Calculates the lines basic data and accumulated values. The positions will be calculated in a different operation"
|
||||
[shape children layout-bounds]
|
||||
|
||||
(let [col? (ctl/col? shape)
|
||||
row? (ctl/row? shape)
|
||||
(let [col? (ctl/col? shape)
|
||||
row? (ctl/row? shape)
|
||||
space-around? (ctl/space-around? shape)
|
||||
space-evenly? (ctl/space-evenly? shape)
|
||||
|
||||
wrap? (and (ctl/wrap? shape)
|
||||
(or col? (not (ctl/auto-width? shape)))
|
||||
@@ -77,8 +79,28 @@
|
||||
next-max-width (+ child-margin-width (if fill-width? child-max-width child-width))
|
||||
next-max-height (+ child-margin-height (if fill-height? child-max-height child-height))
|
||||
|
||||
next-line-min-width (+ line-min-width next-min-width (* layout-gap-col num-children))
|
||||
next-line-min-height (+ line-min-height next-min-height (* layout-gap-row num-children))]
|
||||
total-gap-col (cond
|
||||
space-evenly?
|
||||
(* layout-gap-col (+ num-children 2))
|
||||
|
||||
space-around?
|
||||
(* layout-gap-col (+ num-children 1))
|
||||
|
||||
:else
|
||||
(* layout-gap-col num-children))
|
||||
|
||||
total-gap-row (cond
|
||||
space-evenly?
|
||||
(* layout-gap-row (+ num-children 2))
|
||||
|
||||
space-around?
|
||||
(* layout-gap-row (+ num-children 1))
|
||||
|
||||
:else
|
||||
(* layout-gap-row num-children))
|
||||
|
||||
next-line-min-width (+ line-min-width next-min-width total-gap-col)
|
||||
next-line-min-height (+ line-min-height next-min-height total-gap-row)]
|
||||
|
||||
(if (and (some? line-data)
|
||||
(or (not wrap?)
|
||||
@@ -139,8 +161,12 @@
|
||||
(defn add-lines-positions
|
||||
[parent layout-bounds layout-lines]
|
||||
|
||||
(let [row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
(let [row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
space-evenly? (ctl/space-evenly? parent)
|
||||
space-around? (ctl/space-around? parent)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
@@ -168,48 +194,101 @@
|
||||
(let [[total-min-width total-min-height total-max-width total-max-height]
|
||||
(->> layout-lines (reduce add-ranges [0 0 0 0]))
|
||||
|
||||
get-layout-width (fn [{:keys [num-children]}] (- layout-width (* layout-gap-col (dec num-children))))
|
||||
get-layout-height (fn [{:keys [num-children]}] (- layout-height (* layout-gap-row (dec num-children))))
|
||||
get-layout-width (fn [{:keys [num-children]}]
|
||||
(let [num-gap (cond
|
||||
space-evenly?
|
||||
(inc num-children)
|
||||
|
||||
space-around?
|
||||
num-children
|
||||
|
||||
:else
|
||||
(dec num-children))]
|
||||
(- layout-width (* layout-gap-col num-gap))))
|
||||
get-layout-height (fn [{:keys [num-children]}]
|
||||
(let [num-gap (cond
|
||||
space-evenly?
|
||||
(inc num-children)
|
||||
|
||||
space-around?
|
||||
num-children
|
||||
|
||||
:else
|
||||
(dec num-children))]
|
||||
(- layout-height (* layout-gap-row num-gap))))
|
||||
|
||||
num-lines (count layout-lines)
|
||||
|
||||
;; When align-items is stretch we need to adjust the main axis size to grow for the full content
|
||||
stretch-width-fix
|
||||
(if (and col? (ctl/content-stretch? parent))
|
||||
(if (and col? (ctl/content-stretch? parent) (not auto-width?))
|
||||
(/ (- layout-width (* layout-gap-col (dec num-lines)) total-max-width) num-lines)
|
||||
0)
|
||||
|
||||
stretch-height-fix
|
||||
(if (and row? (ctl/content-stretch? parent))
|
||||
(if (and row? (ctl/content-stretch? parent) (not auto-height?))
|
||||
(/ (- layout-height (* layout-gap-row (dec num-lines)) total-max-height) num-lines)
|
||||
0)
|
||||
|
||||
rest-layout-height (- layout-height (* (dec num-lines) layout-gap-row))
|
||||
rest-layout-width (- layout-width (* (dec num-lines) layout-gap-col))
|
||||
|
||||
;; Distributes the space between the layout lines based on its max/min constraints
|
||||
layout-lines
|
||||
(cond->> layout-lines
|
||||
row?
|
||||
(map #(assoc % :line-width (max (:line-min-width %) (min (get-layout-width %) (:line-max-width %)))))
|
||||
(map #(assoc % :line-width
|
||||
(if (ctl/auto-width? parent)
|
||||
(:line-min-width %)
|
||||
(max (:line-min-width %) (min (get-layout-width %) (:line-max-width %))))))
|
||||
|
||||
col?
|
||||
(map #(assoc % :line-height (max (:line-min-height %) (min (get-layout-height %) (:line-max-height %)))))
|
||||
(map #(assoc % :line-height
|
||||
(if (ctl/auto-height? parent)
|
||||
(:line-min-height %)
|
||||
(max (:line-min-height %) (min (get-layout-height %) (:line-max-height %))))))
|
||||
|
||||
(and row? (>= total-min-height layout-height))
|
||||
(and row? (or (>= total-min-height rest-layout-height) (ctl/auto-height? parent)))
|
||||
(map #(assoc % :line-height (:line-min-height %)))
|
||||
|
||||
(and row? (<= total-max-height layout-height))
|
||||
(and row? (<= total-max-height rest-layout-height) (not (ctl/auto-height? parent)))
|
||||
(map #(assoc % :line-height (+ (:line-max-height %) stretch-height-fix)))
|
||||
|
||||
(and row? (< total-min-height layout-height total-max-height))
|
||||
(distribute-space :line-height :line-min-height :line-max-height total-min-height (- layout-height (* (dec num-lines) layout-gap-row)))
|
||||
(and row? (< total-min-height rest-layout-height total-max-height) (not (ctl/auto-height? parent)))
|
||||
(distribute-space :line-height :line-min-height :line-max-height total-min-height rest-layout-height)
|
||||
|
||||
(and col? (>= total-min-width layout-width))
|
||||
(and col? (or (>= total-min-width rest-layout-width) (ctl/auto-width? parent)))
|
||||
(map #(assoc % :line-width (:line-min-width %)))
|
||||
|
||||
(and col? (<= total-max-width layout-width))
|
||||
(and col? (<= total-max-width rest-layout-width) (not (ctl/auto-width? parent)))
|
||||
(map #(assoc % :line-width (+ (:line-max-width %) stretch-width-fix)))
|
||||
|
||||
(and col? (< total-min-width layout-width total-max-width))
|
||||
(distribute-space :line-width :line-min-width :line-max-width total-min-width (- layout-width (* (dec num-lines) layout-gap-col))))
|
||||
(and col? (< total-min-width rest-layout-width total-max-width) (not (ctl/auto-width? parent)))
|
||||
(distribute-space :line-width :line-min-width :line-max-width total-min-width rest-layout-width))
|
||||
|
||||
;; Add information to limit the growth of width: 100% shapes to the bounds of the layout
|
||||
layout-lines
|
||||
(cond
|
||||
row?
|
||||
(->> layout-lines
|
||||
(reduce
|
||||
(fn [[result rest-layout-height] {:keys [line-height] :as line}]
|
||||
[(conj result (assoc line :to-bound-height rest-layout-height))
|
||||
(- rest-layout-height line-height layout-gap-row)])
|
||||
[[] layout-height])
|
||||
(first))
|
||||
|
||||
col?
|
||||
(->> layout-lines
|
||||
(reduce
|
||||
(fn [[result rest-layout-width] {:keys [line-width] :as line}]
|
||||
[(conj result (assoc line :to-bound-width rest-layout-width))
|
||||
(- rest-layout-width line-width layout-gap-col)])
|
||||
[[] layout-width])
|
||||
(first))
|
||||
|
||||
:else
|
||||
layout-lines)
|
||||
|
||||
[total-width total-height] (->> layout-lines (reduce add-lines [0 0]))
|
||||
|
||||
@@ -226,40 +305,71 @@
|
||||
|
||||
row? (ctl/row? shape)
|
||||
col? (ctl/col? shape)
|
||||
auto-height? (ctl/auto-height? shape)
|
||||
auto-width? (ctl/auto-width? shape)
|
||||
space-between? (ctl/space-between? shape)
|
||||
space-evenly? (ctl/space-evenly? shape)
|
||||
space-around? (ctl/space-around? shape)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps shape)
|
||||
|
||||
margin-x
|
||||
(cond (and row? space-evenly? (not auto-width?))
|
||||
(max layout-gap-col (/ (- width line-width) (inc num-children)))
|
||||
|
||||
(and row? space-around? (not auto-width?))
|
||||
(/ (max layout-gap-col (/ (- width line-width) num-children)) 2)
|
||||
|
||||
(and row? (or space-evenly? space-around?) auto-width?)
|
||||
layout-gap-col
|
||||
|
||||
:else
|
||||
0)
|
||||
|
||||
margin-y
|
||||
(cond (and col? space-evenly? (not auto-height?))
|
||||
(max layout-gap-row (/ (- height line-height) (inc num-children)))
|
||||
|
||||
(and col? space-around? (not auto-height?))
|
||||
(/ (max layout-gap-row (/ (- height line-height) num-children)) 2)
|
||||
|
||||
(and col? (or space-evenly? space-around?) auto-height?)
|
||||
layout-gap-row
|
||||
|
||||
:else
|
||||
0)
|
||||
|
||||
layout-gap-col
|
||||
(cond (and row? space-around?)
|
||||
(cond (and row? space-evenly?)
|
||||
0
|
||||
|
||||
(and row? space-between?)
|
||||
(/ (- width line-width) (dec num-children))
|
||||
(and row? space-around? auto-width?)
|
||||
0
|
||||
|
||||
(and row? space-around?)
|
||||
(/ (max layout-gap-col (/ (- width line-width) num-children)) 2)
|
||||
|
||||
(and row? space-between? (not auto-width?))
|
||||
(max layout-gap-col (/ (- width line-width) (dec num-children)))
|
||||
|
||||
:else
|
||||
layout-gap-col)
|
||||
|
||||
layout-gap-row
|
||||
(cond (and col? space-around?)
|
||||
(cond (and col? space-evenly?)
|
||||
0
|
||||
|
||||
(and col? space-between?)
|
||||
(/ (- height line-height) (dec num-children))
|
||||
(and col? space-evenly? auto-height?)
|
||||
0
|
||||
|
||||
(and col? space-around?)
|
||||
(/ (max layout-gap-row (/ (- height line-height) num-children)) 2)
|
||||
|
||||
(and col? space-between? (not auto-height?))
|
||||
(max layout-gap-row (/ (- height line-height) (dec num-children)))
|
||||
|
||||
:else
|
||||
layout-gap-row)
|
||||
|
||||
margin-x
|
||||
(if (and row? space-around?)
|
||||
(/ (- width line-width) (inc num-children))
|
||||
0)
|
||||
|
||||
margin-y
|
||||
(if (and col? space-around?)
|
||||
(/ (- height line-height) (inc num-children))
|
||||
0)]
|
||||
layout-gap-row)]
|
||||
(assoc line-data
|
||||
:layout-bounds layout-bounds
|
||||
:layout-gap-row layout-gap-row
|
||||
@@ -308,4 +418,3 @@
|
||||
{:layout-lines layout-lines
|
||||
:layout-bounds layout-bounds
|
||||
:reverse? reverse?}))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
transform-inverse
|
||||
child
|
||||
child-origin child-width
|
||||
{:keys [children-data line-width] :as layout-data}]
|
||||
{:keys [children-data line-width to-bound-width] :as layout-data}]
|
||||
|
||||
(cond
|
||||
(ctl/row? parent)
|
||||
@@ -30,8 +30,9 @@
|
||||
:modifiers (ctm/resize-modifiers (gpt/point fill-scale 1) child-origin transform transform-inverse)})
|
||||
|
||||
(ctl/col? parent)
|
||||
(let [target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
|
||||
max-width (ctl/child-max-width child)
|
||||
(let [line-width (min line-width (or to-bound-width line-width))
|
||||
target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
|
||||
max-width (max (ctl/child-max-width child) 0.01)
|
||||
target-width (min max-width target-width)
|
||||
fill-scale (/ target-width child-width)]
|
||||
{:width target-width
|
||||
@@ -43,7 +44,7 @@
|
||||
transform transform-inverse
|
||||
child
|
||||
child-origin child-height
|
||||
{:keys [children-data line-height] :as layout-data}]
|
||||
{:keys [children-data line-height to-bound-height] :as layout-data}]
|
||||
|
||||
(cond
|
||||
(ctl/col? parent)
|
||||
@@ -53,8 +54,9 @@
|
||||
:modifiers (ctm/resize-modifiers (gpt/point 1 fill-scale) child-origin transform transform-inverse)})
|
||||
|
||||
(ctl/row? parent)
|
||||
(let [target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
|
||||
max-height (ctl/child-max-height child)
|
||||
(let [line-height (min line-height (or to-bound-height line-height))
|
||||
target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
|
||||
max-height (max (ctl/child-max-height child) 0.01)
|
||||
target-height (min max-height target-height)
|
||||
fill-scale (/ target-height child-height)]
|
||||
{:height target-height
|
||||
@@ -68,11 +70,16 @@
|
||||
child-height (gpo/height-points child-bounds)
|
||||
|
||||
[_ transform transform-inverse]
|
||||
(when (or (ctl/fill-width? child) (ctl/fill-width? child))
|
||||
(when (or (ctl/fill-width? child) (ctl/fill-height? child))
|
||||
(gtr/calculate-geometry @parent-bounds))
|
||||
|
||||
fill-width (when (ctl/fill-width? child) (calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
|
||||
fill-height (when (ctl/fill-height? child) (calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
|
||||
fill-width
|
||||
(when (ctl/fill-width? child)
|
||||
(calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
|
||||
|
||||
fill-height
|
||||
(when (ctl/fill-height? child)
|
||||
(calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
|
||||
|
||||
child-width (or (:width fill-width) child-width)
|
||||
child-height (or (:height fill-height) child-height)
|
||||
|
||||
@@ -20,9 +20,14 @@
|
||||
hv (partial gpo/start-hv layout-bounds)
|
||||
vv (partial gpo/start-vv layout-bounds)
|
||||
|
||||
end? (ctl/content-end? parent)
|
||||
center? (ctl/content-center? parent)
|
||||
around? (ctl/content-around? parent)
|
||||
wrap? (ctl/wrap? parent)
|
||||
|
||||
end? (or (and wrap? (ctl/content-end? parent))
|
||||
(and (not wrap?) (ctl/align-items-end? parent)))
|
||||
center? (or (and wrap? (ctl/content-center? parent))
|
||||
(and (not wrap?) (ctl/align-items-center? parent)))
|
||||
around? (and wrap? (ctl/content-around? parent))
|
||||
evenly? (and wrap? (ctl/content-evenly? parent))
|
||||
|
||||
;; Adjust the totals so it takes into account the gaps
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
@@ -43,7 +48,10 @@
|
||||
(gpt/add (vv free-height-gap))
|
||||
|
||||
around?
|
||||
(gpt/add (vv (/ free-height (inc num-lines)))))
|
||||
(gpt/add (vv (max lines-gap-row (/ free-height num-lines 2))))
|
||||
|
||||
evenly?
|
||||
(gpt/add (vv (max lines-gap-row (/ free-height (inc num-lines))))))
|
||||
|
||||
col?
|
||||
(cond-> center?
|
||||
@@ -53,7 +61,10 @@
|
||||
(gpt/add (hv free-width-gap))
|
||||
|
||||
around?
|
||||
(gpt/add (hv (/ free-width (inc num-lines))))))))
|
||||
(gpt/add (hv (max lines-gap-col (/ free-width num-lines) 2)))
|
||||
|
||||
evenly?
|
||||
(gpt/add (hv (max lines-gap-col (/ free-width (inc num-lines)))))))))
|
||||
|
||||
(defn get-next-line
|
||||
[parent layout-bounds {:keys [line-width line-height]} base-p total-width total-height num-lines]
|
||||
@@ -63,6 +74,9 @@
|
||||
row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
hv #(gpo/start-hv layout-bounds %)
|
||||
@@ -71,12 +85,16 @@
|
||||
stretch? (ctl/content-stretch? parent)
|
||||
between? (ctl/content-between? parent)
|
||||
around? (ctl/content-around? parent)
|
||||
evenly? (ctl/content-evenly? parent)
|
||||
|
||||
free-width (- layout-width total-width)
|
||||
free-height (- layout-height total-height)
|
||||
|
||||
line-gap-row
|
||||
line-gap-col
|
||||
(cond
|
||||
auto-width?
|
||||
layout-gap-col
|
||||
|
||||
stretch?
|
||||
(/ free-width num-lines)
|
||||
|
||||
@@ -84,13 +102,19 @@
|
||||
(/ free-width (dec num-lines))
|
||||
|
||||
around?
|
||||
(/ free-width num-lines)
|
||||
|
||||
evenly?
|
||||
(/ free-width (inc num-lines))
|
||||
|
||||
:else
|
||||
layout-gap-col)
|
||||
|
||||
line-gap-col
|
||||
line-gap-row
|
||||
(cond
|
||||
auto-height?
|
||||
layout-gap-row
|
||||
|
||||
stretch?
|
||||
(/ free-height num-lines)
|
||||
|
||||
@@ -98,6 +122,9 @@
|
||||
(/ free-height (dec num-lines))
|
||||
|
||||
around?
|
||||
(/ free-height num-lines)
|
||||
|
||||
evenly?
|
||||
(/ free-height (inc num-lines))
|
||||
|
||||
:else
|
||||
@@ -105,10 +132,10 @@
|
||||
|
||||
(cond-> base-p
|
||||
row?
|
||||
(gpt/add (vv (+ line-height (max layout-gap-row line-gap-col))))
|
||||
(gpt/add (vv (+ line-height (max layout-gap-row line-gap-row))))
|
||||
|
||||
col?
|
||||
(gpt/add (hv (+ line-width (max layout-gap-col line-gap-row)))))))
|
||||
(gpt/add (hv (+ line-width (max layout-gap-col line-gap-col)))))))
|
||||
|
||||
(defn get-start-line
|
||||
"Cross axis line. It's position is fixed along the different lines"
|
||||
@@ -121,43 +148,46 @@
|
||||
col? (ctl/col? parent)
|
||||
space-between? (ctl/space-between? parent)
|
||||
space-around? (ctl/space-around? parent)
|
||||
space-evenly? (ctl/space-evenly? parent)
|
||||
h-center? (ctl/h-center? parent)
|
||||
h-end? (ctl/h-end? parent)
|
||||
v-center? (ctl/v-center? parent)
|
||||
v-end? (ctl/v-end? parent)
|
||||
content-stretch? (ctl/content-stretch? parent)
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
hv (partial gpo/start-hv layout-bounds)
|
||||
vv (partial gpo/start-vv layout-bounds)
|
||||
children-gap-width (* layout-gap-col (dec num-children))
|
||||
children-gap-height (* layout-gap-row (dec num-children))
|
||||
|
||||
line-height
|
||||
(if (and row? content-stretch?)
|
||||
(if (and row? content-stretch? (not auto-height?))
|
||||
(+ line-height (/ (- layout-height total-height) num-lines))
|
||||
line-height)
|
||||
|
||||
line-width
|
||||
(if (and col? content-stretch?)
|
||||
(if (and col? content-stretch? (not auto-width?))
|
||||
(+ line-width (/ (- layout-width total-width) num-lines))
|
||||
line-width)
|
||||
|
||||
start-p
|
||||
(cond-> base-p
|
||||
;; X AXIS
|
||||
(and row? h-center? (not space-around?) (not space-between?))
|
||||
(and row? h-center? (not space-around?) (not space-evenly?) (not space-between?))
|
||||
(-> (gpt/add (hv (/ layout-width 2)))
|
||||
(gpt/subtract (hv (/ (+ line-width children-gap-width) 2))))
|
||||
|
||||
(and row? h-end? (not space-around?) (not space-between?))
|
||||
(and row? h-end? (not space-around?) (not space-evenly?) (not space-between?))
|
||||
(-> (gpt/add (hv layout-width))
|
||||
(gpt/subtract (hv (+ line-width children-gap-width))))
|
||||
|
||||
;; Y AXIS
|
||||
(and col? v-center? (not space-around?) (not space-between?))
|
||||
(and col? v-center? (not space-around?) (not space-evenly?) (not space-between?))
|
||||
(-> (gpt/add (vv (/ layout-height 2)))
|
||||
(gpt/subtract (vv (/ (+ line-height children-gap-height) 2))))
|
||||
|
||||
(and col? v-end? (not space-around?) (not space-between?))
|
||||
(and col? v-end? (not space-around?) (not space-evenly?) (not space-between?))
|
||||
(-> (gpt/add (vv layout-height))
|
||||
(gpt/subtract (vv (+ line-height children-gap-height)))))]
|
||||
|
||||
@@ -263,7 +293,7 @@
|
||||
col?
|
||||
(-> (gpt/add (vv (+ margin-top margin-bottom)))
|
||||
(gpt/add (vv (+ child-height layout-gap-row))))
|
||||
|
||||
|
||||
(some? margin-x)
|
||||
(gpt/add (hv margin-x))
|
||||
|
||||
|
||||
@@ -363,10 +363,10 @@
|
||||
c2 (+ (* a2 (:x c)) (* b2 (:y c)))
|
||||
|
||||
;; Cramer's rule
|
||||
det (- (* a1 b2) (* a2 b1))]
|
||||
det (- (* a1 b2) (* a2 b1))
|
||||
det (if (mth/almost-zero? det) 0.001 det)
|
||||
|
||||
;; If almost zero the lines are parallel
|
||||
(when (not (mth/almost-zero? det))
|
||||
(let [x (/ (- (* b2 c1) (* b1 c2)) det)
|
||||
y (/ (- (* c2 a1) (* c1 a2)) det)]
|
||||
(gpt/point x y)))))
|
||||
x (/ (- (* b2 c1) (* b1 c2)) det)
|
||||
y (/ (- (* c2 a1) (* c1 a2)) det)]
|
||||
|
||||
(gpt/point x y)))
|
||||
|
||||
@@ -115,13 +115,15 @@
|
||||
(if (empty? children)
|
||||
modif-tree
|
||||
(let [child-id (first children)
|
||||
child (get objects child-id)
|
||||
child-bounds @(get bounds child-id)
|
||||
child-modifiers (gct/calc-child-modifiers parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds)]
|
||||
(recur (cond-> modif-tree
|
||||
(not (ctm/empty? child-modifiers))
|
||||
(update-in [child-id :modifiers] ctm/add-modifiers child-modifiers))
|
||||
(rest children)))))))))
|
||||
child (get objects child-id)]
|
||||
(if (some? child)
|
||||
(let [child-bounds @(get bounds child-id)
|
||||
child-modifiers (gct/calc-child-modifiers parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds)]
|
||||
(recur (cond-> modif-tree
|
||||
(not (ctm/empty? child-modifiers))
|
||||
(update-in [child-id :modifiers] ctm/add-modifiers child-modifiers))
|
||||
(rest children)))
|
||||
(recur modif-tree (rest children))))))))))
|
||||
|
||||
(defn get-group-bounds
|
||||
[objects bounds modif-tree shape]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.geom.shapes.points
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.intersect :as gsi]
|
||||
@@ -54,11 +55,11 @@
|
||||
|
||||
(defn width-points
|
||||
[[p0 p1 _ _]]
|
||||
(gpt/length (gpt/to-vec p0 p1)))
|
||||
(max 0.01 (gpt/length (gpt/to-vec p0 p1))))
|
||||
|
||||
(defn height-points
|
||||
[[p0 _ _ p3]]
|
||||
(gpt/length (gpt/to-vec p0 p3)))
|
||||
(max 0.01 (gpt/length (gpt/to-vec p0 p3))))
|
||||
|
||||
(defn pad-points
|
||||
[[p0 p1 p2 p3 :as points] pad-top pad-right pad-bottom pad-left]
|
||||
@@ -115,7 +116,9 @@
|
||||
(max tv-max ctv)]))
|
||||
|
||||
[th-min th-max tv-min tv-max]
|
||||
(->> child-bounds (reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
|
||||
(->> child-bounds
|
||||
(filter #(and (d/num? (:x %)) (d/num? (:y %))))
|
||||
(reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
|
||||
|
||||
minv-start (pv tv-min)
|
||||
minv-end (gpt/add minv-start hv)
|
||||
@@ -131,6 +134,7 @@
|
||||
i2 (gsi/line-line-intersect minv-start minv-end maxh-start maxh-end)
|
||||
i3 (gsi/line-line-intersect maxv-start maxv-end maxh-start maxh-end)
|
||||
i4 (gsi/line-line-intersect maxv-start maxv-end minh-start minh-end)]
|
||||
|
||||
[i1 i2 i3 i4])))
|
||||
|
||||
(defn merge-parent-coords-bounds
|
||||
@@ -143,3 +147,8 @@
|
||||
height (height-points points)
|
||||
center (gco/center-points points)]
|
||||
(gre/center->selrect center width height)))
|
||||
|
||||
(defn move
|
||||
[bounds vector]
|
||||
(->> bounds
|
||||
(map #(gpt/add % vector))))
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpa]
|
||||
[app.common.geom.shapes.rect :as gpr]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.uuid :as uuid]))
|
||||
@@ -159,67 +160,79 @@
|
||||
"Calculate the transform matrix to convert from the selrect to the points bounds
|
||||
TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
|
||||
[{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
|
||||
#?(:clj
|
||||
;; NOTE: the source matrix may not be invertible we can't
|
||||
;; calculate the transform, so on exception we return `nil`
|
||||
(ex/ignoring
|
||||
(let [target-points-matrix
|
||||
(->> (list (:x d1) (:x d2) (:x d4)
|
||||
(:y d1) (:y d2) (:y d4)
|
||||
1 1 1 )
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
;; If the coordinates are very close to zero (but not zero) the rounding can mess with the
|
||||
;; transforms. So we round to zero the values
|
||||
(let [x1 (mth/round-to-zero x1)
|
||||
y1 (mth/round-to-zero y1)
|
||||
x2 (mth/round-to-zero x2)
|
||||
y2 (mth/round-to-zero y2)
|
||||
d1x (mth/round-to-zero (:x d1))
|
||||
d1y (mth/round-to-zero (:y d1))
|
||||
d2x (mth/round-to-zero (:x d2))
|
||||
d2y (mth/round-to-zero (:y d2))
|
||||
d4x (mth/round-to-zero (:x d4))
|
||||
d4y (mth/round-to-zero (:y d4))]
|
||||
#?(:clj
|
||||
;; NOTE: the source matrix may not be invertible we can't
|
||||
;; calculate the transform, so on exception we return `nil`
|
||||
(ex/ignoring
|
||||
(let [target-points-matrix
|
||||
(->> (list d1x d2x d4x
|
||||
d1y d2y d4y
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
|
||||
source-points-matrix
|
||||
(->> (list x1 x2 x1
|
||||
y1 y1 y2
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
source-points-matrix
|
||||
(->> (list x1 x2 x1
|
||||
y1 y1 y2
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
|
||||
;; May throw an exception if the matrix is not invertible
|
||||
source-points-matrix-inv
|
||||
(.. source-points-matrix
|
||||
(withInverter LinearAlgebra/GAUSS_JORDAN)
|
||||
(inverse))
|
||||
;; May throw an exception if the matrix is not invertible
|
||||
source-points-matrix-inv
|
||||
(.. source-points-matrix
|
||||
(withInverter LinearAlgebra/GAUSS_JORDAN)
|
||||
(inverse))
|
||||
|
||||
transform-jvm
|
||||
(.. target-points-matrix
|
||||
(multiply source-points-matrix-inv))]
|
||||
transform-jvm
|
||||
(.. target-points-matrix
|
||||
(multiply source-points-matrix-inv))]
|
||||
|
||||
(gmt/matrix (.get transform-jvm 0 0)
|
||||
(.get transform-jvm 1 0)
|
||||
(.get transform-jvm 0 1)
|
||||
(.get transform-jvm 1 1)
|
||||
(.get transform-jvm 0 2)
|
||||
(.get transform-jvm 1 2))))
|
||||
(gmt/matrix (.get transform-jvm 0 0)
|
||||
(.get transform-jvm 1 0)
|
||||
(.get transform-jvm 0 1)
|
||||
(.get transform-jvm 1 1)
|
||||
(.get transform-jvm 0 2)
|
||||
(.get transform-jvm 1 2))))
|
||||
|
||||
:cljs
|
||||
(let [target-points-matrix
|
||||
(Matrix. #js [#js [(:x d1) (:x d2) (:x d4)]
|
||||
#js [(:y d1) (:y d2) (:y d4)]
|
||||
#js [ 1 1 1]])
|
||||
:cljs
|
||||
(let [target-points-matrix
|
||||
(Matrix. #js [#js [d1x d2x d4x]
|
||||
#js [d1y d2y d4y]
|
||||
#js [ 1 1 1]])
|
||||
|
||||
source-points-matrix
|
||||
(Matrix. #js [#js [x1 x2 x1]
|
||||
#js [y1 y1 y2]
|
||||
#js [ 1 1 1]])
|
||||
source-points-matrix
|
||||
(Matrix. #js [#js [x1 x2 x1]
|
||||
#js [y1 y1 y2]
|
||||
#js [ 1 1 1]])
|
||||
|
||||
;; returns nil if not invertible
|
||||
source-points-matrix-inv (.getInverse source-points-matrix)
|
||||
;; returns nil if not invertible
|
||||
source-points-matrix-inv (.getInverse source-points-matrix)
|
||||
|
||||
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
|
||||
transform-js
|
||||
(when source-points-matrix-inv
|
||||
(.multiply target-points-matrix source-points-matrix-inv))]
|
||||
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
|
||||
transform-js
|
||||
(when source-points-matrix-inv
|
||||
(.multiply target-points-matrix source-points-matrix-inv))]
|
||||
|
||||
(when transform-js
|
||||
(gmt/matrix (.getValueAt transform-js 0 0)
|
||||
(.getValueAt transform-js 1 0)
|
||||
(.getValueAt transform-js 0 1)
|
||||
(.getValueAt transform-js 1 1)
|
||||
(.getValueAt transform-js 0 2)
|
||||
(.getValueAt transform-js 1 2))))))
|
||||
(when transform-js
|
||||
(gmt/matrix (.getValueAt transform-js 0 0)
|
||||
(.getValueAt transform-js 1 0)
|
||||
(.getValueAt transform-js 0 1)
|
||||
(.getValueAt transform-js 1 1)
|
||||
(.getValueAt transform-js 0 2)
|
||||
(.getValueAt transform-js 1 2)))))))
|
||||
|
||||
(defn calculate-geometry
|
||||
[points]
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
(defn abs
|
||||
[v]
|
||||
#?(:cljs (js/Math.abs v)
|
||||
:clj (Math/abs v)))
|
||||
:clj (Math/abs (double v))))
|
||||
|
||||
(defn sin
|
||||
"Returns the sine of a number"
|
||||
@@ -174,6 +174,13 @@
|
||||
(defn almost-zero? [num]
|
||||
(< (abs (double num)) 1e-4))
|
||||
|
||||
(defn round-to-zero
|
||||
"Given a number if it's close enough to zero round to the zero to avoid precision problems"
|
||||
[num]
|
||||
(if (almost-zero? num)
|
||||
0
|
||||
num))
|
||||
|
||||
(defonce float-equal-precision 0.001)
|
||||
|
||||
(defn close?
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def valid-font-types #{"font/ttf" "font/woff", "application/font-woff", "font/otf"})
|
||||
;; We have added ".ttf" as string to solve a problem with chrome input selector
|
||||
(def valid-font-types #{"font/ttf", ".ttf", "font/woff", "application/font-woff", "font/otf"})
|
||||
(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
|
||||
(def str-image-types (str/join "," valid-image-types))
|
||||
(def str-font-types (str/join "," valid-font-types))
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
(dm/export common/file-version)
|
||||
(dm/export common/default-color)
|
||||
(dm/export common/component-sync-attrs)
|
||||
(dm/export common/retrieve-used-names)
|
||||
(dm/export common/generate-unique-name)
|
||||
|
||||
;; Focus
|
||||
(dm/export focus/focus-objects)
|
||||
|
||||
@@ -387,6 +387,10 @@
|
||||
is-geometry? (and (or (= group :geometry-group)
|
||||
(and (= group :content-group) (= (:type shape) :path)))
|
||||
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
|
||||
;; TODO: the check of :width and :height probably may be removed
|
||||
;; after the check added in data/workspace/modifiers/check-delta
|
||||
;; function. Better check it and test toroughly when activating
|
||||
;; components-v2 mode.
|
||||
shape-ref (:shape-ref shape)
|
||||
root-name? (and (= group :name-group)
|
||||
(:component-root? shape))
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
(ns app.common.pages.common
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.uuid :as uuid]))
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(def file-version 19)
|
||||
(def file-version 20)
|
||||
(def default-color clr/gray-20)
|
||||
(def root uuid/zero)
|
||||
|
||||
@@ -580,3 +583,31 @@
|
||||
:layout-item-min-w
|
||||
:layout-item-align-self}})
|
||||
|
||||
(defn retrieve-used-names
|
||||
"Return a set with the all unique names used in the
|
||||
elements (any entity thas has a :name)"
|
||||
[elements]
|
||||
(into #{} (comp (map :name) (remove nil?)) (vals elements)))
|
||||
|
||||
(defn- extract-numeric-suffix
|
||||
[basename]
|
||||
(if-let [[_ p1 p2] (re-find #"(.*) ([0-9]+)$" basename)]
|
||||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
|
||||
(s/def ::set-of-strings
|
||||
(s/every ::us/string :kind set?))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
(us/assert! ::set-of-strings used)
|
||||
(us/assert! ::us/string basename)
|
||||
(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))))))
|
||||
|
||||
@@ -109,6 +109,20 @@
|
||||
(recur (conj result parent-id) parent-id)
|
||||
result))))
|
||||
|
||||
(defn get-parent-ids-with-index
|
||||
"Returns a tuple with the list of parents and a map with the position within each parent"
|
||||
[objects shape-id]
|
||||
(loop [parent-list []
|
||||
parent-indices {}
|
||||
current shape-id]
|
||||
(let [parent-id (dm/get-in objects [current :parent-id])
|
||||
parent (get objects parent-id)]
|
||||
(if (and (some? parent) (not= parent-id current))
|
||||
(let [parent-list (conj parent-list parent-id)
|
||||
parent-indices (assoc parent-indices parent-id (d/index-of (:shapes parent) current))]
|
||||
(recur parent-list parent-indices parent-id))
|
||||
[parent-list parent-indices]))))
|
||||
|
||||
(defn get-siblings-ids
|
||||
[objects id]
|
||||
(let [parent (get-parent objects id)]
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
(ns app.common.pages.migrations
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[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.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
@@ -23,13 +24,15 @@
|
||||
|
||||
(defmulti migrate :version)
|
||||
|
||||
(log/set-level! :info)
|
||||
|
||||
(defn migrate-data
|
||||
([data] (migrate-data data cp/file-version))
|
||||
([data to-version]
|
||||
(if (= (:version data) to-version)
|
||||
data
|
||||
(let [migrate-fn #(do
|
||||
(l/trace :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2))
|
||||
(log/trace :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2))
|
||||
(migrate (assoc %1 :version (inc %2))))]
|
||||
(reduce migrate-fn data (range (:version data 0) to-version))))))
|
||||
|
||||
@@ -427,5 +430,31 @@
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(defmethod migrate 20
|
||||
[data]
|
||||
(letfn [(update-object [objects object]
|
||||
(let [frame-id (:frame-id object)
|
||||
calculated-frame-id
|
||||
(or (->> (cph/get-parent-ids objects (:id object))
|
||||
(map (d/getf objects))
|
||||
(d/seek cph/frame-shape?)
|
||||
:id)
|
||||
;; If we cannot find any we let the frame-id as it was before
|
||||
frame-id)]
|
||||
(when (not= frame-id calculated-frame-id)
|
||||
(log/info :hint "Fix wrong frame-id"
|
||||
:shape (:name object)
|
||||
:id (:id object)
|
||||
:current (dm/get-in objects [frame-id :name])
|
||||
:calculated (get-in objects [calculated-frame-id :name])))
|
||||
(assoc object :frame-id calculated-frame-id)))
|
||||
|
||||
(update-container [container]
|
||||
(update container :objects #(update-vals % (partial update-object %))))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
;; TODO: pending to do a migration for delete already not used fill
|
||||
;; and stroke props. This should be done for >1.14.x version.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages.common :as common]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -130,7 +131,7 @@
|
||||
delta (gpt/subtract position orig-pos)
|
||||
|
||||
objects (:objects container)
|
||||
unames (volatile! (ctst/retrieve-used-names objects))
|
||||
unames (volatile! (common/retrieve-used-names objects))
|
||||
|
||||
frame-id (ctst/frame-id-by-position objects (gpt/add orig-pos delta))
|
||||
frame-ids-map (volatile! {})
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
([file-id page-id]
|
||||
(let [page (when (some? page-id)
|
||||
(ctp/make-empty-page page-id "Page-1"))]
|
||||
(ctp/make-empty-page page-id "Page 1"))]
|
||||
(cond-> (-> empty-file-data
|
||||
(assoc :id file-id))
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; :layout ;; :flex, :grid in the future
|
||||
;; :layout-flex-dir ;; :row, :reverse-row, :column, :reverse-column
|
||||
;; :layout-flex-dir ;; :row, :row-reverse, :column, :column-reverse
|
||||
;; :layout-gap-type ;; :simple, :multiple
|
||||
;; :layout-gap ;; {:row-gap number , :column-gap number}
|
||||
;; :layout-align-items ;; :start :end :center :stretch
|
||||
;; :layout-justify-content ;; :start :center :end :space-between :space-around
|
||||
;; :layout-align-content ;; :start :center :end :space-between :space-around :stretch (by default)
|
||||
;; :layout-wrap-type ;; :wrap, :no-wrap
|
||||
;; :layout-justify-content ;; :start :center :end :space-between :space-around :space-evenly
|
||||
;; :layout-align-content ;; :start :center :end :space-between :space-around :space-evenly :stretch (by default)
|
||||
;; :layout-wrap-type ;; :wrap, :nowrap
|
||||
;; :layout-padding-type ;; :simple, :multiple
|
||||
;; :layout-padding ;; {:p1 num :p2 num :p3 num :p4 num} number could be negative
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
;; :layout-item-min-w ;; num
|
||||
|
||||
(s/def ::layout #{:flex :grid})
|
||||
(s/def ::layout-flex-dir #{:row :reverse-row :column :reverse-column})
|
||||
(s/def ::layout-flex-dir #{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) ;;TODO remove reverse-column and reverse-row after script
|
||||
(s/def ::layout-gap-type #{:simple :multiple})
|
||||
(s/def ::layout-gap ::us/safe-number)
|
||||
(s/def ::layout-align-items #{:start :end :center :stretch})
|
||||
(s/def ::layout-align-content #{:start :end :center :space-between :space-around :stretch})
|
||||
(s/def ::layout-justify-content #{:start :center :end :space-between :space-around})
|
||||
(s/def ::layout-wrap-type #{:wrap :no-wrap})
|
||||
(s/def ::layout-align-content #{:start :end :center :space-between :space-around :space-evenly :stretch})
|
||||
(s/def ::layout-justify-content #{:start :center :end :space-between :space-around :space-evenly})
|
||||
(s/def ::layout-wrap-type #{:wrap :nowrap :no-wrap}) ;;TODO remove no-wrap after script
|
||||
(s/def ::layout-padding-type #{:simple :multiple})
|
||||
|
||||
(s/def ::p1 ::us/safe-number)
|
||||
@@ -134,25 +134,41 @@
|
||||
(defn wrap? [{:keys [layout-wrap-type]}]
|
||||
(= layout-wrap-type :wrap))
|
||||
|
||||
(defn fill-width? [child]
|
||||
(= :fill (:layout-item-h-sizing child)))
|
||||
(defn fill-width?
|
||||
([objects id]
|
||||
(= :fill (dm/get-in objects [id :layout-item-h-sizing])))
|
||||
([child]
|
||||
(= :fill (:layout-item-h-sizing child))))
|
||||
|
||||
(defn fill-height? [child]
|
||||
(= :fill (:layout-item-v-sizing child)))
|
||||
(defn fill-height?
|
||||
([objects id]
|
||||
(= :fill (dm/get-in objects [id :layout-item-v-sizing])))
|
||||
([child]
|
||||
(= :fill (:layout-item-v-sizing child))))
|
||||
|
||||
(defn auto-width? [child]
|
||||
(= :auto (:layout-item-h-sizing child)))
|
||||
(defn auto-width?
|
||||
([objects id]
|
||||
(= :auto (dm/get-in objects [id :layout-item-h-sizing])))
|
||||
([child]
|
||||
(= :auto (:layout-item-h-sizing child))))
|
||||
|
||||
(defn auto-height? [child]
|
||||
(= :auto (:layout-item-v-sizing child)))
|
||||
(defn auto-height?
|
||||
([objects id]
|
||||
(= :auto (dm/get-in objects [id :layout-item-v-sizing])))
|
||||
([child]
|
||||
(= :auto (:layout-item-v-sizing child))))
|
||||
|
||||
(defn col?
|
||||
[{:keys [layout-flex-dir]}]
|
||||
(or (= :column layout-flex-dir) (= :reverse-column layout-flex-dir)))
|
||||
([objects id]
|
||||
(col? (get objects id)))
|
||||
([{:keys [layout-flex-dir]}]
|
||||
(or (= :column layout-flex-dir) (= :column-reverse layout-flex-dir))))
|
||||
|
||||
(defn row?
|
||||
[{:keys [layout-flex-dir]}]
|
||||
(or (= :row layout-flex-dir) (= :reverse-row layout-flex-dir)))
|
||||
([objects id]
|
||||
(row? (get objects id)))
|
||||
([{:keys [layout-flex-dir]}]
|
||||
(or (= :row layout-flex-dir) (= :row-reverse layout-flex-dir))))
|
||||
|
||||
(defn gaps
|
||||
[{:keys [layout-gap]}]
|
||||
@@ -270,16 +286,35 @@
|
||||
[{:keys [layout-align-content]}]
|
||||
(= :space-around layout-align-content))
|
||||
|
||||
(defn content-evenly?
|
||||
[{:keys [layout-align-content]}]
|
||||
(= :space-evenly layout-align-content))
|
||||
|
||||
(defn content-stretch?
|
||||
[{:keys [layout-align-content]}]
|
||||
(or (= :stretch layout-align-content)
|
||||
(nil? layout-align-content)))
|
||||
|
||||
(defn align-items-center?
|
||||
[{:keys [layout-align-items]}]
|
||||
(= layout-align-items :center))
|
||||
|
||||
(defn align-items-start?
|
||||
[{:keys [layout-align-items]}]
|
||||
(= layout-align-items :start))
|
||||
|
||||
(defn align-items-end?
|
||||
[{:keys [layout-align-items]}]
|
||||
(= layout-align-items :end))
|
||||
|
||||
(defn align-items-stretch?
|
||||
[{:keys [layout-align-items]}]
|
||||
(= layout-align-items :stretch))
|
||||
|
||||
(defn reverse?
|
||||
[{:keys [layout-flex-dir]}]
|
||||
(or (= :reverse-row layout-flex-dir)
|
||||
(= :reverse-column layout-flex-dir)))
|
||||
(or (= :row-reverse layout-flex-dir)
|
||||
(= :column-reverse layout-flex-dir)))
|
||||
|
||||
(defn space-between?
|
||||
[{:keys [layout-justify-content]}]
|
||||
@@ -289,6 +324,10 @@
|
||||
[{:keys [layout-justify-content]}]
|
||||
(= layout-justify-content :space-around))
|
||||
|
||||
(defn space-evenly?
|
||||
[{:keys [layout-justify-content]}]
|
||||
(= layout-justify-content :space-evenly))
|
||||
|
||||
(defn align-self-start? [{:keys [layout-item-align-self]}]
|
||||
(= :start layout-item-align-self))
|
||||
|
||||
@@ -300,3 +339,21 @@
|
||||
|
||||
(defn align-self-stretch? [{:keys [layout-item-align-self]}]
|
||||
(= :stretch layout-item-align-self))
|
||||
|
||||
(defn change-h-sizing?
|
||||
[frame-id objects children-ids]
|
||||
(and (layout? objects frame-id)
|
||||
(auto-width? objects frame-id)
|
||||
(or (and (col? objects frame-id)
|
||||
(every? (partial fill-width? objects) children-ids))
|
||||
(and (row? objects frame-id)
|
||||
(some (partial fill-width? objects) children-ids)))))
|
||||
|
||||
(defn change-v-sizing?
|
||||
[frame-id objects children-ids]
|
||||
(and (layout? objects frame-id)
|
||||
(auto-height? objects frame-id)
|
||||
(or (and (col? objects frame-id)
|
||||
(some (partial fill-height? objects) children-ids))
|
||||
(and (row? objects frame-id)
|
||||
(every? (partial fill-height? objects) children-ids)))))
|
||||
|
||||
@@ -132,45 +132,34 @@
|
||||
(defn get-base
|
||||
[objects id-a id-b]
|
||||
|
||||
(let [parents-a (reverse (cons id-a (cph/get-parent-ids objects id-a)))
|
||||
parents-b (reverse (cons id-b (cph/get-parent-ids objects id-b)))
|
||||
(let [[parents-a parents-a-index] (cph/get-parent-ids-with-index objects id-a)
|
||||
[parents-b parents-b-index] (cph/get-parent-ids-with-index objects id-b)
|
||||
|
||||
[base base-child-a base-child-b]
|
||||
(loop [parents-a (rest parents-a)
|
||||
parents-b (rest parents-b)
|
||||
base uuid/zero]
|
||||
(cond
|
||||
(not= (first parents-a) (first parents-b))
|
||||
[base (first parents-a) (first parents-b)]
|
||||
parents-a (cons id-a parents-a)
|
||||
parents-b (into #{id-b} parents-b)
|
||||
|
||||
(or (empty? parents-a) (empty? parents-b))
|
||||
[uuid/zero (first parents-a) (first parents-b)]
|
||||
;; Search for the common frame in order
|
||||
base (or (d/seek parents-b parents-a) uuid/zero)
|
||||
|
||||
:else
|
||||
(recur (rest parents-a) (rest parents-b) (first parents-a))))
|
||||
idx-a (get parents-a-index base)
|
||||
idx-b (get parents-b-index base)]
|
||||
|
||||
index-base-a (when base-child-a (cph/get-position-on-parent objects base-child-a))
|
||||
index-base-b (when base-child-b (cph/get-position-on-parent objects base-child-b))]
|
||||
|
||||
[base index-base-a index-base-b]))
|
||||
[base idx-a idx-b]))
|
||||
|
||||
(defn is-shape-over-shape?
|
||||
[objects base-shape-id over-shape-id {:keys [top-frames?]}]
|
||||
[objects base-shape-id over-shape-id]
|
||||
|
||||
(let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)]
|
||||
(cond
|
||||
;; The base the base shape, so the other item is bellow
|
||||
(= base base-shape-id)
|
||||
(and (not top-frames?)
|
||||
(let [object (get objects base-shape-id)]
|
||||
(or (cph/frame-shape? object)
|
||||
(cph/root-frame? object))))
|
||||
false
|
||||
|
||||
;; The base is the testing over, so it's over
|
||||
(= base over-shape-id)
|
||||
(or top-frames?
|
||||
(let [object (get objects over-shape-id)]
|
||||
(or (not (cph/frame-shape? object))
|
||||
(not (cph/root-frame? object)))))
|
||||
true
|
||||
|
||||
;; Check which index is lower
|
||||
:else
|
||||
(< index-a index-b))))
|
||||
|
||||
@@ -183,20 +172,20 @@
|
||||
(let [type-a (dm/get-in objects [id-a :type])
|
||||
type-b (dm/get-in objects [id-b :type])]
|
||||
(cond
|
||||
(and (= :frame type-a) (not= :frame type-b))
|
||||
(if bottom-frames? 1 -1)
|
||||
|
||||
(and (not= :frame type-a) (= :frame type-b))
|
||||
(if bottom-frames? -1 1)
|
||||
|
||||
(and (= :frame type-a) (not= :frame type-b))
|
||||
(if bottom-frames? 1 -1)
|
||||
|
||||
(= id-a id-b)
|
||||
0
|
||||
|
||||
(is-shape-over-shape? objects id-a id-b options)
|
||||
1
|
||||
(is-shape-over-shape? objects id-b id-a)
|
||||
-1
|
||||
|
||||
:else
|
||||
-1)))]
|
||||
1)))]
|
||||
(sort comp ids))))
|
||||
|
||||
(defn frame-id-by-position
|
||||
@@ -216,9 +205,8 @@
|
||||
(defn all-frames-by-position
|
||||
[objects position]
|
||||
(->> (get-frames-ids objects)
|
||||
(sort-z-index objects)
|
||||
(filterv #(and position (gsh/has-point? (get objects %) position)))))
|
||||
|
||||
(filter #(and position (gsh/has-point? (get objects %) position)))
|
||||
(sort-z-index objects)))
|
||||
|
||||
(defn top-nested-frame
|
||||
"Search for the top nested frame for positioning shapes when moving or creating.
|
||||
@@ -268,7 +256,7 @@
|
||||
(if all-frames?
|
||||
identity
|
||||
(remove :hide-in-viewer)))
|
||||
(sort-z-index objects (get-frames-ids objects) {:top-frames? true}))))
|
||||
(sort-z-index objects (get-frames-ids objects)))))
|
||||
|
||||
(defn start-page-index
|
||||
[objects]
|
||||
@@ -286,35 +274,6 @@
|
||||
[frame]
|
||||
(not (mth/almost-zero? (:rotation frame 0))))
|
||||
|
||||
(defn retrieve-used-names
|
||||
[objects]
|
||||
(into #{} (comp (map :name) (remove nil?)) (vals objects)))
|
||||
|
||||
(defn- extract-numeric-suffix
|
||||
[basename]
|
||||
(if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
|
||||
[p1 (+ 1 (d/parse-integer p2))]
|
||||
[basename 1]))
|
||||
|
||||
(s/def ::set-of-strings
|
||||
(s/every ::us/string :kind set?))
|
||||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
(us/assert! ::set-of-strings used)
|
||||
(us/assert! ::us/string basename)
|
||||
;; We have add a condition because UX doesn't want numbers on
|
||||
;; layer names.
|
||||
(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))))))
|
||||
|
||||
(defn clone-object
|
||||
"Gets a copy of the object and all its children, with new ids
|
||||
and with the parent-children links correctly set. Admits functions
|
||||
|
||||
@@ -70,3 +70,14 @@
|
||||
remap-typography
|
||||
content)))))
|
||||
|
||||
(defn remove-external-typographies
|
||||
"Change the shape so that any use of an external typography now is removed"
|
||||
[shape file-id]
|
||||
(let [remove-ref-file #(dissoc % :typography-ref-file :typography-ref-id)]
|
||||
|
||||
(update shape :content
|
||||
(fn [content]
|
||||
(txt/transform-nodes #(not= (:typography-ref-file %) file-id)
|
||||
remove-ref-file
|
||||
content)))))
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
(fn [file-data]
|
||||
(let [id (uuid/next)
|
||||
props (merge {:id id
|
||||
:name "Color-1"
|
||||
:name "Color 1"
|
||||
:color "#000000"
|
||||
:opacity 1}
|
||||
props)]
|
||||
@@ -140,7 +140,7 @@
|
||||
(fn [file-data]
|
||||
(let [id (uuid/next)
|
||||
props (merge {:id id
|
||||
:name "Typography-1"
|
||||
:name "Typography 1"
|
||||
:font-id "sourcesanspro"
|
||||
:font-family "sourcesanspro"
|
||||
:font-size "14"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
;; false)
|
||||
|
||||
(t/is (= (count pages) 2))
|
||||
(t/is (= (:name (first pages)) "Page-1"))
|
||||
(t/is (= (:name (first pages)) "Page 1"))
|
||||
(t/is (= (:name (second pages)) "Library backup"))
|
||||
|
||||
(t/is (= (count components) 1))
|
||||
|
||||
@@ -11,6 +11,7 @@ volumes:
|
||||
postgres_data_pg15:
|
||||
user_data:
|
||||
minio_data:
|
||||
redis_data:
|
||||
|
||||
services:
|
||||
main:
|
||||
@@ -80,22 +81,6 @@ services:
|
||||
- 9000:9000
|
||||
- 9001:9001
|
||||
|
||||
# keycloak:
|
||||
# image: "quay.io/keycloak/keycloak:15.0.2"
|
||||
# environment:
|
||||
# - DB_VENDOR=POSTGRES
|
||||
# - DB_ADDR=postgres
|
||||
# - DB_DATABASE=keycloak
|
||||
# - DB_USER=keycloak
|
||||
# - DB_SCHEMA=public
|
||||
# - DB_PASSWORD=keycloak
|
||||
# - KEYCLOAK_USER=admin
|
||||
# - KEYCLOAK_PASSWORD=admin
|
||||
# expose:
|
||||
# - '8080'
|
||||
# ports:
|
||||
# - "8080:8080"
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
command: postgres -c config_file=/etc/postgresql.conf
|
||||
@@ -116,6 +101,8 @@ services:
|
||||
hostname: "penpot-devenv-redis"
|
||||
container_name: "penpot-devenv-redis"
|
||||
restart: always
|
||||
volumes:
|
||||
- "redis_data:/data"
|
||||
|
||||
mailer:
|
||||
image: sj26/mailcatcher:latest
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
FROM ubuntu:22.04 as jre-build
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
ENV LANG='en_US.UTF-8' \
|
||||
LC_ALL='en_US.UTF-8' \
|
||||
JAVA_HOME="/opt/jdk" \
|
||||
PATH=/opt/jdk/bin:$PATH \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
TZ=Etc/UTC
|
||||
|
||||
RUN set -eux; \
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
apt-get -qq update; \
|
||||
apt-get -qq upgrade; \
|
||||
apt-get -qqy --no-install-recommends install \
|
||||
curl \
|
||||
ca-certificates \
|
||||
binutils \
|
||||
nano \
|
||||
curl \
|
||||
tzdata \
|
||||
locales \
|
||||
ca-certificates \
|
||||
imagemagick \
|
||||
webp \
|
||||
rlwrap \
|
||||
fontconfig \
|
||||
woff-tools \
|
||||
woff2 \
|
||||
python3 \
|
||||
fontforge \
|
||||
; \
|
||||
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
|
||||
locale-gen; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
mkdir -p /opt/penpot; \
|
||||
chown -R penpot:penpot /opt/penpot; \
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN set -eux; \
|
||||
@@ -39,49 +62,6 @@ RUN set -eux; \
|
||||
tar -xf /tmp/openjdk.tar.gz --strip-components=1; \
|
||||
rm -rf /tmp/openjdk.tar.gz;
|
||||
|
||||
RUN /opt/jdk/bin/jlink \
|
||||
--verbose \
|
||||
--module-path /opt/jdk/jmods \
|
||||
--strip-debug \
|
||||
--no-man-pages \
|
||||
--no-header-files \
|
||||
--compress 0 \
|
||||
--add-modules java.base,java.naming,java.xml,java.logging,java.net.http,java.sql,java.management,java.desktop,jdk.jfr,jdk.unsupported,jdk.management.jfr \
|
||||
--output /opt/jre
|
||||
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
ENV LANG='en_US.UTF-8' \
|
||||
LC_ALL='en_US.UTF-8' \
|
||||
JAVA_HOME="/opt/jre" \
|
||||
PATH=/opt/jre/bin:$PATH \
|
||||
TZ=Etc/UTC
|
||||
|
||||
COPY --from=jre-build /opt/jre /opt/jre
|
||||
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
apt-get -qq update; \
|
||||
apt-get -qqy --no-install-recommends install \
|
||||
curl \
|
||||
tzdata \
|
||||
locales \
|
||||
ca-certificates \
|
||||
imagemagick \
|
||||
webp \
|
||||
fontconfig \
|
||||
woff-tools \
|
||||
woff2 \
|
||||
python3 \
|
||||
fontforge \
|
||||
; \
|
||||
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
|
||||
locale-gen; \
|
||||
mkdir -p /opt/penpot/assets; \
|
||||
chown -R penpot:penpot /opt/penpot; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
FROM nginx:1.23
|
||||
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data;
|
||||
|
||||
ADD ./bundle-frontend/ /var/www/app/
|
||||
ADD ./files/config.js /var/www/app/js/config.js
|
||||
ADD ./files/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -4,10 +4,13 @@ set -x
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
ORG=${PENPOT_DOCKER_NAMESPACE:-penpotapp};
|
||||
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
|
||||
IMAGE=${1:-backend};
|
||||
|
||||
IMAGE=${PENPOT_BUILD_IMAGE:-backend}
|
||||
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
|
||||
VERSION=${PENPOT_BUILD_VERSION:-latest}
|
||||
|
||||
DOCKER_IMAGE="$ORG/$IMAGE";
|
||||
OPTIONS="-t $DOCKER_IMAGE:$PENPOT_BUILD_VERSION";
|
||||
OPTIONS="-t $DOCKER_IMAGE:$VERSION";
|
||||
|
||||
IFS=", "
|
||||
read -a TAGS <<< $PENPOT_BUILD_TAGS;
|
||||
@@ -16,10 +19,6 @@ for element in "${TAGS[@]}"; do
|
||||
OPTIONS="$OPTIONS -t $DOCKER_IMAGE:$element";
|
||||
done
|
||||
|
||||
if [ "$PENPOT_BUILD_PUSH" = "true" ]; then
|
||||
OPTIONS="--push $OPTIONS"
|
||||
fi
|
||||
|
||||
docker buildx inspect penpot > /dev/null 2>&1;
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
|
||||
@@ -32,4 +31,5 @@ else
|
||||
fi
|
||||
|
||||
unset IFS;
|
||||
|
||||
docker buildx build --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE "$@" .;
|
||||
|
||||
@@ -36,11 +36,12 @@ services:
|
||||
|
||||
penpot-frontend:
|
||||
image: "penpotapp/frontend:latest"
|
||||
restart: always
|
||||
ports:
|
||||
- 9001:80
|
||||
|
||||
volumes:
|
||||
- penpot_assets:/opt/penpot/assets
|
||||
- penpot_assets:/opt/data/assets
|
||||
|
||||
depends_on:
|
||||
- penpot-backend
|
||||
@@ -96,8 +97,10 @@ services:
|
||||
|
||||
penpot-backend:
|
||||
image: "penpotapp/backend:latest"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
- penpot_assets:/opt/penpot/assets
|
||||
- penpot_assets:/opt/data/assets
|
||||
|
||||
depends_on:
|
||||
- penpot-postgres
|
||||
@@ -134,7 +137,7 @@ services:
|
||||
## environment variables for the backend here:
|
||||
## https://help.penpot.app/technical-guide/configuration/#advanced-configuration
|
||||
|
||||
- PENPOT_FLAGS=enable-registration enable-login disable-email-verification enable-smtp enable-prepl-server
|
||||
- PENPOT_FLAGS=enable-registration enable-login-with-password disable-email-verification enable-smtp enable-prepl-server
|
||||
|
||||
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
|
||||
## (eg http sessions) are derived.
|
||||
@@ -180,7 +183,7 @@ services:
|
||||
## stored in a docker volume.
|
||||
|
||||
- PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
|
||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/penpot/assets
|
||||
- PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets
|
||||
|
||||
## Also can be configured to to use a S3 compatible storage
|
||||
## service like MiniIO. Look below for minio service setup.
|
||||
@@ -214,6 +217,7 @@ services:
|
||||
|
||||
penpot-exporter:
|
||||
image: "penpotapp/exporter:latest"
|
||||
restart: always
|
||||
networks:
|
||||
- penpot
|
||||
|
||||
@@ -259,40 +263,8 @@ services:
|
||||
- '1025'
|
||||
ports:
|
||||
- "1080:1080"
|
||||
|
||||
## An optional admin application for pentpot. It allows manage users, teams and inspect
|
||||
## some parts of the database. You can read more about it on:
|
||||
## https://github.com/penpot/penpot-admin
|
||||
##
|
||||
## If you are going to use admin, ensure to have `enable-prepl-server` in backend flags
|
||||
## and uncomment the `PENPOT_PREPL_HOST` environment variable.
|
||||
##
|
||||
## Status: EXPERIMENTAL
|
||||
|
||||
# penpot-admin:
|
||||
# image: "penpotapp/admin:latest"
|
||||
# networks:
|
||||
# - penpot
|
||||
#
|
||||
# depends_on:
|
||||
# - penpot-postgres
|
||||
# - penpot-backend
|
||||
#
|
||||
# environment:
|
||||
# ## Adjust to the same value as on backend
|
||||
# - PENPOT_PUBLIC_URI=http://localhost:9001
|
||||
#
|
||||
# ## Do not touch it, this is an internal routes
|
||||
# - PENPOT_API_URI=http://penpot-frontend/
|
||||
# - PENPOT_PREPL_URI=tcp://penpot-backend:6063/
|
||||
# - PENPOT_DEBUG="false"
|
||||
#
|
||||
# ## Adjust to the same values as on backend
|
||||
# - PENPOT_DATABASE_HOST=penpot-postgres
|
||||
# - PENPOT_DATABASE_NAME=penpot
|
||||
# - PENPOT_DATABASE_USERNAME=penpot
|
||||
# - PENPOT_DATABASE_PASSWORD=penpot
|
||||
# - PENPOT_REDIS_URI=redis://penpot-redis/0
|
||||
networks:
|
||||
- penpot
|
||||
|
||||
## Example configuration of MiniIO (S3 compatible object storage service); If you don't
|
||||
## have preference, then just use filesystem, this is here just for the completeness.
|
||||
@@ -300,6 +272,7 @@ services:
|
||||
# minio:
|
||||
# image: "minio/minio:latest"
|
||||
# command: minio server /mnt/data --console-address ":9001"
|
||||
# restart: always
|
||||
#
|
||||
# volumes:
|
||||
# - "penpot_minio:/mnt/data"
|
||||
|
||||
@@ -90,7 +90,7 @@ http {
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /opt/penpot/assets;
|
||||
alias /opt/data/assets;
|
||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ http {
|
||||
proxy_pass http://penpot-backend:6060/api;
|
||||
}
|
||||
|
||||
location /admin {
|
||||
proxy_pass http://penpot-admin:6065/admin;
|
||||
}
|
||||
# location /admin {
|
||||
# proxy_pass http://penpot-admin:6065/admin;
|
||||
# }
|
||||
|
||||
location /ws/notifications {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
frontend/resources/images/features/1.17-ally.gif
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
frontend/resources/images/features/1.17-flex-layout.gif
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
frontend/resources/images/features/1.17-inspect.gif
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
frontend/resources/images/features/1.17-webhook.gif
Normal file
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 132.292 132.292">
|
||||
<path d="M0 0v132.292h11.207V0Zm121.085 0v132.292h11.207V0ZM36.023 28.259c-6.487 0-11.745 5.258-11.745 11.744 0 6.487 5.258 11.745 11.745 11.745 6.486 0 11.744-5.26 11.744-11.745 0-6.486-5.258-11.744-11.744-11.744zm30.04 0c-6.486 0-11.744 5.258-11.744 11.744 0 6.487 5.258 11.745 11.744 11.745 6.487 0 11.745-5.26 11.745-11.745 0-6.486-5.259-11.744-11.745-11.744zm30.496 0c-6.487 0-11.745 5.258-11.745 11.744 0 6.487 5.258 11.745 11.745 11.745 6.486 0 11.744-5.26 11.744-11.745 0-6.486-5.258-11.744-11.744-11.744zM36.023 80.545c-6.487 0-11.745 5.26-11.745 11.745 0 6.486 5.258 11.745 11.745 11.745 6.486 0 11.744-5.259 11.744-11.745 0-6.486-5.258-11.744-11.744-11.744zm30.04 0c-6.486 0-11.744 5.26-11.744 11.745 0 6.486 5.258 11.745 11.744 11.745 6.487 0 11.745-5.259 11.745-11.745 0-6.486-5.259-11.744-11.745-11.744z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 926 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 132.292 132.292">
|
||||
<path d="M0 0v11.207h132.292V0H0zm36.567 29.029c-5.96.092-12.09 4.407-12.289 10.974 0 6.487 5.258 11.745 11.745 11.745 6.486 0 11.744-5.26 11.744-11.745-.81-7.848-5.94-11.055-11.2-10.974zm60.536 0c-5.96.092-12.09 4.407-12.289 10.974 0 6.487 5.258 11.745 11.745 11.745 6.486 0 11.744-5.26 11.744-11.745-.81-7.848-5.94-11.055-11.2-10.974zm-30.495 0c-5.96.092-12.09 4.407-12.29 10.974 0 6.487 5.259 11.745 11.745 11.745 6.487 0 11.745-5.26 11.745-11.745-.811-7.848-5.94-11.055-11.2-10.974zM36.023 80.545c-6.487 0-11.745 5.26-11.745 11.745 0 6.486 5.258 11.745 11.745 11.745 6.486 0 11.744-5.259 11.744-11.745 0-6.486-5.258-11.744-11.744-11.744zm30.04 0c-6.486 0-11.744 5.26-11.744 11.745 0 6.486 5.258 11.745 11.744 11.745 6.487 0 11.745-5.259 11.745-11.745 0-6.486-5.26-11.744-11.745-11.744zM0 121.085v11.207h132.292v-11.207H0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 934 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 132.292 132.292">
|
||||
<path d="M0 0v11.207h132.292V0Zm18.947 28.264V57.99h94.4V28.264zm.098 47.096v29.726h94.302V75.36ZM0 121.086v11.206h132.292v-11.207z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 132.292 132.292">
|
||||
<path d="M0 132.292h11.207V0H0Zm28.264-18.947H57.99v-94.4H28.264zm47.096-.098h29.726V18.945H75.36Zm45.726 19.045h11.206V0h-11.207z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
481
frontend/resources/images/login-penpot.svg
Normal file
|
After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 47 KiB |
BIN
frontend/resources/images/onboarding-people.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/resources/images/onboarding-version.jpg
Normal file
|
After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
frontend/resources/images/onboarding-welcome.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -129,6 +129,7 @@ $width-settings-bar: 256px;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inspect-svg-container {
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: #feecfd;
|
||||
background-image: url("/images/login-pink.svg");
|
||||
background-position: center;
|
||||
background-color: #151035;
|
||||
background-image: url("/images/login-penpot.svg");
|
||||
background-position: center 30vh;
|
||||
background-size: 96%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
width: 280px;
|
||||
font-size: $fs18;
|
||||
margin-top: 2vh;
|
||||
color: #2c233e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
svg {
|
||||
fill: #2c233e;
|
||||
fill: white;
|
||||
max-width: 11vw;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
.options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 180px;
|
||||
|
||||
.icon {
|
||||
width: $size-5;
|
||||
@@ -140,6 +141,12 @@
|
||||
margin-left: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&.failure {
|
||||
margin-right: 10px;
|
||||
svg {
|
||||
fill: $color-warning;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -171,7 +178,6 @@
|
||||
|
||||
.dashboard-fonts-hero {
|
||||
font-size: $fs14;
|
||||
|
||||
padding: $size-6;
|
||||
background-color: $color-white;
|
||||
margin-top: $size-6;
|
||||
@@ -179,17 +185,29 @@
|
||||
justify-content: space-between;
|
||||
|
||||
.banner {
|
||||
background-color: unset;
|
||||
|
||||
display: flex;
|
||||
|
||||
background-color: $color-info-lighter;
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
padding-top: 10px;
|
||||
background-color: $color-info;
|
||||
svg {
|
||||
fill: $color-info;
|
||||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 10px;
|
||||
}
|
||||
&.warning {
|
||||
background-color: $color-warning-lighter;
|
||||
.icon {
|
||||
background-color: $color-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,35 @@
|
||||
|
||||
.custom-input {
|
||||
width: 100%;
|
||||
height: 115px;
|
||||
min-height: 116px;
|
||||
max-height: 176px;
|
||||
overflow-y: hidden;
|
||||
input {
|
||||
&.no-padding {
|
||||
padding-top: 12px;
|
||||
}
|
||||
min-height: 40px;
|
||||
}
|
||||
.selected-items {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
max-height: 132px;
|
||||
overflow-y: scroll;
|
||||
.selected-item {
|
||||
.around {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: fit-content;
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
img {
|
||||
width: 274px;
|
||||
margin-bottom: -41px;
|
||||
margin-bottom: -19px;
|
||||
@media (max-width: 1200px) {
|
||||
display: none;
|
||||
width: 0;
|
||||
|
||||
@@ -314,11 +314,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.attributes-shadow-block {
|
||||
.attributes-shadow-block,
|
||||
.attributes-stroke-block,
|
||||
.attributes-fill-block {
|
||||
border-top: 1px solid $color-gray-60;
|
||||
}
|
||||
|
||||
.attributes-shadow-blocks :first-child {
|
||||
.attributes-shadow-blocks :first-child,
|
||||
.attributes-stroke-blocks :first-child,
|
||||
.attributes-fill-blocks :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
|
||||
.modal-footer .action-buttons {
|
||||
justify-content: space-around;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.fields-container {
|
||||
@@ -693,9 +694,7 @@
|
||||
.section-list-item {
|
||||
padding: $size-4 0;
|
||||
border-bottom: 1px solid $color-gray-20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
.item-name {
|
||||
color: $color-gray-60;
|
||||
@@ -720,6 +719,8 @@
|
||||
color: $color-black;
|
||||
padding: $size-2;
|
||||
margin-bottom: 0;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
@@ -801,6 +802,7 @@
|
||||
background-color: $color-white;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
max-height: 710px;
|
||||
min-height: 420px;
|
||||
flex-direction: row;
|
||||
font-family: "sourcesanspro", sans-serif;
|
||||
@@ -1195,9 +1197,10 @@
|
||||
}
|
||||
|
||||
&.right {
|
||||
left: 731px;
|
||||
top: 100px;
|
||||
left: auto;
|
||||
color: $color-primary;
|
||||
top: 100px;
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
&.square {
|
||||
@@ -1400,6 +1403,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #31efb8;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
@@ -1491,10 +1495,6 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
.btn-large {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.btn-primary {
|
||||
max-width: 250px;
|
||||
}
|
||||
@@ -1539,8 +1539,64 @@
|
||||
|
||||
.onboarding-team-members {
|
||||
.team-left {
|
||||
padding: 42px 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: auto;
|
||||
form {
|
||||
margin-top: 32px;
|
||||
margin-top: 5px;
|
||||
.invite-row {
|
||||
.custom-input {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
height: fit-content;
|
||||
max-height: 176px;
|
||||
overflow-y: hidden;
|
||||
input {
|
||||
&.no-padding {
|
||||
padding-top: 12px;
|
||||
}
|
||||
min-height: 40px;
|
||||
}
|
||||
.selected-items {
|
||||
gap: 7px;
|
||||
padding: 7px;
|
||||
max-height: 132px;
|
||||
overflow-y: scroll;
|
||||
.selected-item {
|
||||
.around {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: fit-content;
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
margin: 12px 0;
|
||||
button {
|
||||
height: auto;
|
||||
}
|
||||
input {
|
||||
white-space: break-spaces;
|
||||
word-break: break-word;
|
||||
height: fit-content;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
min-height: 40px;
|
||||
max-height: 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1644,6 +1644,7 @@
|
||||
font-family: "worksans", sans-serif;
|
||||
|
||||
&.justify-content,
|
||||
&.align-content,
|
||||
&.sizing {
|
||||
align-items: start;
|
||||
margin-top: 4px;
|
||||
@@ -1658,7 +1659,8 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
&.justify-content {
|
||||
&.justify-content,
|
||||
&.align-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
@@ -1707,13 +1709,13 @@
|
||||
cursor: pointer;
|
||||
border-right: 1px solid $color-gray-60;
|
||||
padding: 2px;
|
||||
&.reverse-row {
|
||||
&.row-reverse {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.reverse-column {
|
||||
&.column-reverse {
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
|
||||
& .viewer-go-next.right-bar {
|
||||
right: 264px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
& .viewer-go-prev {
|
||||
@@ -97,7 +97,7 @@
|
||||
}
|
||||
|
||||
& .viewer-go-prev.left-bar {
|
||||
left: 256px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
& .viewer-bottom {
|
||||
@@ -111,7 +111,7 @@
|
||||
z-index: 2;
|
||||
|
||||
&.left-bar {
|
||||
width: calc(100% - 512px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reset {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
display: grid;
|
||||
grid-template-areas: "left center right";
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-rows: 100%;
|
||||
padding: 0;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
.right-area {
|
||||
grid-area: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ $height-palette-max: 80px;
|
||||
|
||||
#workspace {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
background-color: $color-canvas;
|
||||
display: grid;
|
||||
@@ -37,6 +37,8 @@ $height-palette-max: 80px;
|
||||
.left-toolbar {
|
||||
grid-area: toolbar;
|
||||
width: $width-left-toolbar;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.settings-bar.settings-bar-left {
|
||||
@@ -273,7 +275,6 @@ $height-palette-max: 80px;
|
||||
|
||||
.frame-thumbnail-wrapper {
|
||||
.fills,
|
||||
.strokes,
|
||||
.frame-clip-def {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.main.data.dashboard
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -426,8 +427,8 @@
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn update-team-photo
|
||||
[{:keys [file] :as params}]
|
||||
(us/assert! ::di/file file)
|
||||
[file]
|
||||
(us/assert! ::di/blob file)
|
||||
(ptk/reify ::update-team-photo
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -664,10 +665,12 @@
|
||||
(ptk/reify ::create-project
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [name (name (gensym (str (tr "dashboard.new-project-prefix") " ")))
|
||||
team-id (:current-team-id state)
|
||||
params {:name name
|
||||
:team-id team-id}
|
||||
(let [projects (get state :dashboard-projects)
|
||||
unames (cp/retrieve-used-names projects)
|
||||
name (cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
|
||||
team-id (:current-team-id state)
|
||||
params {:name name
|
||||
:team-id team-id}
|
||||
{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
@@ -875,7 +878,9 @@
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
|
||||
name (name (gensym (str (tr "dashboard.new-file-prefix") " ")))
|
||||
files (get state :dashboard-files)
|
||||
unames (cp/retrieve-used-names files)
|
||||
name (cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))
|
||||
features (cond-> #{}
|
||||
(features/active-feature? state :components-v2)
|
||||
(conj "components/v2"))
|
||||
@@ -1067,8 +1072,12 @@
|
||||
pparams (:path-params route)
|
||||
in-project? (contains? pparams :project-id)
|
||||
name (if in-project?
|
||||
(name (gensym (str (tr "dashboard.new-file-prefix") " ")))
|
||||
(name (gensym (str (tr "dashboard.new-project-prefix") " "))))
|
||||
(let [files (get state :dashboard-files)
|
||||
unames (cp/retrieve-used-names files)]
|
||||
(cp/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
(let [projects (get state :dashboard-projects)
|
||||
unames (cp/retrieve-used-names projects)]
|
||||
(cp/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
|
||||
params (if in-project?
|
||||
{:project-id (:project-id pparams)
|
||||
:name name}
|
||||
|
||||
@@ -84,16 +84,44 @@
|
||||
map with temporal ID's associated to each font entry."
|
||||
[blobs team-id]
|
||||
(letfn [(prepare [{:keys [font type name data] :as params}]
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily"))
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))]
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily"))
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))
|
||||
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of text.
|
||||
;; For historical reasons, there are three pairs of ascender/descender values, known as hhea, OS/2 and uSWin metrics.
|
||||
;; Depending on the font, operating system and application a different set will be used to render text on the screen.
|
||||
;; On Mac, Safari and Chrome use the hhea values to render text. Firefox will respect the useTypoMetrics setting and will use the OS/2 if it is set.
|
||||
;; If the useTypoMetrics is not set, Firefox will also use metrics from the hhea table.
|
||||
;; On Windows, all browsers use the usWin metrics, but respect the useTypoMetrics setting and if set will use the OS/2 values.
|
||||
|
||||
hhea-ascender (abs (-> font .-tables .-hhea .-ascender))
|
||||
hhea-descender (abs (-> font .-tables .-hhea .-descender))
|
||||
|
||||
win-ascent (abs (-> font .-tables .-os2 .-usWinAscent))
|
||||
win-descent (abs (-> font .-tables .-os2 .-usWinDescent))
|
||||
|
||||
os2-ascent (abs (-> font .-tables .-os2 .-sTypoAscender))
|
||||
os2-descent (abs (-> font .-tables .-os2 .-sTypoDescender))
|
||||
|
||||
;; useTypoMetrics can be read from the 7th bit
|
||||
f-selection (-> (-> font .-tables .-os2 .-fsSelection)
|
||||
(bit-test 7))
|
||||
|
||||
height-warning? (or (not= hhea-ascender win-ascent)
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))]
|
||||
|
||||
{:content {:data (js/Uint8Array. data)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
:font-weight (cm/parse-font-weight variant)
|
||||
:font-style (cm/parse-font-style variant)}))
|
||||
:font-style (cm/parse-font-style variant)
|
||||
:height-warning? height-warning?}))
|
||||
|
||||
(join [res {:keys [content] :as font}]
|
||||
(let [key-fn (juxt :font-family :font-weight :font-style)
|
||||
|
||||
@@ -89,6 +89,10 @@
|
||||
[key]
|
||||
(-> key meta alt))
|
||||
|
||||
(defn alt-shift
|
||||
[key]
|
||||
(-> key alt shift))
|
||||
|
||||
(defn supr
|
||||
[]
|
||||
(if (cf/check-platform? :macos)
|
||||
@@ -133,17 +137,23 @@
|
||||
[key cb]
|
||||
(fn [event]
|
||||
(log/debug :msg (str "Shortcut" key))
|
||||
(.preventDefault event)
|
||||
(when (aget event "preventDefault")
|
||||
(.preventDefault event))
|
||||
(cb event)))
|
||||
|
||||
(defn- bind!
|
||||
[shortcuts]
|
||||
(let [msbind (fn [command callback type]
|
||||
(if type
|
||||
(mousetrap/bind command callback type)
|
||||
(mousetrap/bind command callback)))]
|
||||
(->> shortcuts
|
||||
(remove #(:disabled (second %)))
|
||||
(run! (fn [[key {:keys [command fn type]}]]
|
||||
(if (vector? command)
|
||||
(run! #(mousetrap/bind % (wrap-cb key fn) type) command)
|
||||
(mousetrap/bind command (wrap-cb key fn) type))))))
|
||||
(let [callback (wrap-cb key fn)]
|
||||
(if (vector? command)
|
||||
(run! #(msbind % callback type) command)
|
||||
(msbind command callback type))))))))
|
||||
|
||||
(defn- reset!
|
||||
([]
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.geom.proportions :as gpp]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes-builder :as pcb]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
@@ -27,6 +28,7 @@
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.comments :as dcm]
|
||||
@@ -407,8 +409,8 @@
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [pages (get-in state [:workspace-data :pages-index])
|
||||
unames (ctst/retrieve-used-names pages)
|
||||
name (ctst/generate-unique-name unames "Page-1")
|
||||
unames (cp/retrieve-used-names pages)
|
||||
name (cp/generate-unique-name unames "Page 1")
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-empty-page id name))]
|
||||
@@ -422,9 +424,9 @@
|
||||
(watch [it state _]
|
||||
(let [id (uuid/next)
|
||||
pages (get-in state [:workspace-data :pages-index])
|
||||
unames (ctst/retrieve-used-names pages)
|
||||
unames (cp/retrieve-used-names pages)
|
||||
page (get-in state [:workspace-data :pages-index page-id])
|
||||
name (ctst/generate-unique-name unames (:name page))
|
||||
name (cp/generate-unique-name unames (:name page))
|
||||
|
||||
no_thumbnails_objects (->> (:objects page)
|
||||
(d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?))))
|
||||
@@ -609,6 +611,7 @@
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected-ids (wsh/lookup-selected state)
|
||||
selected-shapes (map (d/getf objects) selected-ids)
|
||||
undo-id (js/Symbol)
|
||||
|
||||
move-shape
|
||||
(fn [changes shape]
|
||||
@@ -631,7 +634,10 @@
|
||||
(pcb/with-objects objects))
|
||||
selected-shapes)]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update selected-ids)
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
|
||||
;; --- Change Shape Order (D&D Ordering)
|
||||
@@ -645,20 +651,20 @@
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
|
||||
; Move the shapes
|
||||
;; Move the shapes
|
||||
(pcb/change-parent parent-id
|
||||
shapes
|
||||
to-index)
|
||||
|
||||
; Remove empty groups
|
||||
;; Remove empty groups
|
||||
(pcb/remove-objects groups-to-delete)
|
||||
|
||||
; Unmask groups whose mask have moved outside
|
||||
;; Unmask groups whose mask have moved outside
|
||||
(pcb/update-shapes groups-to-unmask
|
||||
(fn [shape]
|
||||
(assoc shape :masked-group? false)))
|
||||
|
||||
; Detach shapes moved out of their component
|
||||
;; Detach shapes moved out of their component
|
||||
(pcb/update-shapes shapes-to-detach
|
||||
(fn [shape]
|
||||
(assoc shape :component-id nil
|
||||
@@ -668,17 +674,17 @@
|
||||
:shape-ref nil
|
||||
:touched nil)))
|
||||
|
||||
; Make non root a component moved inside another one
|
||||
;; Make non root a component moved inside another one
|
||||
(pcb/update-shapes shapes-to-deroot
|
||||
(fn [shape]
|
||||
(assoc shape :component-root? nil)))
|
||||
|
||||
; Make root a subcomponent moved outside its parent component
|
||||
;; Make root a subcomponent moved outside its parent component
|
||||
(pcb/update-shapes shapes-to-reroot
|
||||
(fn [shape]
|
||||
(assoc shape :component-root? true)))
|
||||
|
||||
; Reset constraints depending on the new parent
|
||||
;; Reset constraints depending on the new parent
|
||||
(pcb/update-shapes shapes-to-unconstraint
|
||||
(fn [shape]
|
||||
(let [parent (get objects parent-id)
|
||||
@@ -693,7 +699,19 @@
|
||||
:constraints-v (gsh/default-constraints-v moved-shape))))
|
||||
{:ignore-touched true})
|
||||
|
||||
; Resize parent containers that need to
|
||||
;; Fix the sizing when moving a shape
|
||||
(pcb/update-shapes parents
|
||||
(fn [parent]
|
||||
(if (ctl/layout? parent)
|
||||
(cond-> parent
|
||||
(ctl/change-h-sizing? (:id parent) objects (:shapes parent))
|
||||
(assoc :layout-item-h-sizing :fix)
|
||||
|
||||
(ctl/change-v-sizing? (:id parent) objects (:shapes parent))
|
||||
(assoc :layout-item-v-sizing :fix))
|
||||
parent)))
|
||||
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
|
||||
(defn relocate-shapes
|
||||
@@ -713,9 +731,9 @@
|
||||
|
||||
;; If we try to move a parent into a child we remove it
|
||||
ids (filter #(not (cph/is-parent? objects parent-id %)) ids)
|
||||
parents (if ignore-parents?
|
||||
#{parent-id}
|
||||
(into #{parent-id} (map #(cph/get-parent-id objects %)) ids))
|
||||
|
||||
all-parents (into #{parent-id} (map #(cph/get-parent-id objects %)) ids)
|
||||
parents (if ignore-parents? #{parent-id} all-parents)
|
||||
|
||||
groups-to-delete
|
||||
(loop [current-id (first parents)
|
||||
@@ -808,17 +826,12 @@
|
||||
shapes-to-reroot
|
||||
shapes-to-deroot
|
||||
ids)
|
||||
|
||||
layouts-to-update
|
||||
(into #{}
|
||||
(filter (partial ctl/layout? objects))
|
||||
(concat [parent-id] (cph/get-parent-ids objects parent-id)))
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(dwco/expand-collapse parent-id)
|
||||
(ptk/data-event :layout/update layouts-to-update)
|
||||
(ptk/data-event :layout/update (concat all-parents ids))
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
(defn relocate-selected-shapes
|
||||
@@ -835,13 +848,22 @@
|
||||
(ptk/reify ::start-editing-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [selected (wsh/lookup-selected state)]
|
||||
(if-not (= 1 (count selected))
|
||||
(rx/empty)
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
objects (wsh/lookup-page-objects state)]
|
||||
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
{:keys [id type shapes]} (get objects (first selected))]
|
||||
(if (> (count selected) 1)
|
||||
(let [shapes-to-select
|
||||
(->> selected
|
||||
(reduce
|
||||
(fn [result shape-id]
|
||||
(let [children (dm/get-in objects [shape-id :shapes])]
|
||||
(if (empty? children)
|
||||
(conj result shape-id)
|
||||
(into result children))))
|
||||
(d/ordered-set)))]
|
||||
(rx/of (dws/select-shapes shapes-to-select)))
|
||||
|
||||
(let [{:keys [id type shapes]} (get objects (first selected))]
|
||||
(case type
|
||||
:text
|
||||
(rx/of (dwe/start-edition-mode id))
|
||||
@@ -895,9 +917,13 @@
|
||||
(align-objects-list objects selected axis))
|
||||
moved-objects (->> moved (group-by :id))
|
||||
ids (keys moved-objects)
|
||||
update-fn (fn [shape] (first (get moved-objects (:id shape))))]
|
||||
update-fn (fn [shape] (first (get moved-objects (:id shape))))
|
||||
undo-id (js/Symbol)]
|
||||
(when (can-align? selected objects)
|
||||
(rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/update-shapes ids update-fn {:reg-objects? true})
|
||||
(ptk/data-event :layout/update ids)
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn align-object-to-parent
|
||||
[objects object-id axis]
|
||||
@@ -1307,16 +1333,22 @@
|
||||
:file-id (:current-file-id state)
|
||||
:selected selected
|
||||
:objects {}
|
||||
:images #{}}]
|
||||
:images #{}}
|
||||
selected_text (.. js/window getSelection toString)]
|
||||
|
||||
(->> (rx/from (seq (vals pdata)))
|
||||
(rx/merge-map (partial prepare-object objects selected+children))
|
||||
(rx/reduce collect-data initial)
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map t/encode-str)
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))))))
|
||||
(if (not-empty selected_text)
|
||||
(try
|
||||
(wapi/write-to-clipboard selected_text)
|
||||
(catch :default e
|
||||
(on-copy-error e)))
|
||||
(->> (rx/from (seq (vals pdata)))
|
||||
(rx/merge-map (partial prepare-object objects selected+children))
|
||||
(rx/reduce collect-data initial)
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map t/encode-str)
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))
|
||||
|
||||
(declare paste-shape)
|
||||
(declare paste-text)
|
||||
@@ -1408,6 +1440,19 @@
|
||||
(and (= 1 (count selected))
|
||||
(= :frame (get-in objects [(first selected) :type])))))
|
||||
|
||||
(defn same-frame-from-selected? [state frame-id]
|
||||
(let [selected (wsh/lookup-selected state)]
|
||||
(contains? frame-id (first selected))))
|
||||
|
||||
(defn frame-same-size?
|
||||
[paste-obj frame-obj]
|
||||
(and
|
||||
(= (:heigth (:selrect (first (vals paste-obj))))
|
||||
(:heigth (:selrect frame-obj)))
|
||||
(= (:width (:selrect (first (vals paste-obj))))
|
||||
(:width (:selrect frame-obj)))))
|
||||
|
||||
|
||||
(defn- paste-shape
|
||||
[{selected :selected
|
||||
paste-objects :objects ;; rename this because here comes only the clipboard shapes,
|
||||
@@ -1446,55 +1491,67 @@
|
||||
item))
|
||||
|
||||
(calculate-paste-position [state mouse-pos in-viewport?]
|
||||
(let [page-objects (wsh/lookup-page-objects state)
|
||||
selected-objs (map #(get paste-objects %) selected)
|
||||
page-selected (wsh/lookup-selected state)
|
||||
wrapper (gsh/selection-rect selected-objs)
|
||||
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))]
|
||||
(let [page-objects (wsh/lookup-page-objects state)
|
||||
selected-objs (map #(get paste-objects %) selected)
|
||||
first-selected-obj (first selected-objs)
|
||||
page-selected (wsh/lookup-selected state)
|
||||
wrapper (gsh/selection-rect selected-objs)
|
||||
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))
|
||||
frame-id (first page-selected)
|
||||
frame-object (get page-objects frame-id)
|
||||
base (cph/get-base-shape page-objects page-selected)
|
||||
index (cph/get-position-on-parent page-objects (:id base))]
|
||||
|
||||
(cond
|
||||
;; Pasting inside a frame
|
||||
(selected-frame? state)
|
||||
(let [frame-id (first page-selected)
|
||||
frame-object (get page-objects frame-id)
|
||||
|
||||
origin-frame-id (:frame-id (first selected-objs))
|
||||
origin-frame-object (get page-objects origin-frame-id)
|
||||
(if (or (same-frame-from-selected? state (first (vals paste-objects)))
|
||||
(frame-same-size? paste-objects frame-object))
|
||||
;; Paste next to selected frame, if selected is itself or of the same size as the copied
|
||||
(let [selected-frame-obj (get page-objects (first page-selected))
|
||||
parent-id (:parent-id base)
|
||||
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
|
||||
paste-y (:y selected-frame-obj)
|
||||
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
|
||||
|
||||
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
|
||||
(min (- (:width frame-object) (:width wrapper))))
|
||||
[(:frame-id base) parent-id delta index])
|
||||
|
||||
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
|
||||
(min (- (:height frame-object) (:height wrapper))))
|
||||
;; Paste inside selected frame otherwise
|
||||
(let [origin-frame-id (:frame-id first-selected-obj)
|
||||
origin-frame-object (get page-objects origin-frame-id)
|
||||
|
||||
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
|
||||
(min (- (:width frame-object) (:width wrapper))))
|
||||
|
||||
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
|
||||
(min (- (:height frame-object) (:height wrapper))))
|
||||
|
||||
;; Pasted objects mustn't exceed the selected frame x limit
|
||||
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
|
||||
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
|
||||
(:x frame-object))
|
||||
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
|
||||
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
|
||||
(:x frame-object))
|
||||
|
||||
;; Pasted objects mustn't exceed the selected frame y limit
|
||||
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
|
||||
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
|
||||
(:y frame-object))
|
||||
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
|
||||
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
|
||||
(:y frame-object))
|
||||
|
||||
delta (if (= origin-frame-id uuid/zero)
|
||||
delta (if (= origin-frame-id uuid/zero)
|
||||
;; When the origin isn't in a frame the result is pasted in the center.
|
||||
(gpt/subtract (gsh/center-shape frame-object) (gsh/center-selrect wrapper))
|
||||
(gpt/subtract (gsh/center-shape frame-object) (gsh/center-selrect wrapper))
|
||||
;; When pasting from one frame to another frame the object position must be limited to container boundaries. If the pasted object doesn't fit we try to:
|
||||
;; - Align it to the limits on the x and y axis
|
||||
;; - Respect the distance of the object to the right and bottom in the original frame
|
||||
(gpt/point paste-x paste-y))]
|
||||
[frame-id frame-id delta])
|
||||
|
||||
(gpt/point paste-x paste-y))]
|
||||
[frame-id frame-id delta]))
|
||||
|
||||
(empty? page-selected)
|
||||
(let [frame-id (ctst/top-nested-frame page-objects mouse-pos)
|
||||
delta (gpt/subtract mouse-pos orig-pos)]
|
||||
[frame-id frame-id delta])
|
||||
|
||||
:else
|
||||
(let [base (cph/get-base-shape page-objects page-selected)
|
||||
index (cph/get-position-on-parent page-objects (:id base))
|
||||
frame-id (:frame-id base)
|
||||
(let [frame-id (:frame-id base)
|
||||
parent-id (:parent-id base)
|
||||
delta (if in-viewport?
|
||||
(gpt/subtract mouse-pos orig-pos)
|
||||
@@ -1527,7 +1584,8 @@
|
||||
|
||||
;; Proceed with the standard shape paste process.
|
||||
(do-paste [it state mouse-pos media]
|
||||
(let [page (wsh/lookup-page state)
|
||||
(let [file-id (:current-file-id state)
|
||||
page (wsh/lookup-page state)
|
||||
media-idx (d/index-by :prev-id media)
|
||||
|
||||
;; Calculate position for the pasted elements
|
||||
@@ -1542,7 +1600,10 @@
|
||||
;; if foreign instance, detach the shape
|
||||
(cond-> (foreign-instance? shape paste-objects state)
|
||||
(dissoc :component-id :component-file :component-root?
|
||||
:remote-synced? :shape-ref :touched))))
|
||||
:remote-synced? :shape-ref :touched))
|
||||
;; if is a text, remove references to external typographies
|
||||
(cond-> (= (:type shape) :text)
|
||||
(ctt/remove-external-typographies file-id))))
|
||||
|
||||
paste-objects (->> paste-objects (d/mapm process-shape))
|
||||
|
||||
@@ -1564,7 +1625,7 @@
|
||||
(into (d/ordered-set)))
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(dws/select-shapes selected)
|
||||
(ptk/data-event :layout/update [frame-id])
|
||||
|
||||
@@ -34,6 +34,23 @@
|
||||
|
||||
(declare commit-changes)
|
||||
|
||||
|
||||
(defn- add-group-id
|
||||
[changes state]
|
||||
(let [undo (:workspace-undo state)
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))
|
||||
prev-item (when-not (or (empty? items) (= index -1))
|
||||
(get items index))
|
||||
group-id (:group-id prev-item)
|
||||
add-group-id? (and
|
||||
(not (nil? group-id))
|
||||
(= (get-in changes [:redo-changes 0 :type]) :mod-obj)
|
||||
(= (get-in prev-item [:redo-changes 0 :type]) :add-obj)) ;; This is a copy-and-move with mouse+alt
|
||||
]
|
||||
(cond-> changes add-group-id? (assoc :group-id group-id))))
|
||||
|
||||
|
||||
(def commit-changes? (ptk/type? ::commit-changes))
|
||||
|
||||
(defn update-shapes
|
||||
@@ -64,7 +81,8 @@
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/set-save-undo? save-undo?)
|
||||
(pcb/with-objects objects))
|
||||
ids)]
|
||||
ids)
|
||||
changes (add-group-id changes state)]
|
||||
(rx/concat
|
||||
(if (seq (:redo-changes changes))
|
||||
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
|
||||
@@ -147,7 +165,7 @@
|
||||
|
||||
(defn commit-changes
|
||||
[{:keys [redo-changes undo-changes
|
||||
origin save-undo? file-id]
|
||||
origin save-undo? file-id group-id]
|
||||
:or {save-undo? true}}]
|
||||
(log/debug :msg "commit-changes"
|
||||
:js/redo-changes redo-changes
|
||||
@@ -164,7 +182,8 @@
|
||||
:changes redo-changes
|
||||
:page-id page-id
|
||||
:frames frames
|
||||
:save-undo? save-undo?})
|
||||
:save-undo? save-undo?
|
||||
:group-id group-id})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -212,5 +231,6 @@
|
||||
|
||||
(when (and save-undo? (seq undo-changes))
|
||||
(let [entry {:undo-changes undo-changes
|
||||
:redo-changes redo-changes}]
|
||||
:redo-changes redo-changes
|
||||
:group-id group-id}]
|
||||
(rx/of (dwu/append-undo entry)))))))))))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
@@ -26,6 +27,26 @@
|
||||
|
||||
(defn interrupt? [e] (= e :interrupt))
|
||||
|
||||
(declare undo-to-index)
|
||||
|
||||
(defn- assure-valid-current-page
|
||||
[]
|
||||
(ptk/reify ::assure-valid-current-page
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [current_page (:current-page-id state)
|
||||
pages (get-in state [:workspace-data :pages])
|
||||
exists? (some #(= current_page %) pages)
|
||||
|
||||
project-id (:current-project-id state)
|
||||
file-id (:current-file-id state)
|
||||
pparams {:file-id file-id :project-id project-id}
|
||||
qparams {:page-id (first pages)}]
|
||||
(if exists?
|
||||
(rx/empty)
|
||||
(rx/of (rt/nav :workspace pparams qparams)))))))
|
||||
|
||||
|
||||
;; These functions should've been in `src/app/main/data/workspace/undo.cljs` but doing that causes
|
||||
;; a circular dependency with `src/app/main/data/workspace/changes.cljs`
|
||||
(def undo
|
||||
@@ -40,12 +61,25 @@
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))]
|
||||
(when-not (or (empty? items) (= index -1))
|
||||
(let [changes (get-in items [index :undo-changes])]
|
||||
(rx/of (dwu/materialize-undo changes (dec index))
|
||||
(dch/commit-changes {:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:origin it}))))))))))
|
||||
(let [item (get items index)
|
||||
changes (:undo-changes item)
|
||||
group-id (:group-id item)
|
||||
find-first-group-idx (fn ffgidx[index]
|
||||
(let [item (get items index)]
|
||||
(if (= (:group-id item) group-id)
|
||||
(ffgidx (dec index))
|
||||
(inc index))))
|
||||
|
||||
undo-group-index (when group-id
|
||||
(find-first-group-idx index))]
|
||||
(if group-id
|
||||
(rx/of (undo-to-index (dec undo-group-index)))
|
||||
(rx/of (dwu/materialize-undo changes (dec index))
|
||||
(dch/commit-changes {:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:origin it})
|
||||
(assure-valid-current-page)))))))))))
|
||||
|
||||
(def redo
|
||||
(ptk/reify ::redo
|
||||
@@ -53,17 +87,29 @@
|
||||
(watch [it state _]
|
||||
(let [edition (get-in state [:workspace-local :edition])
|
||||
drawing (get state :workspace-drawing)]
|
||||
(when-not (or (some? edition) (not-empty drawing))
|
||||
(when (and (nil? edition) (or (empty drawing) (= :curve (:tool drawing))))
|
||||
(let [undo (:workspace-undo state)
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))]
|
||||
(when-not (or (empty? items) (= index (dec (count items))))
|
||||
(let [changes (get-in items [(inc index) :redo-changes])]
|
||||
(rx/of (dwu/materialize-undo changes (inc index))
|
||||
(dch/commit-changes {:redo-changes changes
|
||||
:undo-changes []
|
||||
:origin it
|
||||
:save-undo? false}))))))))))
|
||||
(let [item (get items (inc index))
|
||||
changes (:redo-changes item)
|
||||
group-id (:group-id item)
|
||||
find-last-group-idx (fn flgidx [index]
|
||||
(let [item (get items index)]
|
||||
(if (= (:group-id item) group-id)
|
||||
(flgidx (inc index))
|
||||
(dec index))))
|
||||
|
||||
redo-group-index (when group-id
|
||||
(find-last-group-idx (inc index)))]
|
||||
(if group-id
|
||||
(rx/of (undo-to-index redo-group-index))
|
||||
(rx/of (dwu/materialize-undo changes (inc index))
|
||||
(dch/commit-changes {:redo-changes changes
|
||||
:undo-changes []
|
||||
:origin it
|
||||
:save-undo? false})))))))))))
|
||||
|
||||
(defn undo-to-index
|
||||
"Repeat undoing or redoing until dest-index is reached."
|
||||
@@ -78,7 +124,7 @@
|
||||
items (:items undo)
|
||||
index (or (:index undo) (dec (count items)))]
|
||||
(when (and (some? items)
|
||||
(<= 0 dest-index (dec (count items))))
|
||||
(<= -1 dest-index (dec (count items))))
|
||||
(let [changes (vec (apply concat
|
||||
(cond
|
||||
(< dest-index index)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes-builder :as pcb]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
flows (get-in page [:options :flows] [])
|
||||
unames (into #{} (map :name flows))
|
||||
name (ctst/generate-unique-name unames "Flow-1")
|
||||
name (cp/generate-unique-name unames "Flow 1")
|
||||
|
||||
new-flow {:id (uuid/next)
|
||||
:name name
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
(pcb/with-objects objects))]
|
||||
(let [group-name (if (= 1 (count shapes))
|
||||
(:name (first shapes))
|
||||
"Component-1")]
|
||||
"Component 1")]
|
||||
(dwg/prepare-create-group it
|
||||
objects
|
||||
page-id
|
||||
|
||||
@@ -79,15 +79,22 @@
|
||||
(gpt/point (- (gsh/left-bound transformed-shape) (gsh/left-bound transformed-root))
|
||||
(- (gsh/top-bound transformed-shape) (gsh/top-bound transformed-root))))
|
||||
|
||||
;; There are cases in that the coordinates change slightly (e.g. when
|
||||
;; rounding to pixel, or when recalculating text positions in different
|
||||
;; zoom levels). To take this into account, we ignore movements smaller
|
||||
;; than 1 pixel.
|
||||
distance (if (and shape-delta transformed-shape-delta)
|
||||
(gpt/distance-vector shape-delta transformed-shape-delta)
|
||||
(gpt/point 0 0))
|
||||
|
||||
ignore-geometry? (and (< (:x distance) 1) (< (:y distance) 1))]
|
||||
selrect (:selrect shape)
|
||||
transformed-selrect (:selrect transformed-shape)
|
||||
|
||||
;; There are cases in that the coordinates change slightly (e.g. when rounding
|
||||
;; to pixel, or when recalculating text positions in different zoom levels).
|
||||
;; To take this into account, we ignore movements smaller than 1 pixel.
|
||||
;;
|
||||
;; When the change is a resize, also has a transformation that may have the
|
||||
;; shape position unchanged. But in this case we do not want to ignore it.
|
||||
ignore-geometry? (and (and (< (:x distance) 1) (< (:y distance) 1))
|
||||
(mth/close? (:width selrect) (:width transformed-selrect))
|
||||
(mth/close? (:height selrect) (:height transformed-selrect)))]
|
||||
|
||||
[root transformed-root ignore-geometry?]))
|
||||
|
||||
@@ -157,43 +164,72 @@
|
||||
(us/verify (s/coll-of uuid?) ids)
|
||||
(into {} (map #(vector % {:modifiers (get-modifier (get objects %))})) ids))
|
||||
|
||||
(defn modifier-remove-from-parent
|
||||
[modif-tree objects shapes]
|
||||
(->> shapes
|
||||
(reduce
|
||||
(fn [modif-tree child-id]
|
||||
(let [parent-id (get-in objects [child-id :parent-id])]
|
||||
(update-in modif-tree [parent-id :modifiers] ctm/remove-children [child-id])))
|
||||
modif-tree)))
|
||||
|
||||
(defn build-change-frame-modifiers
|
||||
[modif-tree objects selected target-frame drop-index]
|
||||
[modif-tree objects selected target-frame-id drop-index]
|
||||
|
||||
(let [origin-frame-ids (->> selected (group-by #(get-in objects [% :frame-id])))
|
||||
child-set (set (get-in objects [target-frame :shapes]))
|
||||
layout? (ctl/layout? objects target-frame)
|
||||
child-set (set (get-in objects [target-frame-id :shapes]))
|
||||
|
||||
target-frame (get objects target-frame-id)
|
||||
target-layout? (ctl/layout? target-frame)
|
||||
|
||||
children-ids (concat (:shapes target-frame) selected)
|
||||
|
||||
set-parent-ids
|
||||
(fn [modif-tree shapes target-frame]
|
||||
(fn [modif-tree shapes target-frame-id]
|
||||
(reduce
|
||||
(fn [modif-tree id]
|
||||
(update-in
|
||||
modif-tree
|
||||
[id :modifiers]
|
||||
#(-> %
|
||||
(ctm/change-property :frame-id target-frame)
|
||||
(ctm/change-property :parent-id target-frame))))
|
||||
(ctm/change-property :frame-id target-frame-id)
|
||||
(ctm/change-property :parent-id target-frame-id))))
|
||||
modif-tree
|
||||
shapes))
|
||||
|
||||
update-frame-modifiers
|
||||
(fn [modif-tree [original-frame shapes]]
|
||||
(let [shapes (->> shapes (d/removev #(= target-frame %)))
|
||||
(let [shapes (->> shapes (d/removev #(= target-frame-id %)))
|
||||
shapes (cond->> shapes
|
||||
(and layout? (= original-frame target-frame))
|
||||
(and target-layout? (= original-frame target-frame-id))
|
||||
;; When movining inside a layout frame remove the shapes that are not immediate children
|
||||
(filterv #(contains? child-set %)))]
|
||||
(filterv #(contains? child-set %)))
|
||||
children-ids (->> (dm/get-in objects [original-frame :shapes])
|
||||
(remove (set selected)))
|
||||
|
||||
h-sizing? (ctl/change-h-sizing? original-frame objects children-ids)
|
||||
v-sizing? (ctl/change-v-sizing? original-frame objects children-ids)]
|
||||
(cond-> modif-tree
|
||||
(not= original-frame target-frame)
|
||||
(-> (update-in [original-frame :modifiers] ctm/remove-children shapes)
|
||||
(update-in [target-frame :modifiers] ctm/add-children shapes drop-index)
|
||||
(set-parent-ids shapes target-frame))
|
||||
(not= original-frame target-frame-id)
|
||||
(-> (modifier-remove-from-parent objects shapes)
|
||||
(update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index)
|
||||
(set-parent-ids shapes target-frame-id)
|
||||
(cond-> h-sizing?
|
||||
(update-in [original-frame :modifiers] ctm/change-property :layout-item-h-sizing :fix))
|
||||
(cond-> v-sizing?
|
||||
(update-in [original-frame :modifiers] ctm/change-property :layout-item-v-sizing :fix)))
|
||||
|
||||
(and layout? (= original-frame target-frame))
|
||||
(update-in [target-frame :modifiers] ctm/add-children shapes drop-index))))]
|
||||
(and target-layout? (= original-frame target-frame-id))
|
||||
(update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index))))]
|
||||
|
||||
(reduce update-frame-modifiers modif-tree origin-frame-ids)))
|
||||
(as-> modif-tree $
|
||||
(reduce update-frame-modifiers $ origin-frame-ids)
|
||||
(cond-> $
|
||||
(ctl/change-h-sizing? target-frame-id objects children-ids)
|
||||
(update-in [target-frame-id :modifiers] ctm/change-property :layout-item-h-sizing :fix))
|
||||
(cond-> $
|
||||
(ctl/change-v-sizing? target-frame-id objects children-ids)
|
||||
(update-in [target-frame-id :modifiers] ctm/change-property :layout-item-v-sizing :fix)))))
|
||||
|
||||
(defn modif->js
|
||||
[modif-tree objects]
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as md]
|
||||
@@ -281,8 +280,6 @@
|
||||
|
||||
|
||||
;; --- Duplicate Shapes
|
||||
(declare prepare-duplicate-change)
|
||||
(declare prepare-duplicate-frame-change)
|
||||
(declare prepare-duplicate-shape-change)
|
||||
(declare prepare-duplicate-flows)
|
||||
(declare prepare-duplicate-guides)
|
||||
@@ -292,7 +289,7 @@
|
||||
move to the desired position, and recalculate parents and frames as needed."
|
||||
[all-objects page ids delta it]
|
||||
(let [shapes (map (d/getf all-objects) ids)
|
||||
unames (volatile! (ctt/retrieve-used-names (:objects page)))
|
||||
unames (volatile! (cp/retrieve-used-names (:objects page)))
|
||||
update-unames! (fn [new-name] (vswap! unames conj new-name))
|
||||
all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) (d/ordered-set) ids)
|
||||
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
|
||||
@@ -304,91 +301,59 @@
|
||||
|
||||
changes
|
||||
(->> shapes
|
||||
(reduce #(prepare-duplicate-change %1
|
||||
all-objects
|
||||
page
|
||||
unames
|
||||
update-unames!
|
||||
ids-map
|
||||
%2
|
||||
delta)
|
||||
(reduce #(prepare-duplicate-shape-change %1
|
||||
all-objects
|
||||
page
|
||||
unames
|
||||
update-unames!
|
||||
ids-map
|
||||
%2
|
||||
delta)
|
||||
init-changes))]
|
||||
|
||||
(-> changes
|
||||
(prepare-duplicate-flows shapes page ids-map)
|
||||
(prepare-duplicate-guides shapes page ids-map delta))))
|
||||
|
||||
(defn- prepare-duplicate-change
|
||||
[changes objects page unames update-unames! ids-map shape delta]
|
||||
(if (cph/frame-shape? shape)
|
||||
(prepare-duplicate-frame-change changes objects page unames update-unames! ids-map shape delta)
|
||||
(prepare-duplicate-shape-change changes objects page unames update-unames! ids-map shape delta (:frame-id shape) (:parent-id shape))))
|
||||
|
||||
(defn- prepare-duplicate-frame-change
|
||||
[changes objects page unames update-unames! ids-map obj delta]
|
||||
(let [new-id (ids-map (:id obj))
|
||||
frame-name (:name obj)
|
||||
|
||||
new-frame (-> obj
|
||||
(assoc :id new-id
|
||||
:name frame-name
|
||||
:shapes [])
|
||||
(dissoc :use-for-thumbnail?)
|
||||
(gsh/move delta)
|
||||
(d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)))
|
||||
|
||||
changes (-> (pcb/add-object changes new-frame)
|
||||
(pcb/amend-last-change #(assoc % :old-id (:id obj))))
|
||||
|
||||
changes (reduce (fn [changes child]
|
||||
(prepare-duplicate-shape-change changes
|
||||
objects
|
||||
page
|
||||
unames
|
||||
update-unames!
|
||||
ids-map
|
||||
child
|
||||
delta
|
||||
new-id
|
||||
new-id))
|
||||
changes
|
||||
(map (d/getf objects) (:shapes obj)))]
|
||||
changes))
|
||||
|
||||
(defn- prepare-duplicate-shape-change
|
||||
[changes objects page unames update-unames! ids-map obj delta frame-id parent-id]
|
||||
(if (some? obj)
|
||||
(let [new-id (ids-map (:id obj))
|
||||
parent-id (or parent-id frame-id)
|
||||
name (:name obj)
|
||||
([changes objects page unames update-unames! ids-map obj delta]
|
||||
(prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj)))
|
||||
|
||||
new-obj (-> obj
|
||||
(assoc :id new-id
|
||||
:name name
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
(dissoc :shapes
|
||||
:main-instance?)
|
||||
(gsh/move delta)
|
||||
(d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)))
|
||||
([changes objects page unames update-unames! ids-map obj delta frame-id parent-id]
|
||||
(if (some? obj)
|
||||
(let [frame? (cph/frame-shape? obj)
|
||||
new-id (ids-map (:id obj))
|
||||
parent-id (or parent-id frame-id)
|
||||
name (:name obj)
|
||||
|
||||
changes (-> (pcb/add-object changes new-obj {:ignore-touched true})
|
||||
(pcb/amend-last-change #(assoc % :old-id (:id obj))))]
|
||||
new-obj (-> obj
|
||||
(assoc :id new-id
|
||||
:name name
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
(dissoc :shapes
|
||||
:main-instance?
|
||||
:use-for-thumbnail?)
|
||||
(gsh/move delta)
|
||||
(d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)))
|
||||
|
||||
(reduce (fn [changes child]
|
||||
(prepare-duplicate-shape-change changes
|
||||
objects
|
||||
page
|
||||
unames
|
||||
update-unames!
|
||||
ids-map
|
||||
child
|
||||
delta
|
||||
frame-id
|
||||
new-id))
|
||||
changes
|
||||
(map (d/getf objects) (:shapes obj))))
|
||||
changes))
|
||||
changes (-> (pcb/add-object changes new-obj {:ignore-touched true})
|
||||
(pcb/amend-last-change #(assoc % :old-id (:id obj))))]
|
||||
|
||||
(reduce (fn [changes child]
|
||||
(prepare-duplicate-shape-change changes
|
||||
objects
|
||||
page
|
||||
unames
|
||||
update-unames!
|
||||
ids-map
|
||||
child
|
||||
delta
|
||||
(if frame? new-id frame-id)
|
||||
new-id))
|
||||
changes
|
||||
(map (d/getf objects) (:shapes obj))))
|
||||
changes)))
|
||||
|
||||
(defn- prepare-duplicate-flows
|
||||
[changes shapes page ids-map]
|
||||
@@ -401,7 +366,7 @@
|
||||
(let [update-flows (fn [flows]
|
||||
(reduce
|
||||
(fn [flows frame]
|
||||
(let [name (ctt/generate-unique-name @unames "Flow-1")
|
||||
(let [name (cp/generate-unique-name @unames "Flow 1")
|
||||
_ (vswap! unames conj name)
|
||||
new-flow {:id (uuid/next)
|
||||
:name name
|
||||
@@ -520,7 +485,10 @@
|
||||
|
||||
(gpt/subtract new-pos pt-obj)))))
|
||||
|
||||
(defn duplicate-selected [move-delta?]
|
||||
(defn duplicate-selected
|
||||
([move-delta?]
|
||||
(duplicate-selected move-delta? false))
|
||||
([move-delta? add-group-id?]
|
||||
(ptk/reify ::duplicate-selected
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -537,6 +505,8 @@
|
||||
changes (->> (prepare-duplicate-changes objects page selected delta it)
|
||||
(duplicate-changes-update-indices objects selected))
|
||||
|
||||
changes (cond-> changes add-group-id? (assoc :group-id (uuid/random)))
|
||||
|
||||
id-original (first selected)
|
||||
|
||||
new-selected (->> changes
|
||||
@@ -560,7 +530,7 @@
|
||||
(select-shapes new-selected)
|
||||
(ptk/data-event :layout/update frames)
|
||||
(memorize-duplicated id-original id-duplicated)
|
||||
(dwu/commit-undo-transaction undo-id)))))))))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn change-hover-state
|
||||
[id value]
|
||||
|
||||