Compare commits

...

115 Commits

Author SHA1 Message Date
alonso.torres
f194e2c1c6 📚 Updates changelog 2024-10-08 15:34:41 +02:00
Andrey Antukh
ea6731e22b Add EOF handling on sse response helper 2024-10-08 15:30:33 +02:00
Andrey Antukh
002b1679c3 ♻️ Clean assertion and schema chechking API 2024-10-08 15:30:33 +02:00
Andrey Antukh
45f3a67950 Relax transaction requeriments for team invitation creation 2024-10-08 14:51:14 +02:00
Andrey Antukh
c6917bb0cf Relax transaction requirements on create-team rpc method 2024-10-08 14:51:14 +02:00
Andrey Antukh
f777845d14 Relax transaction requirement on comment thread creation rpc method 2024-10-08 14:51:14 +02:00
Andrey Antukh
a1f5bcae80 ♻️ Add better ergonomics for the internal quotes API 2024-10-08 14:51:14 +02:00
Andrey Antukh
3e11b4aa74 Add facility for wrap a rpc method in a db transaction 2024-10-08 14:51:14 +02:00
Aitor Moreno
4f48236fee Merge pull request #5141 from penpot/niwinz-enhancements-text-editor-v2-2
 Add minor improvements to text editor v2 events handling
2024-10-07 12:58:17 +02:00
Andrey Antukh
ffadf29ad7 Add minor improvements to text editor v2 events handling
Also updates the editor code to the latest version
2024-10-07 10:13:21 +02:00
Aitor Moreno
352efcb610 Merge pull request #5139 from penpot/niwinz-enhancements-text-editor-v2
 Add minor improvements for text-editor-v2
2024-10-04 09:38:50 +02:00
Andrey Antukh
334e83479f Add minor improvements for text-editor-v2 2024-10-03 09:51:04 +02:00
Alejandro Alonso
476eedbd2c Merge remote-tracking branch 'origin/staging' into develop 2024-10-03 07:19:53 +02:00
Alejandro
d570048f78 Merge pull request #5132 from penpot/niwinz-bugfix-1
🐛 Fix issues on migration 55
2024-10-02 13:36:43 +02:00
Andrey Antukh
dcc49dafd3 Merge pull request #5029 from penpot/azazeln28-refactor-text-editor
♻️ Refactor text editor
2024-10-02 11:05:26 +02:00
AzazelN28
7398f7ce0d ♻️ Replace Draft.js with custom editor 2024-10-01 22:31:16 +02:00
Andrey Antukh
76479a2486 🐛 Fix page background migration 2024-10-01 16:44:54 +02:00
Andrey Antukh
31f62dcc12 🐛 Fix incorrect flows conversion on migration 55 2024-10-01 16:34:22 +02:00
Andrey Antukh
3d7df5b005 Merge pull request #5115 from penpot/alotor-plugins
Plugins update
2024-10-01 12:53:03 +02:00
alonso.torres
c16a116707 Modifications after review 2024-10-01 11:57:52 +02:00
alonso.torres
f7f06f59ce ⬆️ Upgrade plugin runtime 2024-10-01 09:34:45 +02:00
alonso.torres
d1277afee6 New plugin install workflow 2024-09-30 16:03:40 +02:00
alonso.torres
a510d01136 Plugins api changes 2024-09-30 15:49:46 +02:00
alonso.torres
0e651df65f Updates permissions for comments 2024-09-30 15:20:34 +02:00
alonso.torres
758e0458bc 🐛 Fix problem when returning parent proxy 2024-09-30 15:20:34 +02:00
alonso.torres
e18b4666ba Update permissions dialog 2024-09-30 15:20:34 +02:00
Alejandro Alonso
864088eecd Merge remote-tracking branch 'origin/staging' into develop 2024-09-30 09:38:11 +02:00
Alejandro
d5a9961ec8 Merge pull request #5124 from penpot/eva-move-tab-switcher
♻️  Move tab-switcher to its own folder inside DS
2024-09-30 09:29:15 +02:00
Alejandro
7dac7de365 Merge pull request #5123 from penpot/palba-change-emails-footer
 Update emails footer
2024-09-30 07:11:27 +02:00
Alejandro
dd0721e91e Merge pull request #5126 from penpot/palba-fix-show-request-on-all-errors
🐛 Fix request dialog is shown in all errors
2024-09-30 06:19:58 +02:00
Pablo Alba
21fde2e991 🐛 Fix request dialog is shown in all errors 2024-09-27 10:38:24 +02:00
Eva Marco
ca1893164d Add the undefied option to props schema enums 2024-09-26 17:39:52 +02:00
Eva Marco
b619ac3e08 ♻️ Move tab-switcher to its own folder inside DS 2024-09-26 17:39:45 +02:00
Belén Albeza
d7eb86c86d Merge pull request #5095 from penpot/eva-update-ds-components
♻️ Update colors and icons
2024-09-26 12:00:45 +02:00
Pablo Alba
6c4f216da8 Update emails footer 2024-09-26 09:45:42 +02:00
Alejandro Alonso
f786a00e89 Merge remote-tracking branch 'origin/staging' into develop 2024-09-25 11:35:41 +02:00
Alejandro Alonso
bec11220e3 🐛 Fix storage typo 2024-09-25 10:41:55 +02:00
Alejandro Alonso
9b802e1c7d Merge remote-tracking branch 'origin/staging' into develop 2024-09-24 14:27:20 +02:00
Alejandro Alonso
21aa8b0703 Merge remote-tracking branch 'origin/staging' into develop 2024-09-24 09:42:56 +02:00
Pablo Alba
03ebeb0657 🐛 Fix session storage entry name 2024-09-23 12:15:49 +02:00
Alejandro
19a613e90c Merge pull request #5105 from penpot/superalex-merge-conflicts-2
Merge conflicts
2024-09-23 10:57:33 +02:00
Alejandro Alonso
7fe95f218b 🐛 Fix logged in redirect 2024-09-23 10:36:59 +02:00
Alejandro Alonso
a1fc785771 Merge remote-tracking branch 'origin/staging' into superalex-merge-conflicts-2 2024-09-23 10:08:06 +02:00
Andrey Antukh
4f04dbc294 🐛 Fix frame flows issues related to the refactor 2024-09-18 12:15:12 +02:00
alonso.torres
2b2a84da64 🐛 Fix problem with frame guides 2024-09-18 12:15:12 +02:00
Andrey Antukh
21dd9a260c 📎 Rename common files changes test namespace 2024-09-18 12:15:12 +02:00
Andrey Antukh
7b9b5bafc1 🔥 Remove duplicated test 2024-09-18 12:15:12 +02:00
Andrey Antukh
41ebba6ce0 Add generative tests for flows and saved-grids 2024-09-18 12:15:12 +02:00
Andrey Antukh
61446592b3 Move generative test related code to a separated ns 2024-09-18 12:15:12 +02:00
Andrey Antukh
b82c6326cf Add better error reporting on changes validation 2024-09-18 12:15:12 +02:00
Andrey Antukh
a2f466810b ♻️ Add minor refactor on set-plugin-data change 2024-09-18 12:15:12 +02:00
Andrey Antukh
1bd1782d66 ♻️ Add better reporting for generative tests 2024-09-18 12:15:12 +02:00
Andrey Antukh
ea6a1c05fa 🐛 Fix incorrect assignation of plugin data on page data structure 2024-09-18 12:15:12 +02:00
Andrey Antukh
4f84e77b10 Add generative tests for set-guide change 2024-09-18 12:15:12 +02:00
Andrey Antukh
fa75a3539f 📎 Rename shape decode encode test file 2024-09-18 12:15:12 +02:00
Andrey Antukh
fa12d9785a Add tests for basic obj crud change operations
Restored and adapted from already existing commented code
2024-09-18 12:15:12 +02:00
Andrey Antukh
c578e31ae2 📎 Update some docstrings on common/schema ns 2024-09-18 12:15:12 +02:00
Andrey Antukh
749c369080 Add less verbose shape validation 2024-09-18 12:15:12 +02:00
Andrey Antukh
4ad4057878 ♻️ Refactor page options data structure 2024-09-18 12:15:12 +02:00
Andrey Antukh
2dea0b52ed Merge pull request #5077 from penpot/ladybenko-8638-docker-rust
Set up devenv for Rust
2024-09-17 17:30:38 +02:00
Belén Albeza
7590a7ce4d Merge pull request #5072 from penpot/eva-add-schema-to-ds-components
 Add schema validation to all DS components
2024-09-17 16:22:22 +02:00
Belén Albeza
884ceb052b Use dynamic import for wasm module 2024-09-17 14:52:27 +02:00
Belén Albeza
cc7ed497e8 🎉 Enable conditional use of wasm module 2024-09-17 14:51:55 +02:00
Belén Albeza
cd6a739abb 🔧 Add dummy rust project + build scripts with wasm-pack 2024-09-17 14:51:55 +02:00
Belén Albeza
f0cecfd517 🔧 Install Rust+Cargo in devenv 2024-09-17 14:51:55 +02:00
Eva Marco
5ffa56be3d ♻️ Update select background color on input 2024-09-17 14:11:50 +02:00
Eva Marco
076cb0e35b Add schema validation to all DS components 2024-09-17 13:43:35 +02:00
Andrey Antukh
2a90ca6546 Merge pull request #5094 from penpot/alotor-fix-plugins
🐛 Fix small problems in plugins
2024-09-17 12:37:43 +02:00
Eva Marco
a26deafa75 ♻️ Update the colors and icon of some toast notification 2024-09-17 12:08:17 +02:00
alonso.torres
cf705e352b 🐛 Fix small problems in plugins 2024-09-17 10:19:37 +02:00
Andrey Antukh
b50fcee079 Merge pull request #5090 from penpot/alotor-new-apis
Plugins - API's modifications
2024-09-16 18:29:51 +02:00
alonso.torres
9bca42c14a Fixed plugin registration props 2024-09-16 15:46:02 +02:00
alonso.torres
214733c880 ⬆️ Update plugins runtime 2024-09-16 09:48:56 +02:00
alonso.torres
d6f6d78b1e New viewport functions 2024-09-13 12:29:07 +02:00
alonso.torres
8c1fba5160 Add api methods to align, distribute and flatten shapes 2024-09-13 12:29:07 +02:00
alonso.torres
fb39dd5440 Methods for comments 2024-09-13 12:29:07 +02:00
alonso.torres
dd0c5b7806 Add support to guides for plugins 2024-09-13 11:30:59 +02:00
Andrey Antukh
9e94cf7b99 ♻️ Simplify internal implementation of sm/schema namespace 2024-09-13 11:30:55 +02:00
Andrey Antukh
b882b9e283 🔥 Remove usage of public usage of sm/define funcion 2024-09-13 11:30:55 +02:00
Andrey Antukh
cdcff62232 Store some profile props on browser global storage 2024-09-13 11:30:55 +02:00
Andrey Antukh
c8caca77a3 Add storage namespacing
Allows separate global properties from user specific properties
2024-09-13 11:30:55 +02:00
Alejandro Alonso
042b3a71d8 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:46:04 +02:00
Alejandro Alonso
eadae5e2cd Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:05:45 +02:00
Alejandro Alonso
7f9c4df284 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 11:34:35 +02:00
Alejandro Alonso
9e3f8e7827 Merge remote-tracking branch 'origin/staging' into develop 2024-09-09 11:09:53 +02:00
Eva Marco
3a4e9ccc5a 👷 Fix CI error 2024-09-09 10:32:50 +02:00
Belén Albeza
eb720b053a Merge pull request #5057 from penpot/eva-fix-css-compilation
🔧 Rearrange css files for compilation
2024-09-06 14:45:52 +02:00
Alejandro Alonso
efc61241a0 Merge remote-tracking branch 'origin/staging' into develop 2024-09-06 13:50:37 +02:00
Andrey Antukh
cfad1d178f Merge pull request #5068 from penpot/alotor-plugins-install-profile
 Change installation data to profile
2024-09-06 12:05:52 +02:00
alonso.torres
c24b2dadec Change installation data to profile 2024-09-06 11:10:32 +02:00
Andrey Antukh
9a3b5337d7 Merge pull request #5062 from penpot/alotor-plugins-fix-interactions
🐛 Fix plugins add interaction
2024-09-05 16:22:46 +02:00
alonso.torres
396cbb27b2 🐛 Fix plugins add interaction 2024-09-05 16:00:04 +02:00
Alejandro
b4e6f8bc73 Merge pull request #5061 from penpot/niwinz-challenge
 Add support for optional human challenge
2024-09-05 15:49:50 +02:00
Andrey Antukh
d88f28f5c2 Add support for optional human challenge 2024-09-05 15:35:39 +02:00
Eva Marco
e36cf1d963 🐛 Fix onboarding slide after rearrange 2024-09-05 14:46:49 +02:00
Eva Marco
a0bb5e5ef3 ♻️ Remove unnecesary code 2024-09-05 09:41:11 +02:00
Eva Marco
34cc211912 🔧 Rearrange css files for compilation 2024-09-05 09:39:43 +02:00
Eva Marco
e95713c1df 🐛 Fix visual integration test 2024-09-05 09:39:43 +02:00
Alejandro Alonso
e189dc965d Merge remote-tracking branch 'origin/staging' into develop 2024-09-05 09:37:16 +02:00
Belén Albeza
53f580ad40 Merge pull request #5017 from penpot/eva-add-select-to-ds
 Add select component to the DS
2024-09-04 15:51:10 +02:00
Andrey Antukh
cf0045681e Merge pull request #5054 from penpot/alotor-plugins-fixes
Update API types
2024-09-04 14:16:01 +02:00
alonso.torres
762a883b39 🐛 Fix problem with font weight and style 2024-09-04 13:52:48 +02:00
alonso.torres
a63ded1ba1 Change type names in plugins 2024-09-04 13:29:56 +02:00
alonso.torres
f812b28892 ⬆️ Update plugin dependencies 2024-09-04 12:38:50 +02:00
Andrey Antukh
873c9b1903 Merge pull request #5050 from penpot/hiru-ordered-maps
🔧 Add serializable ordered collections
2024-09-04 12:26:21 +02:00
Alejandro Alonso
edeb16bc26 Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 12:02:31 +02:00
Alejandro Alonso
90d947391a Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 08:59:05 +02:00
Andrés Moya
47cc80a93f 🔧 Add serializable ordered collections 2024-09-03 23:35:53 +02:00
Andrey Antukh
1f8cfde1cf Merge pull request #5046 from penpot/alotor-plugins-fixes
Plugins small fixes
2024-09-03 14:36:59 +02:00
Alejandro Alonso
5f2ec595cb 📎 Update changelog 2024-09-03 13:15:48 +02:00
alonso.torres
37a6446e32 🐛 Fix problem with font style 2024-09-03 13:10:28 +02:00
alonso.torres
be84b1cb01 🐛 Change place for circular dependency workaround 2024-09-03 13:10:28 +02:00
Eva Marco
298db46722 Add documentation to select on storybook 2024-09-02 16:56:53 +02:00
Eva Marco
0c6b0598fa Add new select ds component to storybook 2024-08-29 14:14:12 +02:00
Eva Marco
f2a2d772b0 Add new select component to the ds 2024-08-29 14:14:08 +02:00
214 changed files with 12967 additions and 8338 deletions

View File

@@ -1,5 +1,32 @@
# CHANGELOG
## 2.3.0
### :rocket: Epics and highlights
- **New plugin system.**
Penpot now supports custom plugins. Read everything about developing your plugins [HERE](https://help.penpot.app/plugins/)
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
- All our plugins beta testers :heart:.
### :sparkles: New features
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
This refactor adds better IME support, more performant text editing
experience and a better clipboard support while keeping full
retrocompatibility with previous editor.
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
### :bug: Bugs fixed
## 2.2.1
### :bug: Bugs fixed

View File

@@ -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 &lt;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>

View 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 &lt;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]-->

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -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 &lt;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>

View File

@@ -23,6 +23,7 @@ export PENPOT_FLAGS="\
enable-urepl-server \
enable-rpc-climit \
enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \
enable-auto-file-snapshot \
enable-webhooks \

View File

@@ -17,6 +17,7 @@ export PENPOT_FLAGS="\
disable-secure-session-cookies \
enable-rpc-climit \
enable-smtp \
enable-quotes \
enable-file-snapshot \
enable-access-tokens \
enable-tiered-file-data-storage \

View File

@@ -60,6 +60,9 @@
(try
(let [result (handler)]
(events/tap :end result))
(catch java.io.EOFException cause
(events/tap :error (errors/handle' cause request)))
(catch Throwable cause
(l/err :hint "unexpected error on processing sse response"
:cause cause)

View File

@@ -278,18 +278,18 @@
:inc 1)
message)
(def ^:private schema:params
(sm/define
[:map {:title "params"}
[:session-id ::sm/uuid]]))
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (sm/conform! schema:params params)]
(let [session-id (some-> params :session-id sm/parse-uuid)]
(when-not (uuid? session-id)
(ex/raise :type :validation
:code :missing-session-id
:hint "missing or invalid session-id found"))
(cond
(not profile-id)
(ex/raise :type :authentication
:hint "Authentication required.")
:hint "authentication required")
;; WORKAROUND: we use the adapter specific predicate for
;; performance reasons; for now, the ring default impl for

View File

@@ -149,6 +149,13 @@
:hint "authentication required for this endpoint")
(f cfg params)))))
(defn- wrap-db-transaction
[_ f mdata]
(if (::db/transaction mdata)
(fn [cfg params]
(db/tx-run! cfg f params))
f))
(defn- wrap-audit
[_ f mdata]
(if (or (contains? cf/flags :webhooks)
@@ -196,6 +203,7 @@
(defn- wrap-all
[cfg f mdata]
(as-> f $
(wrap-db-transaction cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(climit/wrap cfg $ mdata)

View File

@@ -30,18 +30,17 @@
:tid token-id
:iat created-at})
expires-at (some-> expiration dt/in-future)]
(db/insert! conn :access-token
{:id token-id
:name name
:token token
:profile-id profile-id
:created-at created-at
:updated-at created-at
:expires-at expires-at
:perms (db/create-array conn "text" [])})))
expires-at (some-> expiration dt/in-future)
token (db/insert! conn :access-token
{:id token-id
:name name
:token token
:profile-id profile-id
:created-at created-at
:updated-at created-at
:expires-at expires-at
:perms (db/create-array conn "text" [])})]
(decode-row token)))
(defn repl:create-access-token
[{:keys [::db/pool] :as system} profile-id name expiration]
@@ -60,14 +59,12 @@
(sv/defmethod ::create-access-token
{::doc/added "1.18"
::sm/params schema:create-access-token}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::db/conn conn)]
(quotes/check-quote! conn
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(-> (create-access-token cfg profile-id name expiration)
(decode-row)))))
[cfg {:keys [::rpc/profile-id name expiration]}]
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token
[:map {:title "delete-access-token"}

View File

@@ -71,10 +71,15 @@
[conn comment-id & {:as opts}]
(db/get-by-id conn :comment comment-id opts))
(def ^:private sql:get-next-seqn
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
FROM file AS f
WHERE f.id = ?
FOR UPDATE")
(defn- get-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
(:next-seqn res)))
(def sql:upsert-comment-thread-status
@@ -304,38 +309,41 @@
::rtry/when rtry/conflict-exception?
::sm/params schema:create-comment-thread}
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
(files/check-comment-permissions! cfg profile-id file-id share-id)
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-comment-permissions! cfg profile-id file-id share-id)
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
(run! (partial quotes/check-quote! cfg)
(list {::quotes/id ::quotes/comment-threads-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}))
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/project-id project-id)
(assoc ::quotes/file-id file-id)
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
{::quotes/id ::quotes/comments-per-file}))
(create-comment-thread conn {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id})))))
(let [params {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id}]
(db/tx-run! cfg create-comment-thread params))))
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
[{:keys [::db/conn] :as cfg}
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
(let [;; NOTE: we take the next seq number from a separate query
;; because we need to lock the file for avoid race conditions
;; FIXME: this method touches and locks the file table,which
;; is already heavy-update tablel; we need to think on move
;; the sequence state management to a different table or
;; different storage (example: redis) for alivate the update
;; pression on the file table
(let [;; NOTE: we take the next seq number from a separate query because the whole
;; operation can be retried on conflict, and in this case the new seq shold be
;; retrieved from the database.
seqn (get-next-seqn conn file-id)
thread-id (uuid/next)
thread (db/insert! conn :comment-thread
@@ -364,7 +372,8 @@
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
{:id file-id}
{::db/return-keys false})
(-> thread
(select-keys [:id :file-id :page-id])
@@ -387,7 +396,6 @@
(files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id)))))
;; --- COMMAND: Update Comment Thread
(def ^:private
@@ -432,12 +440,11 @@
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(quotes/check-quote! conn
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id})
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id})
;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread))

View File

@@ -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"

View File

@@ -98,46 +98,49 @@
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true
::sm/params schema:create-file}
[cfg {:keys [::rpc/profile-id project-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
::sm/params schema:create-file
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id})
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
;; FIXME: IMPORTANT: this code can have race
;; conditions, because we have no locks for updating
;; team so, creating two files concurrently can lead
;; to lost team features updating
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))))
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))

View File

@@ -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

View File

@@ -86,6 +86,9 @@
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]])
;; FIXME: IMPORTANT: refactor this, we should not hold a whole db
;; connection around the font creation
(sv/defmethod ::create-font-variant
{::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id]
@@ -96,9 +99,9 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant

View File

@@ -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."

View File

@@ -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]

View File

@@ -168,6 +168,17 @@
;; --- MUTATION: Create Project
(defn- create-project
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false)))
(def ^:private schema:create-project
[:map {:title "create-project"}
[:team-id ::sm/uuid]
@@ -178,23 +189,15 @@
{::doc/added "1.18"
::webhooks/event? true
::sm/params schema:create-project}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
[cfg {:keys [::rpc/profile-id team-id] :as params}]
(let [params (assoc params :profile-id profile-id)
project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false))))
(teams/check-edition-permissions! cfg profile-id team-id)
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id)]
(db/tx-run! cfg create-project params)))
;; --- MUTATION: Toggle Project Pin

View File

@@ -82,19 +82,17 @@
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))))
(defn- check-valid-email-muted
"Check if the member's email is part of the global bounce report."
(defn- check-profile-muted
"Check if the member's email is part of the global bounce report"
[conn member]
(let [email (profile/clean-email (:email member))]
(let [email (profile/clean-email (:email member))]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))))
(defn- check-valid-email-bounce
(defn- check-email-bounce
"Check if the email is part of the global complain report"
[conn email show?]
(when (eml/has-bounce-reports? conn email)
@@ -103,7 +101,7 @@
:email (if show? email "private")
:hint "this email has been repeatedly reported as bounce")))
(defn- check-valid-email-spam
(defn- check-email-spam
"Check if the member email is part of the global complain report"
[conn email show?]
(when (eml/has-complaint-reports? conn email)
@@ -227,16 +225,16 @@
;; --- Query: Team Members
(def sql:team-members
"select tp.*,
"SELECT tp.*,
p.id,
p.email,
p.fullname as name,
p.fullname as fullname,
p.fullname AS name,
p.fullname AS fullname,
p.photo_id,
p.is_active
from team_profile_rel as tp
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
FROM team_profile_rel AS tp
JOIN profile AS p ON (p.id = tp.profile_id)
WHERE tp.team_id = ?")
(defn get-team-members
[conn team-id]
@@ -403,17 +401,19 @@
{::doc/added "1.17"
::sm/params schema:create-team}
[cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params
:profile-id profile-id
:features features))]
(with-meta team
{::audit/props {:id (:id team)}})))))
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (db/tx-run! cfg create-team params)]
(with-meta team
{::audit/props {:id (:id team)}})))
(defn create-team
"This is a complete team creation process, it creates the team
@@ -767,21 +767,50 @@
:member-id member-id}))
(defn- create-profile-identity-token
[cfg profile]
[cfg profile-id]
(dm/assert!
"expected valid uuid for profile-id"
(uuid? profile-id))
(tokens/generate (::setup/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:profile-id profile-id
:exp (dt/in-future {:days 30})}))
(def ^:private schema:create-invitation
[:map {:title "params:create-invitation"}
[:team
[:map
[:id ::sm/uuid]
[:name :string]]]
[:profile
[:map
[:id ::sm/uuid]
[:fullname :string]]]
[:role [::sm/one-of valid-roles]]
[:email ::sm/email]])
(def ^:private check-create-invitation-params!
(sm/check-fn schema:create-invitation))
(defn- create-invitation
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
(dm/assert!
"expected valid connection on cfg parameter"
(db/connection? conn))
(dm/assert!
"expected valid params for `create-invitation` fn"
(check-create-invitation-params! params))
(let [email (profile/clean-email email)
member (profile/get-profile-by-email conn email)]
(check-valid-email-muted conn member)
(check-valid-email-bounce conn email true)
(check-valid-email-spam conn email true)
(check-profile-muted conn member)
(check-email-bounce conn email true)
(check-email-spam conn email true)
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
@@ -815,7 +844,8 @@
(name role) expire
(name role) expire])
updated? (not= id (:id invitation))
tprops {:profile-id (:id profile)
profile-id (:id profile)
tprops {:profile-id profile-id
:invitation-id (:id invitation)
:valid-until expire
:team-id (:id team)
@@ -823,12 +853,11 @@
:member-id (:id member)
:role role}
itoken (create-invitation-token cfg tprops)
ptoken (create-profile-identity-token cfg profile)]
ptoken (create-profile-identity-token cfg profile-id)]
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
@@ -851,26 +880,27 @@
itoken))))
(defn- add-user-to-team
[conn profile team email role]
[conn profile team role email]
(let [team-id (:id team)
member (db/get* conn :profile
{:email (str/lower email)}
{::sql/columns [:id :email]})
params (merge
{:team-id team-id
:profile-id (:id member)}
(role->params role))]
member (db/get* conn :profile
{:email (str/lower email)}
{::sql/columns [:id :email]})
params (merge
{:team-id team-id
:profile-id (:id member)}
(role->params role))]
;; Do not allow blocked users to join teams.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check!
{::db/conn conn
::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@@ -902,6 +932,39 @@
[conn team-id]
(db/exec! conn [sql:valid-requests-email team-id]))
(def ^:private xf:map-email
(map :email))
(defn- create-team-invitations
[{:keys [::db/conn] :as cfg} profile team role emails]
(let [join-requests (into #{} xf:map-email
(get-valid-requests-email conn (:id team)))
team-members (into #{} xf:map-email
(get-team-members conn (:id team)))
invitations (into #{}
(comp
;; We don't re-send inviation to
;; already existing members
(remove team-members)
;; We don't send invitations to
;; join-requested members
(remove join-requests)
(map (fn [email]
{:email email
:team team
:profile profile
:role role}))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> (filter join-requests emails)
(run! (partial add-user-to-team conn profile team role)))
invitations))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
@@ -917,63 +980,36 @@
join the team."
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)]
[cfg {:keys [::rpc/profile-id team-id emails role] :as params}]
(let [perms (get-permissions cfg profile-id team-id)
profile (db/get-by-id cfg :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
;; Check if the current profile is allowed to send emails.
(check-valid-email-muted conn profile)
;; Check if the current profile is allowed to send emails
(check-profile-muted cfg profile)
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
emails-to-add (filter #(contains? requested %) emails)
emails (remove #(contains? requested %) emails)
cfg (assoc cfg ::db/conn conn)
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
invitations (into #{}
(comp
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(map (fn [email]
(-> params
(assoc :email email)
(assoc :team team)
(assoc :profile profile)
(assoc :role role))))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add the user directly to the team
(doseq [email emails-to-add]
(add-user-to-team conn profile team email role))
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
(let [team (db/get-by-id cfg :team team-id)
invitations (db/tx-run! cfg create-team-invitations profile team role emails)]
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}}))))
;; --- Mutation: Create Team & Invite Members
@@ -987,58 +1023,51 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
::sm/params schema:create-team-with-invitations
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (create-team cfg params)
emails (into #{} (map profile/clean-email) emails)]
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id (:id team))
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
{::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
;; Create invitations for all provided emails.
(let [profile (db/get-by-id conn :profile profile-id)]
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg))))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))
;; --- Query: get-team-invitation-token
@@ -1231,11 +1260,11 @@
:code :invalid-parameters))
;; Check that the requester is not muted
(check-valid-email-muted conn requester)
(check-profile-muted conn requester)
;; Check that the owner is not marked as bounce nor spam
(check-valid-email-bounce conn (:email team-owner) false)
(check-valid-email-spam conn (:email team-owner) true)
(check-email-bounce conn (:email team-owner) false)
(check-email-spam conn (:email team-owner) true)
(let [request (create-team-access-request
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]

View File

@@ -37,14 +37,12 @@
::doc/added "1.15"
::doc/module :auth
::sm/params schema:verify-token}
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify (::setup/props cfg) {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
[cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
(db/tx-run! cfg process-token params claims)))
(defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(let [email (profile/clean-email email)]
(when (profile/get-profile-by-email conn email)
(ex/raise :type :validation
@@ -60,7 +58,7 @@
::audit/profile-id profile-id})))
(defmethod process-token :verify-email
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}]
[{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)
claims (assoc claims :profile profile)]
@@ -81,14 +79,14 @@
::audit/profile-id (:id profile)}))))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
@@ -101,10 +99,9 @@
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@@ -140,7 +137,7 @@
(sm/lazy-validator schema:team-invitation-claims))
(defmethod process-token :team-invitation
[{:keys [conn] :as cfg}
[{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]

View File

@@ -7,16 +7,13 @@
(ns app.rpc.quotes
"Penpot resource usage quotes."
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti check-quote ::id)
@@ -26,14 +23,16 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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 valid-quote?
(sm/lazy-validator schema:quote))
(def ^:private enabled (volatile! true))
@@ -47,20 +46,31 @@
[]
(vswap! enabled (constantly false)))
(defn check-quote!
[ds quote]
(dm/assert!
"expected valid quote map"
(sm/validate schema:quote quote))
(defn- check
[cfg quote]
(let [quote (merge cfg quote)
id (::id quote)]
(when (contains? cf/flags :quotes)
(when @enabled
;; This approach add flexibility on how and where the
;; check-quote! can be called (in or out of transaction)
(db/run! ds (fn [cfg]
(-> (merge cfg quote)
(assoc ::target (name (::id quote)))
(check-quote)))))))
(when-not (valid-quote? quote)
(ex/raise :type :internal
:code :invalid-quote-definition
:hint "found invalid data for quote schema"
:quote (name id)))
(-> (assoc quote ::target (name id))
(check-quote))))
(defn check!
([cfg]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg check {}))))
([cfg & others]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg (fn [cfg]
(run! (partial check cfg) others)))))))
(defn- send-notification!
[{:keys [::db/conn] :as params}]
@@ -101,7 +111,7 @@
(map :quote)
(reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default)
total (->> (db/exec! conn count-sql) first :total)]
total (:total (db/exec-one! conn count-sql))]
(when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes)
@@ -113,72 +123,81 @@
:count total)))))
(def ^:private sql:get-quotes-1
"select id, quote from usage_quote
where target = ?
and profile_id = ?
and team_id is null
and project_id is null
and file_id is null;")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND profile_id = ?
AND team_id IS NULL
AND project_id IS NULL
AND file_id IS NULL;")
(def ^:private sql:get-quotes-2
"select id, quote from usage_quote
where target = ?
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-3
"select id, quote from usage_quote
where target = ?
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-4
"select id, quote from usage_quote
where target = ?
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
(project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-teams-per-profile
"select count(*) as total
from team_profile_rel
where profile_id = ?")
(def ^:private schema:teams-per-profile
[:map [::profile-id ::sm/uuid]])
(s/def ::profile-id ::us/uuid)
(s/def ::teams-per-profile
(s/keys :req [::profile-id ::target]))
(def ^:private valid-teams-per-profile-quote?
(sm/lazy-validator schema:teams-per-profile))
(def ^:private sql:get-teams-per-profile
"SELECT count(*) AS total
FROM team_profile_rel
WHERE profile_id = ?")
(defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}]
(us/assert! ::teams-per-profile quote)
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-access-tokens-per-profile
"select count(*) as total
from access_token
where profile_id = ?")
(def ^:private schema:access-tokens-per-profile
[:map [::profile-id ::sm/uuid]])
(s/def ::access-tokens-per-profile
(s/keys :req [::profile-id ::target]))
(def ^:private valid-access-tokens-per-profile-quote?
(sm/lazy-validator schema:access-tokens-per-profile))
(def ^:private sql:get-access-tokens-per-profile
"SELECT count(*) AS total
FROM access_token
WHERE profile_id = ?")
(defmethod check-quote ::access-tokens-per-profile
[{:keys [::profile-id ::target] :as quote}]
(us/assert! ::access-tokens-per-profile quote)
(assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
@@ -189,40 +208,51 @@
;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-projects-per-team
"select count(*) as total
from project as p
where p.team_id = ?
and p.deleted_at is null")
(def ^:private schema:projects-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::team-id ::us/uuid)
(s/def ::projects-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-projects-per-team-quote?
(sm/lazy-validator schema:projects-per-team))
(def ^:private sql:get-projects-per-team
"SELECT count(*) AS total
FROM project AS p
WHERE p.team_id = ?
AND p.deleted_at IS NULL")
(defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-font-variants-per-team
"select count(*) as total
from team_font_variant as v
where v.team_id = ?")
(def ^:private schema:font-variants-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::font-variants-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-font-variant-per-team-quote?
(sm/lazy-validator schema:font-variants-per-team))
(def ^:private sql:get-font-variants-per-team
"SELECT count(*) AS total
FROM team_font_variant AS v
WHERE v.team_id = ?")
(defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::font-variants-per-team quote)
(assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
@@ -234,70 +264,86 @@
;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-invitations-per-team
"select count(*) as total
from team_invitation
where team_id = ?")
(def ^:private schema:invitations-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::invitations-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-invitations-per-team-quote?
(sm/lazy-validator schema:invitations-per-team))
(def ^:private sql:get-invitations-per-team
"SELECT count(*) AS total
FROM team_invitation
WHERE team_id = ?")
(defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::invitations-per-team quote)
(assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:profiles-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-profiles-per-team-quote?
(sm/lazy-validator schema:profiles-per-team))
(def ^:private sql:get-profiles-per-team
"select (select count(*)
from team_profile_rel
where team_id = ?) +
(select count(*)
from team_invitation
where team_id = ?
and valid_until > now()) as total;")
"SELECT (SELECT count(*)
FROM team_profile_rel
WHERE team_id = ?) +
(SELECT count(*)
FROM team_invitation
WHERE team_id = ?
AND valid_until > now()) AS total;")
;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations.
(s/def ::profiles-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::profiles-per-team quote)
(assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-per-project
"select count(*) as total
from file as f
where f.project_id = ?
and f.deleted_at is null")
(def ^:private schema:files-per-project
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::project-id ::us/uuid)
(s/def ::files-per-project
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-files-per-project-quote?
(sm/lazy-validator schema:files-per-project))
(def ^:private sql:get-files-per-project
"SELECT count(*) AS total
FROM file AS f
WHERE f.project_id = ?
AND f.deleted_at IS NULL")
(defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-files-per-project-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
@@ -308,17 +354,24 @@
;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comment-threads-per-file
"select count(*) as total
from comment_thread as ct
where ct.file_id = ?")
(def ^:private schema:comment-threads-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comment-threads-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-comment-threads-per-file-quote?
(sm/lazy-validator schema:comment-threads-per-file))
(def ^:private sql:get-comment-threads-per-file
"SELECT count(*) AS total
FROM comment_thread AS ct
WHERE ct.file_id = ?")
(defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
@@ -326,23 +379,28 @@
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comments-per-file
"select count(*) as total
from comment as c
join comment_thread as ct on (ct.id = c.thread_id)
where ct.file_id = ?")
(def ^:private schema:comments-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comments-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-comments-per-file-quote?
(sm/lazy-validator schema:comments-per-file))
(def ^:private sql:get-comments-per-file
"SELECT count(*) AS total
FROM comment AS c
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
WHERE ct.file_id = ?")
(defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id

View File

@@ -10,6 +10,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.http.client :as http]
@@ -19,28 +20,26 @@
[datoteka.fs :as fs]
[integrant.core :as ig]))
(def ^:private
schema:template
(sm/define
[:map {:title "Template"}
[:id ::sm/word-string]
[:name ::sm/word-string]
[:file-uri ::sm/word-string]]))
(def ^:private schema:template
[: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]))
(def ^:private schema:templates
[:vector schema:template])
(def check-templates!
(sm/check-fn schema:templates
:code :invalid-templates
:hint "invalid templates"))
(defmethod ig/init-key ::setup/templates
[_ _]
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
templates (check-templates! templates)
dest (fs/join fs/*cwd* "builtin-templates")]
(dm/verify!
"expected a valid templates file"
(sm/check! schema:templates templates))
(doseq [{:keys [id path] :as template} templates]
(let [path (or path (fs/join dest id))]
(if (fs/exists? path)
@@ -60,9 +59,9 @@
(let [resp (http/req! cfg
{:method :get :uri (:file-uri template)}
{:response-type :input-stream :sync? true})]
(dm/verify!
"unexpected response found on fetching template"
(= 200 (:status resp)))
(when-not (= 200 (:status resp))
(ex/raise :type :internal
:code :unexpected-status-code
:hint (str "unable to download template, recevied status " (:status resp))))
(io/input-stream (:body resp)))))))

View File

@@ -155,9 +155,10 @@
(defn enable-team-feature!
[team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(when-not (contains? cfeat/supported-features feature)
(ex/raise :type :assertion
:code :feature-not-supported
:hint (str "feature '" feature "' not supported")))
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system
@@ -173,9 +174,11 @@
(defn disable-team-feature!
[team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(when-not (contains? cfeat/supported-features feature)
(ex/raise :type :assertion
:code :feature-not-supported
:hint (str "feature '" feature "' not supported")))
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system
@@ -203,9 +206,11 @@
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
(dm/verify!
["invalid level %" level]
(contains? #{:success :error :info :warning} level))
(when-not (contains? #{:success :error :info :warning} level)
(ex/raise :type :assertion
:code :incorrect-level
:hint (str "level '" level "' not supported")))
(letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest))

View File

@@ -21,7 +21,7 @@
(t/use-fixtures :each th/database-reset)
(t/deftest ttf-font-upload-1
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)

View File

@@ -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}))

View File

@@ -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]

View File

@@ -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

View File

@@ -108,14 +108,6 @@
`(do ~@body)
(reverse (partition 2 bindings))))
(defmacro check
"Applies a predicate to the value, if result is true, return the
value if not, returns nil."
[pred-fn value]
`(if (~pred-fn ~value)
~value
nil))
(defmacro get-prop
"A macro based, optimized variant of `get` that access the property
directly on CLJS, on CLJ works as get."
@@ -124,47 +116,32 @@
(list 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop))
(list `c/get obj prop)))
(def ^:dynamic *assert-context* nil)
(defn runtime-assert
[hint f]
(try
(when-not (f)
(throw (ex-info hint {:type :assertion
:code :expr-validation
:hint hint})))
(catch #?(:clj Throwable :cljs :default) cause
(let [data (-> (ex-data cause)
(assoc :type :assertion)
(assoc :code :expr-validation)
(assoc :hint hint))]
(throw (ex-info hint data cause))))))
(defmacro assert!
([expr]
`(assert! nil ~expr))
([hint expr]
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(some? hint)
hint
(some? hint)
hint
:else
(str "expr assert: " (pr-str expr)))]
:else
(str "expr assert: " (pr-str expr)))]
(when *assert*
`(binding [*assert-context* ~hint]
(when-not ~expr
(let [hint# ~hint
params# {:type :assertion
:code :expr-validation
:hint hint#}]
(throw (ex-info hint# params#)))))))))
(defmacro verify!
([expr]
`(verify! nil ~expr))
([hint expr]
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(some? hint)
hint
:else
(str "expr assert: " (pr-str expr)))]
`(binding [*assert-context* ~hint]
(when-not ~expr
(let [hint# ~hint
params# {:type :assertion
:code :expr-validation
:hint hint#}]
(throw (ex-info hint# params#))))))))
`(runtime-assert ~hint (fn [] ~expr))))))

View File

@@ -49,7 +49,8 @@
"components/v2"
"styles/v2"
"layout/grid"
"plugins/runtime"})
"plugins/runtime"
"text-editor/v2"})
;; A set of features enabled by default
(def default-features
@@ -64,7 +65,8 @@
;; team feature field
(def frontend-only-features
#{"styles/v2"
"plugins/runtime"})
"plugins/runtime"
"text-editor/v2"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
@@ -81,7 +83,8 @@
"fdata/pointer-map"
"layout/grid"
"fdata/shape-data-type"
"plugins/runtime"}
"plugins/runtime"
"text-editor/v2"}
(into frontend-only-features)))
(sm/register! ::features
@@ -101,6 +104,7 @@
:feature-fdata-objects-map "fdata/objects-map"
:feature-fdata-pointer-map "fdata/pointer-map"
:feature-plugins "plugins/runtime"
:feature-text-editor-v2 "text-editor/v2"
nil))
(defn migrate-legacy-features

View File

@@ -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)$"]

View File

@@ -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
@@ -303,10 +414,11 @@
;; If object has changed or is new verify is correct
(when (and (some? shape-new)
(not= shape-old shape-new))
(dm/verify!
"expected valid shape"
(and (cts/check-shape! shape-new)
(cts/shape? shape-new))))))]
(when-not (and (cts/valid-shape? shape-new)
(cts/shape? shape-new))
(ex/raise :type :assertion
:code :data-validation
:hint "invalid shape found after applying changes")))))]
(->> (into #{} (map :page-id) items)
(mapcat (fn [page-id]
@@ -339,12 +451,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 +466,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 +771,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 +837,7 @@
[data _]
data)
;; -- Media
(defmethod process-change :add-media

View File

@@ -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

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 54)
(def version 55)

View File

@@ -1046,6 +1046,35 @@
(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? (:background options))
(not (contains? page :background)))
(assoc :background (:background options))
(and (some? (:flows options))
(or (not (contains? page :flows))
(not (map? (:flows page)))))
(assoc :flows (d/index-by :id (: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 +1120,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}])

View File

@@ -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]

View File

@@ -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)))

View File

@@ -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

View File

@@ -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]

View File

@@ -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]))

View File

@@ -9,7 +9,6 @@
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pprint :as pp]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi]
@@ -29,11 +28,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 +47,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 +159,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 +220,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,102 +239,50 @@
([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]
[s type code hint value]
(when-not ^boolean (-validate s value)
(let [hint (d/nilv dm/*assert-context* "check error")
explain (-explain s value)]
(throw (ex-info hint {:type :assertion
:code :data-validation
(let [explain (-explain s value)]
(throw (ex-info hint {:type type
:code code
:hint hint
::explain explain}))))
true)
value)
(declare define)
(declare ^:private lazy-schema)
(defn check-fn
"Create a predefined check function"
[s]
(let [schema (if (lazy-schema? s) s (define s))]
(partial fast-check! schema)))
[s & {:keys [hint type code]}]
(let [schema (if (lazy-schema? s) s (lazy-schema s))
hint (or ^boolean hint "check error")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
(partial fast-check! schema type code hint)))
(defn check!
"A helper intended to be used on assertions for validate/check the
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)))
(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))
([s value options]
(when-not ^boolean (-validate s value)
(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))))))
(defn validate-fn
"Create a predefined validate function that raises an expception"
[s]
(let [schema (if (lazy-schema? s) s (define 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))
schema over provided data. Raises an assertion exception."
[s value & {:keys [hint type code]}]
(let [s (if (lazy-schema? s) s (lazy-schema s))
hint (or ^boolean hint "check error")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
(fast-check! s type code hint value)))
(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 +325,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]
@@ -1062,6 +980,12 @@
(def check-email!
(check-fn ::email))
(def check-uuid!
(check-fn ::uuid :hint "expected valid uuid instance"))
(def check-string!
(check-fn :string :hint "expected string"))
(def check-coll-of-uuid!
(check-fn ::coll-of-uuid))

View File

@@ -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

View 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?))))

View File

@@ -10,6 +10,7 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
@@ -29,12 +30,12 @@
{:x 0 :y 0 :width 1 :height 1})
(defn- assert-valid-num [attr num]
(dm/verify!
["%1 attribute has invalid value: %2" (d/name attr) num]
(and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int)))
(when-not (and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int))
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid numeric value for `" attr "`: " num)))
(cond
(and (> num 0) (< num 1)) 1
(and (< num 0) (> num -1)) -1
@@ -43,19 +44,21 @@
(defn- assert-valid-pos-num
[attr num]
(dm/verify!
["%1 attribute should be positive" (d/name attr)]
(pos? num))
(when-not (pos? num)
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid numeric value for `" attr "`: " num " (should be positive)")))
num)
(defn- assert-valid-blend-mode
[mode]
(let [clean-value (-> mode str/trim str/lower keyword)]
(dm/verify!
["%1 is not a valid blend mode" clean-value]
(contains? cts/blend-modes clean-value))
clean-value))
(let [value (-> mode str/trim str/lower keyword)]
(when-not (contains? cts/blend-modes value)
(ex/raise :type :assertion
:code :data-validation
:hint (str "unexpected blend mode: " value)))
value))
(defn- svg-dimensions
[{:keys [attrs] :as data}]

View File

@@ -78,6 +78,12 @@
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
(def text-style-attrs
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
(def default-root-attrs
{:vertical-align "top"})
(def default-text-attrs
{:typography-ref-file nil
:typography-ref-id nil
@@ -92,9 +98,13 @@
:text-transform "none"
:text-align "left"
:text-decoration "none"
:text-direction "ltr"
:fills [{:fill-color clr/black
:fill-opacity 1}]})
(def default-attrs
(merge default-root-attrs default-text-attrs))
(def typography-fields
[:font-id
:font-family

View File

@@ -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)

View File

@@ -116,7 +116,7 @@
(sm/register! ::color-attrs schema:color-attrs)
(def check-color!
(sm/check-fn schema:color))
(sm/check-fn schema:color :hint "expected valid color struct"))
(def check-recent-color!
(sm/check-fn schema:recent-color))

View File

@@ -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})

View File

@@ -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)))

View File

@@ -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)

View File

@@ -355,7 +355,11 @@
(sm/check-fn schema:shape-attrs))
(def check-shape!
(sm/check-fn schema:shape))
(sm/check-fn schema:shape
:hint "expected valid shape"))
(def valid-shape?
(sm/lazy-validator schema:shape))
(defn has-images?
[{:keys [fills strokes]}]

View 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})))

View File

@@ -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}))))

View File

@@ -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])))
;; ))
;; ))

View File

@@ -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})))

View File

@@ -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})))

View File

@@ -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}))

View File

@@ -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,27 @@ 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 /home
COPY files/nginx.conf /etc/nginx/nginx.conf

View File

@@ -9,6 +9,9 @@ alias ls='ls --color -F'
alias lsd='ls -d *(/)'
alias lsf='ls -h *(.)'
# init Cargo / Rust env
. "/usr/local/cargo/env"
# include .bashrc if it exists
if [ -f "$HOME/.bashrc.local" ]; then
. "$HOME/.bashrc.local"

View File

@@ -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"
},

View File

@@ -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" }),

5
frontend/renderer/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
target/
debug/
**/*.rs.bk

324
frontend/renderer/Cargo.lock generated Normal file
View File

@@ -0,0 +1,324 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "minicov"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169"
dependencies = [
"cc",
"walkdir",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[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 = "renderer"
version = "0.1.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[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 = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
dependencies = [
"console_error_panic_hook",
"js-sys",
"minicov",
"scoped-tls",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
]
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[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"

View File

@@ -0,0 +1,19 @@
[package]
name = "renderer"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/penpot/penpot"
license-file = "../../../../LICENSE"
description = "Wasm-based canvas renderer for Penpot"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.93"
[profile.release]
opt-level = "s"
[dev-dependencies]
wasm-bindgen-test = "0.3.43"

View File

@@ -0,0 +1,36 @@
use wasm_bindgen::prelude::*;
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn print(msg: &str) {
log(msg);
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[wasm_bindgen_test]
fn it_works_in_wasm() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View 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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
if (!('dynamicImport' in window)) {
window.dynamicImport = function(uri) {
return import(uri);
}
};

View File

@@ -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);
}

View File

@@ -62,6 +62,12 @@
:depends-on #{:shared}
:init-fn app.rasterizer/init}}
:js-options
{:entry-keys ["module" "browser" "main"]
:resolve {"penpot/vendor/text-editor-v2"
{:target :file
:file "vendor/text_editor_v2.js"}}}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
@@ -145,6 +151,12 @@
:ns-regexp "^frontend-tests.*-test$"
:autorun true
:js-options
{:entry-keys ["module" "browser" "main"]
:resolve {"penpot/vendor/text-editor-v2"
{:target :file
:file "vendor/text_editor_v2.js"}}}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -941,7 +941,7 @@
(update-in [:dashboard-projects project-id :count] inc)))))
(defn create-file
[{:keys [project-id] :as params}]
[{:keys [project-id name] :as params}]
(dm/assert! (uuid? project-id))
(ptk/reify ::create-file
ev/Event
@@ -955,7 +955,7 @@
files (get state :dashboard-files)
unames (cfh/get-used-names files)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
features (-> (features/get-team-enabled-features state)
(set/difference cfeat/frontend-only-features))
params (-> params

View File

@@ -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)))]

View File

@@ -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)))))

View File

@@ -0,0 +1,43 @@
;; 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.main.data.plugins
(:require
[app.plugins.register :as pr]
[app.util.globals :as ug]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn open-plugin!
[{:keys [plugin-id name description host code icon permissions]}]
(try
(.ɵloadPlugin
^js ug/global
#js {:pluginId plugin-id
:name name
:description description
:host host
:code code
:icon icon
:permissions (apply array permissions)})
(catch :default e
(.error js/console "Error" e))))
(defn delay-open-plugin
[plugin]
(ptk/reify ::delay-open-plugin
ptk/UpdateEvent
(update [_ state]
(assoc state ::open-plugin (:plugin-id plugin)))))
(defn check-open-plugin
[]
(ptk/reify ::check-open-plugin
ptk/WatchEvent
(watch [_ state _]
(when-let [pid (::open-plugin state)]
(open-plugin! (pr/get-plugin pid))
(rx/of #(dissoc % ::open-plugin))))))

View File

@@ -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))

View File

@@ -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]

View File

@@ -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}]

View File

@@ -42,6 +42,7 @@
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dps]
[app.main.data.plugins :as dp]
[app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.collapse :as dwco]
@@ -74,16 +75,18 @@
[app.main.repo :as rp]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.renderer-v2 :as renderer]
[app.util.dom :as dom]
[app.util.globals :as ug]
[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]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[cuerdas.core :as str]
[potok.v2.core :as ptk]
[promesa.core :as p]))
@@ -130,6 +133,7 @@
(when (and (not (boolean (-> state :profile :props :v2-info-shown)))
(features/active-feature? state "components/v2"))
(modal/show :v2-info {}))
(dp/check-open-plugin)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes)))))
@@ -337,7 +341,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
@@ -353,6 +357,9 @@
(dcm/retrieve-comment-threads file-id)
(fetch-bundle project-id file-id))
(when (contains? cf/flags :renderer-v2)
(rx/of (renderer/init)))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
@@ -565,7 +572,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 +602,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 +978,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 +1007,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
@@ -1535,7 +1546,8 @@
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cfh/clean-loops objects))
features (features/get-team-enabled-features state)
features (-> (features/get-team-enabled-features state)
(set/difference cfeat/frontend-only-features))
file-id (:current-file-id state)
frame-id (cfh/common-parent-frame objects selected)
@@ -1702,17 +1714,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 paste-data-valid?
(sm/lazy-validator schema:paste-data))
(defn- paste-transit
[{:keys [images] :as pdata}]
@@ -1737,9 +1751,10 @@
(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})
(when-not (paste-data-valid? pdata)
(ex/raise :type :validation
:code :invalid-paste-data
:hibt "invalid paste data found"))
(cfeat/check-paste-features! features (:features pdata))
(if (= file-id (:file-id pdata))
@@ -2088,7 +2103,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)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -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))

View File

@@ -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]))
@@ -465,16 +465,16 @@
(defn change-color-in-selected
[operations new-color old-color]
(dm/verify!
"expected valid change color operations"
(dm/assert!
"expected valid color operations"
(check-change-color-operations! operations))
(dm/verify!
"expected a valid color struct for new-color param"
(dm/assert!
"expected valid color structure"
(ctc/check-color! new-color))
(dm/verify!
"expected a valid color struct for old-color param"
(dm/assert!
"expected valid color structure"
(ctc/check-color! old-color))
(ptk/reify ::change-color-in-selected
@@ -498,7 +498,7 @@
[color stroke?]
(dm/assert!
"should be a valid color"
"expected valid color structure"
(ctc/check-color! color))
(ptk/reify ::apply-color-from-palette
@@ -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))

View File

@@ -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)

View File

@@ -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))))))))

View File

@@ -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))

View File

@@ -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)]

View File

@@ -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)))))

View File

@@ -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
@@ -193,9 +193,17 @@
(defn rename-color
[file-id id new-name]
(dm/verify! (uuid? file-id))
(dm/verify! (uuid? id))
(dm/verify! (string? new-name))
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(dm/assert!
"expected valid uuid for `file-id`"
(uuid? file-id))
(dm/assert!
"expected valid string for `new-name`"
(string? new-name))
(ptk/reify ::rename-color
ptk/WatchEvent
@@ -243,8 +251,15 @@
(defn rename-media
[id new-name]
(dm/verify! (uuid? id))
(dm/verify! (string? new-name))
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(dm/assert!
"expected valid string for `new-name`"
(string? new-name))
(ptk/reify ::rename-media
ptk/WatchEvent
(watch [it state _]
@@ -261,8 +276,11 @@
(rx/of (dch/commit-changes changes))))))))
(defn delete-media
[{:keys [id] :as params}]
(dm/assert! (uuid? id))
[{:keys [id]}]
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(ptk/reify ::delete-media
ev/Event
(-data [_] {:id id})
@@ -435,8 +453,14 @@
(defn rename-component
"Rename the component with the given id, in the current file library."
[id new-name]
(dm/verify! (uuid? id))
(dm/verify! (string? new-name))
(dm/assert!
"expected an uuid instance"
(uuid? id))
(dm/assert!
"expected string for new-name"
(string? new-name))
(ptk/reify ::rename-component
ptk/WatchEvent
(watch [it state _]
@@ -487,8 +511,11 @@
(defn delete-component
"Delete the component with the given id, from the current file library."
[{:keys [id] :as params}]
(dm/assert! (uuid? id))
[{:keys [id]}]
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(ptk/reify ::delete-component
ptk/WatchEvent
(watch [it state _]
@@ -682,8 +709,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]
@@ -1122,7 +1156,9 @@
(defn touch-component
"Update the modified-at attribute of the component to now"
[id]
(dm/verify! (uuid? id))
(dm/assert!
"expected valid uuid for `id`"
(uuid? id))
(ptk/reify ::touch-component
cljs.core/IDeref
(-deref [_] [id])

View File

@@ -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}]

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More