mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
134 Commits
2.2.0
...
renderer-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fe91398a | ||
|
|
b36c8cd52a | ||
|
|
f5acfd0787 | ||
|
|
4939bc06ac | ||
|
|
cd63fb78d2 | ||
|
|
3298785436 | ||
|
|
eeb0d21013 | ||
|
|
a11c2af542 | ||
|
|
6d5b0204e9 | ||
|
|
dfe5d861f2 | ||
|
|
445691430b | ||
|
|
88722bcf4f | ||
|
|
43903014c6 | ||
|
|
cf8b62f1a8 | ||
|
|
39b627cb1a | ||
|
|
81680cffe9 | ||
|
|
dc014bd4eb | ||
|
|
0027e77861 | ||
|
|
fa9004d12c | ||
|
|
c7f801dd44 | ||
|
|
0f0b23e38b | ||
|
|
1f8fe2dc4c | ||
|
|
e84622061d | ||
|
|
305de33200 | ||
|
|
80bbfe7a6f | ||
|
|
26ab39a45d | ||
|
|
739b8d7c02 | ||
|
|
e0a9f63015 | ||
|
|
928709a0f2 | ||
|
|
579b157ab7 | ||
|
|
0bf442e626 | ||
|
|
2184af6602 | ||
|
|
78fb938d16 | ||
|
|
dd9185e058 | ||
|
|
5f8d56b366 | ||
|
|
bc0fde68c7 | ||
|
|
024a2ae848 | ||
|
|
4d56bf66f4 | ||
|
|
c83ef201a1 | ||
|
|
6d26abb9e3 | ||
|
|
1b1f08388f | ||
|
|
472c769c9a | ||
|
|
864088eecd | ||
|
|
0b39318b33 | ||
|
|
d5a9961ec8 | ||
|
|
7dac7de365 | ||
|
|
dd0721e91e | ||
|
|
21fde2e991 | ||
|
|
ca1893164d | ||
|
|
b619ac3e08 | ||
|
|
d7eb86c86d | ||
|
|
6c4f216da8 | ||
|
|
f786a00e89 | ||
|
|
47cecb2ac4 | ||
|
|
5d6ceec803 | ||
|
|
bec11220e3 | ||
|
|
9b802e1c7d | ||
|
|
21aa8b0703 | ||
|
|
03ebeb0657 | ||
|
|
19a613e90c | ||
|
|
7fe95f218b | ||
|
|
a1fc785771 | ||
|
|
4f04dbc294 | ||
|
|
2b2a84da64 | ||
|
|
21dd9a260c | ||
|
|
7b9b5bafc1 | ||
|
|
41ebba6ce0 | ||
|
|
61446592b3 | ||
|
|
b82c6326cf | ||
|
|
a2f466810b | ||
|
|
1bd1782d66 | ||
|
|
ea6a1c05fa | ||
|
|
4f84e77b10 | ||
|
|
fa75a3539f | ||
|
|
fa12d9785a | ||
|
|
c578e31ae2 | ||
|
|
749c369080 | ||
|
|
4ad4057878 | ||
|
|
2dea0b52ed | ||
|
|
7590a7ce4d | ||
|
|
884ceb052b | ||
|
|
cc7ed497e8 | ||
|
|
cd6a739abb | ||
|
|
f0cecfd517 | ||
|
|
5ffa56be3d | ||
|
|
076cb0e35b | ||
|
|
2a90ca6546 | ||
|
|
a26deafa75 | ||
|
|
cf705e352b | ||
|
|
b50fcee079 | ||
|
|
9bca42c14a | ||
|
|
214733c880 | ||
|
|
d6f6d78b1e | ||
|
|
8c1fba5160 | ||
|
|
fb39dd5440 | ||
|
|
dd0c5b7806 | ||
|
|
9e94cf7b99 | ||
|
|
b882b9e283 | ||
|
|
cdcff62232 | ||
|
|
c8caca77a3 | ||
|
|
042b3a71d8 | ||
|
|
eadae5e2cd | ||
|
|
7f9c4df284 | ||
|
|
9e3f8e7827 | ||
|
|
3a4e9ccc5a | ||
|
|
eb720b053a | ||
|
|
efc61241a0 | ||
|
|
cfad1d178f | ||
|
|
c24b2dadec | ||
|
|
9a3b5337d7 | ||
|
|
396cbb27b2 | ||
|
|
b4e6f8bc73 | ||
|
|
d88f28f5c2 | ||
|
|
e36cf1d963 | ||
|
|
a0bb5e5ef3 | ||
|
|
34cc211912 | ||
|
|
e95713c1df | ||
|
|
e189dc965d | ||
|
|
53f580ad40 | ||
|
|
cf0045681e | ||
|
|
762a883b39 | ||
|
|
a63ded1ba1 | ||
|
|
f812b28892 | ||
|
|
873c9b1903 | ||
|
|
edeb16bc26 | ||
|
|
90d947391a | ||
|
|
47cc80a93f | ||
|
|
1f8cfde1cf | ||
|
|
5f2ec595cb | ||
|
|
37a6446e32 | ||
|
|
be84b1cb01 | ||
|
|
298db46722 | ||
|
|
0c6b0598fa | ||
|
|
f2a2d772b0 |
12
CHANGES.md
12
CHANGES.md
@@ -1,5 +1,17 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,29 +171,43 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We received a request to change your current email to {{ pending-email }}.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We received a request to change your current email to {{ pending-email }}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Click to the link below to confirm the change:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Click to the link below to confirm the change:</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Confirm email change </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Confirm email change </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -194,17 +215,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, please consider changing your password for security reasons.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, please consider changing your password for security
|
||||
reasons.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -221,258 +249,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
323
backend/resources/app/email/includes/footer.html
Normal file
323
backend/resources/app/email/includes/footer.html
Normal file
@@ -0,0 +1,323 @@
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for
|
||||
cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://x.com/penpotapp" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-x.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://www.linkedin.com/company/penpotdesign/"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-linkedin.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://fosstodon.org/@penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-mastodon.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,36 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Accept invite </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Accept invite </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +208,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +234,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -235,283 +235,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We have received a request to reset your password. Click the link below to choose a new one:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We have received a request to reset your password. Click the link below to choose a new one:
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Reset password </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Reset password </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,17 +209,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, you can safely ignore it. Your password won't be changed.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, you can safely ignore it. Your password won't be changed.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -216,258 +243,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the link below and
|
||||
get started building mockups and prototypes today!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Verify email </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Verify email </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +209,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +235,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -245,283 +245,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -268,283 +268,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -285,283 +285,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -243,283 +243,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
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;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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://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>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<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/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -279,13 +279,20 @@
|
||||
message)
|
||||
|
||||
(def ^:private schema:params
|
||||
(sm/define
|
||||
[:map {:title "params"}
|
||||
[:session-id ::sm/uuid]]))
|
||||
[:map {:title "params"}
|
||||
[:session-id ::sm/uuid]])
|
||||
|
||||
(def ^:private decode-params
|
||||
(sm/decoder schema:params sm/json-transformer))
|
||||
|
||||
(def ^:private validate-params!
|
||||
(sm/validate-fn schema:params))
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [{:keys [session-id]} (sm/conform! schema:params params)]
|
||||
(let [{:keys [session-id]} (-> params
|
||||
decode-params
|
||||
validate-params!)]
|
||||
(cond
|
||||
(not profile-id)
|
||||
(ex/raise :type :authentication
|
||||
|
||||
@@ -174,38 +174,34 @@
|
||||
;; --- COMMAND QUERY: get-file (by id)
|
||||
|
||||
(def schema:file
|
||||
(sm/define
|
||||
[:map {:title "File"}
|
||||
[:id ::sm/uuid]
|
||||
[:features ::cfeat/features]
|
||||
[:has-media-trimmed ::sm/boolean]
|
||||
[:comment-thread-seqn [::sm/int {:min 0}]]
|
||||
[:name [:string {:max 250}]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:modified-at ::dt/instant]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]]))
|
||||
[:map {:title "File"}
|
||||
[:id ::sm/uuid]
|
||||
[:features ::cfeat/features]
|
||||
[:has-media-trimmed ::sm/boolean]
|
||||
[:comment-thread-seqn [::sm/int {:min 0}]]
|
||||
[:name [:string {:max 250}]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:modified-at ::dt/instant]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]])
|
||||
|
||||
(def schema:permissions-mixin
|
||||
(sm/define
|
||||
[:map {:title "PermissionsMixin"}
|
||||
[:permissions ::perms/permissions]]))
|
||||
[:map {:title "PermissionsMixin"}
|
||||
[:permissions ::perms/permissions]])
|
||||
|
||||
(def schema:file-with-permissions
|
||||
(sm/define
|
||||
[:merge {:title "FileWithPermissions"}
|
||||
schema:file
|
||||
schema:permissions-mixin]))
|
||||
[:merge {:title "FileWithPermissions"}
|
||||
schema:file
|
||||
schema:permissions-mixin])
|
||||
|
||||
(def ^:private
|
||||
schema:get-file
|
||||
(sm/define
|
||||
[:map {:title "get-file"}
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:id ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]]))
|
||||
[:map {:title "get-file"}
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:id ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- migrate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
@@ -914,10 +910,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:set-file-shared
|
||||
(sm/define
|
||||
[:map {:title "set-file-shared"}
|
||||
[:id ::sm/uuid]
|
||||
[:is-shared ::sm/boolean]]))
|
||||
[:map {:title "set-file-shared"}
|
||||
[:id ::sm/uuid]
|
||||
[:is-shared ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::set-file-shared
|
||||
{::doc/added "1.17"
|
||||
@@ -944,9 +939,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:delete-file
|
||||
(sm/define
|
||||
[:map {:title "delete-file"}
|
||||
[:id ::sm/uuid]]))
|
||||
[:map {:title "delete-file"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(defn- delete-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
||||
@@ -978,10 +972,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:link-file-to-library
|
||||
(sm/define
|
||||
[:map {:title "link-file-to-library"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:library-id ::sm/uuid]]))
|
||||
[:map {:title "link-file-to-library"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:library-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::link-file-to-library
|
||||
{::doc/added "1.17"
|
||||
|
||||
@@ -179,18 +179,16 @@
|
||||
|
||||
(def ^:private
|
||||
schema:get-file-data-for-thumbnail
|
||||
(sm/define
|
||||
[:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::cfeat/features]]))
|
||||
[:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
|
||||
(def ^:private
|
||||
schema:partial-file
|
||||
(sm/define
|
||||
[:map {:title "PartialFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:page :any]]))
|
||||
[:map {:title "PartialFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:page :any]])
|
||||
|
||||
(sv/defmethod ::get-file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
|
||||
@@ -88,10 +88,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:duplicate-file
|
||||
(sm/define
|
||||
[:map {:title "duplicate-file"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "duplicate-file"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::duplicate-file
|
||||
"Duplicate a single file in the same team."
|
||||
@@ -150,10 +149,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:duplicate-project
|
||||
(sm/define
|
||||
[:map {:title "duplicate-project"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "duplicate-project"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::duplicate-project
|
||||
"Duplicate an entire project with all the files"
|
||||
@@ -327,10 +325,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:move-files
|
||||
(sm/define
|
||||
[:map {:title "move-files"}
|
||||
[:ids ::sm/set-of-uuid]
|
||||
[:project-id ::sm/uuid]]))
|
||||
[:map {:title "move-files"}
|
||||
[:ids ::sm/set-of-uuid]
|
||||
[:project-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::move-files
|
||||
"Move a set of files from one project to other."
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.plugins :refer [schema:plugin-registry]]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -40,6 +41,33 @@
|
||||
(declare strip-private-attrs)
|
||||
(declare verify-password)
|
||||
|
||||
(def schema:props
|
||||
[:map {:title "ProfileProps"}
|
||||
[:plugins {:optional true} schema:plugin-registry]
|
||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||
[:newsletter-news {:optional true} ::sm/boolean]
|
||||
[:onboarding-team-id {:optional true} ::sm/uuid]
|
||||
[:onboarding-viewed {:optional true} ::sm/boolean]
|
||||
[:v2-info-shown {:optional true} ::sm/boolean]
|
||||
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
|
||||
[:release-notes-viewed {:optional true}
|
||||
[::sm/text {:max 100}]]])
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} ::sm/boolean]
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true} schema:props]])
|
||||
|
||||
(defn clean-email
|
||||
"Clean and normalizes email address string"
|
||||
[email]
|
||||
@@ -53,24 +81,6 @@
|
||||
email)]
|
||||
email))
|
||||
|
||||
(def ^:private
|
||||
schema:profile
|
||||
(sm/define
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} ::sm/boolean]
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true}
|
||||
[:map-of {:title "ProfileProps"} :keyword :any]]]))
|
||||
|
||||
;; --- QUERY: Get profile (own)
|
||||
|
||||
(sv/defmethod ::get-profile
|
||||
@@ -99,11 +109,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile
|
||||
(sm/define
|
||||
[:map {:title "update-profile"}
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:lang {:optional true} [:string {:max 8}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "update-profile"}
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:lang {:optional true} [:string {:max 8}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::update-profile
|
||||
{::doc/added "1.0"
|
||||
@@ -144,11 +153,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-password
|
||||
(sm/define
|
||||
[:map {:title "update-profile-password"}
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
;; Social registered users don't have old-password
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
|
||||
[:map {:title "update-profile-password"}
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
;; Social registered users don't have old-password
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
|
||||
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::doc/added "1.0"
|
||||
@@ -199,9 +207,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-photo
|
||||
(sm/define
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]]))
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]])
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{:doc/added "1.1"
|
||||
@@ -268,9 +275,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:request-email-change
|
||||
(sm/define
|
||||
[:map {:title "request-email-change"}
|
||||
[:email ::sm/email]]))
|
||||
[:map {:title "request-email-change"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(sv/defmethod ::request-email-change
|
||||
{::doc/added "1.0"
|
||||
@@ -351,14 +357,12 @@
|
||||
:extra-data ptoken})
|
||||
nil))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Profile Props
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-props
|
||||
(sm/define
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]]))
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props schema:props]])
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
|
||||
@@ -26,14 +26,13 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:quote
|
||||
(sm/define
|
||||
[:map {:title "Quote"}
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]]))
|
||||
[:map {:title "Quote"}
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]])
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
|
||||
@@ -21,16 +21,14 @@
|
||||
|
||||
(def ^:private
|
||||
schema:template
|
||||
(sm/define
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]]))
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]])
|
||||
|
||||
(def ^:private
|
||||
schema:templates
|
||||
(sm/define
|
||||
[:vector schema:template]))
|
||||
[:vector schema:template])
|
||||
|
||||
(defmethod ig/init-key ::setup/templates
|
||||
[_ _]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.transit :as transit]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -84,54 +85,56 @@
|
||||
(t/is (= (hash obj1) (hash obj2))))))
|
||||
|
||||
(t/deftest internal-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty))]
|
||||
(let [obj1 (omap/wrap data)
|
||||
obj2 (omap/create (deref obj1))
|
||||
obj3 (assoc obj2 uuid/zero 1)
|
||||
obj4 (omap/create (deref obj3))]
|
||||
;; (app.common.pprint/pprint data)
|
||||
(t/is (= (hash obj1) (hash obj2)))
|
||||
(t/is (not= (hash obj2) (hash obj3)))
|
||||
(t/is (bytes? (deref obj3)))
|
||||
(t/is (pos? (alength (deref obj3))))
|
||||
(t/is (= (hash obj3) (hash obj4)))))))
|
||||
|
||||
(and (= (hash obj1) (hash obj2))
|
||||
(not= (hash obj2) (hash obj3))
|
||||
(bytes? (deref obj3))
|
||||
(pos? (alength (deref obj3)))
|
||||
(= (hash obj3) (hash obj4)))))
|
||||
{:num 50}))
|
||||
|
||||
(t/deftest fressian-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
|
||||
(let [res (-> data fres/encode fres/decode)]
|
||||
(t/is (contains? res :objects))
|
||||
(t/is (omap/objects-map? (:objects res)))
|
||||
(t/is (= (count (:objects data))
|
||||
(count (:objects res))))
|
||||
(t/is (= (hash (:objects data))
|
||||
(hash (:objects res))))))))
|
||||
(and (contains? res :objects)
|
||||
(omap/objects-map? (:objects res))
|
||||
(= (count (:objects data))
|
||||
(count (:objects res)))
|
||||
(= (hash (:objects data))
|
||||
(hash (:objects res))))))
|
||||
{:num 50}))
|
||||
|
||||
(t/deftest transit-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(let [res (-> data transit/encode transit/decode)]
|
||||
;; (app.common.pprint/pprint data)
|
||||
;; (app.common.pprint/pprint res)
|
||||
(doseq [[k v] (:objects res)]
|
||||
(t/is (= v (get-in data [:objects k]))))
|
||||
|
||||
(t/is (contains? res :objects))
|
||||
(t/is (contains? data :objects))
|
||||
|
||||
(t/is (omap/objects-map? (:objects data)))
|
||||
(t/is (not (omap/objects-map? (:objects res))))
|
||||
|
||||
(t/is (= (count (:objects data))
|
||||
(count (:objects res))))))))
|
||||
(and (every? (fn [[k v]]
|
||||
(= v (get-in data [:objects k])))
|
||||
(:objects res))
|
||||
(contains? res :objects)
|
||||
(contains? data :objects)
|
||||
(omap/objects-map? (:objects data))
|
||||
(not (omap/objects-map? (:objects res)))
|
||||
(= (count (:objects data))
|
||||
(count (:objects res))))))
|
||||
{:num 50}))
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
(ns user
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
|
||||
(defn ordered-map
|
||||
([] lkm/empty-linked-map)
|
||||
([a] (conj lkm/empty-linked-map a))
|
||||
([a & xs] (apply conj lkm/empty-linked-map a xs)))
|
||||
([k a] (assoc lkm/empty-linked-map k a))
|
||||
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
|
||||
|
||||
(defn ordered-set?
|
||||
[o]
|
||||
@@ -564,6 +564,41 @@
|
||||
new-elems
|
||||
(remove p? after))))
|
||||
|
||||
(defn addm-at-index
|
||||
"Insert an element in an ordered map at an arbitrary index"
|
||||
[coll index key element]
|
||||
(assert (ordered-map? coll))
|
||||
(-> (ordered-map)
|
||||
(into (take index coll))
|
||||
(assoc key element)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn insertm-at-index
|
||||
"Insert a map {k v} of elements in an ordered map at an arbitrary index"
|
||||
[coll index new-elems]
|
||||
(assert (ordered-map? coll))
|
||||
(-> (ordered-map)
|
||||
(into (take index coll))
|
||||
(into new-elems)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn adds-at-index
|
||||
"Insert an element in an ordered set at an arbitrary index"
|
||||
[coll index element]
|
||||
(assert (ordered-set? coll))
|
||||
(-> (ordered-set)
|
||||
(into (take index coll))
|
||||
(conj element)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn inserts-at-index
|
||||
"Insert a list of elements in an ordered set at an arbitrary index"
|
||||
[coll index new-elems]
|
||||
(assert (ordered-set? coll))
|
||||
(-> (ordered-set)
|
||||
(into (take index coll))
|
||||
(into new-elems)
|
||||
(into (drop index coll))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
valid? (or (and components-v2
|
||||
(nil? (:component-id change))
|
||||
(nil? (:page-id change)))
|
||||
(ch/check-change! change))]
|
||||
(ch/valid-change? change))]
|
||||
|
||||
(when-not valid?
|
||||
(let [explain (sm/explain ::ch/change change)]
|
||||
@@ -741,46 +741,36 @@
|
||||
|
||||
(defn add-guide
|
||||
[file guide]
|
||||
|
||||
(let [guide (cond-> guide
|
||||
(nil? (:id guide))
|
||||
(assoc :id (uuid/next)))
|
||||
page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (assoc old-guides (:id guide) guide)]
|
||||
page-id (:current-page-id file)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides})
|
||||
:id (:id guide)
|
||||
:params guide})
|
||||
(assoc :last-id (:id guide)))))
|
||||
|
||||
(defn delete-guide
|
||||
[file id]
|
||||
|
||||
(let [page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (dissoc old-guides id)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides}))))
|
||||
(let [page-id (:current-page-id file)]
|
||||
(commit-change file
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params nil})))
|
||||
|
||||
(defn update-guide
|
||||
[file guide]
|
||||
|
||||
(let [page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (assoc old-guides (:id guide) guide)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides}))))
|
||||
(let [page-id (:current-page-id file)]
|
||||
(commit-change file
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id (:id guide)
|
||||
:params guide})))
|
||||
|
||||
(defn strip-image-extension [filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
|
||||
@@ -10,21 +10,25 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-native :as smd]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.colors-list :as ctcl]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.grid :as ctg]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -60,6 +64,111 @@
|
||||
[:type [:= :set-remote-synced]]
|
||||
[:remote-synced {:optional true} [:maybe :boolean]]]]])
|
||||
|
||||
(def schema:set-default-grid-change
|
||||
(let [gen (->> (sg/elements #{:square :column :row})
|
||||
(sg/mcat (fn [grid-type]
|
||||
(sg/fmap (fn [params]
|
||||
{:page-id (uuid/next)
|
||||
:type :set-default-grid
|
||||
:grid-type grid-type
|
||||
:params params})
|
||||
|
||||
(case grid-type
|
||||
:square (sg/generator ctg/schema:square-params)
|
||||
:column (sg/generator ctg/schema:column-params)
|
||||
:row (sg/generator ctg/schema:column-params))))))]
|
||||
|
||||
[:multi {:decode/json #(update % :grid-type keyword)
|
||||
:gen/gen gen
|
||||
:dispatch :grid-type
|
||||
::smd/simplified true}
|
||||
[:square
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :square]]
|
||||
[:params [:maybe ctg/schema:square-params]]]]
|
||||
|
||||
[:column
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :column]]
|
||||
[:params [:maybe ctg/schema:column-params]]]]
|
||||
|
||||
[:row
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :row]]
|
||||
[:params [:maybe ctg/schema:column-params]]]]]))
|
||||
|
||||
(def schema:set-guide-change
|
||||
(let [schema [:map {:title "SetGuideChange"}
|
||||
[:type [:= :set-guide]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/guide]]]
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
(if (some? (:params change))
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
|
||||
(def schema:set-flow-change
|
||||
(let [schema [:map {:title "SetFlowChange"}
|
||||
[:type [:= :set-flow]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/flow]]]
|
||||
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
(if (some? (:params change))
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
|
||||
(def schema:set-plugin-data-change
|
||||
(let [types #{:file :page :shape :color :typography :component}
|
||||
|
||||
schema [:map {:title "SetPagePluginData"}
|
||||
[:type [:= :set-plugin-data]]
|
||||
[:object-type [::sm/one-of types]]
|
||||
;; It's optional because files don't need the id for type :file
|
||||
[:object-id {:optional true} ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:namespace {:gen/gen (sg/word-keyword)} :keyword]
|
||||
[:key {:gen/gen (sg/word-string)} :string]
|
||||
[:value [:maybe [:string {:gen/gen (sg/word-string)}]]]]
|
||||
|
||||
check1 [:fn {:error/path [:page-id]
|
||||
:error/message "missing page-id"}
|
||||
(fn [{:keys [object-type] :as change}]
|
||||
(if (= :shape object-type)
|
||||
(uuid? (:page-id change))
|
||||
true))]
|
||||
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/filter :object-id)
|
||||
(sg/filter :page-id)
|
||||
(sg/fmap (fn [{:keys [object-type] :as change}]
|
||||
(cond
|
||||
(= :file object-type)
|
||||
(-> change
|
||||
(dissoc :object-id)
|
||||
(dissoc :page-id))
|
||||
|
||||
(= :shape object-type)
|
||||
change
|
||||
|
||||
:else
|
||||
(dissoc change :page-id)))))]
|
||||
|
||||
[:and {:gen/gen gen} schema check1]))
|
||||
|
||||
(def schema:change
|
||||
[:schema
|
||||
[:multi {:dispatch :type
|
||||
@@ -67,13 +176,18 @@
|
||||
:decode/json #(update % :type keyword)
|
||||
::smd/simplified true}
|
||||
[:set-option
|
||||
[:map {:title "SetOptionChange"}
|
||||
[:type [:= :set-option]]
|
||||
|
||||
;; DEPRECATED: remove before 2.3 release
|
||||
;;
|
||||
;; Is still there for not cause error when event is received
|
||||
[:map {:title "SetOptionChange"}]]
|
||||
|
||||
[:set-comment-thread-position
|
||||
[:map
|
||||
[:comment-thread-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:option [:union
|
||||
[:keyword]
|
||||
[:vector {:gen/max 10} :keyword]]]
|
||||
[:value :any]]]
|
||||
[:frame-id [:maybe ::sm/uuid]]
|
||||
[:position [:maybe ::gpt/point]]]]
|
||||
|
||||
[:add-obj
|
||||
[:map {:title "AddObjChange"}
|
||||
@@ -103,6 +217,10 @@
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
|
||||
[:set-guide schema:set-guide-change]
|
||||
[:set-flow schema:set-flow-change]
|
||||
[:set-default-grid schema:set-default-grid-change]
|
||||
|
||||
[:fix-obj
|
||||
[:map {:title "FixObjChange"}
|
||||
[:type [:= :fix-obj]]
|
||||
@@ -143,19 +261,12 @@
|
||||
[:map {:title "ModPageChange"}
|
||||
[:type [:= :mod-page]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
;; All props are optional, background can be nil because is the
|
||||
;; way to remove already set background
|
||||
[:background {:optional true} [:maybe ::ctc/rgb-color]]
|
||||
[:name {:optional true} :string]]]
|
||||
|
||||
[:mod-plugin-data
|
||||
[:map {:title "ModPagePluginData"}
|
||||
[:type [:= :mod-plugin-data]]
|
||||
[:object-type [::sm/one-of #{:file :page :shape :color :typography :component}]]
|
||||
;; It's optional because files don't need the id for type :file
|
||||
[:object-id {:optional true} [:maybe ::sm/uuid]]
|
||||
;; Only needed in type shape
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:namespace :keyword]
|
||||
[:key :string]
|
||||
[:value [:maybe :string]]]]
|
||||
[:set-plugin-data schema:set-plugin-data-change]
|
||||
|
||||
[:del-page
|
||||
[:map {:title "DelPageChange"}
|
||||
@@ -263,11 +374,11 @@
|
||||
(sm/register! ::change schema:change)
|
||||
(sm/register! ::changes schema:changes)
|
||||
|
||||
(def check-change!
|
||||
(sm/check-fn ::change))
|
||||
(def valid-change?
|
||||
(sm/lazy-validator schema:change))
|
||||
|
||||
(def check-changes!
|
||||
(sm/check-fn ::changes))
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Specific helpers
|
||||
@@ -305,7 +416,7 @@
|
||||
(not= shape-old shape-new))
|
||||
(dm/verify!
|
||||
"expected valid shape"
|
||||
(and (cts/check-shape! shape-new)
|
||||
(and (cts/valid-shape? shape-new)
|
||||
(cts/shape? shape-new))))))]
|
||||
|
||||
(->> (into #{} (map :page-id) items)
|
||||
@@ -339,12 +450,10 @@
|
||||
(process-changes data items true))
|
||||
|
||||
([data items verify?]
|
||||
;; When verify? false we spec the schema validation. Currently used to make just
|
||||
;; 1 validation even if the changes are applied twice
|
||||
;; When verify? false we spec the schema validation. Currently used
|
||||
;; to make just 1 validation even if the changes are applied twice
|
||||
(when verify?
|
||||
(dm/verify!
|
||||
"expected valid changes"
|
||||
(check-changes! items)))
|
||||
(check-changes! items))
|
||||
|
||||
(binding [*touched-changes* (volatile! #{})]
|
||||
(let [result (reduce #(or (process-change %1 %2) %1) data items)
|
||||
@@ -356,14 +465,71 @@
|
||||
#?(:clj (validate-shapes! data result items))
|
||||
result))))
|
||||
|
||||
;; DEPRECATED: remove before 2.3 release
|
||||
(defmethod process-change :set-option
|
||||
[data {:keys [page-id option value]}]
|
||||
[data _]
|
||||
data)
|
||||
|
||||
;; --- Comment Threads
|
||||
|
||||
(defmethod process-change :set-comment-thread-position
|
||||
[data {:keys [page-id comment-thread-id position frame-id]}]
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [data]
|
||||
(let [path (if (seqable? option) option [option])]
|
||||
(if value
|
||||
(assoc-in data (into [:options] path) value)
|
||||
(assoc data :options (d/dissoc-in (:options data) path)))))))
|
||||
(fn [page]
|
||||
(if (and position frame-id)
|
||||
(update page :comment-thread-positions assoc
|
||||
comment-thread-id {:frame-id frame-id
|
||||
:position position})
|
||||
(update page :comment-thread-positions dissoc
|
||||
comment-thread-id)))))
|
||||
|
||||
;; --- Guides
|
||||
|
||||
(defmethod process-change :set-guide
|
||||
[data {:keys [page-id id params]}]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
(let [guides (get page :guides)
|
||||
guides (dissoc guides id)]
|
||||
(if (empty? guides)
|
||||
(dissoc page :guides)
|
||||
(assoc page :guides guides)))))
|
||||
|
||||
(let [params (assoc params :id id)]
|
||||
(d/update-in-when data [:pages-index page-id] update :guides assoc id params))))
|
||||
|
||||
;; --- Flows
|
||||
|
||||
(defmethod process-change :set-flow
|
||||
[data {:keys [page-id id params]}]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
(let [flows (get page :flows)
|
||||
flows (dissoc flows id)]
|
||||
(if (empty? flows)
|
||||
(dissoc page :flows)
|
||||
(assoc page :flows flows)))))
|
||||
|
||||
(let [params (assoc params :id id)]
|
||||
(d/update-in-when data [:pages-index page-id] update :flows assoc id params))))
|
||||
|
||||
;; --- Grids
|
||||
|
||||
(defmethod process-change :set-default-grid
|
||||
[data {:keys [page-id grid-type params]}]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
(let [default-grids (get page :default-grids)
|
||||
default-grids (dissoc default-grids grid-type)]
|
||||
(if (empty? default-grids)
|
||||
(dissoc page :default-grids)
|
||||
(assoc page :default-grids default-grids)))))
|
||||
(d/update-in-when data [:pages-index page-id] update :default-grids assoc grid-type params)))
|
||||
|
||||
;; --- Shape / Obj
|
||||
|
||||
(defmethod process-change :add-obj
|
||||
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
|
||||
@@ -604,25 +770,34 @@
|
||||
(ctpl/add-page data page)))
|
||||
|
||||
(defmethod process-change :mod-page
|
||||
[data {:keys [id name]}]
|
||||
(d/update-in-when data [:pages-index id] assoc :name name))
|
||||
[data {:keys [id] :as params}]
|
||||
(d/update-in-when data [:pages-index id]
|
||||
(fn [page]
|
||||
(let [name (get params :name)
|
||||
bg (get params :background :not-found)]
|
||||
(cond-> page
|
||||
(string? name)
|
||||
(assoc :name name)
|
||||
|
||||
(defmethod process-change :mod-plugin-data
|
||||
(string? bg)
|
||||
(assoc :background bg)
|
||||
|
||||
(nil? bg)
|
||||
(dissoc :background))))))
|
||||
|
||||
(defmethod process-change :set-plugin-data
|
||||
[data {:keys [object-type object-id page-id namespace key value]}]
|
||||
|
||||
(when (and (= object-type :shape) (nil? page-id))
|
||||
(ex/raise :type :internal :hint "update for shapes needs a page-id"))
|
||||
|
||||
(letfn [(update-fn [data]
|
||||
(if (some? value)
|
||||
(assoc-in data [:plugin-data namespace key] value)
|
||||
(update-in data [:plugin-data namespace] (fnil dissoc {}) key)))]
|
||||
(update-in data [:plugin-data namespace] dissoc key)))]
|
||||
|
||||
(case object-type
|
||||
:file
|
||||
(update-fn data)
|
||||
|
||||
:page
|
||||
(d/update-in-when data [:pages-index object-id :options] update-fn)
|
||||
(d/update-in-when data [:pages-index object-id] update-fn)
|
||||
|
||||
:shape
|
||||
(d/update-in-when data [:pages-index page-id :objects object-id] update-fn)
|
||||
@@ -661,6 +836,7 @@
|
||||
[data _]
|
||||
data)
|
||||
|
||||
|
||||
;; -- Media
|
||||
|
||||
(defmethod process-change :add-media
|
||||
|
||||
@@ -135,12 +135,6 @@
|
||||
(or (contains? (meta changes) ::page-id)
|
||||
(contains? (meta changes) ::component-id))))
|
||||
|
||||
(defn- assert-page!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-page) before using this function"
|
||||
(contains? (meta changes) ::page)))
|
||||
|
||||
(defn- assert-objects!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
@@ -195,41 +189,85 @@
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn mod-page
|
||||
[changes page new-name]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-page :id (:id page) :name new-name})
|
||||
(update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)})
|
||||
(apply-changes-local)))
|
||||
([changes options]
|
||||
(let [page (::page (meta changes))]
|
||||
(mod-page changes page options)))
|
||||
|
||||
(defn mod-plugin-data
|
||||
([changes page {:keys [name background]}]
|
||||
(let [change {:type :mod-page :id (:id page)}
|
||||
redo (cond-> change
|
||||
(some? name)
|
||||
(assoc :name name)
|
||||
|
||||
(some? background)
|
||||
(assoc :background background))
|
||||
|
||||
undo (cond-> change
|
||||
(some? name)
|
||||
(assoc :name (:name page))
|
||||
|
||||
(some? background)
|
||||
(assoc :background (:background page)))]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj redo)
|
||||
(update :undo-changes conj undo)
|
||||
(apply-changes-local)))))
|
||||
|
||||
(defn set-plugin-data
|
||||
([changes namespace key value]
|
||||
(mod-plugin-data changes :file nil nil namespace key value))
|
||||
(set-plugin-data changes :file nil nil namespace key value))
|
||||
([changes type id namespace key value]
|
||||
(mod-plugin-data changes type id nil namespace key value))
|
||||
(set-plugin-data changes type id nil namespace key value))
|
||||
([changes type id page-id namespace key value]
|
||||
(let [data (::file-data (meta changes))
|
||||
old-val
|
||||
(case type
|
||||
:file
|
||||
(get-in data [:plugin-data namespace key])
|
||||
(dm/get-in data [:plugin-data namespace key])
|
||||
|
||||
:page
|
||||
(get-in data [:pages-index id :options :plugin-data namespace key])
|
||||
(dm/get-in data [:pages-index id :options :plugin-data namespace key])
|
||||
|
||||
:shape
|
||||
(get-in data [:pages-index page-id :objects id :plugin-data namespace key])
|
||||
(dm/get-in data [:pages-index page-id :objects id :plugin-data namespace key])
|
||||
|
||||
:color
|
||||
(get-in data [:colors id :plugin-data namespace key])
|
||||
(dm/get-in data [:colors id :plugin-data namespace key])
|
||||
|
||||
:typography
|
||||
(get-in data [:typographies id :plugin-data namespace key])
|
||||
(dm/get-in data [:typographies id :plugin-data namespace key])
|
||||
|
||||
:component
|
||||
(get-in data [:components id :plugin-data namespace key]))]
|
||||
(dm/get-in data [:components id :plugin-data namespace key]))
|
||||
|
||||
redo-change
|
||||
(cond-> {:type :set-plugin-data
|
||||
:object-type type
|
||||
:namespace namespace
|
||||
:key key
|
||||
:value value}
|
||||
(uuid? id)
|
||||
(assoc :object-id id)
|
||||
|
||||
(uuid? page-id)
|
||||
(assoc :page-id page-id))
|
||||
|
||||
undo-change
|
||||
(cond-> {:type :set-plugin-data
|
||||
:object-type type
|
||||
:namespace namespace
|
||||
:key key
|
||||
:value old-val}
|
||||
(uuid? id)
|
||||
(assoc :object-id id)
|
||||
|
||||
(uuid? page-id)
|
||||
(assoc :page-id page-id))]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value value})
|
||||
(update :undo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value old-val})
|
||||
(update :redo-changes conj redo-change)
|
||||
(update :undo-changes conj undo-change)
|
||||
(apply-changes-local)))))
|
||||
|
||||
(defn del-page
|
||||
@@ -246,42 +284,76 @@
|
||||
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn set-page-option
|
||||
[changes option-key option-val]
|
||||
(assert-page! changes)
|
||||
(defn set-guide
|
||||
[changes id guide]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (get-in page [:options option-key])]
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:guides id])]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-option
|
||||
(update :redo-changes conj {:type :set-guide
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value option-val})
|
||||
(update :undo-changes conj {:type :set-option
|
||||
:id id
|
||||
:params guide})
|
||||
(update :undo-changes conj {:type :set-guide
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value old-val})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn update-page-option
|
||||
[changes option-key update-fn & args]
|
||||
(assert-page! changes)
|
||||
:id id
|
||||
:params old-val}))))
|
||||
(defn set-flow
|
||||
[changes id flow]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (get-in page [:options option-key])
|
||||
new-val (apply update-fn old-val args)]
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:flows id])
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-option
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value new-val})
|
||||
(update :undo-changes conj {:type :set-option
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value old-val})
|
||||
(apply-changes-local))))
|
||||
changes (-> changes
|
||||
(update :redo-changes conj {:type :set-flow
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params flow})
|
||||
(update :undo-changes conj {:type :set-flow
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params old-val}))]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
(defn set-comment-thread-position
|
||||
[changes {:keys [id frame-id position] :as thread}]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
|
||||
old-val (dm/get-in page [:comment-thread-positions id])
|
||||
|
||||
changes (-> changes
|
||||
(update :redo-changes conj {:type :set-comment-thread-position
|
||||
:comment-thread-id id
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:position position})
|
||||
(update :undo-changes conj {:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id id
|
||||
:frame-id (:frame-id old-val)
|
||||
:position (:position old-val)}))]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
(defn set-default-grid
|
||||
[changes type params]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:grids type])
|
||||
|
||||
changes (update changes :redo-changes conj {:type :set-default-grid
|
||||
:page-id page-id
|
||||
:grid-type type
|
||||
:params params})
|
||||
changes (update changes :undo-changes conj {:type :set-default-grid
|
||||
:page-id page-id
|
||||
:grid-type type
|
||||
:params old-val})]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
;; Shape tree changes
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 54)
|
||||
(def version 55)
|
||||
|
||||
@@ -1046,6 +1046,30 @@
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(defn migrate-up-55
|
||||
"This migration moves page options to the page level"
|
||||
[data]
|
||||
(let [update-page
|
||||
(fn [{:keys [options] :as page}]
|
||||
(cond-> page
|
||||
(and (some? (:saved-grids options))
|
||||
(not (contains? page :default-grids)))
|
||||
(assoc :default-grids (:saved-grids options))
|
||||
|
||||
(and (some? (:flows options))
|
||||
(not (contains? page :flows)))
|
||||
(assoc :flows (:flows options))
|
||||
|
||||
(and (some? (:guides options))
|
||||
(not (contains? page :guides)))
|
||||
(assoc :guides (:guides options))
|
||||
|
||||
(and (some? (:comment-threads-position options))
|
||||
(not (contains? page :comment-thread-positions)))
|
||||
(assoc :comment-thread-positions (:comment-threads-position options))))]
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -1091,4 +1115,5 @@
|
||||
{:id 51 :migrate-up migrate-up-51}
|
||||
{:id 52 :migrate-up migrate-up-52}
|
||||
{:id 53 :migrate-up migrate-up-26}
|
||||
{:id 54 :migrate-up migrate-up-54}])
|
||||
{:id 54 :migrate-up migrate-up-54}
|
||||
{:id 55 :migrate-up migrate-up-55}])
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
[old-page page check-attrs]
|
||||
|
||||
(let [old-objects (get old-page :objects)
|
||||
old-guides (or (get-in old-page [:options :guides]) [])
|
||||
old-guides (or (get old-page :guides) [])
|
||||
|
||||
new-objects (get page :objects)
|
||||
new-guides (or (get-in page [:options :guides]) [])
|
||||
new-guides (or (get page :guides) [])
|
||||
|
||||
changed-object?
|
||||
(fn [id]
|
||||
|
||||
@@ -57,16 +57,17 @@
|
||||
:misplaced-slot
|
||||
:missing-slot})
|
||||
|
||||
(def ^:private
|
||||
schema:error
|
||||
(sm/define
|
||||
[:map {:title "ValidationError"}
|
||||
[:code {:optional false} [::sm/one-of error-codes]]
|
||||
[:hint {:optional false} :string]
|
||||
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
||||
[:shape-id {:optional true} ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]]))
|
||||
(def ^:private schema:error
|
||||
[:map {:title "ValidationError"}
|
||||
[:code {:optional false} [::sm/one-of error-codes]]
|
||||
[:hint {:optional false} :string]
|
||||
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
||||
[:shape-id {:optional true} ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def check-error!
|
||||
(sm/check-fn schema:error))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ERROR HANDLING
|
||||
@@ -95,7 +96,7 @@
|
||||
|
||||
(dm/assert!
|
||||
"expected valid error"
|
||||
(sm/check! schema:error error))
|
||||
(check-error! error))
|
||||
|
||||
(vswap! *errors* conj error)))
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
java.time.Instant
|
||||
java.time.OffsetDateTime
|
||||
java.util.List
|
||||
linked.map.LinkedMap
|
||||
org.fressian.Reader
|
||||
org.fressian.StreamingWriter
|
||||
org.fressian.Writer
|
||||
@@ -109,6 +110,13 @@
|
||||
(clojure.lang.PersistentArrayMap. (.toArray kvs))
|
||||
(clojure.lang.PersistentHashMap/create (seq kvs)))))
|
||||
|
||||
(defn read-ordered-map
|
||||
[^Reader rdr]
|
||||
(let [kvs ^java.util.List (read-object! rdr)]
|
||||
(reduce #(assoc %1 (first %2) (second %2))
|
||||
(d/ordered-map)
|
||||
(partition-all 2 (seq kvs)))))
|
||||
|
||||
(def ^:dynamic *write-handler-lookup* nil)
|
||||
(def ^:dynamic *read-handler-lookup* nil)
|
||||
|
||||
@@ -225,6 +233,11 @@
|
||||
:wfn write-map-like
|
||||
:rfn read-map-like}
|
||||
|
||||
{:name "linked/map"
|
||||
:class LinkedMap
|
||||
:wfn write-map-like
|
||||
:rfn read-ordered-map}
|
||||
|
||||
{:name "clj/keyword"
|
||||
:class clojure.lang.Keyword
|
||||
:wfn write-named
|
||||
|
||||
@@ -1947,54 +1947,54 @@
|
||||
|
||||
(defn generate-duplicate-flows
|
||||
[changes shapes page ids-map]
|
||||
(let [flows (-> page :options :flows)
|
||||
unames (volatile! (into #{} (map :name flows)))
|
||||
frames-with-flow (->> shapes
|
||||
(filter #(= (:type %) :frame))
|
||||
(filter #(some? (ctp/get-frame-flow flows (:id %)))))]
|
||||
(if-not (empty? frames-with-flow)
|
||||
(let [update-flows (fn [flows]
|
||||
(reduce
|
||||
(fn [flows frame]
|
||||
(let [name (cfh/generate-unique-name @unames "Flow 1")
|
||||
_ (vswap! unames conj name)
|
||||
new-flow {:id (uuid/next)
|
||||
:name name
|
||||
:starting-frame (get ids-map (:id frame))}]
|
||||
(ctp/add-flow flows new-flow)))
|
||||
flows
|
||||
frames-with-flow))]
|
||||
(pcb/update-page-option changes :flows update-flows))
|
||||
changes)))
|
||||
(let [flows (get page :flows)
|
||||
unames (volatile! (cfh/get-used-names (vals flows)))
|
||||
has-flow? (partial ctp/get-frame-flow flows)]
|
||||
|
||||
(reduce (fn [changes frame-id]
|
||||
(let [name (cfh/generate-unique-name @unames "Flow 1")
|
||||
frame-id (get ids-map frame-id)
|
||||
flow-id (uuid/next)
|
||||
new-flow {:id flow-id
|
||||
:name name
|
||||
:starting-frame frame-id}]
|
||||
|
||||
(vswap! unames conj name)
|
||||
(pcb/set-flow changes flow-id new-flow)))
|
||||
|
||||
changes
|
||||
(->> shapes
|
||||
(filter cfh/frame-shape?)
|
||||
(map :id)
|
||||
(filter has-flow?)))))
|
||||
|
||||
(defn generate-duplicate-guides
|
||||
[changes shapes page ids-map delta]
|
||||
(let [guides (get-in page [:options :guides])
|
||||
frames (->> shapes (filter cfh/frame-shape?))
|
||||
(let [guides (get page :guides)
|
||||
frames (filter cfh/frame-shape? shapes)]
|
||||
|
||||
new-guides
|
||||
(reduce
|
||||
(fn [g frame]
|
||||
(let [new-id (ids-map (:id frame))
|
||||
new-frame (-> frame (gsh/move delta))
|
||||
;; FIXME: this can be implemented efficiently just indexing guides
|
||||
;; by frame-id instead of iterate over all guides all the time
|
||||
|
||||
new-guides
|
||||
(->> guides
|
||||
(vals)
|
||||
(filter #(= (:frame-id %) (:id frame)))
|
||||
(map #(-> %
|
||||
(assoc :id (uuid/next))
|
||||
(assoc :frame-id new-id)
|
||||
(assoc :position (if (= (:axis %) :x)
|
||||
(+ (:position %) (- (:x new-frame) (:x frame)))
|
||||
(+ (:position %) (- (:y new-frame) (:y frame))))))))]
|
||||
(cond-> g
|
||||
(not-empty new-guides)
|
||||
(conj (into {} (map (juxt :id identity) new-guides))))))
|
||||
guides
|
||||
frames)]
|
||||
(-> (pcb/with-page changes page)
|
||||
(pcb/set-page-option :guides new-guides))))
|
||||
(reduce (fn [changes frame]
|
||||
(let [new-id (get ids-map (:id frame))
|
||||
new-frame (gsh/move frame delta)]
|
||||
|
||||
(reduce-kv (fn [changes _ guide]
|
||||
(if (= (:id frame) (:frame-id guide))
|
||||
(let [guide-id (uuid/next)
|
||||
position (if (= (:axis guide) :x)
|
||||
(+ (:position guide) (- (:x new-frame) (:x frame)))
|
||||
(+ (:position guide) (- (:y new-frame) (:y frame))))
|
||||
guide {:id guide-id
|
||||
:frame-id new-id
|
||||
:position position}]
|
||||
(pcb/set-guide changes guide-id guide))
|
||||
changes))
|
||||
changes
|
||||
guides)))
|
||||
(pcb/with-page changes page)
|
||||
frames)))
|
||||
|
||||
(defn generate-duplicate-component-change
|
||||
[changes objects page component-root parent-id frame-id delta libraries library-data]
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
(ns app.common.logic.shapes
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]))
|
||||
@@ -85,7 +83,9 @@
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
@@ -110,23 +110,14 @@
|
||||
interactions)))
|
||||
(vals objects))
|
||||
|
||||
ids-set (set ids-to-delete)
|
||||
guides-to-remove
|
||||
(->> (dm/get-in page [:options :guides])
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
changes
|
||||
(reduce (fn [changes {:keys [id] :as flow}]
|
||||
(if (contains? ids-to-delete (:starting-frame flow))
|
||||
(pcb/set-flow changes id nil)
|
||||
changes))
|
||||
changes
|
||||
(:flows page))
|
||||
|
||||
guides
|
||||
(->> guides-to-remove
|
||||
(reduce dissoc (dm/get-in page [:options :guides])))
|
||||
|
||||
starting-flows
|
||||
(filter (fn [flow]
|
||||
;; If any of the deleted is a frame that starts a flow,
|
||||
;; this must be deleted, too.
|
||||
(contains? ids-to-delete (:starting-frame flow)))
|
||||
(-> page :options :flows))
|
||||
|
||||
all-parents
|
||||
(reduce (fn [res id]
|
||||
@@ -176,8 +167,18 @@
|
||||
(into ids-to-delete all-children))
|
||||
[])
|
||||
|
||||
changes (-> changes
|
||||
(pcb/set-page-option :guides guides))
|
||||
ids-set (set ids-to-delete)
|
||||
|
||||
guides-to-delete
|
||||
(->> (:guides page)
|
||||
(vals)
|
||||
(filter #(contains? ids-set (:frame-id %)))
|
||||
(map :id))
|
||||
|
||||
changes (reduce (fn [changes guide-id]
|
||||
(pcb/set-flow changes guide-id nil))
|
||||
changes
|
||||
guides-to-delete)
|
||||
|
||||
changes (reduce (fn [changes component-id]
|
||||
;; It's important to delete the component before the main instance, because we
|
||||
@@ -185,6 +186,7 @@
|
||||
(pcb/delete-component changes component-id (:id page)))
|
||||
changes
|
||||
components-to-delete)
|
||||
|
||||
changes (-> changes
|
||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||
(pcb/remove-objects all-children {:ignore-touched true})
|
||||
@@ -201,11 +203,7 @@
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(contains? ids-to-delete (:destination %))))
|
||||
interactions)))))
|
||||
(cond-> (seq starting-flows)
|
||||
(pcb/update-page-option :flows (fn [flows]
|
||||
(->> (map :id starting-flows)
|
||||
(reduce ctp/remove-flow flows))))))]
|
||||
interactions))))))]
|
||||
[all-parents changes]))
|
||||
|
||||
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
[malli.util :as mu]))
|
||||
|
||||
(defprotocol ILazySchema
|
||||
(-get-schema [_])
|
||||
(-get-validator [_])
|
||||
(-get-explainer [_])
|
||||
(-get-decoder [_])
|
||||
(-get-encoder [_])
|
||||
(-validate [_ o])
|
||||
(-explain [_ o])
|
||||
(-decode [_ o]))
|
||||
@@ -53,27 +48,21 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn lazy-schema?
|
||||
(defn- lazy-schema?
|
||||
[s]
|
||||
(satisfies? ILazySchema s))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-schema s)
|
||||
(m/schema s default-options)))
|
||||
(m/schema s default-options))
|
||||
|
||||
(defn validate
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(-validate s value)
|
||||
(m/validate s value default-options)))
|
||||
(m/validate s value default-options))
|
||||
|
||||
(defn explain
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(-explain s value)
|
||||
(m/explain s value default-options)))
|
||||
(m/explain s value default-options))
|
||||
|
||||
(defn simplify
|
||||
"Given an explain data structure, return a simplified version of it"
|
||||
@@ -171,29 +160,19 @@
|
||||
|
||||
(defn validator
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-validator s)
|
||||
(-> s schema m/validator)))
|
||||
(-> s schema m/validator))
|
||||
|
||||
(defn explainer
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-explainer s)
|
||||
(-> s schema m/explainer)))
|
||||
(-> s schema m/explainer))
|
||||
|
||||
(defn encoder
|
||||
([s]
|
||||
(assert (lazy-schema? s) "expected lazy schema")
|
||||
(-get-decoder s))
|
||||
([s transformer]
|
||||
(m/encoder s default-options transformer))
|
||||
([s options transformer]
|
||||
(m/encoder s options transformer)))
|
||||
|
||||
(defn decoder
|
||||
([s]
|
||||
(assert (lazy-schema? s) "expected lazy schema")
|
||||
(-get-decoder s))
|
||||
([s transformer]
|
||||
(m/decoder s default-options transformer))
|
||||
([s options transformer]
|
||||
@@ -242,6 +221,8 @@
|
||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
||||
|
||||
(defn pretty-explain
|
||||
"A helper that allows print a console-friendly output for the
|
||||
explain; should not be used for other purposes"
|
||||
[explain & {:keys [variant message]
|
||||
:or {variant ::explain
|
||||
message "Validation Error"}}]
|
||||
@@ -259,7 +240,7 @@
|
||||
([s] (lookup sr/default-registry s))
|
||||
([registry s] (schema (mr/schema registry s))))
|
||||
|
||||
(defn fast-check!
|
||||
(defn- fast-check!
|
||||
"A fast path for checking process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
[s value]
|
||||
@@ -272,12 +253,12 @@
|
||||
::explain explain}))))
|
||||
true)
|
||||
|
||||
(declare define)
|
||||
(declare ^:private lazy-schema)
|
||||
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (define s))]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(partial fast-check! schema)))
|
||||
|
||||
(defn check!
|
||||
@@ -285,19 +266,10 @@
|
||||
schema over provided data. Raises an assertion exception, should be
|
||||
used together with `dm/assert!` or `dm/verify!`."
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(fast-check! s value)
|
||||
(do
|
||||
(when-not ^boolean (m/validate s value default-options)
|
||||
(let [hint (d/nilv dm/*assert-context* "check error")
|
||||
explain (explain s value)]
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :data-validation
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
true)))
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(fast-check! s value)))
|
||||
|
||||
(defn fast-validate!
|
||||
(defn- fast-validate!
|
||||
"A fast path for validation process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
([s value] (fast-validate! s value nil))
|
||||
@@ -309,52 +281,33 @@
|
||||
::explain explain}
|
||||
options)
|
||||
hint (get options :hint "schema validation error")]
|
||||
(throw (ex-info hint options))))))
|
||||
(throw (ex-info hint options))))
|
||||
value))
|
||||
|
||||
(defn validate-fn
|
||||
"Create a predefined validate function that raises an expception"
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (define s))]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(partial fast-validate! schema)))
|
||||
|
||||
(defn validate!
|
||||
"A generic validation function for predefined schemas."
|
||||
([s value] (validate! s value nil))
|
||||
([s value options]
|
||||
(if (lazy-schema? s)
|
||||
(fast-validate! s value options)
|
||||
(when-not ^boolean (m/validate s value default-options)
|
||||
(let [explain (explain s value)
|
||||
options (into {:type :validation
|
||||
:code :data-validation
|
||||
::explain explain}
|
||||
options)
|
||||
hint (get options :hint "schema validation error")]
|
||||
(throw (ex-info hint options)))))))
|
||||
|
||||
;; FIXME: revisit
|
||||
(defn conform!
|
||||
[schema value]
|
||||
(assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol")
|
||||
(let [params (-decode schema value)]
|
||||
(fast-validate! schema params nil)
|
||||
params))
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))]
|
||||
(fast-validate! s value options))))
|
||||
|
||||
(defn register! [type s]
|
||||
(let [s (if (map? s) (m/-simple-schema s) s)]
|
||||
(swap! sr/registry assoc type s)
|
||||
nil))
|
||||
|
||||
(defn define
|
||||
(defn- lazy-schema
|
||||
"Create ans instance of ILazySchema"
|
||||
[s & {:keys [transformer] :or {transformer json-transformer} :as options}]
|
||||
[s]
|
||||
(let [schema (delay (schema s))
|
||||
validator (delay (m/validator @schema))
|
||||
explainer (delay (m/explainer @schema))
|
||||
|
||||
options (c/merge default-options (dissoc options :transformer))
|
||||
decoder (delay (m/decoder @schema options transformer))
|
||||
encoder (delay (m/encoder @schema options transformer))]
|
||||
explainer (delay (m/explainer @schema))]
|
||||
|
||||
(reify
|
||||
m/AST
|
||||
@@ -397,16 +350,6 @@
|
||||
(m/-form @schema))
|
||||
|
||||
ILazySchema
|
||||
(-get-schema [_]
|
||||
@schema)
|
||||
(-get-validator [_]
|
||||
@validator)
|
||||
(-get-explainer [_]
|
||||
@explainer)
|
||||
(-get-encoder [_]
|
||||
@encoder)
|
||||
(-get-decoder [_]
|
||||
@decoder)
|
||||
(-validate [_ o]
|
||||
(@validator o))
|
||||
(-explain [_ o]
|
||||
|
||||
@@ -5,46 +5,21 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.generators
|
||||
(:refer-clojure :exclude [set subseq uuid for filter map let boolean])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.schema.registry :as sr]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.core :as c]
|
||||
[clojure.test.check :as tc]
|
||||
[clojure.test.check.generators :as tg]
|
||||
[clojure.test.check.properties :as tp]
|
||||
[cuerdas.core :as str]
|
||||
[malli.generator :as mg]))
|
||||
|
||||
(defn default-reporter-fn
|
||||
[{:keys [type result] :as args}]
|
||||
(case type
|
||||
:complete
|
||||
(prn (select-keys args [:result :num-tests :seed "time-elapsed-ms"]))
|
||||
|
||||
:failure
|
||||
(do
|
||||
(prn (select-keys args [:num-tests :seed :failed-after-ms]))
|
||||
(when #?(:clj (instance? Throwable result)
|
||||
:cljs (instance? js/Error result))
|
||||
(throw result)))
|
||||
|
||||
nil))
|
||||
|
||||
(defmacro for
|
||||
[& params]
|
||||
`(tp/for-all ~@params))
|
||||
|
||||
(defmacro let
|
||||
[& params]
|
||||
`(tg/let ~@params))
|
||||
|
||||
(defn check!
|
||||
[p & {:keys [num] :or {num 20} :as options}]
|
||||
(tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50)))
|
||||
|
||||
(defn sample
|
||||
([g]
|
||||
(mg/sample g {:registry sr/default-registry}))
|
||||
@@ -83,6 +58,11 @@
|
||||
(tg/such-that (fn [v] (>= (count v) 4)) $$ 100)
|
||||
(tg/fmap str/lower $$)))
|
||||
|
||||
(defn word-keyword
|
||||
[]
|
||||
(->> (word-string)
|
||||
(tg/fmap keyword)))
|
||||
|
||||
(defn email
|
||||
[]
|
||||
(->> (word-string)
|
||||
@@ -91,7 +71,6 @@
|
||||
(tg/fmap (fn [v]
|
||||
(str v "@example.net")))))
|
||||
|
||||
|
||||
(defn uri
|
||||
[]
|
||||
(tg/let [scheme (tg/elements ["http" "https"])
|
||||
@@ -103,8 +82,7 @@
|
||||
|
||||
(defn uuid
|
||||
[]
|
||||
(->> tg/small-integer
|
||||
(tg/fmap (fn [_] (uuid/next)))))
|
||||
(tg/fmap (fn [_] (uuid/next)) (small-int)))
|
||||
|
||||
(defn subseq
|
||||
"Given a collection, generates \"subsequences\" which are sequences
|
||||
|
||||
97
common/src/app/common/schema/test.cljc
Normal file
97
common/src/app/common/schema/test.cljc
Normal file
@@ -0,0 +1,97 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.test
|
||||
(:refer-clojure :exclude [for])
|
||||
#?(:cljs (:require-macros [app.common.schema.test]))
|
||||
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[clojure.test :as ct]
|
||||
[clojure.test.check :as tc]
|
||||
[clojure.test.check.properties :as tp]))
|
||||
|
||||
(defn- get-testing-var
|
||||
[]
|
||||
(let [testing-vars #?(:clj ct/*testing-vars*
|
||||
:cljs (:testing-vars ct/*current-env*))]
|
||||
(first testing-vars)))
|
||||
|
||||
(defn- get-testing-sym
|
||||
[var]
|
||||
(let [tmeta (meta var)]
|
||||
(:name tmeta)))
|
||||
|
||||
(defn default-reporter-fn
|
||||
"Default function passed as the :reporter-fn to clojure.test.check/quick-check.
|
||||
Delegates to clojure.test/report."
|
||||
[{:keys [type] :as args}]
|
||||
(case type
|
||||
:complete
|
||||
(ct/report {:type ::complete ::params args})
|
||||
|
||||
:trial
|
||||
(ct/report {:type ::trial ::params args})
|
||||
|
||||
:failure
|
||||
(ct/report {:type ::fail ::params args})
|
||||
|
||||
:shrunk
|
||||
(ct/report {:type ::thrunk ::params args})
|
||||
|
||||
nil))
|
||||
|
||||
(defmethod ct/report #?(:clj ::complete :cljs [:cljs.test/default ::complete])
|
||||
[{:keys [::params] :as m}]
|
||||
#?(:clj (ct/inc-report-counter :pass)
|
||||
:cljs (ct/inc-report-counter! :pass))
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
time (:time-elapsed-ms params)]
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=TRUE, tests=" (:num-tests params) ", seed=" (:seed params) ", elapsed=" time "ms)"))))
|
||||
|
||||
(defmethod ct/report #?(:clj ::thrunk :cljs [:cljs.test/default ::thrunk])
|
||||
[{:keys [::params] :as m}]
|
||||
(let [smallest (-> params :shrunk :smallest vec)]
|
||||
(println)
|
||||
(println "Condition failed with the following params:")
|
||||
(println)
|
||||
(pp/pprint smallest)))
|
||||
|
||||
(defmethod ct/report #?(:clj ::trial :cljs [:cljs.test/default ::trial])
|
||||
[_]
|
||||
#?(:clj (ct/inc-report-counter :pass)
|
||||
:cljs (ct/inc-report-counter! :pass)))
|
||||
|
||||
(defmethod ct/report #?(:clj ::fail :cljs [:cljs.test/default ::fail])
|
||||
[{:keys [::params] :as m}]
|
||||
#?(:clj (ct/inc-report-counter :fail)
|
||||
:cljs (ct/inc-report-counter! :fail))
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
res (:result params)]
|
||||
(println)
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=FALSE, tests=" (:num-tests params) ", seed=" (:seed params) ")"))
|
||||
|
||||
(when (ex/exception? res)
|
||||
#?(:clj (ex/print-throwable res)
|
||||
:cljs (js/console.error res)))))
|
||||
|
||||
(defmacro for
|
||||
[bindings & body]
|
||||
`(tp/for-all ~bindings ~@body))
|
||||
|
||||
(defn check!
|
||||
[p & {:keys [num] :or {num 20} :as options}]
|
||||
(let [result (tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50))
|
||||
pass? (:pass? result)
|
||||
total-tests (:num-tests result)]
|
||||
|
||||
(ct/is (= num total-tests))
|
||||
(ct/is (true? pass?))))
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.uri :as uri]
|
||||
[cognitect.transit :as t]
|
||||
[lambdaisland.uri :as luri]
|
||||
[linked.core :as lk]
|
||||
[linked.map :as lkm]
|
||||
[linked.set :as lks])
|
||||
#?(:clj
|
||||
(:import
|
||||
@@ -24,6 +24,7 @@
|
||||
java.time.Instant
|
||||
java.time.OffsetDateTime
|
||||
lambdaisland.uri.URI
|
||||
linked.map.LinkedMap
|
||||
linked.set.LinkedSet)))
|
||||
|
||||
(def write-handlers (atom nil))
|
||||
@@ -118,10 +119,15 @@
|
||||
{:id "u"
|
||||
:rfn parse-uuid})
|
||||
|
||||
{:id "ordered-map"
|
||||
:class #?(:clj LinkedMap :cljs lkm/LinkedMap)
|
||||
:wfn vec
|
||||
:rfn #(into lkm/empty-linked-map %)}
|
||||
|
||||
{:id "ordered-set"
|
||||
:class #?(:clj LinkedSet :cljs lks/LinkedSet)
|
||||
:wfn vec
|
||||
:rfn #(into (lk/set) %)}
|
||||
:rfn #(into lks/empty-linked-set %)}
|
||||
|
||||
{:id "duration"
|
||||
:class #?(:clj Duration :cljs lxn/Duration)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.types.grid
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as ctc]))
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
[:display :boolean]
|
||||
[:params schema:square-params]]]])
|
||||
|
||||
(def schema:saved-grids
|
||||
(def schema:default-grids
|
||||
[:map {:title "PageGrid"}
|
||||
[:square {:optional true} ::square-params]
|
||||
[:row {:optional true} ::column-params]
|
||||
@@ -63,4 +64,24 @@
|
||||
(sm/register! ::square-params schema:square-params)
|
||||
(sm/register! ::column-params schema:column-params)
|
||||
(sm/register! ::grid schema:grid)
|
||||
(sm/register! ::saved-grids schema:saved-grids)
|
||||
(sm/register! ::default-grids schema:default-grids)
|
||||
|
||||
(def ^:private default-square-params
|
||||
{:size 16
|
||||
:color {:color clr/info
|
||||
:opacity 0.4}})
|
||||
|
||||
(def ^:private default-layout-params
|
||||
{:size 12
|
||||
:type :stretch
|
||||
:item-length nil
|
||||
:gutter 8
|
||||
:margin 0
|
||||
:color {:color clr/default-layout
|
||||
:opacity 0.1}})
|
||||
|
||||
(def default-grid-params
|
||||
{:square default-square-params
|
||||
:column default-layout-params
|
||||
:row default-layout-params})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.common.types.page
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as-alias gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as-alias ctc]
|
||||
[app.common.types.grid :as ctg]
|
||||
@@ -24,38 +25,56 @@
|
||||
[:name :string]
|
||||
[:starting-frame ::sm/uuid]])
|
||||
|
||||
(def schema:flows
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:flow])
|
||||
|
||||
(def schema:guide
|
||||
[:map {:title "Guide"}
|
||||
[:id ::sm/uuid]
|
||||
[:axis [::sm/one-of #{:x :y}]]
|
||||
[:position ::sm/safe-number]
|
||||
;; FIXME: remove maybe?
|
||||
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def schema:guides
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
|
||||
|
||||
(def schema:objects
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape])
|
||||
|
||||
(def schema:comment-thread-position
|
||||
[:map {:title "CommentThreadPosition"}
|
||||
[:frame-id ::sm/uuid]
|
||||
[:position ::gpt/point]])
|
||||
|
||||
(def schema:page
|
||||
[:map {:title "FilePage"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:objects
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape]]
|
||||
[:objects schema:objects]
|
||||
[:default-grids {:optional true} ::ctg/default-grids]
|
||||
[:flows {:optional true} schema:flows]
|
||||
[:guides {:optional true} schema:guides]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]
|
||||
[:background {:optional true} ::ctc/rgb-color]
|
||||
|
||||
[:comment-thread-positions {:optional true}
|
||||
[:map-of ::sm/uuid schema:comment-thread-position]]
|
||||
|
||||
[:options
|
||||
[:map {:title "PageOptions"}
|
||||
[:background {:optional true} ::ctc/rgb-color]
|
||||
[:saved-grids {:optional true} ::ctg/saved-grids]
|
||||
[:flows {:optional true}
|
||||
[:vector {:gen/max 2} schema:flow]]
|
||||
[:guides {:optional true}
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]]]])
|
||||
;; DEPERECATED: remove after 2.3 release
|
||||
[:map {:title "PageOptions"}]]])
|
||||
|
||||
(sm/register! ::page schema:page)
|
||||
(sm/register! ::guide schema:guide)
|
||||
(sm/register! ::flow schema:flow)
|
||||
|
||||
(def check-page-guide!
|
||||
(sm/check-fn ::guide))
|
||||
(def valid-guide?
|
||||
(sm/lazy-validator schema:guide))
|
||||
|
||||
;; FIXME: convert to validator
|
||||
(def check-page!
|
||||
(sm/check-fn ::page))
|
||||
(sm/check-fn schema:page))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INIT & HELPERS
|
||||
@@ -80,25 +99,6 @@
|
||||
(assoc :id (or id (uuid/next)))
|
||||
(assoc :name (or name "Page 1"))))
|
||||
|
||||
;; --- Helpers for flow
|
||||
|
||||
(defn rename-flow
|
||||
[flow name]
|
||||
(assoc flow :name name))
|
||||
|
||||
(defn add-flow
|
||||
[flows flow]
|
||||
(conj (or flows []) flow))
|
||||
|
||||
(defn remove-flow
|
||||
[flows flow-id]
|
||||
(d/removev #(= (:id %) flow-id) flows))
|
||||
|
||||
(defn update-flow
|
||||
[flows flow-id update-fn]
|
||||
(let [index (d/index-of-pred flows #(= (:id %) flow-id))]
|
||||
(update flows index update-fn)))
|
||||
|
||||
(defn get-frame-flow
|
||||
[flows frame-id]
|
||||
(d/seek #(= (:starting-frame %) frame-id) flows))
|
||||
(d/seek #(= (:starting-frame %) frame-id) (vals flows)))
|
||||
|
||||
@@ -29,3 +29,25 @@
|
||||
schema:string]])
|
||||
|
||||
(sm/register! ::plugin-data schema:plugin-data)
|
||||
|
||||
|
||||
(def ^:private schema:registry-entry
|
||||
[:map
|
||||
[:plugin-id :string]
|
||||
[:name :string]
|
||||
[:description {:optional true} :string]
|
||||
[:host :string]
|
||||
[:code :string]
|
||||
[:icon {:optional true} :string]
|
||||
[:permissions [:set :string]]])
|
||||
|
||||
(def schema:plugin-registry
|
||||
[:map
|
||||
[:ids [:vector :string]]
|
||||
[:data
|
||||
[:map-of {:gen/max 5}
|
||||
:string
|
||||
schema:registry-entry]]])
|
||||
|
||||
(sm/register! ::plugin-registry schema:plugin-registry)
|
||||
(sm/register! ::registry-entry schema:registry-entry)
|
||||
|
||||
@@ -357,6 +357,9 @@
|
||||
(def check-shape!
|
||||
(sm/check-fn schema:shape))
|
||||
|
||||
(def valid-shape?
|
||||
(sm/lazy-validator schema:shape))
|
||||
|
||||
(defn has-images?
|
||||
[{:keys [fills strokes]}]
|
||||
(or (some :fill-image fills)
|
||||
|
||||
864
common/test/common_tests/files_changes_test.cljc
Normal file
864
common/test/common_tests/files_changes_test.cljc
Normal file
@@ -0,0 +1,864 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.files-changes-test
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]
|
||||
[common-tests.types.shape-decode-encode-test :refer [json-roundtrip]]))
|
||||
|
||||
(defn- make-file-data
|
||||
[file-id page-id]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data file-id page-id)))
|
||||
|
||||
(t/deftest add-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
id-a (uuid/custom 2 1)
|
||||
id-b (uuid/custom 2 2)
|
||||
id-c (uuid/custom 2 3)]
|
||||
|
||||
(t/testing "Adds single object"
|
||||
(let [chg {:type :add-obj
|
||||
:page-id page-id
|
||||
:id id-a
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:id id-a
|
||||
:type :rect
|
||||
:name "rect"})}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 2 (count objects)))
|
||||
(t/is (= (:obj chg) (get objects id-a)))
|
||||
|
||||
(t/is (= [id-a] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
|
||||
(t/testing "Adds several objects with different indexes"
|
||||
(let [chg (fn [id index]
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:frame-id uuid/zero
|
||||
:index index
|
||||
:obj (cts/setup-shape
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name (str id)})})
|
||||
res (ch/process-changes data [(chg id-a 0)
|
||||
(chg id-b 0)
|
||||
(chg id-c 1)])]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (clojure.pprint/pprint res)
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 4 (count objects)))
|
||||
(t/is (not (nil? (get objects id-a))))
|
||||
(t/is (not (nil? (get objects id-b))))
|
||||
(t/is (not (nil? (get objects id-c))))
|
||||
(t/is (= [id-b id-c id-a] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
(t/deftest mod-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "simple mod-obj"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id uuid/zero
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= "foobar" (get-in objects [uuid/zero :name]))))))
|
||||
|
||||
(t/testing "mod-obj for not existing shape"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id (uuid/next)
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= res data))))))
|
||||
|
||||
|
||||
(t/deftest del-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
id (uuid/custom 2 1)
|
||||
data (make-file-data file-id page-id)
|
||||
data (-> data
|
||||
(assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id])
|
||||
(assoc-in [:pages-index page-id :objects id]
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name "rect"}))]
|
||||
(t/testing "delete"
|
||||
(let [chg {:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 1 (count objects)))
|
||||
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
(t/testing "delete idempotency"
|
||||
(let [chg {:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}
|
||||
res1 (ch/process-changes data [chg])
|
||||
res2 (ch/process-changes res1 [chg])]
|
||||
|
||||
(t/is (= res1 res2))
|
||||
(let [objects (get-in res1 [:pages-index page-id :objects])]
|
||||
(t/is (= 1 (count objects)))
|
||||
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
|
||||
(t/deftest move-objects-1
|
||||
(let [frame-a-id (uuid/custom 0 1)
|
||||
frame-b-id (uuid/custom 0 2)
|
||||
group-a-id (uuid/custom 0 3)
|
||||
group-b-id (uuid/custom 0 4)
|
||||
rect-a-id (uuid/custom 0 5)
|
||||
rect-b-id (uuid/custom 0 6)
|
||||
rect-c-id (uuid/custom 0 7)
|
||||
rect-d-id (uuid/custom 0 8)
|
||||
rect-e-id (uuid/custom 0 9)
|
||||
|
||||
file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
|
||||
data (update-in data [:pages-index page-id :objects]
|
||||
#(-> %
|
||||
(assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id])
|
||||
(assoc-in [frame-a-id]
|
||||
(cts/setup-shape
|
||||
{:id frame-a-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:name "Frame a"
|
||||
:shapes [group-a-id group-b-id rect-e-id]
|
||||
:type :frame}))
|
||||
|
||||
(assoc-in [frame-b-id]
|
||||
(cts/setup-shape
|
||||
{:id frame-b-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:name "Frame b"
|
||||
:shapes []
|
||||
:type :frame}))
|
||||
|
||||
;; Groups
|
||||
(assoc-in [group-a-id]
|
||||
(cts/setup-shape
|
||||
{:id group-a-id
|
||||
:name "Group A"
|
||||
:type :group
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id
|
||||
:shapes [rect-a-id rect-b-id rect-c-id]}))
|
||||
(assoc-in [group-b-id]
|
||||
(cts/setup-shape
|
||||
{:id group-b-id
|
||||
:name "Group B"
|
||||
:type :group
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id
|
||||
:shapes [rect-d-id]}))
|
||||
|
||||
;; Shapes
|
||||
(assoc-in [rect-a-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-a-id
|
||||
:name "Rect A"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-b-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-b-id
|
||||
:name "Rect B"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-c-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-c-id
|
||||
:name "Rect C"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-d-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-d-id
|
||||
:name "Rect D"
|
||||
:parent-id group-b-id
|
||||
:type :rect
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-e-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-e-id
|
||||
:name "Rect E"
|
||||
:type :rect
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id}))))]
|
||||
|
||||
(t/testing "Create new group an add objects from the same group"
|
||||
(let [new-group-id (uuid/next)
|
||||
changes [{:type :add-obj
|
||||
:page-id page-id
|
||||
:id new-group-id
|
||||
:frame-id frame-a-id
|
||||
:obj (cts/setup-shape
|
||||
{:id new-group-id
|
||||
:type :group
|
||||
:frame-id frame-a-id
|
||||
:name "Group C"})}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id new-group-id
|
||||
:shapes [rect-b-id rect-c-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (println "===============")
|
||||
;; (clojure.pprint/pprint res)
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id new-group-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [new-group-id :shapes])))
|
||||
(t/is (= [rect-a-id]
|
||||
(get-in objects [group-a-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-c-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-c-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements from group and frame to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements from several groups"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move all elements from a group"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-a-id
|
||||
:shapes [rect-d-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (empty? (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements to a group with different frame"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id frame-b-id
|
||||
:shapes [group-a-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects]))
|
||||
;; (println "==========")
|
||||
;; (pprint (get-in res [:pages-index page-id :objects]))
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [group-a-id] (get-in objects [frame-b-id :shapes])))
|
||||
(t/is (= frame-b-id (get-in objects [group-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-b-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-c-id :frame-id]))))))
|
||||
|
||||
(t/testing "Move elements to frame zero"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:shapes [group-a-id]
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (pprint (get-in data [:objects uuid/zero]))
|
||||
;; (println "==========")
|
||||
;; (pprint (get-in objects [uuid/zero]))
|
||||
|
||||
(t/is (= [group-a-id frame-a-id frame-b-id]
|
||||
(get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
(t/testing "Don't allow to move inside self"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-a-id
|
||||
:shapes [group-a-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
(t/is (= data res))))))
|
||||
|
||||
|
||||
(t/deftest mov-objects-regression-1
|
||||
(let [shape-1-id (uuid/custom 2 1)
|
||||
shape-2-id (uuid/custom 2 2)
|
||||
shape-3-id (uuid/custom 2 3)
|
||||
frame-id (uuid/custom 1 1)
|
||||
file-id (uuid/custom 4 4)
|
||||
page-id (uuid/custom 0 1)
|
||||
|
||||
changes [{:type :add-obj
|
||||
:id frame-id
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :frame
|
||||
:name "Frame"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:parent-id frame-id
|
||||
:id shape-1-id
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 1"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-2-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 2"})}
|
||||
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-3-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 3"})}]
|
||||
data (make-file-data file-id page-id)
|
||||
data (ch/process-changes data changes)]
|
||||
|
||||
(t/testing "preserve order on multiple shape mov 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id shape-3-id]
|
||||
:parent-id uuid/zero
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (println "==> BEFORE")
|
||||
;; (pprint (get-in data [:objects]))
|
||||
;; (println "==> AFTER")
|
||||
;; (pprint (get-in res [:objects]))
|
||||
|
||||
(t/is (= [frame-id shape-2-id shape-3-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
(t/is (= [shape-2-id shape-3-id frame-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
(t/testing "preserve order on multiple shape mov 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-3-id shape-2-id]
|
||||
:parent-id uuid/zero
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (println "==> BEFORE")
|
||||
;; (pprint (get-in data [:objects]))
|
||||
;; (println "==> AFTER")
|
||||
;; (pprint (get-in res [:objects]))
|
||||
|
||||
(t/is (= [frame-id shape-2-id shape-3-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
(t/is (= [shape-3-id shape-2-id frame-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
(t/testing "move inside->outside-inside"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id]
|
||||
:parent-id frame-id}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id]
|
||||
:parent-id uuid/zero}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id])
|
||||
(get-in data [:pages-index page-id :objects shape-1-id :frame-id])))
|
||||
(t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id])
|
||||
(get-in data [:pages-index page-id :objects shape-2-id :frame-id])))))))
|
||||
|
||||
|
||||
(t/deftest move-objects-2
|
||||
(let [shape-1-id (uuid/custom 1 1)
|
||||
shape-2-id (uuid/custom 1 2)
|
||||
shape-3-id (uuid/custom 1 3)
|
||||
shape-4-id (uuid/custom 1 4)
|
||||
group-1-id (uuid/custom 1 5)
|
||||
file-id (uuid/custom 1 6)
|
||||
page-id (uuid/custom 0 1)
|
||||
|
||||
changes [{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-1-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-1-id
|
||||
:type :rect
|
||||
:name "Shape a"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-2-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-2-id
|
||||
:type :rect
|
||||
:name "Shape b"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-3-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-3-id
|
||||
:type :rect
|
||||
:name "Shape c"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-4-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-4-id
|
||||
:type :rect
|
||||
:name "Shape d"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id group-1-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id group-1-id
|
||||
:type :group
|
||||
:name "Group"})}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:shapes [shape-1-id shape-2-id]}]
|
||||
|
||||
data (make-file-data file-id page-id)
|
||||
data (ch/process-changes data changes)]
|
||||
|
||||
(t/testing "case 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 2
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id shape-3-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
))
|
||||
|
||||
(t/testing "case 2"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 2
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After:
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id shape-3-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 3"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 1
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-3-id shape-2-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 4"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 0
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-3-id shape-1-id shape-2-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 5"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 0
|
||||
:shapes [shape-2-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-2-id shape-3-id shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))))
|
||||
|
||||
(t/testing "case 6"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 0
|
||||
:shapes [shape-2-id shape-1-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (not= nil
|
||||
(get-in res [:pages-index page-id :objects group-1-id])))))))
|
||||
|
||||
(t/deftest set-guide-json-encode-decode
|
||||
(let [schema ch/schema:set-guide-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-guide-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-guide-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :guides (:id change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-guide-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-guide-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :guides (:id change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :guides])))))
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-plugin-data-json-encode-decode
|
||||
(let [schema ch/schema:set-plugin-data-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-plugin-data-gen-and-validate
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-plugin-data-change)]
|
||||
(sm/validate ch/schema:set-plugin-data-change change))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-json-encode-decode
|
||||
(let [schema ch/schema:set-flow-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-flow-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :flows (:id change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-flow-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :flows (:id change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :flows])))))
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-json-encode-decode
|
||||
(let [schema ch/schema:set-default-grid-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (println "==========")
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
;; (println "==========")
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-default-grid-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
;; (app.common.pprint/pprint change)
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :default-grids (:grid-type change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-default-grid-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
;; (app.common.pprint/pprint change1)
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :default-grids (:grid-type change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :default-grids])))))
|
||||
|
||||
{:num 1000})))
|
||||
@@ -289,42 +289,3 @@
|
||||
(t/is (= (:fill-opacity fill') 1))
|
||||
(t/is (= (:touched copy2-root') nil))
|
||||
(t/is (= (:touched copy2-child') #{:fill-group}))))
|
||||
|
||||
(t/deftest test-touched-when-changing-lower
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-nested-component-with-copy :component1
|
||||
:main1-root
|
||||
:main1-child
|
||||
:component2
|
||||
:main2-root
|
||||
:main2-nested-head
|
||||
:copy2-root
|
||||
:copy2-root-params {:children-labels [:copy2-child]}))
|
||||
page (thf/current-page file)
|
||||
copy2-child (ths/get-shape file :copy2-child)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id copy2-child)}
|
||||
(fn [shape]
|
||||
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
copy2-root' (ths/get-shape file' :copy2-root)
|
||||
copy2-child' (ths/get-shape file' :copy2-child)
|
||||
fills' (:fills copy2-child')
|
||||
fill' (first fills')]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? copy2-root'))
|
||||
(t/is (some? copy2-child'))
|
||||
(t/is (= (count fills') 1))
|
||||
(t/is (= (:fill-color fill') "#fabada"))
|
||||
(t/is (= (:fill-opacity fill') 1))
|
||||
(t/is (= (:touched copy2-root') nil))
|
||||
(t/is (= (:touched copy2-child') #{:fill-group}))))
|
||||
@@ -1,740 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.pages-test
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(defn- make-file-data
|
||||
[file-id page-id]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data file-id page-id)))
|
||||
|
||||
(t/deftest process-change-set-option
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
(t/testing "Sets option single"
|
||||
(let [chg {:type :set-option
|
||||
:page-id page-id
|
||||
:option :test
|
||||
:value "test"}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= "test" (get-in res [:pages-index page-id :options :test])))))
|
||||
|
||||
(t/testing "Sets option nested"
|
||||
(let [chgs [{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :a]
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :b]
|
||||
:value "b"}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (= {:a "a" :b "b"}
|
||||
(get-in res [:pages-index page-id :options :values :test])))))
|
||||
|
||||
(t/testing "Remove option single"
|
||||
(let [chg {:type :set-option
|
||||
:page-id page-id
|
||||
:option :test
|
||||
:value nil}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
|
||||
|
||||
(t/testing "Remove option nested 1"
|
||||
(let [chgs [{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :a]
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :b]
|
||||
:value "b"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test]
|
||||
:value nil}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
|
||||
|
||||
(t/testing "Remove option nested 2"
|
||||
(let [chgs [{:type :set-option
|
||||
:option [:values :test1 :a]
|
||||
:page-id page-id
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:option [:values :test2 :b]
|
||||
:page-id page-id
|
||||
:value "b"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test2]
|
||||
:value nil}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (= [:test1] (keys (get-in res [:pages-index page-id :options :values]))))))))
|
||||
|
||||
(t/deftest process-change-add-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
id-a (uuid/custom 2 1)
|
||||
id-b (uuid/custom 2 2)
|
||||
id-c (uuid/custom 2 3)]
|
||||
|
||||
(t/testing "Adds single object"
|
||||
(let [chg {:type :add-obj
|
||||
:page-id page-id
|
||||
:id id-a
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:id id-a
|
||||
:type :rect
|
||||
:name "rect"})}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 2 (count objects)))
|
||||
(t/is (= (:obj chg) (get objects id-a)))
|
||||
|
||||
(t/is (= [id-a] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
|
||||
(t/testing "Adds several objects with different indexes"
|
||||
(let [chg (fn [id index]
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:frame-id uuid/zero
|
||||
:index index
|
||||
:obj (cts/setup-shape
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name (str id)})})
|
||||
res (ch/process-changes data [(chg id-a 0)
|
||||
(chg id-b 0)
|
||||
(chg id-c 1)])]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (clojure.pprint/pprint res)
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 4 (count objects)))
|
||||
(t/is (not (nil? (get objects id-a))))
|
||||
(t/is (not (nil? (get objects id-b))))
|
||||
(t/is (not (nil? (get objects id-c))))
|
||||
(t/is (= [id-b id-c id-a] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
(t/deftest process-change-mod-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "simple mod-obj"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id uuid/zero
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= "foobar" (get-in objects [uuid/zero :name]))))))
|
||||
|
||||
(t/testing "mod-obj for not existing shape"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id (uuid/next)
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= res data))))))
|
||||
|
||||
|
||||
;; (t/deftest process-change-del-obj
|
||||
;; (let [file-id (uuid/custom 2 2)
|
||||
;; page-id (uuid/custom 1 1)
|
||||
;; id (uuid/custom 2 1)
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (-> data
|
||||
;; (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id])
|
||||
;; (assoc-in [:pages-index page-id :objects id]
|
||||
;; {:id id
|
||||
;; :frame-id uuid/zero
|
||||
;; :type :rect
|
||||
;; :name "rect"}))]
|
||||
;; (t/testing "delete"
|
||||
;; (let [chg {:type :del-obj
|
||||
;; :page-id page-id
|
||||
;; :id id}
|
||||
;; res (ch/process-changes data [chg])]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= 1 (count objects)))
|
||||
;; (t/is (= [] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
;; (t/testing "delete idempotency"
|
||||
;; (let [chg {:type :del-obj
|
||||
;; :page-id page-id
|
||||
;; :id id}
|
||||
;; res1 (ch/process-changes data [chg])
|
||||
;; res2 (ch/process-changes res1 [chg])]
|
||||
|
||||
;; (t/is (= res1 res2))
|
||||
;; (let [objects (get-in res1 [:pages-index page-id :objects])]
|
||||
;; (t/is (= 1 (count objects)))
|
||||
;; (t/is (= [] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
|
||||
;; (t/deftest process-change-move-objects
|
||||
;; (let [frame-a-id (uuid/custom 0 1)
|
||||
;; frame-b-id (uuid/custom 0 2)
|
||||
;; group-a-id (uuid/custom 0 3)
|
||||
;; group-b-id (uuid/custom 0 4)
|
||||
;; rect-a-id (uuid/custom 0 5)
|
||||
;; rect-b-id (uuid/custom 0 6)
|
||||
;; rect-c-id (uuid/custom 0 7)
|
||||
;; rect-d-id (uuid/custom 0 8)
|
||||
;; rect-e-id (uuid/custom 0 9)
|
||||
|
||||
;; file-id (uuid/custom 2 2)
|
||||
;; page-id (uuid/custom 1 1)
|
||||
;; data (make-file-data file-id page-id)
|
||||
|
||||
;; data (update-in data [:pages-index page-id :objects]
|
||||
;; #(-> %
|
||||
;; (assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id])
|
||||
;; (assoc-in [frame-a-id]
|
||||
;; {:id frame-a-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :name "Frame a"
|
||||
;; :shapes [group-a-id group-b-id rect-e-id]
|
||||
;; :type :frame})
|
||||
|
||||
;; (assoc-in [frame-b-id]
|
||||
;; {:id frame-b-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :name "Frame b"
|
||||
;; :shapes []
|
||||
;; :type :frame})
|
||||
|
||||
;; ;; Groups
|
||||
;; (assoc-in [group-a-id]
|
||||
;; {:id group-a-id
|
||||
;; :name "Group A"
|
||||
;; :type :group
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :shapes [rect-a-id rect-b-id rect-c-id]})
|
||||
;; (assoc-in [group-b-id]
|
||||
;; {:id group-b-id
|
||||
;; :name "Group B"
|
||||
;; :type :group
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :shapes [rect-d-id]})
|
||||
|
||||
;; ;; Shapes
|
||||
;; (assoc-in [rect-a-id]
|
||||
;; {:id rect-a-id
|
||||
;; :name "Rect A"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-b-id]
|
||||
;; {:id rect-b-id
|
||||
;; :name "Rect B"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-c-id]
|
||||
;; {:id rect-c-id
|
||||
;; :name "Rect C"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-d-id]
|
||||
;; {:id rect-d-id
|
||||
;; :name "Rect D"
|
||||
;; :parent-id group-b-id
|
||||
;; :type :rect
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-e-id]
|
||||
;; {:id rect-e-id
|
||||
;; :name "Rect E"
|
||||
;; :type :rect
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id})))]
|
||||
|
||||
;; (t/testing "Create new group an add objects from the same group"
|
||||
;; (let [new-group-id (uuid/next)
|
||||
;; changes [{:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id new-group-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :obj {:id new-group-id
|
||||
;; :type :group
|
||||
;; :frame-id frame-a-id
|
||||
;; :name "Group C"}}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id new-group-id
|
||||
;; :shapes [rect-b-id rect-c-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (clojure.pprint/pprint data)
|
||||
;; ;; (println "===============")
|
||||
;; ;; (clojure.pprint/pprint res)
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id new-group-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [new-group-id :shapes])))
|
||||
;; (t/is (= [rect-a-id]
|
||||
;; (get-in objects [group-a-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements to an existing group at index"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-c-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-c-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements from group and frame to an existing group at index"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-e-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements from several groups"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-e-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move all elements from a group"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-a-id
|
||||
;; :shapes [rect-d-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (empty? (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements to a group with different frame"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id frame-b-id
|
||||
;; :shapes [group-a-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects]))
|
||||
;; ;; (println "==========")
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects]))
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [group-a-id] (get-in objects [frame-b-id :shapes])))
|
||||
;; (t/is (= frame-b-id (get-in objects [group-a-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-a-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-b-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-c-id :frame-id]))))))
|
||||
|
||||
;; (t/testing "Move elements to frame zero"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :shapes [group-a-id]
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; ;; (pprint (get-in data [:objects uuid/zero]))
|
||||
;; ;; (println "==========")
|
||||
;; ;; (pprint (get-in objects [uuid/zero]))
|
||||
|
||||
;; (t/is (= [group-a-id frame-a-id frame-b-id]
|
||||
;; (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
;; (t/testing "Don't allow to move inside self"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-a-id
|
||||
;; :shapes [group-a-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
;; (t/is (= data res))))
|
||||
;; ))
|
||||
|
||||
|
||||
;; (t/deftest process-change-mov-objects-regression
|
||||
;; (let [shape-1-id (uuid/custom 2 1)
|
||||
;; shape-2-id (uuid/custom 2 2)
|
||||
;; shape-3-id (uuid/custom 2 3)
|
||||
;; frame-id (uuid/custom 1 1)
|
||||
;; file-id (uuid/custom 4 4)
|
||||
;; page-id (uuid/custom 0 1)
|
||||
|
||||
;; changes [{:type :add-obj
|
||||
;; :id frame-id
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :frame
|
||||
;; :name "Frame"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :frame-id frame-id
|
||||
;; :parent-id frame-id
|
||||
;; :id shape-1-id
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 1"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-2-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 2"}}
|
||||
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-3-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 3"}}
|
||||
;; ]
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (ch/process-changes data changes)]
|
||||
|
||||
;; (t/testing "preserve order on multiple shape mov 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id shape-3-id]
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (println "==> BEFORE")
|
||||
;; ;; (pprint (get-in data [:objects]))
|
||||
;; ;; (println "==> AFTER")
|
||||
;; ;; (pprint (get-in res [:objects]))
|
||||
|
||||
;; (t/is (= [frame-id shape-2-id shape-3-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
;; (t/is (= [shape-2-id shape-3-id frame-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
;; (t/testing "preserve order on multiple shape mov 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-3-id shape-2-id]
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (println "==> BEFORE")
|
||||
;; ;; (pprint (get-in data [:objects]))
|
||||
;; ;; (println "==> AFTER")
|
||||
;; ;; (pprint (get-in res [:objects]))
|
||||
|
||||
;; (t/is (= [frame-id shape-2-id shape-3-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
;; (t/is (= [shape-3-id shape-2-id frame-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
;; (t/testing "move inside->outside-inside"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id]
|
||||
;; :parent-id frame-id}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id]
|
||||
;; :parent-id uuid/zero}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id])
|
||||
;; (get-in data [:pages-index page-id :objects shape-1-id :frame-id])))
|
||||
;; (t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id])
|
||||
;; (get-in data [:pages-index page-id :objects shape-2-id :frame-id])))))
|
||||
|
||||
;; ))
|
||||
|
||||
|
||||
;; (t/deftest process-change-move-objects-2
|
||||
;; (let [shape-1-id (uuid/custom 1 1)
|
||||
;; shape-2-id (uuid/custom 1 2)
|
||||
;; shape-3-id (uuid/custom 1 3)
|
||||
;; shape-4-id (uuid/custom 1 4)
|
||||
;; group-1-id (uuid/custom 1 5)
|
||||
;; file-id (uuid/custom 1 6)
|
||||
;; page-id (uuid/custom 0 1)
|
||||
|
||||
;; changes [{:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-1-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-1-id
|
||||
;; :type :rect
|
||||
;; :name "Shape a"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-2-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-2-id
|
||||
;; :type :rect
|
||||
;; :name "Shape b"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-3-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-3-id
|
||||
;; :type :rect
|
||||
;; :name "Shape c"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-4-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-4-id
|
||||
;; :type :rect
|
||||
;; :name "Shape d"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id group-1-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id group-1-id
|
||||
;; :type :group
|
||||
;; :name "Group"}}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :shapes [shape-1-id shape-2-id]}]
|
||||
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (ch/process-changes data changes)]
|
||||
|
||||
;; (t/testing "case 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 2
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id shape-3-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 2"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 2
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After:
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id shape-3-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 3"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 1
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-3-id shape-2-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 4"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 0
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-3-id shape-1-id shape-2-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 5"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0
|
||||
;; :shapes [shape-2-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-2-id shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 6"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0
|
||||
;; :shapes [shape-2-id shape-1-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (not= nil
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id])))
|
||||
|
||||
;; ))
|
||||
|
||||
;; ))
|
||||
@@ -4,12 +4,13 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types.decoder-test
|
||||
(ns common-tests.types.shape-decode-encode-test
|
||||
(:require
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.types.color :refer [schema:color schema:gradient]]
|
||||
[app.common.types.plugins :refer [schema:plugin-data]]
|
||||
[app.common.types.shape :as tsh]
|
||||
@@ -49,102 +50,102 @@
|
||||
(t/deftest gradient-json-roundtrip
|
||||
(let [encode (sm/encoder schema:gradient (sm/json-transformer))
|
||||
decode (sm/decoder schema:gradient (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [gradient (sg/generator schema:gradient)]
|
||||
(smt/check!
|
||||
(smt/for [gradient (sg/generator schema:gradient)]
|
||||
(let [gradient-1 (encode gradient)
|
||||
gradient-2 (json-roundtrip gradient-1)
|
||||
gradient-3 (decode gradient-2)]
|
||||
;; (app.common.pprint/pprint gradient)
|
||||
;; (app.common.pprint/pprint gradient-3)
|
||||
(t/is (= gradient gradient-3))))
|
||||
(= gradient gradient-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest color-json-roundtrip
|
||||
(let [encode (sm/encoder schema:color (sm/json-transformer))
|
||||
decode (sm/decoder schema:color (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [color (sg/generator schema:color)]
|
||||
(smt/check!
|
||||
(smt/for [color (sg/generator schema:color)]
|
||||
(let [color-1 (encode color)
|
||||
color-2 (json-roundtrip color-1)
|
||||
color-3 (decode color-2)]
|
||||
;; (app.common.pprint/pprint color)
|
||||
;; (app.common.pprint/pprint color-3)
|
||||
(t/is (= color color-3))))
|
||||
(= color color-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-shadow-json-roundtrip
|
||||
(let [encode (sm/encoder schema:shadow (sm/json-transformer))
|
||||
decode (sm/decoder schema:shadow (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [shadow (sg/generator schema:shadow)]
|
||||
(smt/check!
|
||||
(smt/for [shadow (sg/generator schema:shadow)]
|
||||
(let [shadow-1 (encode shadow)
|
||||
shadow-2 (json-roundtrip shadow-1)
|
||||
shadow-3 (decode shadow-2)]
|
||||
;; (app.common.pprint/pprint shadow)
|
||||
;; (app.common.pprint/pprint shadow-3)
|
||||
(t/is (= shadow shadow-3))))
|
||||
(= shadow shadow-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-animation-json-roundtrip
|
||||
(let [encode (sm/encoder schema:animation (sm/json-transformer))
|
||||
decode (sm/decoder schema:animation (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [animation (sg/generator schema:animation)]
|
||||
(smt/check!
|
||||
(smt/for [animation (sg/generator schema:animation)]
|
||||
(let [animation-1 (encode animation)
|
||||
animation-2 (json-roundtrip animation-1)
|
||||
animation-3 (decode animation-2)]
|
||||
;; (app.common.pprint/pprint animation)
|
||||
;; (app.common.pprint/pprint animation-3)
|
||||
(t/is (= animation animation-3))))
|
||||
(= animation animation-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-interaction-json-roundtrip
|
||||
(let [encode (sm/encoder schema:interaction (sm/json-transformer))
|
||||
decode (sm/decoder schema:interaction (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [interaction (sg/generator schema:interaction)]
|
||||
(smt/check!
|
||||
(smt/for [interaction (sg/generator schema:interaction)]
|
||||
(let [interaction-1 (encode interaction)
|
||||
interaction-2 (json-roundtrip interaction-1)
|
||||
interaction-3 (decode interaction-2)]
|
||||
;; (app.common.pprint/pprint interaction)
|
||||
;; (app.common.pprint/pprint interaction-3)
|
||||
(t/is (= interaction interaction-3))))
|
||||
(= interaction interaction-3)))
|
||||
{:num 500})))
|
||||
|
||||
|
||||
(t/deftest shape-path-content-json-roundtrip
|
||||
(let [encode (sm/encoder schema:path-content (sm/json-transformer))
|
||||
decode (sm/decoder schema:path-content (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [path-content (sg/generator schema:path-content)]
|
||||
(smt/check!
|
||||
(smt/for [path-content (sg/generator schema:path-content)]
|
||||
(let [path-content-1 (encode path-content)
|
||||
path-content-2 (json-roundtrip path-content-1)
|
||||
path-content-3 (decode path-content-2)]
|
||||
;; (app.common.pprint/pprint path-content)
|
||||
;; (app.common.pprint/pprint path-content-3)
|
||||
(t/is (= path-content path-content-3))))
|
||||
(= path-content path-content-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest plugin-data-json-roundtrip
|
||||
(let [encode (sm/encoder schema:plugin-data (sm/json-transformer))
|
||||
decode (sm/decoder schema:plugin-data (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [data (sg/generator schema:plugin-data)]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema:plugin-data)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
(t/is (= data data-3))))
|
||||
(= data data-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-json-roundtrip
|
||||
(let [encode (sm/encoder ::tsh/shape (sm/json-transformer))
|
||||
decode (sm/decoder ::tsh/shape (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [shape (sg/generator ::tsh/shape)]
|
||||
(smt/check!
|
||||
(smt/for [shape (sg/generator ::tsh/shape)]
|
||||
(let [shape-1 (encode shape)
|
||||
shape-2 (json-roundtrip shape-1)
|
||||
shape-3 (decode shape-2)]
|
||||
;; (app.common.pprint/pprint shape)
|
||||
;; (app.common.pprint/pprint shape-3)
|
||||
(t/is (= shape shape-3))))
|
||||
(= shape shape-3)))
|
||||
{:num 1000})))
|
||||
@@ -1,33 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.transit :as transit]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape :as cts]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest transit-encode-decode-with-shape
|
||||
(sg/check!
|
||||
(sg/for [fdata (sg/generator ::cts/shape)]
|
||||
(let [res (-> fdata transit/encode-str transit/decode-str)]
|
||||
(t/is (= res fdata))))
|
||||
{:num 18 :seed 1683548002439}))
|
||||
|
||||
(t/deftest types-shape-spec
|
||||
(sg/check!
|
||||
(sg/for [fdata (sg/generator ::cts/shape)]
|
||||
(binding [app.common.data.macros/*assert-context* true]
|
||||
(t/is (sm/validate ::cts/shape fdata))))))
|
||||
|
||||
(t/deftest types-page-spec
|
||||
(-> (sg/for [fdata (sg/generator ::ctp/page)]
|
||||
(t/is (sm/validate ::ctp/page fdata)))
|
||||
(sg/check! {:num 30})))
|
||||
@@ -1,18 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.uuid-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest non-repeating-uuid-next-1-schema
|
||||
(sg/check!
|
||||
(sg/for [uuid1 (sg/generator ::sm/uuid)
|
||||
uuid2 (sg/generator ::sm/uuid)]
|
||||
(t/is (not= uuid1 uuid2)))
|
||||
{:num 100}))
|
||||
@@ -8,6 +8,8 @@ ENV NODE_VERSION=v20.11.1 \
|
||||
CLJKONDO_VERSION=2024.03.13 \
|
||||
BABASHKA_VERSION=1.3.189 \
|
||||
CLJFMT_VERSION=0.12.0 \
|
||||
RUSTUP_VERSION=1.27.1 \
|
||||
RUST_VERSION=1.81.0 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
@@ -242,6 +244,37 @@ RUN set -ex; \
|
||||
mv /tmp/mc /usr/local/bin/; \
|
||||
chmod +x /usr/local/bin/mc;
|
||||
|
||||
# Install Rust toolchain
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH;
|
||||
|
||||
RUN set -eux; \
|
||||
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
|
||||
dpkgArch="$(dpkg --print-architecture)"; \
|
||||
case "${dpkgArch##*-}" in \
|
||||
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
|
||||
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
|
||||
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
|
||||
esac; \
|
||||
url="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \
|
||||
wget "$url"; \
|
||||
echo "${rustupSha256} *rustup-init" | sha256sum -c -; \
|
||||
chmod +x rustup-init; \
|
||||
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
|
||||
rm rustup-init; \
|
||||
chmod -R a+w $RUSTUP_HOME $CARGO_HOME;
|
||||
|
||||
WORKDIR /usr/local
|
||||
|
||||
# Install emscripten SDK and activate it
|
||||
RUN set -eux; \
|
||||
git clone https://github.com/emscripten-core/emsdk.git; \
|
||||
cd emsdk; \
|
||||
./emsdk install latest; \
|
||||
./emsdk activate latest; \
|
||||
rustup target add wasm32-unknown-emscripten;
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -9,6 +9,10 @@ alias ls='ls --color -F'
|
||||
alias lsd='ls -d *(/)'
|
||||
alias lsf='ls -h *(.)'
|
||||
|
||||
# init Cargo / Rust env
|
||||
. "/usr/local/cargo/env"
|
||||
EMSDK_QUIET=1 . "/usr/local/emsdk/emsdk_env.sh"
|
||||
|
||||
# include .bashrc if it exists
|
||||
if [ -f "$HOME/.bashrc.local" ]; then
|
||||
. "$HOME/.bashrc.local"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build",
|
||||
"build:storybook:assets": "node ./scripts/build-storybook-assets.js",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook",
|
||||
"build:renderer": "yarn run wasm-pack build ./renderer --target web --out-dir ../resources/public/js/renderer --release",
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"e2e:test": "playwright test --project default",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
@@ -86,6 +87,7 @@
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.1.4",
|
||||
"vitest": "^1.3.1",
|
||||
"wasm-pack": "^0.13.0",
|
||||
"watcher": "^2.3.1",
|
||||
"workerpool": "^9.1.1"
|
||||
},
|
||||
|
||||
@@ -117,7 +117,7 @@ test("User goes to the Viewer Inspect code, code tab", async ({ page }) => {
|
||||
});
|
||||
|
||||
await viewerPage.showCode();
|
||||
await viewerPage.page.getByTestId("code").click();
|
||||
await viewerPage.page.getByRole("tab", { name: "code" }).click();
|
||||
|
||||
await expect(
|
||||
viewerPage.page.getByRole("button", { name: "Copy all code" }),
|
||||
|
||||
1
frontend/render_v2/cpp/.gitignore
vendored
Normal file
1
frontend/render_v2/cpp/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
out
|
||||
108
frontend/render_v2/cpp/Dockerfile
Normal file
108
frontend/render_v2/cpp/Dockerfile
Normal file
@@ -0,0 +1,108 @@
|
||||
FROM debian:11
|
||||
RUN apt update && apt dist-upgrade -y && apt install -y \
|
||||
git \
|
||||
clang \
|
||||
python \
|
||||
curl \
|
||||
build-essential \
|
||||
libfontconfig-dev \
|
||||
libgl1-mesa-dev \
|
||||
libglu1-mesa-dev \
|
||||
procps \
|
||||
vim \
|
||||
binaryen \
|
||||
wabt \
|
||||
&& groupadd -g 2000 skia \
|
||||
&& useradd -u 2000 -g 2000 skia
|
||||
|
||||
# TODO(kjlubick): Try a shallow clone of depot_tools
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://chromium.googlesource.com/chromium/tools/depot_tools.git'
|
||||
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://gn.googlesource.com/gn'
|
||||
|
||||
RUN cd /tmp \
|
||||
&& git clone 'https://skia.googlesource.com/skia' \
|
||||
&& cd skia \
|
||||
&& git checkout 'chrome/m129'
|
||||
|
||||
ENV PATH=${PATH}:/tmp/depot_tools
|
||||
ENV PATH=${PATH}:/tmp/gn
|
||||
|
||||
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libGLESv2.so /usr/local/lib/libGLESv2.so
|
||||
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libEGL.so /usr/local/lib/libEGL.so
|
||||
|
||||
# FIXME: I don't like this approach because it implies that
|
||||
# git-sync-deps is going to fail and we need to run it two
|
||||
# times. The weird thing is that git-sync-deps fails consistently
|
||||
# the first time.
|
||||
RUN cd /tmp/skia; \
|
||||
tools/git-sync-deps; \
|
||||
tools/git-sync-deps; \
|
||||
exit 0
|
||||
|
||||
RUN cd /tmp/skia && python3 bin/fetch-ninja
|
||||
|
||||
RUN . "tmp/skia/third_party/externals/emsdk/emsdk_env.sh"
|
||||
|
||||
RUN cd /tmp/skia && ./bin/gn gen out/wasm \
|
||||
--args="is_debug=false \
|
||||
is_official_build=true \
|
||||
is_component_build=false \
|
||||
is_trivial_abi=true \
|
||||
werror=true \
|
||||
target_cpu=\"wasm\" \
|
||||
skia_use_angle=false \
|
||||
skia_use_dng_sdk=false \
|
||||
skia_use_dawn=false \
|
||||
skia_use_webgl=true \
|
||||
skia_use_webgpu=false \
|
||||
skia_use_expat=false \
|
||||
skia_use_fontconfig=false \
|
||||
skia_use_freetype=true \
|
||||
skia_use_libheif=false \
|
||||
skia_use_libjpeg_turbo_decode=true \
|
||||
skia_use_libjpeg_turbo_encode=true \
|
||||
skia_use_no_jpeg_encode=false \
|
||||
skia_use_libpng_decode=true \
|
||||
skia_use_libpng_encode=true \
|
||||
skia_use_no_png_encode=false \
|
||||
skia_use_libwebp_decode=true \
|
||||
skia_use_libwebp_encode=true \
|
||||
skia_use_no_webp_encode=false \
|
||||
skia_use_lua=false \
|
||||
skia_use_piex=false \
|
||||
skia_use_system_freetype2=false \
|
||||
skia_use_system_libjpeg_turbo=false \
|
||||
skia_use_system_libpng=false \
|
||||
skia_use_system_libwebp=false \
|
||||
skia_use_system_zlib=false \
|
||||
skia_use_vulkan=false \
|
||||
skia_use_wuffs=true \
|
||||
skia_use_zlib=true \
|
||||
skia_enable_ganesh=true \
|
||||
skia_enable_sksl=true \
|
||||
skia_build_for_debugger=false \
|
||||
skia_enable_sksl_tracing=true \
|
||||
skia_use_icu=true \
|
||||
skia_use_client_icu=false \
|
||||
skia_use_libgrapheme=false \
|
||||
skia_use_system_icu=false \
|
||||
skia_use_harfbuzz=true \
|
||||
skia_use_system_harfbuzz=false \
|
||||
skia_enable_fontmgr_custom_directory=false \
|
||||
skia_enable_fontmgr_custom_embedded=true \
|
||||
skia_enable_fontmgr_custom_empty=false \
|
||||
skia_fontmgr_factory=\":fontmgr_custom_embedded_factory\" \
|
||||
skia_use_freetype_woff2=true \
|
||||
skia_enable_skshaper=true \
|
||||
skia_enable_skparagraph=true \
|
||||
skia_enable_pdf=false"
|
||||
|
||||
RUN cd /tmp/skia; ninja -C out/wasm
|
||||
RUN echo "source '/tmp/skia/third_party/externals/emsdk/emsdk_env.sh'" >> /root/.bashrc
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
65
frontend/render_v2/cpp/Makefile
Normal file
65
frontend/render_v2/cpp/Makefile
Normal file
@@ -0,0 +1,65 @@
|
||||
all:
|
||||
# -fno-rtti: Removes C++ Run-Time Type Info support.
|
||||
# --no-entry: Disables the necessity of an entry point.
|
||||
# -sALLOW_MEMORY_GROWTH: Creates a resizable memory ArrayBuffer.
|
||||
# -sMODULARIZE: Exports emscripten as a CommonJS/AMD module.
|
||||
# -sENVIRONMENT: Removes unnecessary environments (node,worker,etc).
|
||||
# -sUSE_PTHREADS: Disables pthreads.
|
||||
# -sMAX_WEBGL_VERSION: Max WebGL set to 2
|
||||
# -sUSE_WEBGL2: Uses WebGL2 by default.
|
||||
em++ \
|
||||
-std=c++20 \
|
||||
-lembind \
|
||||
-fno-rtti \
|
||||
--no-entry \
|
||||
-sALLOW_MEMORY_GROWTH \
|
||||
-sUSE_PTHREADS=0 \
|
||||
-sMODULARIZE=1 \
|
||||
-sDISABLE_EXCEPTION_CATCHING \
|
||||
-sNODEJS_CATCH_EXIT=0 \
|
||||
-sMAX_WEBGL_VERSION=2 \
|
||||
-sUSE_WEBGL2=1 \
|
||||
-sFORCE_FILESYSTEM=0 \
|
||||
-sDYNAMIC_EXECUTION=0 \
|
||||
-sFILESYSTEM=0 \
|
||||
-sENVIRONMENT='web' \
|
||||
-sINITIAL_MEMORY=128MB \
|
||||
-DCK_ENABLE_WEBGL \
|
||||
-DCK_NO_FONTS \
|
||||
-DSK_RELEASE \
|
||||
-DSK_DISABLE_TRACING \
|
||||
-DSK_FORCE_AAA \
|
||||
-DSK_FORCE_8_BYTE_ALIGNMENT \
|
||||
-DSK_SHAPER_HARFBUZZ_AVAILABLE \
|
||||
-DCK_SERIALIZE_SKP \
|
||||
-DSK_GANESH \
|
||||
-DSK_DISABLE_LEGACY_SHADERCONTEXT \
|
||||
-DCK_INCLUDE_PATHOPS \
|
||||
-DCK_INCLUDE_RUNTIME_EFFECT \
|
||||
-DSKSL_ENABLE_TRACING \
|
||||
-DNDEBUG \
|
||||
-DSK_TRIVIAL_ABI="[[clang::trivial_abi]]" \
|
||||
-DSK_TYPEFACE_FACTORY_FREETYPE \
|
||||
-DSK_GL \
|
||||
-DSK_CODEC_DECODES_JPEG \
|
||||
-DSK_CODEC_DECODES_PNG \
|
||||
-DSK_CODEC_DECODES_WEBP \
|
||||
-DSK_HAS_WUFFS_LIBRARY \
|
||||
-DSK_ENABLE_SKSL \
|
||||
-DSK_ENABLE_PRECOMPILE \
|
||||
-DSKNX_NO_SIMD \
|
||||
-DSK_ASSUME_WEBGL=1 \
|
||||
-DSK_USE_WEBGL \
|
||||
-DSK_ENABLE_PARAGRAPH \
|
||||
-DSK_UNICODE_AVAILABLE \
|
||||
-DSK_UNICODE_ICU_IMPLEMENTATION \
|
||||
-DSK_ENABLE_SKOTTIE \
|
||||
-DSK_ENABLE_SKOTTIE_SKSLEFFECT \
|
||||
-DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 \
|
||||
--pre-js js/preamble.js \
|
||||
--pre-js js/postamble.js \
|
||||
-I/tmp/skia \
|
||||
-o out/renderer.js \
|
||||
/tmp/skia/out/wasm/modules/canvaskit/fonts/NotoMono-Regular.ttf.ninja.cpp \
|
||||
/tmp/skia/out/wasm/libskia.a \
|
||||
src/main.cpp
|
||||
49
frontend/render_v2/cpp/README.md
Normal file
49
frontend/render_v2/cpp/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Renderer
|
||||
|
||||
## How this works?
|
||||
|
||||
First of all we need a proper environment to build Skia, this
|
||||
environment is heavily based on the [Skia docker image](https://github.com/google/skia/blob/main/docker/skia-release/Dockerfile) but with some tweaks to support building
|
||||
a C++ WebAssembly module using [Emscripten](https://emscripten.org/index.html).
|
||||
|
||||
## Building everything
|
||||
|
||||
From the root directory of `frontend/renderer` just run:
|
||||
|
||||
```sh
|
||||
./build
|
||||
```
|
||||
|
||||
This is going to build the docker image and run the container to build
|
||||
the artifacts and then copy them to the necessary directories.
|
||||
|
||||
> :smile_cat: Be patient, the first time the docker image is built usually takes
|
||||
> a few minutes.
|
||||
|
||||
## Building the Skia build tools Docker image
|
||||
|
||||
To build just the Skia build tools image:
|
||||
|
||||
```sh
|
||||
cd frontend/renderer
|
||||
docker build . -t skia-build-tools
|
||||
```
|
||||
|
||||
## Building the renderer WebAssembly module
|
||||
|
||||
Just run the container and it will generate all the necessary
|
||||
artifacts in the `out` directory.
|
||||
|
||||
```sh
|
||||
cd frontend/renderer
|
||||
docker run -t -v ${PWD}:/tmp/renderer skia-build-tools
|
||||
```
|
||||
|
||||
Once the `renderer.js` and `renderer.wasm` are created in the `out` directory
|
||||
we need to move them where Penpot can have access to them, so we need to execute
|
||||
`./scripts/copy-artifacts`.
|
||||
|
||||
## C++ <-> JS
|
||||
|
||||
To add some extra functionality to the exported `Module` by the Emscripten
|
||||
compiler, we use a series of javascript scripts that exist on the `js` directory.
|
||||
11
frontend/render_v2/cpp/TODO.md
Normal file
11
frontend/render_v2/cpp/TODO.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# TO DO
|
||||
|
||||
- [x] Compile Skia.
|
||||
- [x] Compile simple `renderer.wasm` with a exported function.
|
||||
- [ ] Compile a `renderer.wasm` that uses a WebGL context.
|
||||
|
||||
## Notes
|
||||
|
||||
- I've used the Skia `main` branch and it looks that there's something missing from the last release (`chrome/m117`) so I tried to switch to that branch but now I have different issues.
|
||||
|
||||
- It is necessary to use the GL emscripten module to deal with WebGL contexts. See `js/preamble.js` and
|
||||
4
frontend/render_v2/cpp/build
Executable file
4
frontend/render_v2/cpp/build
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
docker build . -t skia-build-tools
|
||||
docker run -t -v $PWD:/tmp/renderer skia-build-tools
|
||||
./scripts/copy-artifacts
|
||||
4
frontend/render_v2/cpp/docker/entrypoint.sh
Executable file
4
frontend/render_v2/cpp/docker/entrypoint.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
source /root/.bashrc
|
||||
cd /tmp/renderer
|
||||
emmake make
|
||||
2
frontend/render_v2/cpp/js/postamble.js
Normal file
2
frontend/render_v2/cpp/js/postamble.js
Normal file
@@ -0,0 +1,2 @@
|
||||
console.log("postamble");
|
||||
}(Module));
|
||||
69
frontend/render_v2/cpp/js/preamble.js
Normal file
69
frontend/render_v2/cpp/js/preamble.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// Adds compile-time JS functions to augment Renderer interface.
|
||||
(function (Renderer) {
|
||||
console.log("preamble", Renderer);
|
||||
|
||||
const LCG_MULTIPLIER = 1103515245;
|
||||
const LCG_INCREMENT = 12345;
|
||||
const LCG_MODULUS = Math.pow(2, 31);
|
||||
const LCG_MASK = (LCG_MODULUS - 1);
|
||||
|
||||
function lcg(x, a, c, m) {
|
||||
return (x * a + c) % m;
|
||||
}
|
||||
|
||||
class Random {
|
||||
constructor(seed) {
|
||||
this._seed = seed;
|
||||
}
|
||||
|
||||
value() {
|
||||
this._seed = lcg(this._seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (this._seed & LCG_MASK) / LCG_MODULUS;
|
||||
}
|
||||
}
|
||||
|
||||
const random = new Random(0)
|
||||
|
||||
// Sets canvas.
|
||||
Renderer.setCanvas = function setCanvas(canvas, attrs) {
|
||||
const context = GL.createContext(canvas, attrs);
|
||||
if (!context) {
|
||||
throw new Error('Could not create a new WebGL context')
|
||||
}
|
||||
GL.makeContextCurrent(context);
|
||||
|
||||
// Emscripten does not enable this by default and Skia needs this
|
||||
// to handle certain GPU corner cases.
|
||||
GL.currentContext.GLctx.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
// Initializes everything needed.
|
||||
this._InitCanvas(canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
Renderer.setObjects = function setObjects(vbox, zoom, objects) {
|
||||
// this._SetObjects(objects.cnt);
|
||||
const numObjects = 20_000;
|
||||
this._SetObjects(numObjects);
|
||||
for (let index = 0; index < numObjects; index++) {
|
||||
// const object = objects.arr[index * 2 + 1];
|
||||
this._SetObjectRect(
|
||||
index,
|
||||
// object.selrect.x,
|
||||
random.value() * 2000,
|
||||
// object.selrect.y,
|
||||
random.value() * 2000,
|
||||
// object.selrect.width,
|
||||
random.value() * 200,
|
||||
// object.selrect.height,
|
||||
random.value() * 200
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Renderer.drawCanvas = function drawCanvas(vbox, zoom, objects) {
|
||||
performance.mark('draw-canvas:start');
|
||||
this._DrawCanvas(vbox.x, vbox.y, zoom);
|
||||
performance.mark('draw-canvas:end');
|
||||
const { duration } = performance.measure('draw-canvas', 'draw-canvas:start', 'draw-canvas:end');
|
||||
console.log('draw-canvas', `${duration}ms`);
|
||||
};
|
||||
11
frontend/render_v2/cpp/scripts/copy-artifacts
Executable file
11
frontend/render_v2/cpp/scripts/copy-artifacts
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
mkdir -p ../../resources/public/js/render_v2/cpp
|
||||
mkdir -p ../../src/app/render_v2/
|
||||
|
||||
# FIXME: This is a VERY HACKY way to set the correct `scriptDirectory` but
|
||||
# I didn't find a better way yet.
|
||||
PREAMBLE_LINES=`wc -l js/preamble.js | egrep -o [0-9]+`
|
||||
POSTAMBLE_LINES=`wc -l js/postamble.js | egrep -o [0-9]+`
|
||||
LINE_NUMBER=`echo "200 + ${PREAMBLE_LINES} + ${POSTAMBLE_LINES}" | bc | egrep -o [0-9]+`
|
||||
sed "${LINE_NUMBER} i \ \ scriptDirectory += 'js/render_v2/cpp/';" out/renderer.js > ../../src/app/render_v2/cpp.js
|
||||
cp out/renderer.wasm ../../resources/public/js/render_v2/cpp
|
||||
275
frontend/render_v2/cpp/src/main.cpp
Normal file
275
frontend/render_v2/cpp/src/main.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
#include "include/android/SkAnimatedImage.h"
|
||||
#include "include/codec/SkAndroidCodec.h"
|
||||
#include "include/codec/SkEncodedImageFormat.h"
|
||||
#include "include/core/SkBBHFactory.h"
|
||||
#include "include/core/SkBlendMode.h"
|
||||
#include "include/core/SkBlender.h"
|
||||
#include "include/core/SkBlurTypes.h"
|
||||
#include "include/core/SkCanvas.h"
|
||||
#include "include/core/SkColor.h"
|
||||
#include "include/core/SkColorFilter.h"
|
||||
#include "include/core/SkColorSpace.h"
|
||||
#include "include/core/SkData.h"
|
||||
#include "include/core/SkImage.h"
|
||||
#include "include/core/SkImageFilter.h"
|
||||
#include "include/core/SkImageGenerator.h"
|
||||
#include "include/core/SkImageInfo.h"
|
||||
#include "include/core/SkM44.h"
|
||||
#include "include/core/SkMaskFilter.h"
|
||||
#include "include/core/SkPaint.h"
|
||||
#include "include/core/SkPath.h"
|
||||
#include "include/core/SkPathEffect.h"
|
||||
#include "include/core/SkPathMeasure.h"
|
||||
#include "include/core/SkPathUtils.h"
|
||||
#include "include/core/SkPicture.h"
|
||||
#include "include/core/SkPictureRecorder.h"
|
||||
#include "include/core/SkPoint3.h"
|
||||
#include "include/core/SkRRect.h"
|
||||
#include "include/core/SkSamplingOptions.h"
|
||||
#include "include/core/SkScalar.h"
|
||||
#include "include/core/SkSerialProcs.h"
|
||||
#include "include/core/SkShader.h"
|
||||
#include "include/core/SkStream.h"
|
||||
#include "include/core/SkString.h"
|
||||
#include "include/core/SkStrokeRec.h"
|
||||
#include "include/core/SkSurface.h"
|
||||
#include "include/core/SkTextBlob.h"
|
||||
#include "include/core/SkTypeface.h"
|
||||
#include "include/core/SkTypes.h"
|
||||
#include "include/core/SkVertices.h"
|
||||
#include "include/effects/Sk1DPathEffect.h"
|
||||
#include "include/effects/Sk2DPathEffect.h"
|
||||
#include "include/effects/SkCornerPathEffect.h"
|
||||
#include "include/effects/SkDashPathEffect.h"
|
||||
#include "include/effects/SkDiscretePathEffect.h"
|
||||
#include "include/effects/SkGradientShader.h"
|
||||
#include "include/effects/SkImageFilters.h"
|
||||
#include "include/effects/SkLumaColorFilter.h"
|
||||
#include "include/effects/SkPerlinNoiseShader.h"
|
||||
#include "include/effects/SkRuntimeEffect.h"
|
||||
#include "include/effects/SkTrimPathEffect.h"
|
||||
#include "include/encode/SkJpegEncoder.h"
|
||||
#include "include/encode/SkPngEncoder.h"
|
||||
#include "include/encode/SkWebpEncoder.h"
|
||||
#include "include/private/SkShadowFlags.h"
|
||||
#include "include/utils/SkParsePath.h"
|
||||
#include "include/utils/SkShadowUtils.h"
|
||||
#include "src/core/SkPathPriv.h"
|
||||
#include "src/core/SkResourceCache.h"
|
||||
#include "src/image/SkImage_Base.h"
|
||||
#include "src/sksl/SkSLCompiler.h"
|
||||
|
||||
#include "modules/canvaskit/WasmCommon.h"
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
#if defined(CK_ENABLE_WEBGL) || defined(CK_ENABLE_WEBGPU)
|
||||
#define ENABLE_GPU
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_GPU
|
||||
#include "include/gpu/GpuTypes.h"
|
||||
#include "include/gpu/GrDirectContext.h"
|
||||
#include "include/gpu/ganesh/GrExternalTextureGenerator.h"
|
||||
#include "include/gpu/ganesh/SkImageGanesh.h"
|
||||
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
|
||||
#include "src/gpu/ganesh/GrCaps.h"
|
||||
#endif // ENABLE_GPU
|
||||
|
||||
#ifdef CK_ENABLE_WEBGL
|
||||
#include "include/gpu/GrBackendSurface.h"
|
||||
#include "include/gpu/GrTypes.h"
|
||||
#include "include/gpu/ganesh/gl/GrGLBackendSurface.h"
|
||||
#include "include/gpu/gl/GrGLInterface.h"
|
||||
#include "include/gpu/gl/GrGLTypes.h"
|
||||
#include "src/gpu/RefCntedCallback.h"
|
||||
#include "src/gpu/ganesh/GrProxyProvider.h"
|
||||
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
|
||||
#include "src/gpu/ganesh/gl/GrGLDefines.h"
|
||||
|
||||
#include <webgl/webgl1.h>
|
||||
#endif // CK_ENABLE_WEBGL
|
||||
|
||||
#ifdef CK_ENABLE_WEBGPU
|
||||
#include <emscripten/html5_webgpu.h>
|
||||
#include <webgpu/webgpu.h>
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#endif // CK_ENABLE_WEBGPU
|
||||
|
||||
#ifndef CK_NO_FONTS
|
||||
#include "include/core/SkFont.h"
|
||||
#include "include/core/SkFontMetrics.h"
|
||||
#include "include/core/SkFontMgr.h"
|
||||
#include "include/core/SkFontTypes.h"
|
||||
#ifdef CK_INCLUDE_PARAGRAPH
|
||||
#include "modules/skparagraph/include/Paragraph.h"
|
||||
#endif // CK_INCLUDE_PARAGRAPH
|
||||
#endif // CK_NO_FONTS
|
||||
|
||||
#ifdef CK_INCLUDE_PATHOPS
|
||||
#include "include/pathops/SkPathOps.h"
|
||||
#endif
|
||||
|
||||
#if defined(CK_INCLUDE_RUNTIME_EFFECT) && defined(SKSL_ENABLE_TRACING)
|
||||
#include "include/sksl/SkSLDebugTrace.h"
|
||||
#endif
|
||||
|
||||
#ifndef CK_NO_FONTS
|
||||
#include "include/ports/SkFontMgr_data.h"
|
||||
#endif
|
||||
|
||||
// Global data needed to keep everything in place.
|
||||
sk_sp<GrDirectContext> context = nullptr;
|
||||
sk_sp<SkSurface> surface = nullptr;
|
||||
SkCanvas *canvas = nullptr;
|
||||
|
||||
struct PenpotRect {
|
||||
float x, y, width, height;
|
||||
};
|
||||
|
||||
struct PenpotColor {
|
||||
float r, g, b, a;
|
||||
};
|
||||
|
||||
struct PenpotObject {
|
||||
PenpotRect selRect;
|
||||
};
|
||||
|
||||
std::vector<PenpotObject> objects(0);
|
||||
|
||||
constexpr uint32_t LCG_MULTIPLIER = 1103515245;
|
||||
constexpr uint32_t LCG_INCREMENT = 12345;
|
||||
constexpr uint32_t LCG_MODULUS = 0x80000000;
|
||||
constexpr uint32_t LCG_MASK = 0x7FFFFFFF;
|
||||
|
||||
uint32_t lcg(uint32_t x, uint32_t a, uint32_t c, uint32_t m)
|
||||
{
|
||||
return (x * a + c) % m;
|
||||
}
|
||||
|
||||
class Random {
|
||||
private:
|
||||
uint32_t _seed;
|
||||
|
||||
public:
|
||||
Random(uint32_t seed) : _seed(seed) {};
|
||||
|
||||
void reset(uint32_t new_seed) {
|
||||
_seed = new_seed;
|
||||
}
|
||||
|
||||
float value() {
|
||||
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (float)(_seed & LCG_MASK) / (float)LCG_MODULUS;
|
||||
}
|
||||
|
||||
uint8_t byte() {
|
||||
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
|
||||
return (_seed & LCG_MASK) % 0xFF;
|
||||
}
|
||||
};
|
||||
|
||||
Random lcg_random(0);
|
||||
|
||||
// Initializes all the structures and elements needed to start rendering things.
|
||||
void InitCanvas(int width, int height)
|
||||
{
|
||||
emscripten_log(EM_LOG_CONSOLE, "Initializing canvas %d %d", width, height);
|
||||
|
||||
// We assume that any calls we make to GL for the remainder of this function will go to the
|
||||
// desired WebGL Context.
|
||||
// setup interface.
|
||||
auto interface = GrGLMakeNativeInterface();
|
||||
// setup context.
|
||||
context = GrDirectContext::MakeGL(interface);
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "GL context initialized");
|
||||
|
||||
GrGLint sampleCnt = 0;
|
||||
GrGLint stencil = 16;
|
||||
|
||||
// WebGL should already be clearing the color and stencil buffers, but do it again here to
|
||||
// ensure Skia receives them in the expected state.
|
||||
emscripten_glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
emscripten_glClearColor(0, 0, 0, 0);
|
||||
emscripten_glClearStencil(0);
|
||||
emscripten_glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
context->resetContext(kRenderTarget_GrGLBackendState | kMisc_GrGLBackendState);
|
||||
|
||||
// The on-screen canvas is FBO 0. Wrap it in a Skia render target so Skia can render to it.
|
||||
GrGLFramebufferInfo info;
|
||||
info.fFBOID = 0;
|
||||
|
||||
// Create the colorspace needed to represent graphics.
|
||||
sk_sp<SkColorSpace> colorSpace = SkColorSpace::MakeSRGB();
|
||||
|
||||
info.fFormat = GR_GL_RGBA8; // kRGBA_8888_SkColorType;
|
||||
auto target = GrBackendRenderTargets::MakeGL(
|
||||
width,
|
||||
height,
|
||||
sampleCnt,
|
||||
stencil,
|
||||
info
|
||||
);
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "Creating new surface");
|
||||
sk_sp<SkSurface> new_surface(
|
||||
SkSurfaces::WrapBackendRenderTarget(
|
||||
context.get(),
|
||||
target,
|
||||
kBottomLeft_GrSurfaceOrigin,
|
||||
kRGBA_8888_SkColorType,
|
||||
colorSpace,
|
||||
nullptr));
|
||||
|
||||
surface = new_surface;
|
||||
canvas = surface->getCanvas();
|
||||
emscripten_log(EM_LOG_CONSOLE, "Everything's ready!");
|
||||
}
|
||||
|
||||
void DrawCanvas(float x, float y, float zoom)
|
||||
{
|
||||
canvas->clear(SK_ColorTRANSPARENT);
|
||||
canvas->save();
|
||||
canvas->scale(zoom, zoom);
|
||||
canvas->translate(-x, -y);
|
||||
lcg_random.reset(0);
|
||||
emscripten_log(EM_LOG_CONSOLE, "Clearing canvas");
|
||||
for (auto object : objects) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Drawing object");
|
||||
|
||||
SkPaint paint;
|
||||
paint.setARGB(255, lcg_random.byte(), lcg_random.byte(), lcg_random.byte());
|
||||
paint.setStyle(SkPaint::Style::kFill_Style);
|
||||
|
||||
SkRect rect = SkRect::MakeXYWH(object.selRect.x, object.selRect.y, object.selRect.width, object.selRect.height);
|
||||
canvas->drawRect(rect, paint);
|
||||
}
|
||||
canvas->restore();
|
||||
|
||||
emscripten_log(EM_LOG_CONSOLE, "Flushing and submitting");
|
||||
skgpu::ganesh::FlushAndSubmit(surface);
|
||||
}
|
||||
|
||||
void SetObjects(int num_objects) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Resizing objects vector capacity %d", num_objects);
|
||||
objects.resize(num_objects);
|
||||
}
|
||||
|
||||
void SetObjectRect(int index, float x, float y, float width, float height) {
|
||||
// emscripten_log(EM_LOG_CONSOLE, "Setting object at %d %f %f %f %f", index, x, y, width, height);
|
||||
objects[index].selRect.x = x;
|
||||
objects[index].selRect.y = y;
|
||||
objects[index].selRect.width = width;
|
||||
objects[index].selRect.height = height;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(Renderer)
|
||||
{
|
||||
function("_InitCanvas", InitCanvas);
|
||||
function("_DrawCanvas", DrawCanvas);
|
||||
function("_SetObjects", SetObjects);
|
||||
function("_SetObjectRect", SetObjectRect);
|
||||
}
|
||||
11
frontend/render_v2/rs/.gitignore
vendored
Normal file
11
frontend/render_v2/rs/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
664
frontend/render_v2/rs/Cargo.lock
generated
Normal file
664
frontend/render_v2/rs/Cargo.lock
generated
Normal file
@@ -0,0 +1,664 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
|
||||
dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
|
||||
dependencies = [
|
||||
"khronos_api",
|
||||
"log",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "render"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"gl",
|
||||
"skia-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "skia-bindings"
|
||||
version = "0.78.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29880a81b088de322e9c5306236c70761a61b5fa4df3c15c93bad3ce890ce34c"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"flate2",
|
||||
"heck",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skia-safe"
|
||||
version = "0.78.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f15700ac678c06649077495acbba07e7ae01e5ca46b7dc18213f2c3477ada71"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"lazy_static",
|
||||
"skia-bindings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
|
||||
22
frontend/render_v2/rs/Cargo.toml
Normal file
22
frontend/render_v2/rs/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "render"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/penpot/penpot"
|
||||
license-file = "../../../../LICENSE"
|
||||
description = "Wasm-based canvas render for Penpot"
|
||||
|
||||
[features]
|
||||
default = ["skia-safe/gl", "skia-safe/textlayout"]
|
||||
|
||||
[[bin]]
|
||||
name = "render_v2"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
gl = "0.14.0"
|
||||
skia-safe = "0.78.2"
|
||||
base64 = "0.13"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
6
frontend/render_v2/rs/build.sh
Executable file
6
frontend/render_v2/rs/build.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
EMSDK_QUIET=1 . "/usr/local/emsdk/emsdk_env.sh"
|
||||
|
||||
EMCC_CFLAGS="--no-entry -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2 -s MODULARIZE=1 -s EXPORT_NAME=createRustSkiaModule -s EXPORTED_RUNTIME_METHODS=GL -s ENVIRONMENT=web" cargo build --target=wasm32-unknown-emscripten
|
||||
|
||||
BIN
frontend/render_v2/rs/src/RobotoMono-Regular.ttf
Normal file
BIN
frontend/render_v2/rs/src/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
362
frontend/render_v2/rs/src/main.rs
Normal file
362
frontend/render_v2/rs/src/main.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
// use skia_safe::{
|
||||
// gpu::{self, gl::FramebufferInfo, DirectContext},
|
||||
// textlayout::{
|
||||
// FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle, TypefaceFontProvider,
|
||||
// },
|
||||
// Canvas, Data, EncodedImageFormat, FontMgr, Paint, PaintStyle, Path, SurfaceProps,
|
||||
// };
|
||||
use skia_safe::{
|
||||
gpu::{self, gl::FramebufferInfo, DirectContext},
|
||||
textlayout::TypefaceFontProvider,
|
||||
PaintStyle,
|
||||
};
|
||||
use std::boxed::Box;
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
static ROBOTO_REGULAR: &[u8] = include_bytes!("RobotoMono-Regular.ttf");
|
||||
static TYPEFACE_ALIAS: &str = "roboto-regular";
|
||||
|
||||
extern "C" {
|
||||
pub fn emscripten_GetProcAddress(
|
||||
name: *const ::std::os::raw::c_char,
|
||||
) -> *const ::std::os::raw::c_void;
|
||||
}
|
||||
|
||||
struct GpuState {
|
||||
context: DirectContext,
|
||||
framebuffer_info: FramebufferInfo,
|
||||
}
|
||||
|
||||
/// This struct holds the state of the Rust application between JS calls.
|
||||
///
|
||||
/// It is created by [init] and passed to the other exported functions. Note that rust-skia data
|
||||
/// structures are not thread safe, so a state must not be shared between different Web Workers.
|
||||
pub struct State {
|
||||
gpu_state: GpuState,
|
||||
surface: skia::Surface,
|
||||
typeface_font_provider: TypefaceFontProvider,
|
||||
default_font: skia_safe::Font,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(
|
||||
gpu_state: GpuState,
|
||||
surface: skia::Surface,
|
||||
typeface_font_provider: TypefaceFontProvider,
|
||||
default_font: skia_safe::Font,
|
||||
) -> Self {
|
||||
State {
|
||||
gpu_state,
|
||||
surface,
|
||||
typeface_font_provider,
|
||||
default_font,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_surface(&mut self, surface: skia::Surface) {
|
||||
self.surface = surface;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_gl() {
|
||||
unsafe {
|
||||
gl::load_with(|addr| {
|
||||
let addr = std::ffi::CString::new(addr).unwrap();
|
||||
emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// This needs to be done once per WebGL context.
|
||||
fn create_gpu_state() -> GpuState {
|
||||
let interface = skia_safe::gpu::gl::Interface::new_native().unwrap();
|
||||
let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap();
|
||||
let framebuffer_info = {
|
||||
let mut fboid: gl::types::GLint = 0;
|
||||
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
|
||||
|
||||
FramebufferInfo {
|
||||
fboid: fboid.try_into().unwrap(),
|
||||
format: skia_safe::gpu::gl::Format::RGBA8.into(),
|
||||
protected: skia_safe::gpu::Protected::No,
|
||||
}
|
||||
};
|
||||
|
||||
GpuState {
|
||||
context,
|
||||
framebuffer_info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the Skia surface that will be used for rendering.
|
||||
fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) -> skia::Surface {
|
||||
let backend_render_target =
|
||||
gpu::backend_render_targets::make_gl((width, height), 1, 8, gpu_state.framebuffer_info);
|
||||
|
||||
gpu::surfaces::wrap_backend_render_target(
|
||||
&mut gpu_state.context,
|
||||
&backend_render_target,
|
||||
skia_safe::gpu::SurfaceOrigin::BottomLeft,
|
||||
skia_safe::ColorType::RGBA8888,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_rect(surface: &mut skia::Surface, rect: skia::Rect, color: skia::Color) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_color(color);
|
||||
// paint.set_anti_alias(true);
|
||||
surface.canvas().draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
fn render_rect_ref(surface: &mut skia::Surface, rect: &skia::Rect, paint: &skia::Paint) {
|
||||
surface.canvas().draw_rect(rect, paint);
|
||||
}
|
||||
|
||||
/// This is called from JS after the WebGL context has been created.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Box<State> {
|
||||
let mut gpu_state = create_gpu_state();
|
||||
let surface = create_surface(&mut gpu_state, width, height);
|
||||
|
||||
// skia_safe::Font::default() is empty, let's use something better
|
||||
let font_mgr = skia_safe::FontMgr::new();
|
||||
let typeface = font_mgr
|
||||
.new_from_data(ROBOTO_REGULAR, None)
|
||||
.expect("Failed to load ROBOTO font");
|
||||
let default_font = skia_safe::Font::new(typeface.clone(), 10.0);
|
||||
|
||||
let typeface_font_provider = {
|
||||
let mut typeface_font_provider = TypefaceFontProvider::new();
|
||||
// We need a system font manager to be able to load typefaces.
|
||||
typeface_font_provider.register_typeface(typeface, TYPEFACE_ALIAS);
|
||||
typeface_font_provider
|
||||
};
|
||||
|
||||
let state = State::new(gpu_state, surface, typeface_font_provider, default_font);
|
||||
|
||||
Box::new(state)
|
||||
}
|
||||
|
||||
/// This is called from JS when the window is resized.
|
||||
/// # Safety
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
let surface = create_surface(&mut state.gpu_state, width, height);
|
||||
state.set_surface(surface);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Color {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Rect {
|
||||
left: f32,
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn alloc_rects(len: usize) -> *mut Rect {
|
||||
// create a new mutable buffer with capacity `len`
|
||||
let mut buf: Vec<Rect> = Vec::with_capacity(len);
|
||||
let ptr = buf.as_mut_ptr();
|
||||
// take ownership of the memory block and ensure the its destructor is not
|
||||
// called when the object goes out of scope at the end of the function
|
||||
std::mem::forget(buf);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe fn free_rects(ptr: *mut Rect, len: usize) {
|
||||
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
|
||||
std::mem::forget(buf);
|
||||
}
|
||||
|
||||
/// Draws a rect at the specified coordinates with the give ncolor
|
||||
/// # Safety
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn draw_rect(state: *mut State, rect: &Rect, color: &Color) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
|
||||
let color = skia::Color::from_argb((color.a * 255.0) as u8, color.r, color.g, color.b);
|
||||
render_rect(&mut state.surface, r, color);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn flush(state: *mut State) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
state
|
||||
.gpu_state
|
||||
.context
|
||||
.flush_and_submit_surface(&mut state.surface, None);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn translate(state: *mut State, dx: f32, dy: f32) {
|
||||
(*state).surface.canvas().translate((dx, dy));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn scale(state: *mut State, sx: f32, sy: f32) {
|
||||
(*state).surface.canvas().scale((sx, sy));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn reset_canvas(state: *mut State) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
state.surface.canvas().clear(skia_safe::Color::TRANSPARENT);
|
||||
state.surface.canvas().reset_matrix();
|
||||
flush(state);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn draw_shapes(
|
||||
state: *mut State,
|
||||
ptr: *mut Rect,
|
||||
len: usize,
|
||||
zoom: f32,
|
||||
dx: f32,
|
||||
dy: f32,
|
||||
) {
|
||||
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
|
||||
reset_canvas(state);
|
||||
scale(state, zoom, zoom);
|
||||
translate(state, dx, dy);
|
||||
// create a `Vec<Rect>` from the pointer to the linear memory and length
|
||||
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
|
||||
|
||||
// let mut text_paint = skia::Paint::default();
|
||||
// text_paint.set_anti_alias(true);
|
||||
// text_paint.set_style(skia_safe::paint::Style::StrokeAndFill);
|
||||
// text_paint.set_stroke_width(1.0);
|
||||
|
||||
// let mut path_paint = skia::Paint::default();
|
||||
// path_paint.set_color(skia_safe::Color::BLACK);
|
||||
// path_paint.set_anti_alias(true);
|
||||
// path_paint.set_stroke_width(1.0);
|
||||
// path_paint.set_style(PaintStyle::Stroke);
|
||||
|
||||
// let svg_canvas = skia_safe::svg::Canvas::new(skia_safe::Rect::from_size((10000, 10000)), None);
|
||||
|
||||
// let mut memory = Vec::new();
|
||||
// let mut document =
|
||||
// skia_safe::pdf::new_document(&mut memory, None).begin_page((10000, 10000), None);
|
||||
// // let pdf_canvas = document.canvas();
|
||||
|
||||
for rect in buf.iter() {
|
||||
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
|
||||
|
||||
let color = skia::Color::from_argb(
|
||||
(rect.a * 255.0) as u8,
|
||||
rect.r as u8,
|
||||
rect.g as u8,
|
||||
rect.b as u8,
|
||||
);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_color(color);
|
||||
|
||||
// render_rect_ref(&mut state.surface, &r, &paint);
|
||||
state.surface.canvas().draw_rect(&r, &paint);
|
||||
// render_rect(&mut state.surface, r, color);
|
||||
|
||||
// paint.set_anti_alias(true);
|
||||
// state.surface.canvas().draw_rect(r, &paint);
|
||||
// state.surface.canvas().draw_text_align(
|
||||
// String::from("Lorem ipsum"),
|
||||
// (rect.left, rect.top),
|
||||
// &state.default_font,
|
||||
// &paint,
|
||||
// skia::utils::text_utils::Align::Left,
|
||||
// );
|
||||
|
||||
// let mut paint = skia::Paint::default();
|
||||
// paint.set_style(skia::PaintStyle::Fill);
|
||||
// paint.set_color(color);
|
||||
// paint.set_anti_alias(true);
|
||||
|
||||
// svg_canvas.draw_rect(r, &paint);
|
||||
// pdf_canvas.draw_rect(r, &paint);
|
||||
|
||||
// text_paint.set_color(color);
|
||||
// state.surface.canvas().draw_str(
|
||||
// "SKIA TEXT",
|
||||
// (rect.left, rect.top),
|
||||
// &state.default_font,
|
||||
// &text_paint,
|
||||
// );
|
||||
|
||||
// svg_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
|
||||
// pdf_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
|
||||
|
||||
// let mut path = Path::new();
|
||||
// path.move_to((rect.left, rect.top));
|
||||
// path.line_to((rect.right, rect.bottom));
|
||||
// state.surface.canvas().draw_path(&path, &path_paint);
|
||||
// svg_canvas.draw_path(&path, &path_paint);
|
||||
// pdf_canvas.draw_path(&path, &path_paint);
|
||||
|
||||
// https://github.com/rust-skia/rust-skia/blob/02c89a87649af8d2870fb631aae4a5e171887367/skia-org/src/skparagraph_example.rs#L18
|
||||
// let mut font_collection = FontCollection::new();
|
||||
// font_collection
|
||||
// .set_default_font_manager(Some(state.typeface_font_provider.clone().into()), None);
|
||||
// let paragraph_style = ParagraphStyle::new();
|
||||
// let mut paragraph_builder = ParagraphBuilder::new(¶graph_style, font_collection);
|
||||
// let mut ts = TextStyle::new();
|
||||
// ts.set_foreground_paint(&Paint::default())
|
||||
// .set_font_families(&[TYPEFACE_ALIAS]);
|
||||
// paragraph_builder.push_style(&ts);
|
||||
// paragraph_builder.add_text("Other skia text");
|
||||
// let mut paragraph = paragraph_builder.build();
|
||||
// paragraph.layout(256.0);
|
||||
// paragraph.paint(state.surface.canvas(), (rect.left, rect.top));
|
||||
// paragraph.paint(&svg_canvas, (rect.left, rect.top));
|
||||
}
|
||||
|
||||
/*
|
||||
// base64 image of the canvas
|
||||
let image = state.surface.image_snapshot();
|
||||
let mut context = state.surface.direct_context();
|
||||
let encoded_image = image.encode(context.as_mut(), EncodedImageFormat::PNG, None).unwrap();
|
||||
let base64_image = base64::encode(&encoded_image.as_bytes());
|
||||
println!("data:image/png;base64,{}", base64_image);
|
||||
|
||||
// SVG representation
|
||||
let svg_data = svg_canvas.end();
|
||||
let svg = String::from_utf8_lossy(svg_data.as_bytes());
|
||||
println!("svg: {}", svg.replace('\n', ""));
|
||||
|
||||
// PDF
|
||||
document.end_page().close();
|
||||
println!("PDF: ");
|
||||
print!("echo ");
|
||||
for byte in &memory {
|
||||
print!("{:02x}", byte);
|
||||
}
|
||||
println!("| xxd -r -p > output.pdf");
|
||||
*/
|
||||
|
||||
flush(state);
|
||||
std::mem::forget(buf);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_gl();
|
||||
}
|
||||
BIN
frontend/resources/images/email/logo-linkedin.png
Normal file
BIN
frontend/resources/images/email/logo-linkedin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
frontend/resources/images/email/logo-mastodon.png
Normal file
BIN
frontend/resources/images/email/logo-mastodon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
frontend/resources/images/email/logo-x.png
Normal file
BIN
frontend/resources/images/email/logo-x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
3
frontend/resources/images/icons/info.svg
Normal file
3
frontend/resources/images/icons/info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 9V5m0 6h0ZM1.333 8.082a6.667 6.667 0 1 1 13.333-.163 6.667 6.667 0 0 1-13.333.163Zh0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
File diff suppressed because it is too large
Load Diff
5
frontend/resources/polyfills/dynamicImport.js
Normal file
5
frontend/resources/polyfills/dynamicImport.js
Normal file
@@ -0,0 +1,5 @@
|
||||
if (!('dynamicImport' in window)) {
|
||||
window.dynamicImport = function(uri) {
|
||||
return import(uri);
|
||||
}
|
||||
};
|
||||
@@ -115,20 +115,30 @@ export async function compileSassAll(worker) {
|
||||
return path.startsWith("app/main/ui/ds/");
|
||||
};
|
||||
|
||||
const isOldComponentSystemFile = (path) => {
|
||||
return path.startsWith("app/main/ui/components/");
|
||||
};
|
||||
|
||||
let files = (await fs.readdir(sourceDir, { recursive: true })).filter(
|
||||
isSassFile,
|
||||
);
|
||||
|
||||
const appFiles = files
|
||||
.filter((path) => !isDesignSystemFile(path))
|
||||
.filter((path) => !isOldComponentSystemFile(path))
|
||||
.map((path) => ph.join(sourceDir, path));
|
||||
|
||||
const dsFiles = files
|
||||
.filter(isDesignSystemFile)
|
||||
.map((path) => ph.join(sourceDir, path));
|
||||
|
||||
const oldComponentsFiles = files
|
||||
.filter(isOldComponentSystemFile)
|
||||
.map((path) => ph.join(sourceDir, path));
|
||||
|
||||
const procs = [compileSass(worker, "resources/styles/main-default.scss", {})];
|
||||
|
||||
for (let path of [...dsFiles, ...appFiles]) {
|
||||
for (let path of [...oldComponentsFiles, ...dsFiles, ...appFiles]) {
|
||||
const proc = limitFn(() => compileSass(worker, path, { modules: true }));
|
||||
procs.push(proc);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/technical-guide/plugins/getting-started/#examples"))
|
||||
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples"))
|
||||
|
||||
(defn- normalize-uri
|
||||
[uri-str]
|
||||
|
||||
@@ -109,9 +109,12 @@
|
||||
file-id file-revn undo-group tags stack-undo? source]}]
|
||||
|
||||
(dm/assert!
|
||||
"expect valid vector of changes"
|
||||
(and (cpc/check-changes! redo-changes)
|
||||
(cpc/check-changes! undo-changes)))
|
||||
"expect valid vector of changes for redo-changes"
|
||||
(cpc/check-changes! redo-changes))
|
||||
|
||||
(dm/assert!
|
||||
"expect valid vector of changes for undo-changes"
|
||||
(cpc/check-changes! undo-changes))
|
||||
|
||||
(let [commit-id (or commit-id (uuid/next))
|
||||
source (d/nilv source :local)
|
||||
|
||||
@@ -19,33 +19,31 @@
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private schema:comment-thread
|
||||
(sm/define
|
||||
[:map {:title "CommentThread"}
|
||||
[:id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:page-name :string]
|
||||
[:file-name :string]
|
||||
[:seqn :int]
|
||||
[:content :string]
|
||||
[:participants ::sm/set-of-uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:modified-at ::sm/inst]
|
||||
[:position ::gpt/point]
|
||||
[:count-unread-comments {:optional true} :int]
|
||||
[:count-comments {:optional true} :int]]))
|
||||
[:map {:title "CommentThread"}
|
||||
[:id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:page-name :string]
|
||||
[:file-name :string]
|
||||
[:seqn :int]
|
||||
[:content :string]
|
||||
[:participants ::sm/set-of-uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:modified-at ::sm/inst]
|
||||
[:position ::gpt/point]
|
||||
[:count-unread-comments {:optional true} :int]
|
||||
[:count-comments {:optional true} :int]])
|
||||
|
||||
(def ^:private schema:comment
|
||||
(sm/define
|
||||
[:map {:title "Comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:thread-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:modified-at ::sm/inst]
|
||||
[:content :string]]))
|
||||
[:map {:title "Comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:thread-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:created-at ::sm/inst]
|
||||
[:modified-at ::sm/inst]
|
||||
[:content :string]])
|
||||
|
||||
(def check-comment-thread!
|
||||
(sm/check-fn schema:comment-thread))
|
||||
@@ -58,58 +56,63 @@
|
||||
(declare refresh-comment-thread)
|
||||
|
||||
(defn created-thread-on-workspace
|
||||
[{:keys [id comment page-id] :as thread}]
|
||||
(ptk/reify ::created-thread-on-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [position (select-keys thread [:position :frame-id])]
|
||||
(-> state
|
||||
(update :comment-threads assoc id (dissoc thread :comment))
|
||||
(update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position)
|
||||
(update :comments-local assoc :open id)
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :comments-local dissoc :draft)
|
||||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment))))
|
||||
([params]
|
||||
(created-thread-on-workspace params true))
|
||||
([{:keys [id comment page-id] :as thread} open?]
|
||||
(ptk/reify ::created-thread-on-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [position (select-keys thread [:position :frame-id])]
|
||||
(-> state
|
||||
(update :comment-threads assoc id (dissoc thread :comment))
|
||||
(update-in [:workspace-data :pages-index page-id :comment-thread-positions] assoc id position)
|
||||
(cond-> open?
|
||||
(update :comments-local assoc :open id))
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :comments-local dissoc :draft)
|
||||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (ptk/data-event ::ev/event
|
||||
{::ev/name "create-comment-thread"
|
||||
::ev/origin "workspace"
|
||||
:id id
|
||||
:content-size (count (:content comment))})))))
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (ptk/data-event ::ev/event
|
||||
{::ev/name "create-comment-thread"
|
||||
::ev/origin "workspace"
|
||||
:id id
|
||||
:content-size (count (:content comment))}))))))
|
||||
|
||||
|
||||
|
||||
(def ^:private
|
||||
schema:create-thread-on-workspace
|
||||
(sm/define
|
||||
[:map {:title "created-thread-on-workspace"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]]))
|
||||
[:map {:title "created-thread-on-workspace"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]])
|
||||
|
||||
(defn create-thread-on-workspace
|
||||
[params]
|
||||
(dm/assert! (sm/check! schema:create-thread-on-workspace params))
|
||||
([params]
|
||||
(create-thread-on-workspace params identity true))
|
||||
([params on-thread-created open?]
|
||||
(dm/assert! (sm/check! schema:create-thread-on-workspace params))
|
||||
|
||||
(ptk/reify ::create-thread-on-workspace
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame-id (ctst/get-frame-id-by-position objects (:position params))
|
||||
params (assoc params :frame-id frame-id)]
|
||||
(->> (rp/cmd! :create-comment-thread params)
|
||||
(rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)}))
|
||||
(rx/map created-thread-on-workspace)
|
||||
(rx/catch (fn [{:keys [type code] :as cause}]
|
||||
(if (and (= type :restriction)
|
||||
(= code :max-quote-reached))
|
||||
(rx/throw cause)
|
||||
(rx/throw {:type :comment-error})))))))))
|
||||
(ptk/reify ::create-thread-on-workspace
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame-id (ctst/get-frame-id-by-position objects (:position params))
|
||||
params (assoc params :frame-id frame-id)]
|
||||
(->> (rp/cmd! :create-comment-thread params)
|
||||
(rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)}))
|
||||
(rx/tap on-thread-created)
|
||||
(rx/map #(created-thread-on-workspace % open?))
|
||||
(rx/catch (fn [{:keys [type code] :as cause}]
|
||||
(if (and (= type :restriction)
|
||||
(= code :max-quote-reached))
|
||||
(rx/throw cause)
|
||||
(rx/throw {:type :comment-error}))))))))))
|
||||
|
||||
(defn created-thread-on-viewer
|
||||
[{:keys [id comment page-id] :as thread}]
|
||||
@@ -119,7 +122,7 @@
|
||||
(let [position (select-keys thread [:position :frame-id])]
|
||||
(-> state
|
||||
(update :comment-threads assoc id (dissoc thread :comment))
|
||||
(update-in [:viewer :pages page-id :options :comment-threads-position] assoc id position)
|
||||
(update-in [:viewer :pages page-id :comment-thread-positions] assoc id position)
|
||||
(update :comments-local assoc :open id)
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :comments-local dissoc :draft)
|
||||
@@ -136,13 +139,12 @@
|
||||
|
||||
(def ^:private
|
||||
schema:create-thread-on-viewer
|
||||
(sm/define
|
||||
[:map {:title "created-thread-on-viewer"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]]))
|
||||
[:map {:title "created-thread-on-viewer"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]])
|
||||
|
||||
(defn create-thread-on-viewer
|
||||
[params]
|
||||
@@ -261,29 +263,31 @@
|
||||
(rx/map #(retrieve-comment-threads file-id)))))))
|
||||
|
||||
(defn delete-comment-thread-on-workspace
|
||||
[{:keys [id] :as thread}]
|
||||
(dm/assert!
|
||||
"expected valid comment thread"
|
||||
(check-comment-thread! thread))
|
||||
(ptk/reify ::delete-comment-thread-on-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)]
|
||||
(-> state
|
||||
(update-in [:workspace-data :pages-index page-id :options :comment-threads-position] dissoc id)
|
||||
(update :comments dissoc id)
|
||||
(update :comment-threads dissoc id))))
|
||||
([params]
|
||||
(delete-comment-thread-on-workspace params identity))
|
||||
([{:keys [id] :as thread} on-delete]
|
||||
(dm/assert! (uuid? id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(->> (rp/cmd! :delete-comment-thread {:id id})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/ignore))
|
||||
(rx/of (ptk/data-event ::ev/event
|
||||
{::ev/name "delete-comment-thread"
|
||||
::ev/origin "workspace"
|
||||
:id id}))))))
|
||||
(ptk/reify ::delete-comment-thread-on-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)]
|
||||
(-> state
|
||||
(update-in [:workspace-data :pages-index page-id :comment-thread-positions] dissoc id)
|
||||
(update :comments dissoc id)
|
||||
(update :comment-threads dissoc id))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(->> (rp/cmd! :delete-comment-thread {:id id})
|
||||
(rx/catch #(rx/throw {:type :comment-error}))
|
||||
(rx/tap on-delete)
|
||||
(rx/ignore))
|
||||
(rx/of (ptk/data-event ::ev/event
|
||||
{::ev/name "delete-comment-thread"
|
||||
::ev/origin "workspace"
|
||||
:id id})))))))
|
||||
|
||||
(defn delete-comment-thread-on-viewer
|
||||
[{:keys [id] :as thread}]
|
||||
@@ -295,7 +299,7 @@
|
||||
(update [_ state]
|
||||
(let [page-id (:current-page-id state)]
|
||||
(-> state
|
||||
(update-in [:viewer :pages page-id :options :comment-threads-position] dissoc id)
|
||||
(update-in [:viewer :pages page-id :comment-thread-positions] dissoc id)
|
||||
(update :comments dissoc id)
|
||||
(update :comment-threads dissoc id))))
|
||||
|
||||
@@ -352,7 +356,7 @@
|
||||
[file-id]
|
||||
(dm/assert! (uuid? file-id))
|
||||
(letfn [(set-comment-threds [state comment-thread]
|
||||
(let [path [:workspace-data :pages-index (:page-id comment-thread) :options :comment-threads-position (:id comment-thread)]
|
||||
(let [path [:workspace-data :pages-index (:page-id comment-thread) :comment-thread-positions (:id comment-thread)]
|
||||
thread-position (get-in state path)]
|
||||
(cond-> state
|
||||
(nil? thread-position)
|
||||
@@ -469,11 +473,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:create-draft
|
||||
(sm/define
|
||||
[:map {:title "create-draft"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]]))
|
||||
[:map {:title "create-draft"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]])
|
||||
|
||||
(defn create-draft
|
||||
[params]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.object :as obj]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
[beicon.v2.operators :as rxo]
|
||||
@@ -170,7 +170,7 @@
|
||||
(let [session (atom nil)
|
||||
stopper (rx/filter (ptk/type? ::initialize) stream)
|
||||
buffer (atom #queue [])
|
||||
profile (->> (rx/from-atom storage {:emit-current-value? true})
|
||||
profile (->> (rx/from-atom storage/user {:emit-current-value? true})
|
||||
(rx/map :profile)
|
||||
(rx/map :id)
|
||||
(rx/pipe (rxo/distinct-contiguous)))]
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.webapi :as wa]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@@ -335,8 +335,9 @@
|
||||
(assoc-in state [:workspace-data :recent-fonts] most-recent-fonts)))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [most-recent-fonts (get-in state [:workspace-data :recent-fonts])]
|
||||
(swap! storage assoc ::recent-fonts most-recent-fonts)))))
|
||||
(let [most-recent-fonts (get-in state [:workspace-data :recent-fonts])]
|
||||
;; FIXME: this should be prefixed by team
|
||||
(swap! storage/user assoc ::recent-fonts most-recent-fonts)))))
|
||||
|
||||
(defn load-recent-fonts
|
||||
[fonts]
|
||||
@@ -344,7 +345,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [fonts-map (d/index-by :id fonts)
|
||||
saved-recent-fonts (->> (::recent-fonts @storage)
|
||||
saved-recent-fonts (->> (::recent-fonts storage/user)
|
||||
(keep #(get fonts-map (:id %)))
|
||||
(into #{}))]
|
||||
(assoc-in state [:workspace-data :recent-fonts] saved-recent-fonts)))))
|
||||
|
||||
@@ -129,12 +129,11 @@
|
||||
|
||||
(def ^:private
|
||||
schema:shortcuts
|
||||
(sm/define
|
||||
[:map-of :keyword
|
||||
[:map
|
||||
[:command [:or :string [:vector :any]]]
|
||||
[:fn {:optional true} fn?]
|
||||
[:tooltip {:optional true} :string]]]))
|
||||
[:map-of :keyword
|
||||
[:map
|
||||
[:command [:or :string [:vector :any]]]
|
||||
[:fn {:optional true} fn?]
|
||||
[:tooltip {:optional true} :string]]])
|
||||
|
||||
(def check-shortcuts!
|
||||
(sm/check-fn schema:shortcuts))
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.plugins.register :as register]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as s]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -31,14 +32,13 @@
|
||||
|
||||
(def ^:private
|
||||
schema:profile
|
||||
(sm/define
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at {:optional true} :any]
|
||||
[:fullname {:optional true} :string]
|
||||
[:email {:optional true} :string]
|
||||
[:lang {:optional true} :string]
|
||||
[:theme {:optional true} :string]]))
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at {:optional true} :any]
|
||||
[:fullname {:optional true} :string]
|
||||
[:email {:optional true} :string]
|
||||
[:lang {:optional true} :string]
|
||||
[:theme {:optional true} :string]])
|
||||
|
||||
(def check-profile!
|
||||
(sm/check-fn schema:profile))
|
||||
@@ -51,14 +51,14 @@
|
||||
|
||||
(defn get-current-team-id
|
||||
[profile]
|
||||
(let [team-id (::current-team-id @s/storage)]
|
||||
(let [team-id (::current-team-id storage/user)]
|
||||
(or team-id (:default-team-id profile))))
|
||||
|
||||
(defn set-current-team!
|
||||
[team-id]
|
||||
(if (nil? team-id)
|
||||
(swap! s/storage dissoc ::current-team-id)
|
||||
(swap! s/storage assoc ::current-team-id team-id)))
|
||||
(swap! storage/user dissoc ::current-team-id)
|
||||
(swap! storage/user assoc ::current-team-id team-id)))
|
||||
|
||||
;; --- EVENT: fetch-teams
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
;; if not, dissoc it from storage.
|
||||
|
||||
(let [ids (into #{} (map :id) teams)]
|
||||
(when-let [ctid (::current-team-id @s/storage)]
|
||||
(when-let [ctid (::current-team-id storage/user)]
|
||||
(when-not (contains? ids ctid)
|
||||
(swap! s/storage dissoc ::current-team-id)))))))
|
||||
(swap! storage/user dissoc ::current-team-id)))))))
|
||||
|
||||
(defn fetch-teams
|
||||
[]
|
||||
@@ -131,13 +131,15 @@
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)
|
||||
email (:email profile)
|
||||
previous-profile (:profile @s/storage)
|
||||
previous-profile (:profile storage/user)
|
||||
previous-email (:email previous-profile)]
|
||||
(when profile
|
||||
(swap! s/storage assoc :profile profile)
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(when (not= previous-email email)
|
||||
(set-current-team! nil)))))))
|
||||
(set-current-team! nil))
|
||||
|
||||
(register/init))))))
|
||||
|
||||
(defn- on-fetch-profile-exception
|
||||
[cause]
|
||||
@@ -170,13 +172,13 @@
|
||||
(letfn [(get-redirect-events []
|
||||
(let [team-id (get-current-team-id profile)
|
||||
welcome-file-id (dm/get-in profile [:props :welcome-file-id])
|
||||
redirect-href (:login-redirect @s/session)
|
||||
redirect-href (:login-redirect @storage/session)
|
||||
current-href (rt/get-current-href)]
|
||||
|
||||
(cond
|
||||
(some? redirect-href)
|
||||
(binding [s/*sync* true]
|
||||
(swap! s/session dissoc :login-redirect)
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session dissoc :login-redirect)
|
||||
(if (= current-href redirect-href)
|
||||
(rx/of (rt/reload true))
|
||||
(rx/of (rt/nav-raw :href redirect-href))))
|
||||
@@ -262,10 +264,9 @@
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def ^:private schema:login-with-ldap
|
||||
(sm/define
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:password :string]]))
|
||||
[:map {:title "login-with-ldap"}
|
||||
[:email ::sm/email]
|
||||
[:password :string]])
|
||||
|
||||
(defn login-with-ldap
|
||||
[params]
|
||||
@@ -345,7 +346,7 @@
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(swap! s/storage (constantly {}))))))
|
||||
(swap! storage/user (constantly {}))))))
|
||||
|
||||
(defn logout
|
||||
([] (logout {}))
|
||||
@@ -495,6 +496,7 @@
|
||||
|
||||
;; TODO: for the release 1.13 we should skip fetching profile and just use
|
||||
;; the response value of update-profile-props RPC call
|
||||
;; FIXME
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :update-profile-props {:props props})
|
||||
@@ -604,9 +606,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:request-profile-recovery
|
||||
(sm/define
|
||||
[:map {:title "request-profile-recovery" :closed true}
|
||||
[:email ::sm/email]]))
|
||||
[:map {:title "request-profile-recovery" :closed true}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(defn request-profile-recovery
|
||||
[data]
|
||||
@@ -630,10 +631,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:recover-profile
|
||||
(sm/define
|
||||
[:map {:title "recover-profile" :closed true}
|
||||
[:password :string]
|
||||
[:token :string]]))
|
||||
[:map {:title "recover-profile" :closed true}
|
||||
[:password :string]
|
||||
[:token :string]])
|
||||
|
||||
(defn recover-profile
|
||||
[data]
|
||||
|
||||
@@ -49,11 +49,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:initialize
|
||||
(sm/define
|
||||
[:map {:title "initialize"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:page-id {:optional true} ::sm/uuid]]))
|
||||
[:map {:title "initialize"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:page-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn initialize
|
||||
[{:keys [file-id share-id interactions-show?] :as params}]
|
||||
@@ -102,11 +101,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:fetch-bundle
|
||||
(sm/define
|
||||
[:map {:title "fetch-bundle"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]]))
|
||||
[:map {:title "fetch-bundle"}
|
||||
[:page-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- fetch-bundle
|
||||
[{:keys [file-id share-id] :as params}]
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -337,7 +337,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state
|
||||
:recent-colors (:recent-colors @storage)
|
||||
:recent-colors (:recent-colors storage/user)
|
||||
:workspace-ready? false
|
||||
:current-file-id file-id
|
||||
:current-project-id project-id
|
||||
@@ -565,7 +565,7 @@
|
||||
(watch [it state _]
|
||||
(let [page (get-in state [:workspace-data :pages-index id])
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/mod-page page name))]
|
||||
(pcb/mod-page page {:name name}))]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
@@ -595,7 +595,7 @@
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-file-data file-data)
|
||||
(assoc :file-id file-id)
|
||||
(pcb/mod-plugin-data type id page-id namespace key value))]
|
||||
(pcb/set-plugin-data type id page-id namespace key value))]
|
||||
(rx/of (dch/commit-changes changes)))))))
|
||||
|
||||
(declare purge-page)
|
||||
@@ -971,25 +971,27 @@
|
||||
(map #(gal/align-to-rect % rect axis) selected-objs)))
|
||||
|
||||
(defn align-objects
|
||||
[axis]
|
||||
(dm/assert!
|
||||
"expected valid align axis value"
|
||||
(contains? gal/valid-align-axis axis))
|
||||
([axis]
|
||||
(align-objects axis nil))
|
||||
([axis selected]
|
||||
(dm/assert!
|
||||
"expected valid align axis value"
|
||||
(contains? gal/valid-align-axis axis))
|
||||
|
||||
(ptk/reify ::align-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (wsh/lookup-selected state)
|
||||
moved (if (= 1 (count selected))
|
||||
(align-object-to-parent objects (first selected) axis)
|
||||
(align-objects-list objects selected axis))
|
||||
undo-id (js/Symbol)]
|
||||
(when (can-align? selected objects)
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwt/position-shapes moved)
|
||||
(ptk/data-event :layout/update {:ids selected})
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
(ptk/reify ::align-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
selected (or selected (wsh/lookup-selected state))
|
||||
moved (if (= 1 (count selected))
|
||||
(align-object-to-parent objects (first selected) axis)
|
||||
(align-objects-list objects selected axis))
|
||||
undo-id (js/Symbol)]
|
||||
(when (can-align? selected objects)
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwt/position-shapes moved)
|
||||
(ptk/data-event :layout/update {:ids selected})
|
||||
(dwu/commit-undo-transaction undo-id))))))))
|
||||
|
||||
(defn can-distribute? [selected]
|
||||
(cond
|
||||
@@ -998,25 +1000,27 @@
|
||||
:else true))
|
||||
|
||||
(defn distribute-objects
|
||||
[axis]
|
||||
(dm/assert!
|
||||
"expected valid distribute axis value"
|
||||
(contains? gal/valid-dist-axis axis))
|
||||
([axis]
|
||||
(distribute-objects axis nil))
|
||||
([axis ids]
|
||||
(dm/assert!
|
||||
"expected valid distribute axis value"
|
||||
(contains? gal/valid-dist-axis axis))
|
||||
|
||||
(ptk/reify ::distribute-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected (wsh/lookup-selected state)
|
||||
moved (-> (map #(get objects %) selected)
|
||||
(gal/distribute-space axis))
|
||||
undo-id (js/Symbol)]
|
||||
(when (can-distribute? selected)
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwt/position-shapes moved)
|
||||
(ptk/data-event :layout/update {:ids selected})
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
(ptk/reify ::distribute-objects
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
selected (or ids (wsh/lookup-selected state))
|
||||
moved (-> (map #(get objects %) selected)
|
||||
(gal/distribute-space axis))
|
||||
undo-id (js/Symbol)]
|
||||
(when (can-distribute? selected)
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwt/position-shapes moved)
|
||||
(ptk/data-event :layout/update {:ids selected})
|
||||
(dwu/commit-undo-transaction undo-id))))))))
|
||||
|
||||
;; --- Shape Proportions
|
||||
|
||||
@@ -1702,17 +1706,19 @@
|
||||
|
||||
(def ^:private
|
||||
schema:paste-data
|
||||
(sm/define
|
||||
[:map {:title "paste-data"}
|
||||
[:type [:= :copied-shapes]]
|
||||
[:features ::sm/set-of-strings]
|
||||
[:version :int]
|
||||
[:file-id ::sm/uuid]
|
||||
[:selected ::sm/set-of-uuid]
|
||||
[:objects
|
||||
[:map-of ::sm/uuid :map]]
|
||||
[:images [:set :map]]
|
||||
[:position {:optional true} ::gpt/point]]))
|
||||
[:map {:title "paste-data"}
|
||||
[:type [:= :copied-shapes]]
|
||||
[:features ::sm/set-of-strings]
|
||||
[:version :int]
|
||||
[:file-id ::sm/uuid]
|
||||
[:selected ::sm/set-of-uuid]
|
||||
[:objects
|
||||
[:map-of ::sm/uuid :map]]
|
||||
[:images [:set :map]]
|
||||
[:position {:optional true} ::gpt/point]])
|
||||
|
||||
(def validate-paste-data!
|
||||
(sm/validate-fn schema:paste-data))
|
||||
|
||||
(defn- paste-transit
|
||||
[{:keys [images] :as pdata}]
|
||||
@@ -1737,9 +1743,8 @@
|
||||
(let [file-id (:current-file-id state)
|
||||
features (features/get-team-enabled-features state)]
|
||||
|
||||
(sm/validate! schema:paste-data pdata
|
||||
{:hint "invalid paste data"
|
||||
:code :invalid-paste-data})
|
||||
(validate-paste-data! pdata {:hint "invalid paste data"
|
||||
:code :invalid-paste-data})
|
||||
|
||||
(cfeat/check-paste-features! features (:features pdata))
|
||||
(if (= file-id (:file-id pdata))
|
||||
@@ -2088,7 +2093,7 @@
|
||||
page (wsh/lookup-page state page-id)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-page-option :background (:color color)))]
|
||||
(pcb/mod-page {:background (:color color)}))]
|
||||
(rx/of (dch/commit-changes changes)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -7,22 +7,22 @@
|
||||
(ns app.main.data.workspace.assets
|
||||
"Workspace assets management events and helpers."
|
||||
(:require
|
||||
[app.util.storage :refer [storage]]))
|
||||
[app.util.storage :as storage]))
|
||||
|
||||
(defn get-current-assets-ordering
|
||||
[]
|
||||
(let [ordering (::ordering @storage)]
|
||||
(let [ordering (::ordering storage/user)]
|
||||
(or ordering :asc)))
|
||||
|
||||
(defn set-current-assets-ordering!
|
||||
[ordering]
|
||||
(swap! storage assoc ::ordering ordering))
|
||||
(swap! storage/user assoc ::ordering ordering))
|
||||
|
||||
(defn get-current-assets-list-style
|
||||
[]
|
||||
(let [list-style (::list-style @storage)]
|
||||
(let [list-style (::list-style storage/user)]
|
||||
(or list-style :thumbs)))
|
||||
|
||||
(defn set-current-assets-list-style!
|
||||
[list-style]
|
||||
(swap! storage assoc ::list-style list-style))
|
||||
(swap! storage/user assoc ::list-style list-style))
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -815,9 +815,9 @@
|
||||
|
||||
(defn get-active-color-tab
|
||||
[]
|
||||
(let [tab (::tab @storage)]
|
||||
(let [tab (::tab storage/user)]
|
||||
(or tab :ramp)))
|
||||
|
||||
(defn set-active-color-tab!
|
||||
[tab]
|
||||
(swap! storage assoc ::tab tab))
|
||||
(swap! storage/user assoc ::tab tab))
|
||||
|
||||
@@ -132,21 +132,20 @@
|
||||
(ptk/reify ::update-comment-thread-position
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [thread-id (:id thread)
|
||||
page (wsh/lookup-page state)
|
||||
page-id (:id page)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
new-frame-id (if (nil? frame-id)
|
||||
(ctst/get-frame-id-by-position objects (gpt/point new-x new-y))
|
||||
(:frame-id thread))
|
||||
thread (assoc thread
|
||||
:position (gpt/point new-x new-y)
|
||||
:frame-id new-frame-id)
|
||||
(let [page (wsh/lookup-page state)
|
||||
page-id (:id page)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
frame-id (if (nil? frame-id)
|
||||
(ctst/get-frame-id-by-position objects (gpt/point new-x new-y))
|
||||
(:frame-id thread))
|
||||
|
||||
changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :comment-threads-position assoc thread-id (select-keys thread [:position :frame-id])))]
|
||||
thread (-> thread
|
||||
(assoc :position (gpt/point new-x new-y))
|
||||
(assoc :frame-id frame-id))
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position thread))]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
@@ -164,25 +163,28 @@
|
||||
(ptk/reify ::move-frame-comment-threads
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (wsh/lookup-page-objects state)
|
||||
(let [page (wsh/lookup-page state)
|
||||
objects (get page :objects)
|
||||
|
||||
is-frame? (fn [id] (= :frame (get-in objects [id :type])))
|
||||
is-frame? (fn [id] (= :frame (get-in objects [id :type])))
|
||||
frame-ids? (into #{} (filter is-frame?) ids)
|
||||
|
||||
object-modifiers (:workspace-modifiers state)
|
||||
threads-position-map
|
||||
(get page :comment-thread-positions)
|
||||
|
||||
threads-position-map (:comment-threads-position (wsh/lookup-page-options state))
|
||||
object-modifiers
|
||||
(:workspace-modifiers state)
|
||||
|
||||
build-move-event
|
||||
(fn [comment-thread]
|
||||
(let [frame (get objects (:frame-id comment-thread))
|
||||
(let [frame (get objects (:frame-id comment-thread))
|
||||
modifiers (get-in object-modifiers [(:frame-id comment-thread) :modifiers])
|
||||
frame' (gsh/transform-shape frame modifiers)
|
||||
moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
|
||||
(gpt/point (:x frame') (:y frame')))
|
||||
position (get-in threads-position-map [(:id comment-thread) :position])
|
||||
new-x (+ (:x position) (:x moved))
|
||||
new-y (+ (:y position) (:y moved))]
|
||||
frame' (gsh/transform-shape frame modifiers)
|
||||
moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
|
||||
(gpt/point (:x frame') (:y frame')))
|
||||
position (get-in threads-position-map [(:id comment-thread) :position])
|
||||
new-x (+ (:x position) (:x moved))
|
||||
new-y (+ (:y position) (:y moved))]
|
||||
(update-comment-thread-position comment-thread [new-x new-y] (:id frame))))]
|
||||
|
||||
(->> (:comment-threads state)
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
(ns app.main.data.workspace.grid
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.types.grid :as ctg]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
@@ -20,25 +20,6 @@
|
||||
;; Grid
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce ^:private default-square-params
|
||||
{:size 16
|
||||
:color {:color clr/info
|
||||
:opacity 0.4}})
|
||||
|
||||
(defonce ^:private default-layout-params
|
||||
{:size 12
|
||||
:type :stretch
|
||||
:item-length nil
|
||||
:gutter 8
|
||||
:margin 0
|
||||
:color {:color clr/default-layout
|
||||
:opacity 0.1}})
|
||||
|
||||
(defonce default-grid-params
|
||||
{:square default-square-params
|
||||
:column default-layout-params
|
||||
:row default-layout-params})
|
||||
|
||||
(defn add-frame-grid
|
||||
[frame-id]
|
||||
(dm/assert! (uuid? frame-id))
|
||||
@@ -46,9 +27,9 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
data (get-in state [:workspace-data :pages-index page-id])
|
||||
params (or (get-in data [:options :saved-grids :square])
|
||||
(:square default-grid-params))
|
||||
page (dm/get-in state [:workspace-data :pages-index page-id])
|
||||
params (or (dm/get-in page [:default-grids :square])
|
||||
(:square ctg/default-grid-params))
|
||||
grid {:type :square
|
||||
:params params
|
||||
:display true}]
|
||||
@@ -79,4 +60,4 @@
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-page-option [:saved-grids type] params))))))))
|
||||
(pcb/set-default-grid type params))))))))
|
||||
|
||||
@@ -17,18 +17,12 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn make-update-guide
|
||||
[guide]
|
||||
(fn [other]
|
||||
(cond-> other
|
||||
(= (:id other) (:id guide))
|
||||
(merge guide))))
|
||||
|
||||
(defn update-guides
|
||||
[guide]
|
||||
[{:keys [id] :as guide}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid guide"
|
||||
(ctp/check-page-guide! guide))
|
||||
(ctp/valid-guide? guide))
|
||||
|
||||
(ptk/reify ::update-guides
|
||||
ev/Event
|
||||
@@ -41,14 +35,15 @@
|
||||
changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :guides assoc (:id guide) guide))]
|
||||
(pcb/set-guide id guide))]
|
||||
(rx/of (dwc/commit-changes changes))))))
|
||||
|
||||
(defn remove-guide
|
||||
[guide]
|
||||
[{:keys [id] :as guide}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid guide"
|
||||
(ctp/check-page-guide! guide))
|
||||
(ctp/valid-guide? guide))
|
||||
|
||||
(ptk/reify ::remove-guide
|
||||
ev/Event
|
||||
@@ -57,7 +52,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [sdisj (fnil disj #{})]
|
||||
(update-in state [:workspace-guides :hover] sdisj (:id guide))))
|
||||
(update-in state [:workspace-guides :hover] sdisj id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -65,18 +60,22 @@
|
||||
changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :guides dissoc (:id guide)))]
|
||||
(pcb/set-guide id nil))]
|
||||
(rx/of (dwc/commit-changes changes))))))
|
||||
|
||||
(defn remove-guides
|
||||
[ids]
|
||||
|
||||
(dm/assert!
|
||||
"expected a set of ids"
|
||||
(every? uuid? ids))
|
||||
|
||||
(ptk/reify ::remove-guides
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page (wsh/lookup-page state)
|
||||
guides (get-in page [:options :guides] {})
|
||||
(let [{:keys [guides] :as page} (wsh/lookup-page state)
|
||||
guides (-> (select-keys guides ids) (vals))]
|
||||
(rx/from (->> guides (mapv #(remove-guide %))))))))
|
||||
(rx/from (mapv remove-guide guides))))))
|
||||
|
||||
(defmethod ptk/resolve ::move-frame-guides
|
||||
[_ args]
|
||||
@@ -105,7 +104,7 @@
|
||||
guide (update guide :position + (get moved (:axis guide)))]
|
||||
(update-guides guide)))
|
||||
|
||||
guides (-> state wsh/lookup-page-options :guides vals)]
|
||||
guides (-> state wsh/lookup-page :guides vals)]
|
||||
|
||||
(->> guides
|
||||
(filter (comp frame-ids? :frame-id))
|
||||
|
||||
@@ -43,18 +43,20 @@
|
||||
(wsh/lookup-page state page-id)
|
||||
(wsh/lookup-page state))
|
||||
|
||||
flows (get-in page [:options :flows] [])
|
||||
unames (cfh/get-used-names flows)
|
||||
flows (get page :flows)
|
||||
unames (cfh/get-used-names (vals flows))
|
||||
name (or name (cfh/generate-unique-name unames "Flow 1"))
|
||||
|
||||
new-flow {:id (or flow-id (uuid/next))
|
||||
:name name
|
||||
:starting-frame starting-frame}]
|
||||
flow-id (or flow-id (uuid/next))
|
||||
|
||||
flow {:id flow-id
|
||||
:name name
|
||||
:starting-frame starting-frame}]
|
||||
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :flows ctp/add-flow new-flow)))))))))
|
||||
(pcb/set-flow flow-id flow)))))))))
|
||||
|
||||
(defn add-flow-selected-frame
|
||||
[]
|
||||
@@ -79,35 +81,40 @@
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :flows ctp/remove-flow flow-id)))))))))
|
||||
(pcb/set-flow flow-id nil)))))))))
|
||||
|
||||
(defn update-flow
|
||||
[page-id flow-id update-fn]
|
||||
(dm/assert! (uuid? flow-id))
|
||||
|
||||
(assert (uuid? flow-id) "expect valid flow-id")
|
||||
(assert (uuid? page-id) "expect valid page-id")
|
||||
|
||||
(ptk/reify ::update-flow
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page (if page-id
|
||||
(wsh/lookup-page state page-id)
|
||||
(wsh/lookup-page state))]
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :flows ctp/update-flow flow-id update-fn))))))))
|
||||
(wsh/lookup-page state))
|
||||
flow (dm/get-in page [:flows flow-id])
|
||||
flow (some-> flow update-fn)]
|
||||
|
||||
(when (some? flow)
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-flow flow-id flow)))))))))
|
||||
|
||||
(defn rename-flow
|
||||
[flow-id name]
|
||||
(dm/assert! (uuid? flow-id))
|
||||
(dm/assert! (string? name))
|
||||
|
||||
(assert (uuid? flow-id) "expected valid flow-id")
|
||||
(assert (string? name) "expected valid name")
|
||||
|
||||
(ptk/reify ::rename-flow
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(watch [_ state _]
|
||||
(let [page (wsh/lookup-page state)]
|
||||
(rx/of (dch/commit-changes
|
||||
(-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/update-page-option :flows ctp/update-flow flow-id
|
||||
#(ctp/rename-flow % name)))))))))
|
||||
(rx/of (update-flow (:id page) flow-id #(assoc % :name name)))))))
|
||||
|
||||
(defn start-rename-flow
|
||||
[id]
|
||||
@@ -153,13 +160,11 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state page-id)
|
||||
page (wsh/lookup-page state page-id)
|
||||
objects (get page :objects)
|
||||
frame (cfh/get-root-frame objects (:id shape))
|
||||
flows (get-in state [:workspace-data
|
||||
:pages-index
|
||||
page-id
|
||||
:options
|
||||
:flows] [])
|
||||
|
||||
flows (get page :objects)
|
||||
flow (ctp/get-frame-flow flows (:id frame))]
|
||||
(rx/concat
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.storage :as storage]
|
||||
[clojure.set :as set]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
stored in Storage."
|
||||
[layout]
|
||||
(reduce (fn [layout [flag key]]
|
||||
(condp = (get @storage key ::none)
|
||||
(condp = (get storage/user key ::none)
|
||||
::none layout
|
||||
false (disj layout flag)
|
||||
true (conj layout flag)))
|
||||
@@ -155,7 +155,7 @@
|
||||
"Given a set of layout flags, and persist a subset of them to the Storage."
|
||||
[layout]
|
||||
(doseq [[flag key] layout-flags-persistence-mapping]
|
||||
(swap! storage assoc key (contains? layout flag))))
|
||||
(swap! storage/user assoc key (contains? layout flag))))
|
||||
|
||||
(def layout-state-persistence-mapping
|
||||
"A mapping of keys that need to be persisted from `:workspace-global` into Storage."
|
||||
@@ -167,7 +167,7 @@
|
||||
props that are previously persisted in the Storage."
|
||||
[state]
|
||||
(reduce (fn [state [key skey]]
|
||||
(let [val (get @storage skey ::none)]
|
||||
(let [val (get storage/user skey ::none)]
|
||||
(if (= val ::none)
|
||||
state
|
||||
(assoc state key val))))
|
||||
@@ -181,7 +181,7 @@
|
||||
(doseq [[key skey] layout-state-persistence-mapping]
|
||||
(let [val (get state key ::does-not-exist)]
|
||||
(if (= val ::does-not-exist)
|
||||
(swap! storage dissoc skey)
|
||||
(swap! storage assoc skey val)))))
|
||||
(swap! storage/user dissoc skey)
|
||||
(swap! storage/user assoc skey val)))))
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :as s]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.time :as dt]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@@ -152,7 +152,7 @@
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [recent-colors (:recent-colors state)]
|
||||
(swap! s/storage assoc :recent-colors recent-colors)))))
|
||||
(swap! storage/user assoc :recent-colors recent-colors)))))
|
||||
|
||||
(def clear-color-for-rename
|
||||
(ptk/reify ::clear-color-for-rename
|
||||
@@ -682,8 +682,15 @@
|
||||
|
||||
(defn ext-library-changed
|
||||
[library-id modified-at revn changes]
|
||||
(dm/assert! (uuid? library-id))
|
||||
(dm/assert! (ch/check-changes! changes))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for library-id"
|
||||
(uuid? library-id))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid changes vector"
|
||||
(ch/check-changes! changes))
|
||||
|
||||
(ptk/reify ::ext-library-changed
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
||||
@@ -200,14 +200,13 @@
|
||||
|
||||
(def ^:private
|
||||
schema:process-media-objects
|
||||
(sm/define
|
||||
[:map {:title "process-media-objects"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:local? :boolean]
|
||||
[:name {:optional true} :string]
|
||||
[:data {:optional true} :any] ; FIXME
|
||||
[:uris {:optional true} [:sequential :string]]
|
||||
[:mtype {:optional true} :string]]))
|
||||
[:map {:title "process-media-objects"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:local? :boolean]
|
||||
[:name {:optional true} :string]
|
||||
[:data {:optional true} :any] ; FIXME
|
||||
[:uris {:optional true} [:sequential :string]]
|
||||
[:mtype {:optional true} :string]])
|
||||
|
||||
(defn- process-media-objects
|
||||
[{:keys [uris on-error] :as params}]
|
||||
@@ -427,10 +426,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:clone-media-object
|
||||
(sm/define
|
||||
[:map {:title "clone-media-object"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id ::sm/uuid]]))
|
||||
[:map {:title "clone-media-object"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id ::sm/uuid]])
|
||||
|
||||
(defn clone-media-object
|
||||
[{:keys [file-id object-id] :as params}]
|
||||
|
||||
@@ -198,21 +198,23 @@
|
||||
|
||||
(def ^:private
|
||||
schema:handle-file-change
|
||||
(sm/define
|
||||
[:map {:title "handle-file-change"}
|
||||
[:type :keyword]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:changes ::cpc/changes]]))
|
||||
[:map {:title "handle-file-change"}
|
||||
[:type :keyword]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:changes ::cpc/changes]])
|
||||
|
||||
(def ^:private check-file-change-params!
|
||||
(sm/check-fn schema:handle-file-change))
|
||||
|
||||
(defn handle-file-change
|
||||
[{:keys [file-id changes revn] :as msg}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(sm/check! schema:handle-file-change msg))
|
||||
(check-file-change-params! msg))
|
||||
|
||||
(ptk/reify ::handle-file-change
|
||||
IDeref
|
||||
@@ -230,23 +232,24 @@
|
||||
:redo-changes (vec changes)
|
||||
:undo-changes []})))))
|
||||
|
||||
(def ^:private
|
||||
schema:handle-library-change
|
||||
(sm/define
|
||||
[:map {:title "handle-library-change"}
|
||||
[:type :keyword]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:modified-at ::sm/inst]
|
||||
[:changes ::cpc/changes]]))
|
||||
(def ^:private schema:handle-library-change
|
||||
[:map {:title "handle-library-change"}
|
||||
[:type :keyword]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:modified-at ::sm/inst]
|
||||
[:changes ::cpc/changes]])
|
||||
|
||||
(def ^:private check-library-change-params!
|
||||
(sm/check-fn schema:handle-library-change))
|
||||
|
||||
(defn handle-library-change
|
||||
[{:keys [file-id modified-at changes revn] :as msg}]
|
||||
(dm/assert!
|
||||
"expected valid arguments"
|
||||
(sm/check! schema:handle-library-change msg))
|
||||
(check-library-change-params! msg))
|
||||
|
||||
(ptk/reify ::handle-library-change
|
||||
ptk/WatchEvent
|
||||
|
||||
@@ -27,20 +27,19 @@
|
||||
|
||||
(def ^:private
|
||||
schema:path-content
|
||||
(sm/define
|
||||
[:vector {:title "PathContent"}
|
||||
[:map {:title "PathContentEntry"}
|
||||
[:command [::sm/one-of valid-commands]]
|
||||
;; FIXME: remove the `?` from prop name
|
||||
[:relative? {:optional true} :boolean]
|
||||
[:params {:optional true}
|
||||
[:map {:title "PathContentEntryParams"}
|
||||
[:x :double]
|
||||
[:y :double]
|
||||
[:c1x {:optional true} :double]
|
||||
[:c1y {:optional true} :double]
|
||||
[:c2x {:optional true} :double]
|
||||
[:c2y {:optional true} :double]]]]]))
|
||||
[:vector {:title "PathContent"}
|
||||
[:map {:title "PathContentEntry"}
|
||||
[:command [::sm/one-of valid-commands]]
|
||||
;; FIXME: remove the `?` from prop name
|
||||
[:relative? {:optional true} :boolean]
|
||||
[:params {:optional true}
|
||||
[:map {:title "PathContentEntryParams"}
|
||||
[:x :double]
|
||||
[:y :double]
|
||||
[:c1x {:optional true} :double]
|
||||
[:c1y {:optional true} :double]
|
||||
[:c2x {:optional true} :double]
|
||||
[:c2y {:optional true} :double]]]]])
|
||||
|
||||
(def check-path-content!
|
||||
(sm/check-fn schema:path-content))
|
||||
|
||||
@@ -15,24 +15,27 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn convert-selected-to-path []
|
||||
(ptk/reify ::convert-selected-to-path
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
selected (->> (wsh/lookup-selected state)
|
||||
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
|
||||
(defn convert-selected-to-path
|
||||
([]
|
||||
(convert-selected-to-path nil))
|
||||
([ids]
|
||||
(ptk/reify ::convert-selected-to-path
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
selected (->> (or ids (wsh/lookup-selected state))
|
||||
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
|
||||
|
||||
children-ids
|
||||
(into #{}
|
||||
(mapcat #(cph/get-children-ids objects %))
|
||||
selected)
|
||||
children-ids
|
||||
(into #{}
|
||||
(mapcat #(cph/get-children-ids objects %))
|
||||
selected)
|
||||
|
||||
changes
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/update-shapes selected #(upsp/convert-to-path % objects))
|
||||
(pcb/remove-objects children-ids))]
|
||||
changes
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/update-shapes selected #(upsp/convert-to-path % objects))
|
||||
(pcb/remove-objects children-ids))]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
(rx/of (dch/commit-changes changes)))))))
|
||||
|
||||
@@ -20,15 +20,12 @@
|
||||
([state page-id]
|
||||
(get-in state [:workspace-data :pages-index page-id])))
|
||||
|
||||
(defn lookup-data-objects
|
||||
[data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
(defn lookup-page-objects
|
||||
([state]
|
||||
(lookup-page-objects state (:current-page-id state)))
|
||||
([state page-id]
|
||||
(dm/get-in state [:workspace-data :pages-index page-id :objects])))
|
||||
(-> (lookup-page state page-id)
|
||||
(get :objects))))
|
||||
|
||||
(defn lookup-viewer-objects
|
||||
([state page-id]
|
||||
@@ -45,12 +42,6 @@
|
||||
(lookup-page-objects state page-id)
|
||||
(lookup-library-objects state file-id page-id))))
|
||||
|
||||
(defn lookup-page-options
|
||||
([state]
|
||||
(lookup-page-options state (:current-page-id state)))
|
||||
([state page-id]
|
||||
(dm/get-in state [:workspace-data :pages-index page-id :options])))
|
||||
|
||||
(defn lookup-local-components
|
||||
([state]
|
||||
(dm/get-in state [:workspace-data :components])))
|
||||
|
||||
@@ -182,6 +182,11 @@
|
||||
[page-id [event [old-data new-data]]]
|
||||
(let [changes (:changes event)
|
||||
|
||||
lookup-data-objects
|
||||
(fn [data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
|
||||
extract-ids
|
||||
(fn [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
@@ -193,8 +198,8 @@
|
||||
|
||||
get-frame-ids
|
||||
(fn get-frame-ids [id]
|
||||
(let [old-objects (wsh/lookup-data-objects old-data page-id)
|
||||
new-objects (wsh/lookup-data-objects new-data page-id)
|
||||
(let [old-objects (lookup-data-objects old-data page-id)
|
||||
new-objects (lookup-data-objects new-data page-id)
|
||||
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
|
||||
@@ -26,10 +26,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:undo-entry
|
||||
(sm/define
|
||||
[:map {:title "undo-entry"}
|
||||
[:undo-changes [:vector ::cpc/change]]
|
||||
[:redo-changes [:vector ::cpc/change]]]))
|
||||
[:map {:title "undo-entry"}
|
||||
[:undo-changes [:vector ::cpc/change]]
|
||||
[:redo-changes [:vector ::cpc/change]]])
|
||||
|
||||
(def check-undo-entry!
|
||||
(sm/check-fn schema:undo-entry))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as-alias sm]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.users :as du]
|
||||
@@ -32,8 +32,11 @@
|
||||
|
||||
(defn- print-explain!
|
||||
[data]
|
||||
(when-let [explain (or (ex/explain data)
|
||||
(:explain data))]
|
||||
(when-let [{:keys [errors] :as explain} (::sm/explain data)]
|
||||
(let [errors (mapv #(update % :schema sm/form) errors)]
|
||||
(pp/pprint errors {:width 100 :level 15 :length 20})))
|
||||
|
||||
(when-let [explain (:explain data)]
|
||||
(js/console.log explain)))
|
||||
|
||||
(defn- print-trace!
|
||||
|
||||
@@ -281,6 +281,9 @@
|
||||
(dm/get-in data [:pages-index page-id])))
|
||||
st/state))
|
||||
|
||||
(def workspace-page-flows
|
||||
(l/derived #(-> % :flows not-empty) workspace-page))
|
||||
|
||||
(defn workspace-page-objects-by-id
|
||||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
@@ -343,9 +346,6 @@
|
||||
(into [] (keep (d/getf objects)) children-ids)))
|
||||
workspace-page-objects =))
|
||||
|
||||
(def workspace-page-options
|
||||
(l/derived :options workspace-page))
|
||||
|
||||
(def workspace-frames
|
||||
(l/derived ctt/get-frames workspace-page-objects =))
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
shapes (cfh/get-immediate-children objects)
|
||||
dim (calculate-dimensions objects aspect-ratio)
|
||||
vbox (format-viewbox dim)
|
||||
bgcolor (dm/get-in data [:options :background] default-color)
|
||||
bgcolor (get data :background default-color)
|
||||
|
||||
shape-wrapper
|
||||
(mf/use-memo
|
||||
@@ -232,7 +232,7 @@
|
||||
:fill "none"}
|
||||
|
||||
(when include-metadata
|
||||
[:& export/export-page {:id (:id data) :options (:options data)}])
|
||||
[:& export/export-page {:page data}])
|
||||
|
||||
(let [shapes (->> shapes
|
||||
(remove cfh/frame-shape?)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user