Compare commits

...

221 Commits

Author SHA1 Message Date
Belén Albeza
32fe91398a further adjustments to record video 2024-10-23 13:07:29 +02:00
Belén Albeza
b36c8cd52a modify stress test (rs) to record video 2024-10-22 17:25:55 +02:00
Belén Albeza
f5acfd0787 stashed 2024-10-22 17:25:55 +02:00
AzazelN28
4939bc06ac wip: 20_000 test 2024-10-11 12:27:14 +02:00
Belén Albeza
cd63fb78d2 render 20k rects (rust) 2024-10-11 12:15:25 +02:00
Belén Albeza
3298785436 rust benchmark wip 2024-10-11 11:44:22 +02:00
Alejandro Alonso
eeb0d21013 Playing with pdf render 2024-10-11 09:07:41 +02:00
Alejandro Alonso
a11c2af542 Playing with image export and svg generation 2024-10-10 14:23:32 +02:00
Alejandro Alonso
6d5b0204e9 Playing with image export and svg generation 2024-10-10 14:12:38 +02:00
Alejandro Alonso
dfe5d861f2 Playing with image export and svg generation 2024-10-10 14:06:16 +02:00
Alejandro Alonso
445691430b Refactoring texts 2024-10-10 09:36:56 +02:00
Alejandro Alonso
88722bcf4f Refactoring texts 2024-10-10 09:35:50 +02:00
Alejandro Alonso
43903014c6 Path test 2024-10-10 09:09:53 +02:00
AzazelN28
cf8b62f1a8 wip: renderer redone completely 2024-10-09 15:18:36 +02:00
Alejandro Alonso
39b627cb1a Rendering some texts 2024-10-09 13:28:19 +02:00
AzazelN28
81680cffe9 wip: draw rect 2024-10-08 15:49:11 +02:00
Alejandro Alonso
dc014bd4eb Draw colors from memory too 2024-10-08 13:22:11 +02:00
Alejandro Alonso
0027e77861 Draw shapes from memory 2024-10-08 11:32:25 +02:00
Belén Albeza
fa9004d12c Pass struct to wasm (rust) 2024-10-04 12:50:16 +02:00
AzazelN28
c7f801dd44 wip: add build script for rust module 2024-10-03 16:05:08 +02:00
Alejandro Alonso
0f0b23e38b WIP: improve flush 2024-10-03 11:45:29 +02:00
Alejandro Alonso
1f8fe2dc4c WIP: basic color support 2024-10-03 08:00:32 +02:00
Belén Albeza
e84622061d Fix wrong order for args for draw_react (rust) 2024-10-02 17:04:29 +02:00
Belén Albeza
305de33200 fix zoom drawing glitch (rust) 2024-10-02 16:10:56 +02:00
Belén Albeza
80bbfe7a6f draw shapes with zoom (rust) 2024-10-02 15:40:38 +02:00
Belén Albeza
26ab39a45d re-render canvas when panning (rust) 2024-10-02 15:25:01 +02:00
Belén Albeza
739b8d7c02 fix vbox being nil when calling translate 2024-10-02 14:50:38 +02:00
Belén Albeza
e0a9f63015 add gitignore for rust project 2024-10-02 14:49:35 +02:00
Alejandro Alonso
928709a0f2 WIP: exposing translate 2024-10-02 14:16:51 +02:00
Alejandro Alonso
579b157ab7 WIP: exposing translate 2024-10-02 14:11:19 +02:00
Alejandro Alonso
0bf442e626 WIP Fix typo render-v2 2024-10-02 12:38:07 +02:00
Alejandro Alonso
2184af6602 WIP refactor rerender render-v2 2024-10-02 12:35:05 +02:00
AzazelN28
78fb938d16 wip: fix wrong namespace 2024-10-02 12:06:19 +02:00
Alejandro Alonso
dd9185e058 WIP refactor rerender render-v2 2024-10-02 12:01:49 +02:00
Alejandro Alonso
5f8d56b366 WIP: proper initialization 2024-10-02 11:17:36 +02:00
AzazelN28
bc0fde68c7 wip: add common namespace and fix cpp errors 2024-10-02 10:53:34 +02:00
AzazelN28
024a2ae848 wip: fix build script 2024-10-02 10:06:33 +02:00
AzazelN28
4d56bf66f4 wip: fix README and build script 2024-10-02 09:46:11 +02:00
Belén Albeza
c83ef201a1 wip: target emscripten for rust poc 2024-10-02 07:43:07 +02:00
AzazelN28
6d26abb9e3 wip: fix wrong call to renderer instead of renderer-cpp 2024-10-01 15:59:35 +02:00
AzazelN28
1b1f08388f wip: fix renderer-cpp config flag 2024-10-01 15:44:00 +02:00
AzazelN28
472c769c9a wip: c++ initial version 2024-10-01 15:35:41 +02:00
Alejandro Alonso
864088eecd Merge remote-tracking branch 'origin/staging' into develop 2024-09-30 09:38:11 +02:00
Pablo Alba
0b39318b33 🐛 Fix request dialog is shown in all errors 2024-09-30 09:36:41 +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
47cecb2ac4 Merge pull request #5119 from penpot/eva-add-props
  Add shema prop
2024-09-25 11:31:57 +02:00
Eva Marco
5d6ceec803 Add shema prop 2024-09-25 11:07:40 +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
16cf16c422 Merge pull request #5114 from penpot/superalex-fix-fetch-profile-exception
🐛 Fix fetch profile exception
2024-09-24 14:24:56 +02:00
Alejandro Alonso
4e1eee197e 🐛 Fix fetch profile exception 2024-09-24 14:13:46 +02:00
Alejandro
91c8af9e38 Merge pull request #5108 from penpot/palba-fix-login-redirect
🐛 Fix bad redirect on new oops page with penpot login
2024-09-24 11:27:04 +02:00
Pablo Alba
58593a9428 🐛 Fix bad redirect on new oops page with penpot login 2024-09-24 10:32:57 +02:00
Alejandro Alonso
21aa8b0703 Merge remote-tracking branch 'origin/staging' into develop 2024-09-24 09:42:56 +02:00
Eva Marco
17cf57f7ca Merge pull request #5113 from penpot/superalex-fix-show-in-assets-panel
🐛 Fix show in assets panel
2024-09-24 09:38:32 +02:00
Alejandro Alonso
f7cfe36f37 🐛 Fix show in assets panel 2024-09-24 08:49:52 +02:00
Alejandro
c26f909565 Merge pull request #5110 from penpot/eva-fix-code-block-height
🐛 Fix code block height
2024-09-24 06:54:44 +02:00
Eva Marco
6db7fe5f7b 🐛 Fix code block height 2024-09-23 17:03:25 +02:00
Pablo Alba
a207114d95 Merge pull request #5109 from penpot/hiru-fix-swap-inside-group
🐛 Fix error when swapping a copy that is the only child of a group
2024-09-23 15:12:55 +02:00
Eva Marco
b8299a5ea5 🐛 Fix create team without invitations on onboarding 2024-09-23 15:09:43 +02:00
Andrés Moya
1fa461e996 🐛 Fix error when swapping a copy that is the only child of a group 2024-09-23 15:01:01 +02:00
Alejandro
2e3745099b Merge pull request #5107 from penpot/alotor-remove-export-option
🐛 Removed "merge assets" option from export
2024-09-23 13:22:56 +02:00
Pablo Alba
03ebeb0657 🐛 Fix session storage entry name 2024-09-23 12:15:49 +02:00
alonso.torres
6892cffe54 🐛 Removed "merge assets" option from export 2024-09-23 12:13:33 +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
Alejandro Alonso
e0034dc205 🐛 Fix onboarding edn urls 2024-09-23 06:30:19 +02:00
Alejandro
bd9eab08b7 Merge pull request #5101 from penpot/palba-migrate-templates-v2
 Update templates links to binary v2
2024-09-23 06:12:15 +02:00
Pablo Alba
b5121657ee Update templates links to binary v2 2024-09-18 16:23:12 +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
Aitor Moreno
ca257d1caf Merge pull request #5097 from penpot/eva-fix-arrow-keys-tabs
🐛  Fix arrow key movement on tabs
2024-09-17 17:23:32 +02:00
Aitor Moreno
e164692391 Merge pull request #5098 from penpot/eva-fix-desing-panel
🐛  Fix path side panel options
2024-09-17 17:22:21 +02:00
Eva Marco
b58edea544 🐛 Fix path side panel options 2024-09-17 17:00:40 +02:00
Eva Marco
9a587c91a8 🐛 Fix arrow key movement on tabs 2024-09-17 16:36:44 +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
Aitor Moreno
aae1571a5c Merge pull request #5096 from penpot/alotor-bugfixes
Bugfixes
2024-09-17 16:14:59 +02:00
alonso.torres
ebaf30727c 🐛 Fix copy/paste images in Safari 2024-09-17 15:38:18 +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
alonso.torres
f5f255e2d5 🐛 Fix problem with comments max length 2024-09-17 14:18:51 +02:00
Eva Marco
5ffa56be3d ♻️ Update select background color on input 2024-09-17 14:11:50 +02:00
Alejandro
e65c0d9f48 Merge pull request #5088 from penpot/niwinz-bugfix-2
🐛 Fix issues related to invalid colors inserted on shape shadow
2024-09-17 13:58:43 +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
86c5ca4213 🐛 Fix incorrect redirect handling on request-access go-home button 2024-09-16 18:53:56 +02:00
Andrey Antukh
179d534237 🐛 Fix several issues related to invalid colors inserted on shadows 2024-09-16 18:32:40 +02:00
Andrey Antukh
162507264c 🐛 Reexecute file migration 26 again for shapes that has transform prop as nil 2024-09-16 18:32:40 +02:00
Andrey Antukh
7e0a8b6227 Merge pull request #5092 from penpot/palbs-fix-request-acces-dont-go-your-team
🐛 Fix request access to the Team don't go to Your Penpot team
2024-09-16 18:32:19 +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
Pablo Alba
475d14edf4 🐛 Fix request access to the Team don't go to Your Penpot team 2024-09-16 14:42:52 +02:00
alonso.torres
214733c880 ⬆️ Update plugins runtime 2024-09-16 09:48:56 +02:00
alonso.torres
979828ffe3 🐛 Fix issue when exporting libraries when merging libraries 2024-09-16 09:08:51 +02:00
alonso.torres
65bb795199 🐛 Fix visual problem with stroke cap menu 2024-09-16 09:07:57 +02:00
Alejandro
a0546b2e63 Merge pull request #5086 from penpot/niwinz-bugfix-1
🐛 Ignore object thumbnail requests if file is already marked as deleted
2024-09-13 12:30:27 +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
Andrey Antukh
f291125377 🐛 Add migration for invalid value on layout-wrap-type on shape prop 2024-09-12 21:33:01 +02:00
Andrey Antukh
0ce981a68c 🐛 Add missing ref-id unsassign on srepl helpers for process file 2024-09-12 21:32:34 +02:00
Andrey Antukh
a8814dcaba 🐛 Add missing fields on file-gc libraries fetching sql 2024-09-12 21:07:19 +02:00
Pablo Alba
229eeae6db 🐛 Fix bad redirect on new oops page with social login 2024-09-12 16:35:49 +02:00
Andrey Antukh
d03788af93 🐛 Ignore object thumbnail requests if file is already marked as deleted 2024-09-12 15:23:31 +02:00
Alejandro Alonso
017aad6454 🐛 Fix export failed error when exporting multiple shapes 2024-09-12 12:55:32 +02:00
Alejandro Alonso
042b3a71d8 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:46:04 +02:00
Alejandro Alonso
767ec37b83 Merge remote-tracking branch 'origin/main' into staging 2024-09-11 12:45:39 +02:00
Alejandro Alonso
89f64e0c49 🐛 Fix challenge redirect with parameters 2024-09-11 12:30:12 +02:00
Alejandro Alonso
eadae5e2cd Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:05:45 +02:00
Alejandro Alonso
d108ad904e Merge remote-tracking branch 'origin/main' into staging 2024-09-11 12:05:21 +02:00
Alejandro Alonso
6564736d3e 🐛 Fix challenge redirect with parameters 2024-09-11 12:03:32 +02:00
Alejandro Alonso
7f9c4df284 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 11:34:35 +02:00
Alejandro Alonso
d01cd70c6b Merge remote-tracking branch 'origin/main' into staging 2024-09-11 11:34:04 +02:00
Alejandro
ea7768117c Merge pull request #5082 from penpot/superalex-fix-challenge-redirect-with-parameters
🐛 Fix challenge redirect with parameters
2024-09-11 11:33:45 +02:00
Alejandro Alonso
5bfb39cdf6 🐛 Fix challenge redirect with parameters 2024-09-11 11:23:19 +02:00
Pablo Alba
29f1c2bdad Merge pull request #5080 from penpot/niwinz-oidc-fix-limits-issues
🐛 Fix oidc auth internal limits issue
2024-09-10 16:41:44 +02:00
Andrey Antukh
e79f9ba40f 🐛 Increase token limit 2024-09-10 12:39:54 +02:00
Andrey Antukh
452aabdec6 🐛 Don't send user props on auth token after oidc login 2024-09-10 12:39:54 +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
Aitor Moreno
860e32d965 Merge pull request #5070 from penpot/superalex-fix-onboarding
🐛 Fix onboarding questions
2024-09-09 10:19:03 +02:00
Andrey Antukh
495f9dfa84 Merge pull request #5064 from penpot/superalex-release-notes-2.2
 Release notes for 2.2
2024-09-09 09:54:50 +02:00
Alejandro
133ca33cb5 Merge pull request #5076 from penpot/niwinz-exporter-config-parse
🐛 Fix issues on parsing configuration on exporter
2024-09-09 09:54:12 +02:00
Andrey Antukh
1c69a9fd8a 🐛 Fix config parsing on exporter 2024-09-09 09:47:55 +02:00
Andrey Antukh
15faa57e01 🐛 Fix decoding on sm/set schema 2024-09-09 09:46:50 +02:00
Alejandro
f5510234cf Merge pull request #5066 from penpot/eva-fix-frame-row-menu
🐛 Fix guides submenu visualization
2024-09-09 06:49:05 +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
Alejandro Alonso
5b0331611d Merge remote-tracking branch 'origin/main' into staging 2024-09-06 13:50:09 +02:00
Alejandro
f8f1c58f61 Merge pull request #5069 from penpot/niwinz-storybook-build-fix
🐛 Fix storybook build related to commonjs to esm module conversion issue
2024-09-06 13:43:04 +02:00
Andrey Antukh
3f34aa92fa Add support for optional human challenge 2024-09-06 13:39:53 +02:00
Alejandro Alonso
c99102e49b 🐛 Fix onboarding questions 2024-09-06 13:18:39 +02:00
Andrey Antukh
d583661e58 🐛 Fix storybook build related to commonjs to esm module conversion issue 2024-09-06 12:31:48 +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
Andrey Antukh
ffa326e08f Merge pull request #5067 from penpot/alotor-fix-validation
🐛 Fix problem when dismissing shared library update
2024-09-06 11:31:22 +02:00
alonso.torres
c24b2dadec Change installation data to profile 2024-09-06 11:10:32 +02:00
alonso.torres
03040ed40b 🐛 Fix problem when dismissing shared library update 2024-09-06 11:02:02 +02:00
Alejandro Alonso
5e89cd1cb3 Release notes for 2.2 2024-09-06 10:49:20 +02:00
Eva Marco
bf202473e9 🐛 Fix guides submenu visualization 2024-09-06 09:47:09 +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
Andrey Antukh
886c0c596f Merge pull request #5060 from penpot/alotor-bugfixes
Bugfixes
2024-09-05 15:32:44 +02:00
alonso.torres
b15b394c65 🐛 Fix problem when creating a component instance from grid layout 2024-09-05 15:11:04 +02:00
Eva Marco
e36cf1d963 🐛 Fix onboarding slide after rearrange 2024-09-05 14:46:49 +02:00
alonso.torres
caf78a6b4d 🐛 Fix layer panel overflowing 2024-09-05 11:50:16 +02:00
alonso.torres
6a161267ba 🐛 Fix problem with overlay positions in viewer 2024-09-05 10:48:00 +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
alonso.torres
a180c33a32 🐛 Fix problem with SVG import 2024-09-05 09:26:22 +02:00
Alejandro
ea8febdb7d Merge pull request #5056 from penpot/niwinz-refactor-recent-colors
♻️ Refactor recent colors and local storage abstraction
2024-09-05 09:07:26 +02:00
Alejandro
f765cc8dbc Merge pull request #5011 from penpot/palba-testab-start-workspace
A/B test start directly at the workspace
2024-09-05 07:05:57 +02:00
Pablo Alba
81b7972347 🎉 Test A/B for start in workspace 2024-09-04 17:19:39 +02:00
Andrey Antukh
1281670c61 Clear storage on user logout 2024-09-04 16:20:00 +02:00
Andrey Antukh
b8c6103858 Add performance enhancements for util/storage abstraction layer 2024-09-04 16:20:00 +02:00
Andrey Antukh
b2c0bed84c Add efficiency improvements to use-resize-hook 2024-09-04 16:20:00 +02:00
Andrey Antukh
9619fcbc1f Make efficiency improvements to use-shared-state hook 2024-09-04 16:20:00 +02:00
Andrey Antukh
e9c55e9eb4 Make recent colors to be stored locally instead of on file 2024-09-04 16:20:00 +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
Andrey Antukh
488d034a58 Merge pull request #5055 from penpot/eva-fix-webhook-checkbox
🐛  Fix webhook checkbox position
2024-09-04 14:15:37 +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
Eva Marco
8d66275187 🐛 Fix webhook checkbox position 2024-09-04 12:51:30 +02:00
alonso.torres
f812b28892 ⬆️ Update plugin dependencies 2024-09-04 12:38:50 +02:00
Alejandro
59063e861c Merge pull request #5053 from penpot/niwinz-update-file-refactor
♻️ Refactor file-update for make it more reusable
2024-09-04 12:30:36 +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
Andrey Antukh
9da891e9b0 📎 Enable auto-file-snapshot feature scripts/repl 2024-09-04 12:18:31 +02:00
Andrey Antukh
a6de12323e ♻️ Refactor file-update for make it more reusable 2024-09-04 12:18:31 +02:00
Alejandro Alonso
edeb16bc26 Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 12:02:31 +02:00
Alejandro Alonso
52d099c80e Merge remote-tracking branch 'origin/main' into staging 2024-09-04 12:02:04 +02:00
Alejandro
b2010e5fd8 Merge pull request #5052 from penpot/niwinz-bugfix-srepl-helpers
🐛 Fix issues with srepl helper for profile deletion in bulk
2024-09-04 12:01:55 +02:00
Alejandro
5808bd3743 Merge pull request #5045 from penpot/niwinz-fix-file-gc
🐛 Fix issues on file-gc task
2024-09-04 11:11:13 +02:00
Andrey Antukh
d5f5c440dd 🐛 Fix issues with srepl helper for profile deletion in bulk 2024-09-04 11:09:05 +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
50df2279a7 🐛 Make the media cleaning on file-gc task aware of snapshots
It now takes in account the snapshots, and prevents
deletion of media files used in snapshots.
2024-09-03 14:50:17 +02:00
Andrey Antukh
71ba0242c7 🐛 Add missing type decoding on changes schema 2024-09-03 14:42:00 +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
236 changed files with 27655 additions and 8321 deletions

View File

@@ -1,11 +1,27 @@
# CHANGELOG
## 2.3.0
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features
### :bug: Bugs fixed
## 2.2.0
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
### :heart: Community contributions (Thank you!)
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
@@ -71,10 +87,21 @@
### :bug: Bugs fixed
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888)
- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464)
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
## 2.1.5

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

@@ -1,39 +1,42 @@
[{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
{:id "prototype-examples"
:name "Prototype template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/prototype-examples.penpot"}
{:id "plants-app"
:name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
{:id "lucide-icons"
:name "Lucide Icons"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Lucide-icons.penpot"}
{:id "font-awesome"
:name "Font Awesome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/FontAwesome.penpot"}
{:id "black-white-mobile-templates"
:name "Black & White Mobile Templates"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Black-&-White-Mobile-Templates.penpot"}
{:id "avataaars"
:name "Avataaars"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Avataaars-by-Pablo-Stanley.penpot"}
{:id "ux-notes"
:name "UX Notes"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/UX-Notes.penpot"}
{:id "whiteboarding-kit"
:name "Whiteboarding Kit"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
{:id "open-color-scheme"
:name "Open Color Scheme"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
{:id "welcome"
:name "Welcome"
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]

View File

@@ -24,7 +24,7 @@ export PENPOT_FLAGS="\
enable-rpc-climit \
enable-rpc-rlimit \
enable-soft-rpc-rlimit \
enable-file-snapshot \
enable-auto-file-snapshot \
enable-webhooks \
enable-access-tokens \
enable-tiered-file-data-storage \

View File

@@ -567,7 +567,6 @@
(tokens/generate (::setup/props cfg)
{:iss :auth
:exp (dt/in-future "15m")
:props (:props info)
:profile-id (:id profile)}))
props (audit/profile->props profile)
context (d/without-nils {:external-session-id (:external-session-id info)})]

View File

@@ -279,13 +279,20 @@
message)
(def ^:private schema:params
(sm/define
[:map {:title "params"}
[:session-id ::sm/uuid]]))
[:map {:title "params"}
[:session-id ::sm/uuid]])
(def ^:private decode-params
(sm/decoder schema:params sm/json-transformer))
(def ^:private validate-params!
(sm/validate-fn schema:params))
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (sm/conform! schema:params params)]
(let [{:keys [session-id]} (-> params
decode-params
validate-params!)]
(cond
(not profile-id)
(ex/raise :type :authentication

View File

@@ -409,7 +409,10 @@
:fn (mg/resource "app/migrations/sql/0128-mod-task-table.sql")}
{:name "0129-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}])
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}
{:name "0130-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,2 @@
ALTER TABLE file_change
ADD COLUMN version integer NULL;

View File

@@ -27,9 +27,11 @@
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.setup :as-alias setup]
[app.setup.welcome-file :refer [create-welcome-file]]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[cuerdas.core :as str]))
(def schema:password
@@ -241,6 +243,7 @@
params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
@@ -350,7 +353,7 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
(let [theme (when (= theme "light") theme)
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
params (-> claims
@@ -380,8 +383,13 @@
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
props (audit/profile->props profile)]
props (audit/profile->props profile)
create-welcome-file-when-needed
(fn []
(when (:create-welcome-file params)
(let [cfg (dissoc cfg ::db/conn)]
(wrk/submit! executor (create-welcome-file cfg profile)))))]
(cond
;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile)
@@ -418,6 +426,7 @@
(if (:is-active profile)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "login"}
@@ -427,10 +436,12 @@
(when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)})))
(-> {:email (:email profile)}
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)}))))
:else
(let [elapsed? (elapsed-verify-threshold? profile)
@@ -462,7 +473,8 @@
[:map {:title "register-profile"}
[:token schema:token]
[:fullname [::sm/word-string {:max 100}]]
[:theme {:optional true} [:string {:max 10}]]])
[:theme {:optional true} [:string {:max 10}]]
[:create-welcome-file {:optional true} :boolean]])
(sv/defmethod ::register-profile
{::rpc/auth false

View File

@@ -292,7 +292,7 @@
[:map {:title "create-comment-thread"}
[:file-id ::sm/uuid]
[:position ::gpt/point]
[:content [:string {:max 250}]]
[:content [:string {:max 750}]]
[:page-id ::sm/uuid]
[:frame-id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]])

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}]
@@ -273,7 +269,7 @@
(defn get-minimal-file
[cfg id & {:as opts}]
(let [opts (assoc opts ::sql/columns [:id :modified-at :revn :data-ref-id :data-backend])]
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :data-ref-id :data-backend])]
(db/get cfg :file {:id id} opts)))
(defn get-file-etag
@@ -487,7 +483,7 @@
[:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]]
[:object-id {:optional true} [:or ::sm/uuid [::sm/set ::sm/uuid]]]
[:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-page
@@ -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"
@@ -1058,7 +1051,7 @@
(def ^:private schema:ignore-file-library-sync-status
[:map {:title "ignore-file-library-sync-status"}
[:file-id ::sm/uuid]
[:date ::dt/duration]])
[:date ::dt/instant]])
;; TODO: improve naming
(sv/defmethod ::ignore-file-library-sync-status

View File

@@ -103,6 +103,7 @@
(db/update! conn :file
{:data (:data snapshot)
:revn (inc (:revn file))
:version (:version snapshot)
:data-backend nil
:data-ref-id nil
:has-media-trimmed false
@@ -170,7 +171,7 @@
(update :data blob/encode)))))
(defn take-file-snapshot!
[cfg {:keys [file-id label]}]
[cfg {:keys [file-id label ::rpc/profile-id]}]
(let [file (get-file cfg file-id)
id (uuid/next)]
@@ -182,7 +183,9 @@
{:id id
:revn (:revn file)
:data (:data file)
:version (:version file)
:features (:features file)
:profile-id profile-id
:file-id (:id file)
:label label}
{::db/return-keys false})

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
@@ -233,7 +231,7 @@
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
DO UPDATE SET updated_at=?, media_id=?, deleted_at=?
RETURNING *")
(defn- persist-thumbnail!
@@ -251,17 +249,19 @@
:content-type mtype
:bucket "file-object-thumbnail"})))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[{:keys [::sto/storage] :as cfg} file object-id media tag]
(let [file-id (:id file)
timestamp (dt/now)
media (persist-thumbnail! storage media timestamp)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media)
tsnow (:id media)])]
file-id object-id tag
(:id media)
timestamp
(:id media)
(:deleted-at file)])]
[th1 th2])))]
(when (and (some? th1)
@@ -294,8 +294,8 @@
(media/validate-media-size! media)
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
(create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

@@ -38,6 +38,20 @@
[clojure.set :as set]
[promesa.exec :as px]))
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
(declare ^:private update-file)
(declare ^:private update-file*)
(declare ^:private process-changes-and-validate)
(declare ^:private take-snapshot?)
(declare ^:private delete-old-snapshots!)
;; PUBLIC API; intended to be used outside of this module
(declare update-file!)
(declare update-file-data!)
(declare persist-file!)
(declare get-file)
;; --- SCHEMA
(def ^:private
@@ -97,41 +111,6 @@
(or (contains? library-change-types type)
(contains? file-change-types type)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR
f.deleted_at > now())
FOR KEY SHARE")
(defn get-file
[conn id]
(let [file (db/exec-one! conn [sql:get-file id])]
(when-not file
(ex/raise :type :not-found
:code :object-not-found
:hint (format "file with id '%s' does not exists" id)))
(update file :features db/decode-pgarray #{})))
(defn- wrap-with-pointer-map-context
[f]
(fn [cfg {:keys [id] :as file}]
(binding [pmap/*tracked* (pmap/create-tracked)
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [result (f cfg file)]
(feat.fdata/persist-pointers! cfg id)
result))))
(declare ^:private delete-old-snapshots!)
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
(declare ^:private take-snapshot?)
(declare ^:private update-file)
(declare ^:private update-file*)
(declare ^:private update-file-data)
;; If features are specified from params and the final feature
;; set is different than the persisted one, update it on the
;; database.
@@ -147,7 +126,8 @@
::sm/result schema:update-file-result
::doc/module :files
::doc/added "1.17"}
[cfg {:keys [::rpc/profile-id id] :as params}]
[{:keys [::mtx/metrics] :as cfg}
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
@@ -161,14 +141,30 @@
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
params (assoc params
:profile-id profile-id
:features features
:team team
:file file)
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features)
(assoc :team team)
(assoc :file file)
(assoc :changes changes))
cfg (assoc cfg ::timestamp (dt/now))
tpoint (dt/tpoint)]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
@@ -177,89 +173,126 @@
{:features features}
{:id (:id team)})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [l/*context* (some-> (meta params)
(get :app.http/request)
(errors/request->context))]
(-> (update-file cfg params)
(-> (update-file* cfg params)
(rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
(defn update-file
[{:keys [::mtx/metrics] :as cfg}
{:keys [file features changes changes-with-metadata] :as params}]
(let [features (-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file)))
update-fn (cond-> update-file*
(contains? features "fdata/pointer-map")
(wrap-with-pointer-map-context))
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(let [file (assoc file :features features)
params (-> params
(assoc :file file)
(assoc :changes changes)
(assoc ::created-at (dt/now)))]
(-> (update-fn cfg params)
(vary-meta assoc ::audit/replace-props
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))))
(defn- update-file*
[{:keys [::db/conn ::wrk/executor] :as cfg}
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
"Internal function, part of the update-file process, that encapsulates
the changes application offload to a separated thread and emit all
corresponding notifications.
Follow the inner implementation to `update-file-data!` function.
Only intended for internal use on this module."
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
{:keys [profile-id file features changes session-id skip-validate] :as params}]
(let [;; Retrieve the file data
file (feat.fdata/resolve-file-data cfg file)
file (feat.fdata/resolve-file-data cfg file)
file (assoc file :features
(-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file))))
;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
file (px/invoke! executor
(fn []
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))))]
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
features (db/create-array conn "text" (:features file))]
;; NOTE: if file was offloaded, we need to touch the referenced
;; storage object because on this update operation the data will
;; be overwritted.
(when (= "objects-storage" (:data-backend file))
(when (feat.fdata/offloaded? file)
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
(sto/touch-object! storage (:data-ref-id file))))
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at created-at
:file-id (:id file)
:revn (:revn file)
:label (::snapshot-label file)
:data (::snapshot-data file)
:features (db/create-array conn "text" (:features file))
:changes (blob/encode changes)}
{::db/return-keys false})
(some->> (:data-ref-id file) (sto/touch-object! storage))))
;; TODO: move this to asynchronous task
(when (::snapshot-data file)
(delete-old-snapshots! cfg file))
(persist-file! cfg file)
(let [params (assoc params :file file)
response {:revn (:revn file)
:lagged (get-lagged-changes conn params)}
features (db/create-array conn "text" (:features file))]
;; Insert change (xlog)
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at timestamp
:file-id (:id file)
:revn (:revn file)
:version (:version file)
:features features
:label (::snapshot-label file)
:data (::snapshot-data file)
:changes (blob/encode changes)}
{::db/return-keys false})
;; Send asynchronous notifications
(send-notifications! cfg params)
(vary-meta response assoc ::audit/replace-props
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))
(defn update-file!
"A public api that allows apply a transformation to a file with all context setup."
[cfg file-id update-fn & args]
(let [file (get-file cfg file-id)
file (apply update-file-data! cfg file update-fn args)]
(persist-file! cfg file)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR
f.deleted_at > now())
FOR KEY SHARE")
(defn get-file
"Get not-decoded file, only decodes the features set."
[conn id]
(let [file (db/exec-one! conn [sql:get-file id])]
(when-not file
(ex/raise :type :not-found
:code :object-not-found
:hint (format "file with id '%s' does not exists" id)))
(update file :features db/decode-pgarray #{})))
(defn persist-file!
"Function responsible of persisting already encoded file. Should be
used together with `get-file` and `update-file-data!`.
It also updates the project modified-at attr."
[{:keys [::db/conn ::timestamp]} file]
(let [features (db/create-array conn "text" (:features file))
;; The timestamp can be nil because this function is also
;; intended to be used outside of this module
modified-at (or timestamp (dt/now))]
(db/update! conn :project
{:modified-at modified-at}
{:id (:project-id file)}
{::db/return-keys false})
(db/update! conn :file
{:revn (:revn file)
:data (:data file)
@@ -267,20 +300,95 @@
:features features
:data-backend nil
:data-ref-id nil
:modified-at created-at
:modified-at modified-at
:has-media-trimmed false}
{:id (:id file)})
{:id (:id file)}
{::db/return-keys false})))
(db/update! conn :project
{:modified-at created-at}
{:id (:project-id file)})
(defn- update-file-data!
"Perform a file data transformation in with all update context setup.
(let [params (assoc params :file file)]
;; Send asynchronous notifications
(send-notifications! cfg params)
This function expected not-decoded file and transformation function. Returns
an encoded file.
{:revn (:revn file)
:lagged (get-lagged-changes conn params)})))
This function is not responsible of saving the file. It only saves
fdata/pointer-map modified fragments."
[cfg {:keys [id] :as file} update-fn & args]
(binding [pmap/*tracked* (pmap/create-tracked)
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [file (update file :data (fn [data]
(-> data
(blob/decode)
(assoc :id (:id file)))))
;; For avoid unnecesary overhead of creating multiple pointers
;; and handly internally with objects map in their worst
;; case (when probably all shapes and all pointers will be
;; readed in any case), we just realize/resolve them before
;; applying the migration to the file
file (if (fmg/need-migration? file)
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))
file)
file (apply update-fn cfg file args)
;; TODO: reuse operations if file is migrated
;; TODO: move encoding to a separated thread
file (if (take-snapshot? file)
(let [tpoint (dt/tpoint)
snapshot (-> (:data file)
(feat.fdata/process-pointers deref)
(feat.fdata/process-objects (partial into {}))
(blob/encode))
elapsed (tpoint)
label (str "internal/snapshot/" (:revn file))]
(l/trc :hint "take snapshot"
:file-id (str (:id file))
:revn (:revn file)
:label label
:elapsed (dt/format-duration elapsed))
(-> file
(assoc ::snapshot-data snapshot)
(assoc ::snapshot-label label)))
file)
file (cond-> file
(contains? cfeat/*current* "fdata/objects-map")
(feat.fdata/enable-objects-map)
(contains? cfeat/*current* "fdata/pointer-map")
(feat.fdata/enable-pointer-map)
:always
(update :data blob/encode))]
(feat.fdata/persist-pointers! cfg id)
file)))
(defn- get-file-libraries
"A helper for preload file libraries, mainly used for perform file
semantical and structural validation"
[{:keys [::db/conn] :as cfg} file]
(->> (files/get-file-libraries conn (:id file))
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
;; We do not resolve the objects maps here
;; because there is a lower probability that all
;; shapes needed to be loded into memory, so we
;; leeave it on lazy status
(-> (files/get-file cfg id :migrate? false)
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))))))
(d/index-by :id)))
(defn- soft-validate-file-schema!
[file]
@@ -297,68 +405,19 @@
(l/error :hint "file validation error"
:cause cause))))
(defn- update-file-data
[{:keys [::db/conn] :as cfg} file changes skip-validate]
(let [file (update file :data (fn [data]
(-> data
(blob/decode)
(assoc :id (:id file)))))
;; For avoid unnecesary overhead of creating multiple pointers
;; and handly internally with objects map in their worst
;; case (when probably all shapes and all pointers will be
;; readed in any case), we just realize/resolve them before
;; applying the migration to the file
file (if (fmg/need-migration? file)
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))
file)
;; WARNING: this ruins performance; maybe we need to find
(defn- process-changes-and-validate
[cfg file changes skip-validate]
(let [;; WARNING: this ruins performance; maybe we need to find
;; some other way to do general validation
libs (when (and (or (contains? cf/flags :file-validation)
(contains? cf/flags :soft-file-validation))
(not skip-validate))
(->> (files/get-file-libraries conn (:id file))
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
;; We do not resolve the objects maps here
;; because there is a lower probability that all
;; shapes needed to be loded into memory, so we
;; leeave it on lazy status
(-> (files/get-file cfg id :migrate? false)
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))))))
(d/index-by :id)))
(get-file-libraries cfg file))
file (-> (files/check-version! file)
(update :revn inc)
(update :data cpc/process-changes changes)
(update :data d/without-nils))
file (if (take-snapshot? file)
(let [tpoint (dt/tpoint)
snapshot (-> (:data file)
(feat.fdata/process-pointers deref)
(feat.fdata/process-objects (partial into {}))
(blob/encode))
elapsed (tpoint)
label (str "internal/snapshot/" (:revn file))]
(l/trc :hint "take snapshot"
:file-id (str (:id file))
:revn (:revn file)
:label label
:elapsed (dt/format-duration elapsed))
(-> file
(assoc ::snapshot-data snapshot)
(assoc ::snapshot-label label)))
file)]
(update :data d/without-nils))]
(binding [pmap/*tracked* nil]
(when (contains? cf/flags :soft-file-validation)
@@ -375,15 +434,7 @@
(not skip-validate))
(val/validate-file-schema! file)))
(cond-> file
(contains? cfeat/*current* "fdata/objects-map")
(feat.fdata/enable-objects-map)
(contains? cfeat/*current* "fdata/pointer-map")
(feat.fdata/enable-pointer-map)
:always
(update :data blob/encode))))
file))
(defn- take-snapshot?
"Defines the rule when file `data` snapshot should be saved."
@@ -425,8 +476,7 @@
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
(def ^:private
sql:lagged-changes
(def ^:private sql:lagged-changes
"select s.id, s.revn, s.file_id,
s.session_id, s.changes
from file_change as s

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."
@@ -396,8 +393,8 @@
;; --- COMMAND: Clone Template
(defn- clone-template
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
(defn clone-template
[cfg {:keys [project-id profile-id] :as params} template]
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
;; NOTE: the importation process performs some operations that
;; are not very friendly with virtual threads, and for avoid
@@ -416,6 +413,7 @@
(doseq [file-id result]
(let [props (assoc props :id file-id)
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id profile-id)
(assoc ::audit/name "create-file")
(assoc ::audit/props props))]
(audit/submit! cfg event))))
@@ -437,7 +435,8 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
template (tmpl/get-template-stream cfg template-id)]
template (tmpl/get-template-stream cfg template-id)
params (assoc params :profile-id profile-id)]
(when-not template
(ex/raise :type :not-found

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,36 +357,38 @@
: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]
(let [profile (get-profile conn profile-id ::sql/for-update true)
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
(if (nil? v)
(dissoc props k)
(assoc props k v))
props))
(:props profile)
props)]
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
(filter-props props)))
(sv/defmethod ::update-profile-props
{::doc/added "1.0"
::sm/params schema:update-profile-props}
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
(db/with-atomic [conn pool]
(let [profile (get-profile conn profile-id ::sql/for-update true)
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
(if (nil? v)
(dissoc props k)
(assoc props k v))
props))
(:props profile)
props)]
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
(filter-props props))))
[cfg {:keys [::rpc/profile-id props]}]
(db/tx-run! cfg (fn [cfg]
(update-profile-props cfg profile-id props))))
;; --- MUTATION: Delete Profile

View File

@@ -30,7 +30,7 @@
(def ^:private schema:verify-token
[:map {:title "verify-token"}
[:token [:string {:max 1000}]]])
[:token [:string {:max 5000}]]])
(sv/defmethod ::verify-token
{::rpc/auth false
@@ -82,16 +82,8 @@
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
props (merge (:props profile)
(:props claims))]
(when (not= props (:props profile))
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}))
(let [profile (assoc profile :props props)]
(assoc claims :profile profile))))
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation

View File

@@ -26,14 +26,13 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:quote
(sm/define
[:map {:title "Quote"}
[::team-id {:optional true} ::sm/uuid]
[::project-id {:optional true} ::sm/uuid]
[::file-id {:optional true} ::sm/uuid]
[::incr {:optional true} [::sm/int {:min 0}]]
[::id :keyword]
[::profile-id ::sm/uuid]]))
[:map {:title "Quote"}
[::team-id {:optional true} ::sm/uuid]
[::project-id {:optional true} ::sm/uuid]
[::file-id {:optional true} ::sm/uuid]
[::incr {:optional true} [::sm/int {:min 0}]]
[::id :keyword]
[::profile-id ::sm/uuid]])
(def ^:private enabled (volatile! true))

View File

@@ -21,16 +21,14 @@
(def ^:private
schema:template
(sm/define
[:map {:title "Template"}
[:id ::sm/word-string]
[:name ::sm/word-string]
[:file-uri ::sm/word-string]]))
[:map {:title "Template"}
[:id ::sm/word-string]
[:name ::sm/word-string]
[:file-uri ::sm/word-string]])
(def ^:private
schema:templates
(sm/define
[:vector schema:template]))
[:vector schema:template])
(defmethod ig/init-key ::setup/templates
[_ _]

View File

@@ -0,0 +1,64 @@
;; 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.setup.welcome-file
(:require
[app.common.logging :as l]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files-update :as fupdate]
[app.rpc.commands.management :as management]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.worker :as-alias wrk]))
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
(def ^:private update-path
[:data :pages-index page-id :objects shape-id
:content :children 0 :children 0 :children 0])
(def ^:private sql:mark-file-object-thumbnails-deleted
"UPDATE file_tagged_object_thumbnail
SET deleted_at = now()
WHERE file_id = ?")
(def ^:private sql:mark-file-thumbnail-deleted
"UPDATE file_thumbnail
SET deleted_at = now()
WHERE file_id = ?")
(defn- update-welcome-shape
[_ file name]
(let [text (str "Welcome to Penpot, " name "!")]
(-> file
(update-in update-path assoc :text text)
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
(defn create-welcome-file
[cfg {:keys [id fullname] :as profile}]
(try
(let [cfg (dissoc cfg ::db/conn)
params {:profile-id (:id profile)
:project-id (:default-project-id profile)}
template-stream (tmpl/get-template-stream cfg "welcome")
file-id (-> (management/clone-template cfg params template-stream)
first)]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
(profile/update-profile-props cfg id {:welcome-file-id file-id})
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
(catch Throwable cause
(l/error :hint "unexpected error on create welcome file " :cause cause))))

View File

@@ -75,6 +75,7 @@
:created-at (:created-at file)
:modified-at (:modified-at file)
:data-backend nil
:data-ref-id nil
:has-media-trimmed false}
{:id (:id file)})))

View File

@@ -727,13 +727,15 @@
deleted 0
total 0]
(if-let [email (first emails)]
(if-let [profile (db/get* system :profile
{:email (str/lower email)}
{::db/remove-deleted false})]
(if-let [profile (some-> (db/get* system :profile
{:email (str/lower email)}
{::db/remove-deleted false})
(profile/decode-row))]
(do
(audit/insert! system
{::audit/name "delete-profile"
::audit/type "action"
::audit/profile-id (:id profile)
::audit/tracked-at deleted-at
::audit/props (audit/profile->props profile)
::audit/context {:triggered-by "srepl"

View File

@@ -11,7 +11,6 @@
inactivity (the default threshold is 72h)."
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.files.migrations :as fmg]
[app.common.files.validate :as cfv]
[app.common.logging :as l]
@@ -35,16 +34,36 @@
(declare ^:private decode-file)
(declare ^:private persist-file!)
(def ^:private sql:get-snapshots
"SELECT f.file_id AS id,
f.data,
f.revn,
f.version,
f.features,
f.data_backend,
f.data_ref_id
FROM file_change AS f
WHERE f.file_id = ?
AND f.label IS NOT NULL
ORDER BY f.created_at ASC")
(def ^:private sql:mark-file-media-object-deleted
"UPDATE file_media_object
SET deleted_at = now()
WHERE file_id = ? AND id != ALL(?::uuid[])
RETURNING id")
(def ^:private xf:collect-used-media
(comp (map :data) (mapcat bfc/collect-used-media)))
(defn- clean-file-media!
"Performs the garbage collection of file media objects."
[{:keys [::db/conn]} {:keys [id data] :as file}]
(let [used (bfc/collect-used-media data)
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(let [used (into #{}
xf:collect-used-media
(cons file
(->> (db/cursor conn [sql:get-snapshots id])
(map (partial decode-file cfg)))))
ids (db/create-array conn "uuid" used)
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids])
(into #{} (map :id)))]
@@ -114,7 +133,13 @@
file))
(def ^:private sql:get-files-for-library
"SELECT f.id, f.data, f.modified_at, f.features, f.version
"SELECT f.id,
f.data,
f.modified_at,
f.features,
f.version,
f.data_backend,
f.data_ref_id
FROM file AS f
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
WHERE fl.library_file_id = ?
@@ -170,11 +195,6 @@
(l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused))
file))
(def ^:private sql:get-changes
"SELECT id, data FROM file_change
WHERE file_id = ? AND data IS NOT NULL
ORDER BY created_at ASC")
(def ^:private sql:mark-deleted-data-fragments
"UPDATE file_data_fragment
SET deleted_at = now()
@@ -190,8 +210,7 @@
(defn- clean-data-fragments!
[{:keys [::db/conn]} {:keys [id] :as file}]
(let [used (into #{} xf:collect-pointers
(cons file (db/cursor conn [sql:get-changes id])))
(let [used (into #{} xf:collect-pointers [file])
unused (let [ids (db/create-array conn "uuid" used)]
(->> (db/exec! conn [sql:mark-deleted-data-fragments id ids])
@@ -220,7 +239,9 @@
f.revn,
f.version,
f.features,
f.modified_at
f.modified_at,
f.data_backend,
f.data_ref_id
FROM file AS f
WHERE f.has_media_trimmed IS false
AND f.modified_at < now() - ?::interval
@@ -236,18 +257,8 @@
(defn- decode-file
[cfg {:keys [id] :as file}]
;; NOTE: a preventive check that does not allow proceed the gc for
;; already offloaded file; if this exception happens, means that
;; something external modified the file flag without preloading the
;; file back again to the table
(when (feat.fdata/offloaded? file)
(ex/raise :hint "unable to run file-gc on an already offloaded file"
:type :internal
:code :file-already-offloaded
:file-id id))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(-> file
(-> (feat.fdata/resolve-file-data cfg file)
(update :features db/decode-pgarray #{})
(update :data blob/decode)
(update :data feat.fdata/process-pointers deref)
@@ -256,7 +267,7 @@
(fmg/migrate-file))))
(defn- persist-file!
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [id] :as file}]
(let [file (if (contains? (:features file) "fdata/objects-map")
(feat.fdata/enable-objects-map file)
file)
@@ -272,11 +283,18 @@
(update :features db/encode-pgarray conn "text")
(update :data blob/encode))]
;; If file was already offloaded, we touch the underlying storage
;; object for properly trigger storage-gc-touched task
(when (feat.fdata/offloaded? file)
(some->> (:data-ref-id file) (sto/touch-object! storage)))
(db/update! conn :file
{:has-media-trimmed true
:features (:features file)
:version (:version file)
:data (:data file)}
:data (:data file)
:data-backend nil
:data-ref-id nil}
{:id id}
{::db/return-keys true})))

View File

@@ -10,6 +10,7 @@
(:require
[app.common.logging :as l]
[app.db :as db]
[app.features.fdata :as feat.fdata]
[app.storage :as sto]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -26,8 +27,8 @@
(def xf:filter-offloded
(comp
(filter #(= "objects-storage" (:data-backend %)))
(map :data-ref-id)))
(filter feat.fdata/offloaded?)
(keep :data-ref-id)))
(defn- delete-in-chunks
[{:keys [::chunk-size ::threshold] :as cfg}]

View File

@@ -141,21 +141,22 @@
;; --- INSTANT
(defn instant?
[v]
(instance? Instant v))
(defn instant
([s]
(if (int? s)
(Instant/ofEpochMilli s)
(Instant/parse s)))
(cond
(instant? s) s
(int? s) (Instant/ofEpochMilli s)
:else (Instant/parse s)))
([s fmt]
(case fmt
:rfc1123 (Instant/from (.parse DateTimeFormatter/RFC_1123_DATE_TIME ^String s))
:iso (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s))
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
(defn instant?
[v]
(instance? Instant v))
(defn is-after?
[da db]
(.isAfter ^Instant da ^Instant db))

View File

@@ -76,7 +76,7 @@
:enable-feature-fdata-pointer-map
:enable-feature-fdata-objets-map
:enable-feature-components-v2
:enable-file-snapshot
:enable-auto-file-snapshot
:disable-file-validation])
(defn state-init
@@ -304,16 +304,18 @@
([params] (update-file* *system* params))
([system {:keys [file-id changes session-id profile-id revn]
:or {session-id (uuid/next) revn 0}}]
(db/tx-run! system (fn [{:keys [::db/conn] :as system}]
(let [file (files.update/get-file conn file-id)]
(files.update/update-file system
(-> system
(assoc ::files.update/timestamp (dt/now))
(db/tx-run! (fn [{:keys [::db/conn] :as system}]
(let [file (files.update/get-file conn file-id)]
(#'files.update/update-file* system
{:id file-id
:revn revn
:file file
:features (:features file)
:changes changes
:session-id session-id
:profile-id profile-id}))))))
:profile-id profile-id})))))))
(declare command!)

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

@@ -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,29 +10,32 @@
[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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private
schema:operation
(def schema:operation
[:multi {:dispatch :type
:title "Operation"
:decode/json #(update % :type keyword)
@@ -61,17 +64,130 @@
[:type [:= :set-remote-synced]]
[:remote-synced {:optional true} [:maybe :boolean]]]]])
(sm/register! ::change
(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 :title "Change" ::smd/simplified true}
[:multi {:dispatch :type
:title "Change"
: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"}
@@ -101,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]]
@@ -141,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"}
@@ -188,10 +301,9 @@
[:type [:= :del-color]]
[:id ::sm/uuid]]]
;; DEPRECATED: remove before 2.3
[:add-recent-color
[:map {:title "AddRecentColorChange"}
[:type [:= :add-recent-color]]
[:color ::ctc/recent-color]]]
[:map {:title "AddRecentColorChange"}]]
[:add-media
[:map {:title "AddMediaChange"}
@@ -256,14 +368,17 @@
[:type [:= :del-typography]]
[:id ::sm/uuid]]]]])
(sm/register! ::changes
[:sequential {:gen/max 2} ::change])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
(def check-change!
(sm/check-fn ::change))
(sm/register! ::change schema:change)
(sm/register! ::changes schema:changes)
(def valid-change?
(sm/lazy-validator schema:change))
(def check-changes!
(sm/check-fn ::changes))
(sm/check-fn schema:changes))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Specific helpers
@@ -301,7 +416,7 @@
(not= shape-old shape-new))
(dm/verify!
"expected valid shape"
(and (cts/check-shape! shape-new)
(and (cts/valid-shape? shape-new)
(cts/shape? shape-new))))))]
(->> (into #{} (map :page-id) items)
@@ -335,12 +450,10 @@
(process-changes data items true))
([data items verify?]
;; When verify? false we spec the schema validation. Currently used to make just
;; 1 validation even if the changes are applied twice
;; When verify? false we spec the schema validation. Currently used
;; to make just 1 validation even if the changes are applied twice
(when verify?
(dm/verify!
"expected valid changes"
(check-changes! items)))
(check-changes! items))
(binding [*touched-changes* (volatile! #{})]
(let [result (reduce #(or (process-change %1 %2) %1) data items)
@@ -352,14 +465,71 @@
#?(:clj (validate-shapes! data result items))
result))))
;; DEPRECATED: remove before 2.3 release
(defmethod process-change :set-option
[data {:keys [page-id option value]}]
[data _]
data)
;; --- Comment Threads
(defmethod process-change :set-comment-thread-position
[data {:keys [page-id comment-thread-id position frame-id]}]
(d/update-in-when data [:pages-index page-id]
(fn [data]
(let [path (if (seqable? option) option [option])]
(if value
(assoc-in data (into [:options] path) value)
(assoc data :options (d/dissoc-in (:options data) path)))))))
(fn [page]
(if (and position frame-id)
(update page :comment-thread-positions assoc
comment-thread-id {:frame-id frame-id
:position position})
(update page :comment-thread-positions dissoc
comment-thread-id)))))
;; --- Guides
(defmethod process-change :set-guide
[data {:keys [page-id id params]}]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
(let [guides (get page :guides)
guides (dissoc guides id)]
(if (empty? guides)
(dissoc page :guides)
(assoc page :guides guides)))))
(let [params (assoc params :id id)]
(d/update-in-when data [:pages-index page-id] update :guides assoc id params))))
;; --- Flows
(defmethod process-change :set-flow
[data {:keys [page-id id params]}]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
(let [flows (get page :flows)
flows (dissoc flows id)]
(if (empty? flows)
(dissoc page :flows)
(assoc page :flows flows)))))
(let [params (assoc params :id id)]
(d/update-in-when data [:pages-index page-id] update :flows assoc id params))))
;; --- Grids
(defmethod process-change :set-default-grid
[data {:keys [page-id grid-type params]}]
(if (nil? params)
(d/update-in-when data [:pages-index page-id]
(fn [page]
(let [default-grids (get page :default-grids)
default-grids (dissoc default-grids grid-type)]
(if (empty? default-grids)
(dissoc page :default-grids)
(assoc page :default-grids default-grids)))))
(d/update-in-when data [:pages-index page-id] update :default-grids assoc grid-type params)))
;; --- Shape / Obj
(defmethod process-change :add-obj
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
@@ -541,6 +711,7 @@
(d/update-in-when [pid :shapes] d/without-obj sid)
(d/update-in-when [pid :shapes] d/vec-without-nils)
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
(update-parent-id [objects id]
(-> objects
(d/update-when id assoc :parent-id parent-id)))
@@ -599,25 +770,34 @@
(ctpl/add-page data page)))
(defmethod process-change :mod-page
[data {:keys [id name]}]
(d/update-in-when data [:pages-index id] assoc :name name))
[data {:keys [id] :as params}]
(d/update-in-when data [:pages-index id]
(fn [page]
(let [name (get params :name)
bg (get params :background :not-found)]
(cond-> page
(string? name)
(assoc :name name)
(defmethod process-change :mod-plugin-data
(string? bg)
(assoc :background bg)
(nil? bg)
(dissoc :background))))))
(defmethod process-change :set-plugin-data
[data {:keys [object-type object-id page-id namespace key value]}]
(when (and (= object-type :shape) (nil? page-id))
(ex/raise :type :internal :hint "update for shapes needs a page-id"))
(letfn [(update-fn [data]
(if (some? value)
(assoc-in data [:plugin-data namespace key] value)
(update-in data [:plugin-data namespace] (fnil dissoc {}) key)))]
(update-in data [:plugin-data namespace] dissoc key)))]
(case object-type
:file
(update-fn data)
:page
(d/update-in-when data [:pages-index object-id :options] update-fn)
(d/update-in-when data [:pages-index object-id] update-fn)
:shape
(d/update-in-when data [:pages-index page-id :objects object-id] update-fn)
@@ -651,18 +831,11 @@
[data {:keys [id]}]
(ctcl/delete-color data id))
;; DEPRECATED: remove before 2.3
(defmethod process-change :add-recent-color
[data {:keys [color]}]
;; Moves the color to the top of the list and then truncates up to 15
(update
data
:recent-colors
(fn [rc]
(let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color)))
rc (-> rc (conj color))]
(cond-> rc
(> (count rc) 15)
(subvec 1))))))
[data _]
data)
;; -- 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
@@ -607,13 +679,6 @@
(reduce resize-parent changes all-parents)))
;; Library changes
(defn add-recent-color
[changes color]
(-> changes
(update :redo-changes conj {:type :add-recent-color :color color})
(apply-changes-local)))
(defn add-color
[changes color]
(-> changes

View File

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

View File

@@ -863,11 +863,9 @@
(assoc shadow :color color)))
(update-object [object]
(d/update-when object :shadow
#(into []
(comp (map fix-shadow)
(filter valid-shadow?))
%)))
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when object :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
@@ -1010,13 +1008,68 @@
(defn migrate-up-51
"This migration fixes library invalid colors"
[data]
(let [update-colors
(fn [colors]
(into {} (filter #(-> % val valid-color?) colors)))]
(update data :colors update-colors)))
(defn migrate-up-52
"Fixes incorrect value on `layout-wrap-type` prop"
[data]
(letfn [(update-shape [shape]
(if (= :no-wrap (:layout-wrap-type shape))
(assoc shape :layout-wrap-type :nowrap)
shape))
(update-page [page]
(d/update-when page :objects update-vals update-shape))]
(update data :pages-index update-vals update-page)))
(defn migrate-up-54
"Fixes shapes with invalid colors in shadow: it first tries a non
destructive fix, and if it is not possible, then, shadow is removed"
[data]
(letfn [(fix-shadow [shadow]
(update shadow :color d/without-nils))
(update-shape [shape]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when shape :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defn migrate-up-55
"This migration moves page options to the page level"
[data]
(let [update-page
(fn [{:keys [options] :as page}]
(cond-> page
(and (some? (:saved-grids options))
(not (contains? page :default-grids)))
(assoc :default-grids (:saved-grids options))
(and (some? (:flows options))
(not (contains? page :flows)))
(assoc :flows (:flows options))
(and (some? (:guides options))
(not (contains? page :guides)))
(assoc :guides (:guides options))
(and (some? (:comment-threads-position options))
(not (contains? page :comment-thread-positions)))
(assoc :comment-thread-positions (:comment-threads-position options))))]
(update data :pages-index d/update-vals update-page)))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1059,4 +1112,8 @@
{:id 48 :migrate-up migrate-up-48}
{:id 49 :migrate-up migrate-up-49}
{:id 50 :migrate-up migrate-up-50}
{:id 51 :migrate-up migrate-up-51}])
{: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 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]
@@ -158,7 +149,11 @@
empty-parents
;; Any parent whose children are all deleted, must be deleted too.
(into (d/ordered-set) (find-all-empty-parents #{}))
;; Unless we are during a component swap: in this case we are replacing a shape by
;; other one, so must not delete empty parents.
(if-not component-swap
(into (d/ordered-set) (find-all-empty-parents #{}))
#{})
components-to-delete
(if components-v2
@@ -172,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
@@ -181,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})
@@ -197,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

@@ -29,11 +29,6 @@
[malli.util :as mu]))
(defprotocol ILazySchema
(-get-schema [_])
(-get-validator [_])
(-get-explainer [_])
(-get-decoder [_])
(-get-encoder [_])
(-validate [_ o])
(-explain [_ o])
(-decode [_ o]))
@@ -53,27 +48,21 @@
[s]
(m/type-properties s))
(defn lazy-schema?
(defn- lazy-schema?
[s]
(satisfies? ILazySchema s))
(defn schema
[s]
(if (lazy-schema? s)
(-get-schema s)
(m/schema s default-options)))
(m/schema s default-options))
(defn validate
[s value]
(if (lazy-schema? s)
(-validate s value)
(m/validate s value default-options)))
(m/validate s value default-options))
(defn explain
[s value]
(if (lazy-schema? s)
(-explain s value)
(m/explain s value default-options)))
(m/explain s value default-options))
(defn simplify
"Given an explain data structure, return a simplified version of it"
@@ -171,29 +160,19 @@
(defn validator
[s]
(if (lazy-schema? s)
(-get-validator s)
(-> s schema m/validator)))
(-> s schema m/validator))
(defn explainer
[s]
(if (lazy-schema? s)
(-get-explainer s)
(-> s schema m/explainer)))
(-> s schema m/explainer))
(defn encoder
([s]
(assert (lazy-schema? s) "expected lazy schema")
(-get-decoder s))
([s transformer]
(m/encoder s default-options transformer))
([s options transformer]
(m/encoder s options transformer)))
(defn decoder
([s]
(assert (lazy-schema? s) "expected lazy schema")
(-get-decoder s))
([s transformer]
(m/decoder s default-options transformer))
([s options transformer]
@@ -242,6 +221,8 @@
(v/-block "Schema" (v/-visit schema printer) printer)]})
(defn pretty-explain
"A helper that allows print a console-friendly output for the
explain; should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
@@ -259,7 +240,7 @@
([s] (lookup sr/default-registry s))
([registry s] (schema (mr/schema registry s))))
(defn fast-check!
(defn- fast-check!
"A fast path for checking process, assumes the ILazySchema protocol
implemented on the provided `s` schema. Sould not be used directly."
[s value]
@@ -272,12 +253,12 @@
::explain explain}))))
true)
(declare define)
(declare ^:private lazy-schema)
(defn check-fn
"Create a predefined check function"
[s]
(let [schema (if (lazy-schema? s) s (define s))]
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
(partial fast-check! schema)))
(defn check!
@@ -285,19 +266,10 @@
schema over provided data. Raises an assertion exception, should be
used together with `dm/assert!` or `dm/verify!`."
[s value]
(if (lazy-schema? s)
(fast-check! s value)
(do
(when-not ^boolean (m/validate s value default-options)
(let [hint (d/nilv dm/*assert-context* "check error")
explain (explain s value)]
(throw (ex-info hint {:type :assertion
:code :data-validation
:hint hint
::explain explain}))))
true)))
(let [s (if (lazy-schema? s) s (lazy-schema s))]
(fast-check! s value)))
(defn fast-validate!
(defn- fast-validate!
"A fast path for validation process, assumes the ILazySchema protocol
implemented on the provided `s` schema. Sould not be used directly."
([s value] (fast-validate! s value nil))
@@ -309,52 +281,33 @@
::explain explain}
options)
hint (get options :hint "schema validation error")]
(throw (ex-info hint options))))))
(throw (ex-info hint options))))
value))
(defn validate-fn
"Create a predefined validate function that raises an expception"
[s]
(let [schema (if (lazy-schema? s) s (define s))]
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
(partial fast-validate! schema)))
(defn validate!
"A generic validation function for predefined schemas."
([s value] (validate! s value nil))
([s value options]
(if (lazy-schema? s)
(fast-validate! s value options)
(when-not ^boolean (m/validate s value default-options)
(let [explain (explain s value)
options (into {:type :validation
:code :data-validation
::explain explain}
options)
hint (get options :hint "schema validation error")]
(throw (ex-info hint options)))))))
;; FIXME: revisit
(defn conform!
[schema value]
(assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol")
(let [params (-decode schema value)]
(fast-validate! schema params nil)
params))
(let [s (if (lazy-schema? s) s (lazy-schema s))]
(fast-validate! s value options))))
(defn register! [type s]
(let [s (if (map? s) (m/-simple-schema s) s)]
(swap! sr/registry assoc type s)
nil))
(defn define
(defn- lazy-schema
"Create ans instance of ILazySchema"
[s & {:keys [transformer] :or {transformer json-transformer} :as options}]
[s]
(let [schema (delay (schema s))
validator (delay (m/validator @schema))
explainer (delay (m/explainer @schema))
options (c/merge default-options (dissoc options :transformer))
decoder (delay (m/decoder @schema options transformer))
encoder (delay (m/encoder @schema options transformer))]
explainer (delay (m/explainer @schema))]
(reify
m/AST
@@ -397,16 +350,6 @@
(m/-form @schema))
ILazySchema
(-get-schema [_]
@schema)
(-get-validator [_]
@validator)
(-get-explainer [_]
@explainer)
(-get-encoder [_]
@encoder)
(-get-decoder [_]
@decoder)
(-validate [_ o]
(@validator o))
(-explain [_ o]
@@ -476,7 +419,7 @@
::oapi/type "string"
::oapi/format "email"}})
(def non-empty-strings-xf
(def xf:filter-word-strings
(comp
(filter string?)
(remove str/empty?)
@@ -489,11 +432,8 @@
:min 0
:max 1
:compile
(fn [{:keys [coerce kind max min] :as props} children _]
(let [xform (if coerce
(comp non-empty-strings-xf (map coerce))
non-empty-strings-xf)
kind (or (last children) kind)
(fn [{:keys [kind max min] :as props} children _]
(let [kind (or (last children) kind)
pred
(cond
@@ -501,9 +441,6 @@
(nil? kind) any?
:else (validator kind))
encode-child
(encoder kind string-transformer)
pred
(cond
(and max min)
@@ -531,31 +468,49 @@
(fn [value]
(every? pred value)))
decode
decode-string-child
(decoder kind string-transformer)
decode-string
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} xform v)))
(let [v (if (string? v) (str/split v #"[\s,]+") v)
x (comp xf:filter-word-strings (map decode-string-child))]
(into #{} x v)))
decode-json-child
(decoder kind json-transformer)
decode-json
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)
x (comp xf:filter-word-strings (map decode-json-child))]
(into #{} x v)))
encode-string-child
(encoder kind string-transformer)
encode-string
(fn [o]
(if (set? o)
(str/join ", " (map encode-string-child o))
o))
encode-json
(fn [o]
(if (set? o)
(vec o)
o))
encode-string
(fn [o]
(if (set? o)
(str/join ", " (map encode-child o))
o))]
{:pred pred
:type-properties
{:title "set"
:description "Set of Strings"
:error/message "should be a set of strings"
:gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:decode/string decode-string
:decode/json decode-json
:encode/string encode-string
:encode/json encode-json
::oapi/type "array"
@@ -569,21 +524,14 @@
:min 0
:max 1
:compile
(fn [{:keys [coerce kind max min] :as props} children _]
(let [xform (if coerce
(comp non-empty-strings-xf (map coerce))
non-empty-strings-xf)
kind (or (last children) kind)
(fn [{:keys [kind max min] :as props} children _]
(let [kind (or (last children) kind)
pred
(cond
(fn? kind) kind
(nil? kind) any?
:else (validator kind))
encode-child
(encoder kind string-transformer)
pred
(cond
(and max min)
@@ -611,15 +559,31 @@
(fn [value]
(every? pred value)))
decode
decode-string-child
(decoder kind string-transformer)
decode-json-child
(decoder kind json-transformer)
decode-string
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into [] xform v)))
(let [v (if (string? v) (str/split v #"[\s,]+") v)
x (comp xf:filter-word-strings (map decode-string-child))]
(into #{} x v)))
decode-json
(fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)
x (comp xf:filter-word-strings (map decode-json-child))]
(into #{} x v)))
encode-string-child
(encoder kind string-transformer)
encode-string
(fn [o]
(if (vector? o)
(str/join ", " (map encode-child o))
(str/join ", " (map encode-string-child o))
o))]
{:pred pred
@@ -628,8 +592,8 @@
:description "Set of Strings"
:error/message "should be a set of strings"
:gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:decode/string decode-string
:decode/json decode-json
:encode/string encode-string
::oapi/type "array"
::oapi/format "set"
@@ -646,7 +610,7 @@
:gen/gen (-> :string sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} non-empty-strings-xf v)))
(into #{} xf:filter-word-strings v)))
::oapi/type "array"
::oapi/format "set"
::oapi/items {:type "string"}
@@ -662,7 +626,7 @@
:gen/gen (-> :keyword sg/generator sg/set)
:decode/string (fn [v]
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
(into #{} (comp non-empty-strings-xf (map keyword)) v)))
(into #{} (comp xf:filter-word-strings (map keyword)) v)))
::oapi/type "array"
::oapi/format "set"
::oapi/items {:type "string" :format "keyword"}

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

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

@@ -80,21 +80,23 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]])
(def schema:color-attrs
[:map {:title "ColorAttrs"}
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:path {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe ::rgb-color]]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:color
[:and
[:map {:title "Color"}
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:path {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe ::rgb-color]]
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]]
[:and schema:color-attrs
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(def schema:recent-color
@@ -107,11 +109,11 @@
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::rgb-color type:rgb-color)
(sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color)
(sm/register! ::color-attrs schema:color-attrs)
(def check-color!
(sm/check-fn schema:color))
@@ -392,13 +394,22 @@
(process-shape-colors shape sync-color)))
(defn eq-recent-color?
(defn- eq-recent-color?
[c1 c2]
(or (= c1 c2)
(and (some? (:color c1))
(some? (:color c2))
(= (:color c1) (:color c2)))))
(defn add-recent-color
"Moves the color to the top of the list and then truncates up to 15"
[state file-id color]
(update state file-id (fn [colors]
(let [colors (d/removev (partial eq-recent-color? color) colors)
colors (conj colors color)]
(cond-> colors
(> (count colors) 15)
(subvec 1))))))
(defn stroke->color-att
[stroke file-id shared-libs]

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]
@@ -18,40 +19,62 @@
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/register! ::flow
[:map {:title "PageFlow"}
(def schema:flow
[:map {:title "Flow"}
[:id ::sm/uuid]
[:name :string]
[:starting-frame ::sm/uuid]])
(sm/register! ::guide
[:map {:title "PageGuide"}
(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]]])
(sm/register! ::page
(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} ::flow]]
[:guides {:optional true}
[:map-of {:gen/max 2} ::sm/uuid ::guide]]
[:plugin-data {:optional true} ::ctpg/plugin-data]]]])
;; DEPERECATED: remove after 2.3 release
[:map {:title "PageOptions"}]]])
(def check-page-guide!
(sm/check-fn ::guide))
(sm/register! ::page schema:page)
(sm/register! ::guide schema:guide)
(sm/register! ::flow schema:flow)
(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
@@ -76,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

@@ -125,6 +125,9 @@
(sm/register! ::stroke schema:stroke)
(def check-stroke!
(sm/check-fn schema:stroke))
(def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid]
@@ -354,6 +357,9 @@
(def check-shape!
(sm/check-fn schema:shape))
(def valid-shape?
(sm/lazy-validator schema:shape))
(defn has-images?
[{:keys [fills strokes]}]
(or (some :fill-image fills)

View File

@@ -1622,13 +1622,17 @@
(defn remap-grid-cells
"Remaps the shapes ids inside the cells"
[shape ids-map]
(let [do-remap-cells
(let [remap-shape
(fn [id]
(get ids-map id id))
remap-cell
(fn [cell]
(-> cell
(update :shapes #(into [] (keep ids-map) %))))
(update :shapes #(into [] (keep remap-shape) %))))
shape
(-> shape
(update :layout-grid-cells update-vals do-remap-cells))]
(update :layout-grid-cells update-vals remap-cell))]
shape))
(defn merge-cells

View File

@@ -27,3 +27,6 @@
[:color ::ctc/color]])
(sm/register! ::shadow schema:shadow)
(def check-shadow!
(sm/check-fn schema:shadow))

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,37 @@ RUN set -ex; \
mv /tmp/mc /usr/local/bin/; \
chmod +x /usr/local/bin/mc;
# Install Rust toolchain
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH;
RUN set -eux; \
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
dpkgArch="$(dpkg --print-architecture)"; \
case "${dpkgArch##*-}" in \
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
esac; \
url="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \
wget "$url"; \
echo "${rustupSha256} *rustup-init" | sha256sum -c -; \
chmod +x rustup-init; \
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
rm rustup-init; \
chmod -R a+w $RUSTUP_HOME $CARGO_HOME;
WORKDIR /usr/local
# Install emscripten SDK and activate it
RUN set -eux; \
git clone https://github.com/emscripten-core/emsdk.git; \
cd emsdk; \
./emsdk install latest; \
./emsdk activate latest; \
rustup target add wasm32-unknown-emscripten;
WORKDIR /home
COPY files/nginx.conf /etc/nginx/nginx.conf

View File

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

View File

@@ -26,16 +26,24 @@
(def ^:private
schema:config
(sm/define
[:map {:title "config"}
[:public-uri {:optional true} ::sm/uri]
[:host {:optional true} :string]
[:tenant {:optional true} :string]
[:flags {:optional true} ::sm/set-of-keywords]
[:redis-uri {:optional true} :string]
[:tempdir {:optional true} :string]
[:browser-pool-max {:optional true} :int]
[:browser-pool-min {:optional true} :int]]))
[:map {:title "config"}
[:public-uri {:optional true} ::sm/uri]
[:host {:optional true} :string]
[:tenant {:optional true} :string]
[:flags {:optional true} [::sm/set :keyword]]
[:redis-uri {:optional true} :string]
[:tempdir {:optional true} :string]
[:browser-pool-max {:optional true} ::sm/int]
[:browser-pool-min {:optional true} ::sm/int]])
(def ^:private decode-config
(sm/decoder schema:config sm/string-transformer))
(def ^:private explain-config
(sm/explainer schema:config))
(def ^:private valid-config?
(sm/validator schema:config))
(defn- parse-flags
[config]
@@ -60,15 +68,15 @@
[]
(let [env (read-env "penpot")
env (d/without-nils env)
data (merge defaults env)]
data (merge defaults env)
data (decode-config data)]
(try
(sm/conform! schema:config data)
(catch :default cause
(if-let [explain (some->> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.error cause))
(process/exit -1)))))
(when-not (valid-config? data)
(let [explain (explain-config data)]
(println (sm/humanize-explain explain))
(process/exit -1)))
data))
(def config
(prepare-config))

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"
},
@@ -96,6 +98,7 @@
"highlight.js": "^11.9.0",
"js-beautify": "^1.15.1",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"mousetrap": "^1.6.5",
"opentype.js": "^1.3.4",

View File

@@ -177,3 +177,15 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-
),
).toBeVisible();
});
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.pageName.click();
await page.keyboard.press("ArrowLeft");
await expect(workspacePage.pageName).toHaveText("Page 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" }),

1
frontend/render_v2/cpp/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
out

View File

@@ -0,0 +1,108 @@
FROM debian:11
RUN apt update && apt dist-upgrade -y && apt install -y \
git \
clang \
python \
curl \
build-essential \
libfontconfig-dev \
libgl1-mesa-dev \
libglu1-mesa-dev \
procps \
vim \
binaryen \
wabt \
&& groupadd -g 2000 skia \
&& useradd -u 2000 -g 2000 skia
# TODO(kjlubick): Try a shallow clone of depot_tools
RUN cd /tmp \
&& git clone 'https://chromium.googlesource.com/chromium/tools/depot_tools.git'
RUN cd /tmp \
&& git clone 'https://gn.googlesource.com/gn'
RUN cd /tmp \
&& git clone 'https://skia.googlesource.com/skia' \
&& cd skia \
&& git checkout 'chrome/m129'
ENV PATH=${PATH}:/tmp/depot_tools
ENV PATH=${PATH}:/tmp/gn
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libGLESv2.so /usr/local/lib/libGLESv2.so
ADD --chown=skia:skia https://storage.googleapis.com/skia-swiftshader/libEGL.so /usr/local/lib/libEGL.so
# FIXME: I don't like this approach because it implies that
# git-sync-deps is going to fail and we need to run it two
# times. The weird thing is that git-sync-deps fails consistently
# the first time.
RUN cd /tmp/skia; \
tools/git-sync-deps; \
tools/git-sync-deps; \
exit 0
RUN cd /tmp/skia && python3 bin/fetch-ninja
RUN . "tmp/skia/third_party/externals/emsdk/emsdk_env.sh"
RUN cd /tmp/skia && ./bin/gn gen out/wasm \
--args="is_debug=false \
is_official_build=true \
is_component_build=false \
is_trivial_abi=true \
werror=true \
target_cpu=\"wasm\" \
skia_use_angle=false \
skia_use_dng_sdk=false \
skia_use_dawn=false \
skia_use_webgl=true \
skia_use_webgpu=false \
skia_use_expat=false \
skia_use_fontconfig=false \
skia_use_freetype=true \
skia_use_libheif=false \
skia_use_libjpeg_turbo_decode=true \
skia_use_libjpeg_turbo_encode=true \
skia_use_no_jpeg_encode=false \
skia_use_libpng_decode=true \
skia_use_libpng_encode=true \
skia_use_no_png_encode=false \
skia_use_libwebp_decode=true \
skia_use_libwebp_encode=true \
skia_use_no_webp_encode=false \
skia_use_lua=false \
skia_use_piex=false \
skia_use_system_freetype2=false \
skia_use_system_libjpeg_turbo=false \
skia_use_system_libpng=false \
skia_use_system_libwebp=false \
skia_use_system_zlib=false \
skia_use_vulkan=false \
skia_use_wuffs=true \
skia_use_zlib=true \
skia_enable_ganesh=true \
skia_enable_sksl=true \
skia_build_for_debugger=false \
skia_enable_sksl_tracing=true \
skia_use_icu=true \
skia_use_client_icu=false \
skia_use_libgrapheme=false \
skia_use_system_icu=false \
skia_use_harfbuzz=true \
skia_use_system_harfbuzz=false \
skia_enable_fontmgr_custom_directory=false \
skia_enable_fontmgr_custom_embedded=true \
skia_enable_fontmgr_custom_empty=false \
skia_fontmgr_factory=\":fontmgr_custom_embedded_factory\" \
skia_use_freetype_woff2=true \
skia_enable_skshaper=true \
skia_enable_skparagraph=true \
skia_enable_pdf=false"
RUN cd /tmp/skia; ninja -C out/wasm
RUN echo "source '/tmp/skia/third_party/externals/emsdk/emsdk_env.sh'" >> /root/.bashrc
COPY docker/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,65 @@
all:
# -fno-rtti: Removes C++ Run-Time Type Info support.
# --no-entry: Disables the necessity of an entry point.
# -sALLOW_MEMORY_GROWTH: Creates a resizable memory ArrayBuffer.
# -sMODULARIZE: Exports emscripten as a CommonJS/AMD module.
# -sENVIRONMENT: Removes unnecessary environments (node,worker,etc).
# -sUSE_PTHREADS: Disables pthreads.
# -sMAX_WEBGL_VERSION: Max WebGL set to 2
# -sUSE_WEBGL2: Uses WebGL2 by default.
em++ \
-std=c++20 \
-lembind \
-fno-rtti \
--no-entry \
-sALLOW_MEMORY_GROWTH \
-sUSE_PTHREADS=0 \
-sMODULARIZE=1 \
-sDISABLE_EXCEPTION_CATCHING \
-sNODEJS_CATCH_EXIT=0 \
-sMAX_WEBGL_VERSION=2 \
-sUSE_WEBGL2=1 \
-sFORCE_FILESYSTEM=0 \
-sDYNAMIC_EXECUTION=0 \
-sFILESYSTEM=0 \
-sENVIRONMENT='web' \
-sINITIAL_MEMORY=128MB \
-DCK_ENABLE_WEBGL \
-DCK_NO_FONTS \
-DSK_RELEASE \
-DSK_DISABLE_TRACING \
-DSK_FORCE_AAA \
-DSK_FORCE_8_BYTE_ALIGNMENT \
-DSK_SHAPER_HARFBUZZ_AVAILABLE \
-DCK_SERIALIZE_SKP \
-DSK_GANESH \
-DSK_DISABLE_LEGACY_SHADERCONTEXT \
-DCK_INCLUDE_PATHOPS \
-DCK_INCLUDE_RUNTIME_EFFECT \
-DSKSL_ENABLE_TRACING \
-DNDEBUG \
-DSK_TRIVIAL_ABI="[[clang::trivial_abi]]" \
-DSK_TYPEFACE_FACTORY_FREETYPE \
-DSK_GL \
-DSK_CODEC_DECODES_JPEG \
-DSK_CODEC_DECODES_PNG \
-DSK_CODEC_DECODES_WEBP \
-DSK_HAS_WUFFS_LIBRARY \
-DSK_ENABLE_SKSL \
-DSK_ENABLE_PRECOMPILE \
-DSKNX_NO_SIMD \
-DSK_ASSUME_WEBGL=1 \
-DSK_USE_WEBGL \
-DSK_ENABLE_PARAGRAPH \
-DSK_UNICODE_AVAILABLE \
-DSK_UNICODE_ICU_IMPLEMENTATION \
-DSK_ENABLE_SKOTTIE \
-DSK_ENABLE_SKOTTIE_SKSLEFFECT \
-DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 \
--pre-js js/preamble.js \
--pre-js js/postamble.js \
-I/tmp/skia \
-o out/renderer.js \
/tmp/skia/out/wasm/modules/canvaskit/fonts/NotoMono-Regular.ttf.ninja.cpp \
/tmp/skia/out/wasm/libskia.a \
src/main.cpp

View File

@@ -0,0 +1,49 @@
# Renderer
## How this works?
First of all we need a proper environment to build Skia, this
environment is heavily based on the [Skia docker image](https://github.com/google/skia/blob/main/docker/skia-release/Dockerfile) but with some tweaks to support building
a C++ WebAssembly module using [Emscripten](https://emscripten.org/index.html).
## Building everything
From the root directory of `frontend/renderer` just run:
```sh
./build
```
This is going to build the docker image and run the container to build
the artifacts and then copy them to the necessary directories.
> :smile_cat: Be patient, the first time the docker image is built usually takes
> a few minutes.
## Building the Skia build tools Docker image
To build just the Skia build tools image:
```sh
cd frontend/renderer
docker build . -t skia-build-tools
```
## Building the renderer WebAssembly module
Just run the container and it will generate all the necessary
artifacts in the `out` directory.
```sh
cd frontend/renderer
docker run -t -v ${PWD}:/tmp/renderer skia-build-tools
```
Once the `renderer.js` and `renderer.wasm` are created in the `out` directory
we need to move them where Penpot can have access to them, so we need to execute
`./scripts/copy-artifacts`.
## C++ <-> JS
To add some extra functionality to the exported `Module` by the Emscripten
compiler, we use a series of javascript scripts that exist on the `js` directory.

View File

@@ -0,0 +1,11 @@
# TO DO
- [x] Compile Skia.
- [x] Compile simple `renderer.wasm` with a exported function.
- [ ] Compile a `renderer.wasm` that uses a WebGL context.
## Notes
- I've used the Skia `main` branch and it looks that there's something missing from the last release (`chrome/m117`) so I tried to switch to that branch but now I have different issues.
- It is necessary to use the GL emscripten module to deal with WebGL contexts. See `js/preamble.js` and

4
frontend/render_v2/cpp/build Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
docker build . -t skia-build-tools
docker run -t -v $PWD:/tmp/renderer skia-build-tools
./scripts/copy-artifacts

View File

@@ -0,0 +1,4 @@
#!/bin/bash
source /root/.bashrc
cd /tmp/renderer
emmake make

View File

@@ -0,0 +1,2 @@
console.log("postamble");
}(Module));

View File

@@ -0,0 +1,69 @@
// Adds compile-time JS functions to augment Renderer interface.
(function (Renderer) {
console.log("preamble", Renderer);
const LCG_MULTIPLIER = 1103515245;
const LCG_INCREMENT = 12345;
const LCG_MODULUS = Math.pow(2, 31);
const LCG_MASK = (LCG_MODULUS - 1);
function lcg(x, a, c, m) {
return (x * a + c) % m;
}
class Random {
constructor(seed) {
this._seed = seed;
}
value() {
this._seed = lcg(this._seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
return (this._seed & LCG_MASK) / LCG_MODULUS;
}
}
const random = new Random(0)
// Sets canvas.
Renderer.setCanvas = function setCanvas(canvas, attrs) {
const context = GL.createContext(canvas, attrs);
if (!context) {
throw new Error('Could not create a new WebGL context')
}
GL.makeContextCurrent(context);
// Emscripten does not enable this by default and Skia needs this
// to handle certain GPU corner cases.
GL.currentContext.GLctx.getExtension('WEBGL_debug_renderer_info');
// Initializes everything needed.
this._InitCanvas(canvas.width, canvas.height);
};
Renderer.setObjects = function setObjects(vbox, zoom, objects) {
// this._SetObjects(objects.cnt);
const numObjects = 20_000;
this._SetObjects(numObjects);
for (let index = 0; index < numObjects; index++) {
// const object = objects.arr[index * 2 + 1];
this._SetObjectRect(
index,
// object.selrect.x,
random.value() * 2000,
// object.selrect.y,
random.value() * 2000,
// object.selrect.width,
random.value() * 200,
// object.selrect.height,
random.value() * 200
);
}
};
Renderer.drawCanvas = function drawCanvas(vbox, zoom, objects) {
performance.mark('draw-canvas:start');
this._DrawCanvas(vbox.x, vbox.y, zoom);
performance.mark('draw-canvas:end');
const { duration } = performance.measure('draw-canvas', 'draw-canvas:start', 'draw-canvas:end');
console.log('draw-canvas', `${duration}ms`);
};

View File

@@ -0,0 +1,11 @@
#!/bin/bash
mkdir -p ../../resources/public/js/render_v2/cpp
mkdir -p ../../src/app/render_v2/
# FIXME: This is a VERY HACKY way to set the correct `scriptDirectory` but
# I didn't find a better way yet.
PREAMBLE_LINES=`wc -l js/preamble.js | egrep -o [0-9]+`
POSTAMBLE_LINES=`wc -l js/postamble.js | egrep -o [0-9]+`
LINE_NUMBER=`echo "200 + ${PREAMBLE_LINES} + ${POSTAMBLE_LINES}" | bc | egrep -o [0-9]+`
sed "${LINE_NUMBER} i \ \ scriptDirectory += 'js/render_v2/cpp/';" out/renderer.js > ../../src/app/render_v2/cpp.js
cp out/renderer.wasm ../../resources/public/js/render_v2/cpp

View File

@@ -0,0 +1,275 @@
#include "include/android/SkAnimatedImage.h"
#include "include/codec/SkAndroidCodec.h"
#include "include/codec/SkEncodedImageFormat.h"
#include "include/core/SkBBHFactory.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkBlender.h"
#include "include/core/SkBlurTypes.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorFilter.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkImageGenerator.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkM44.h"
#include "include/core/SkMaskFilter.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPath.h"
#include "include/core/SkPathEffect.h"
#include "include/core/SkPathMeasure.h"
#include "include/core/SkPathUtils.h"
#include "include/core/SkPicture.h"
#include "include/core/SkPictureRecorder.h"
#include "include/core/SkPoint3.h"
#include "include/core/SkRRect.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSerialProcs.h"
#include "include/core/SkShader.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkStrokeRec.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTextBlob.h"
#include "include/core/SkTypeface.h"
#include "include/core/SkTypes.h"
#include "include/core/SkVertices.h"
#include "include/effects/Sk1DPathEffect.h"
#include "include/effects/Sk2DPathEffect.h"
#include "include/effects/SkCornerPathEffect.h"
#include "include/effects/SkDashPathEffect.h"
#include "include/effects/SkDiscretePathEffect.h"
#include "include/effects/SkGradientShader.h"
#include "include/effects/SkImageFilters.h"
#include "include/effects/SkLumaColorFilter.h"
#include "include/effects/SkPerlinNoiseShader.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/effects/SkTrimPathEffect.h"
#include "include/encode/SkJpegEncoder.h"
#include "include/encode/SkPngEncoder.h"
#include "include/encode/SkWebpEncoder.h"
#include "include/private/SkShadowFlags.h"
#include "include/utils/SkParsePath.h"
#include "include/utils/SkShadowUtils.h"
#include "src/core/SkPathPriv.h"
#include "src/core/SkResourceCache.h"
#include "src/image/SkImage_Base.h"
#include "src/sksl/SkSLCompiler.h"
#include "modules/canvaskit/WasmCommon.h"
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/html5.h>
#if defined(CK_ENABLE_WEBGL) || defined(CK_ENABLE_WEBGPU)
#define ENABLE_GPU
#endif
#ifdef ENABLE_GPU
#include "include/gpu/GpuTypes.h"
#include "include/gpu/GrDirectContext.h"
#include "include/gpu/ganesh/GrExternalTextureGenerator.h"
#include "include/gpu/ganesh/SkImageGanesh.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "src/gpu/ganesh/GrCaps.h"
#endif // ENABLE_GPU
#ifdef CK_ENABLE_WEBGL
#include "include/gpu/GrBackendSurface.h"
#include "include/gpu/GrTypes.h"
#include "include/gpu/ganesh/gl/GrGLBackendSurface.h"
#include "include/gpu/gl/GrGLInterface.h"
#include "include/gpu/gl/GrGLTypes.h"
#include "src/gpu/RefCntedCallback.h"
#include "src/gpu/ganesh/GrProxyProvider.h"
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
#include "src/gpu/ganesh/gl/GrGLDefines.h"
#include <webgl/webgl1.h>
#endif // CK_ENABLE_WEBGL
#ifdef CK_ENABLE_WEBGPU
#include <emscripten/html5_webgpu.h>
#include <webgpu/webgpu.h>
#include <webgpu/webgpu_cpp.h>
#endif // CK_ENABLE_WEBGPU
#ifndef CK_NO_FONTS
#include "include/core/SkFont.h"
#include "include/core/SkFontMetrics.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontTypes.h"
#ifdef CK_INCLUDE_PARAGRAPH
#include "modules/skparagraph/include/Paragraph.h"
#endif // CK_INCLUDE_PARAGRAPH
#endif // CK_NO_FONTS
#ifdef CK_INCLUDE_PATHOPS
#include "include/pathops/SkPathOps.h"
#endif
#if defined(CK_INCLUDE_RUNTIME_EFFECT) && defined(SKSL_ENABLE_TRACING)
#include "include/sksl/SkSLDebugTrace.h"
#endif
#ifndef CK_NO_FONTS
#include "include/ports/SkFontMgr_data.h"
#endif
// Global data needed to keep everything in place.
sk_sp<GrDirectContext> context = nullptr;
sk_sp<SkSurface> surface = nullptr;
SkCanvas *canvas = nullptr;
struct PenpotRect {
float x, y, width, height;
};
struct PenpotColor {
float r, g, b, a;
};
struct PenpotObject {
PenpotRect selRect;
};
std::vector<PenpotObject> objects(0);
constexpr uint32_t LCG_MULTIPLIER = 1103515245;
constexpr uint32_t LCG_INCREMENT = 12345;
constexpr uint32_t LCG_MODULUS = 0x80000000;
constexpr uint32_t LCG_MASK = 0x7FFFFFFF;
uint32_t lcg(uint32_t x, uint32_t a, uint32_t c, uint32_t m)
{
return (x * a + c) % m;
}
class Random {
private:
uint32_t _seed;
public:
Random(uint32_t seed) : _seed(seed) {};
void reset(uint32_t new_seed) {
_seed = new_seed;
}
float value() {
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
return (float)(_seed & LCG_MASK) / (float)LCG_MODULUS;
}
uint8_t byte() {
_seed = lcg(_seed, LCG_MULTIPLIER, LCG_INCREMENT, LCG_MODULUS);
return (_seed & LCG_MASK) % 0xFF;
}
};
Random lcg_random(0);
// Initializes all the structures and elements needed to start rendering things.
void InitCanvas(int width, int height)
{
emscripten_log(EM_LOG_CONSOLE, "Initializing canvas %d %d", width, height);
// We assume that any calls we make to GL for the remainder of this function will go to the
// desired WebGL Context.
// setup interface.
auto interface = GrGLMakeNativeInterface();
// setup context.
context = GrDirectContext::MakeGL(interface);
emscripten_log(EM_LOG_CONSOLE, "GL context initialized");
GrGLint sampleCnt = 0;
GrGLint stencil = 16;
// WebGL should already be clearing the color and stencil buffers, but do it again here to
// ensure Skia receives them in the expected state.
emscripten_glBindFramebuffer(GL_FRAMEBUFFER, 0);
emscripten_glClearColor(0, 0, 0, 0);
emscripten_glClearStencil(0);
emscripten_glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
context->resetContext(kRenderTarget_GrGLBackendState | kMisc_GrGLBackendState);
// The on-screen canvas is FBO 0. Wrap it in a Skia render target so Skia can render to it.
GrGLFramebufferInfo info;
info.fFBOID = 0;
// Create the colorspace needed to represent graphics.
sk_sp<SkColorSpace> colorSpace = SkColorSpace::MakeSRGB();
info.fFormat = GR_GL_RGBA8; // kRGBA_8888_SkColorType;
auto target = GrBackendRenderTargets::MakeGL(
width,
height,
sampleCnt,
stencil,
info
);
emscripten_log(EM_LOG_CONSOLE, "Creating new surface");
sk_sp<SkSurface> new_surface(
SkSurfaces::WrapBackendRenderTarget(
context.get(),
target,
kBottomLeft_GrSurfaceOrigin,
kRGBA_8888_SkColorType,
colorSpace,
nullptr));
surface = new_surface;
canvas = surface->getCanvas();
emscripten_log(EM_LOG_CONSOLE, "Everything's ready!");
}
void DrawCanvas(float x, float y, float zoom)
{
canvas->clear(SK_ColorTRANSPARENT);
canvas->save();
canvas->scale(zoom, zoom);
canvas->translate(-x, -y);
lcg_random.reset(0);
emscripten_log(EM_LOG_CONSOLE, "Clearing canvas");
for (auto object : objects) {
// emscripten_log(EM_LOG_CONSOLE, "Drawing object");
SkPaint paint;
paint.setARGB(255, lcg_random.byte(), lcg_random.byte(), lcg_random.byte());
paint.setStyle(SkPaint::Style::kFill_Style);
SkRect rect = SkRect::MakeXYWH(object.selRect.x, object.selRect.y, object.selRect.width, object.selRect.height);
canvas->drawRect(rect, paint);
}
canvas->restore();
emscripten_log(EM_LOG_CONSOLE, "Flushing and submitting");
skgpu::ganesh::FlushAndSubmit(surface);
}
void SetObjects(int num_objects) {
// emscripten_log(EM_LOG_CONSOLE, "Resizing objects vector capacity %d", num_objects);
objects.resize(num_objects);
}
void SetObjectRect(int index, float x, float y, float width, float height) {
// emscripten_log(EM_LOG_CONSOLE, "Setting object at %d %f %f %f %f", index, x, y, width, height);
objects[index].selRect.x = x;
objects[index].selRect.y = y;
objects[index].selRect.width = width;
objects[index].selRect.height = height;
}
EMSCRIPTEN_BINDINGS(Renderer)
{
function("_InitCanvas", InitCanvas);
function("_DrawCanvas", DrawCanvas);
function("_SetObjects", SetObjects);
function("_SetObjectRect", SetObjectRect);
}

11
frontend/render_v2/rs/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

664
frontend/render_v2/rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,664 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "bindgen"
version = "0.69.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
"which",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "cc"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "flate2"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "gl"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
dependencies = [
"gl_generator",
]
[[package]]
name = "gl_generator"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
dependencies = [
"khronos_api",
"log",
"xml-rs",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "khronos_api"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "prettyplease"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "render"
version = "0.1.0"
dependencies = [
"base64",
"gl",
"skia-safe",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "skia-bindings"
version = "0.78.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29880a81b088de322e9c5306236c70761a61b5fa4df3c15c93bad3ce890ce34c"
dependencies = [
"bindgen",
"cc",
"flate2",
"heck",
"lazy_static",
"regex",
"serde_json",
"tar",
"toml",
]
[[package]]
name = "skia-safe"
version = "0.78.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f15700ac678c06649077495acbba07e7ae01e5ca46b7dc18213f2c3477ada71"
dependencies = [
"bitflags",
"lazy_static",
"skia-bindings",
]
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tar"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]
[[package]]
name = "xattr"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
dependencies = [
"libc",
"linux-raw-sys",
"rustix",
]
[[package]]
name = "xml-rs"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"

View File

@@ -0,0 +1,22 @@
[package]
name = "render"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/penpot/penpot"
license-file = "../../../../LICENSE"
description = "Wasm-based canvas render for Penpot"
[features]
default = ["skia-safe/gl", "skia-safe/textlayout"]
[[bin]]
name = "render_v2"
path = "src/main.rs"
[dependencies]
gl = "0.14.0"
skia-safe = "0.78.2"
base64 = "0.13"
[profile.release]
opt-level = "s"

6
frontend/render_v2/rs/build.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
EMSDK_QUIET=1 . "/usr/local/emsdk/emsdk_env.sh"
EMCC_CFLAGS="--no-entry -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s MAX_WEBGL_VERSION=2 -s MODULARIZE=1 -s EXPORT_NAME=createRustSkiaModule -s EXPORTED_RUNTIME_METHODS=GL -s ENVIRONMENT=web" cargo build --target=wasm32-unknown-emscripten

View File

Binary file not shown.

View File

@@ -0,0 +1,362 @@
// use skia_safe::{
// gpu::{self, gl::FramebufferInfo, DirectContext},
// textlayout::{
// FontCollection, ParagraphBuilder, ParagraphStyle, TextStyle, TypefaceFontProvider,
// },
// Canvas, Data, EncodedImageFormat, FontMgr, Paint, PaintStyle, Path, SurfaceProps,
// };
use skia_safe::{
gpu::{self, gl::FramebufferInfo, DirectContext},
textlayout::TypefaceFontProvider,
PaintStyle,
};
use std::boxed::Box;
use skia_safe as skia;
static ROBOTO_REGULAR: &[u8] = include_bytes!("RobotoMono-Regular.ttf");
static TYPEFACE_ALIAS: &str = "roboto-regular";
extern "C" {
pub fn emscripten_GetProcAddress(
name: *const ::std::os::raw::c_char,
) -> *const ::std::os::raw::c_void;
}
struct GpuState {
context: DirectContext,
framebuffer_info: FramebufferInfo,
}
/// This struct holds the state of the Rust application between JS calls.
///
/// It is created by [init] and passed to the other exported functions. Note that rust-skia data
/// structures are not thread safe, so a state must not be shared between different Web Workers.
pub struct State {
gpu_state: GpuState,
surface: skia::Surface,
typeface_font_provider: TypefaceFontProvider,
default_font: skia_safe::Font,
}
impl State {
fn new(
gpu_state: GpuState,
surface: skia::Surface,
typeface_font_provider: TypefaceFontProvider,
default_font: skia_safe::Font,
) -> Self {
State {
gpu_state,
surface,
typeface_font_provider,
default_font,
}
}
fn set_surface(&mut self, surface: skia::Surface) {
self.surface = surface;
}
}
fn init_gl() {
unsafe {
gl::load_with(|addr| {
let addr = std::ffi::CString::new(addr).unwrap();
emscripten_GetProcAddress(addr.into_raw() as *const _) as *const _
});
}
}
/// This needs to be done once per WebGL context.
fn create_gpu_state() -> GpuState {
let interface = skia_safe::gpu::gl::Interface::new_native().unwrap();
let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap();
let framebuffer_info = {
let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo {
fboid: fboid.try_into().unwrap(),
format: skia_safe::gpu::gl::Format::RGBA8.into(),
protected: skia_safe::gpu::Protected::No,
}
};
GpuState {
context,
framebuffer_info,
}
}
/// Create the Skia surface that will be used for rendering.
fn create_surface(gpu_state: &mut GpuState, width: i32, height: i32) -> skia::Surface {
let backend_render_target =
gpu::backend_render_targets::make_gl((width, height), 1, 8, gpu_state.framebuffer_info);
gpu::surfaces::wrap_backend_render_target(
&mut gpu_state.context,
&backend_render_target,
skia_safe::gpu::SurfaceOrigin::BottomLeft,
skia_safe::ColorType::RGBA8888,
None,
None,
)
.unwrap()
}
fn render_rect(surface: &mut skia::Surface, rect: skia::Rect, color: skia::Color) {
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Fill);
paint.set_color(color);
// paint.set_anti_alias(true);
surface.canvas().draw_rect(rect, &paint);
}
fn render_rect_ref(surface: &mut skia::Surface, rect: &skia::Rect, paint: &skia::Paint) {
surface.canvas().draw_rect(rect, paint);
}
/// This is called from JS after the WebGL context has been created.
#[no_mangle]
pub extern "C" fn init(width: i32, height: i32) -> Box<State> {
let mut gpu_state = create_gpu_state();
let surface = create_surface(&mut gpu_state, width, height);
// skia_safe::Font::default() is empty, let's use something better
let font_mgr = skia_safe::FontMgr::new();
let typeface = font_mgr
.new_from_data(ROBOTO_REGULAR, None)
.expect("Failed to load ROBOTO font");
let default_font = skia_safe::Font::new(typeface.clone(), 10.0);
let typeface_font_provider = {
let mut typeface_font_provider = TypefaceFontProvider::new();
// We need a system font manager to be able to load typefaces.
typeface_font_provider.register_typeface(typeface, TYPEFACE_ALIAS);
typeface_font_provider
};
let state = State::new(gpu_state, surface, typeface_font_provider, default_font);
Box::new(state)
}
/// This is called from JS when the window is resized.
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn resize_surface(state: *mut State, width: i32, height: i32) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
let surface = create_surface(&mut state.gpu_state, width, height);
state.set_surface(surface);
}
#[repr(C)]
pub struct Color {
r: u8,
g: u8,
b: u8,
a: f32,
}
#[repr(C)]
pub struct Rect {
left: f32,
top: f32,
right: f32,
bottom: f32,
r: f32,
g: f32,
b: f32,
a: f32,
}
#[no_mangle]
pub extern "C" fn alloc_rects(len: usize) -> *mut Rect {
// create a new mutable buffer with capacity `len`
let mut buf: Vec<Rect> = Vec::with_capacity(len);
let ptr = buf.as_mut_ptr();
// take ownership of the memory block and ensure the its destructor is not
// called when the object goes out of scope at the end of the function
std::mem::forget(buf);
return ptr;
}
#[no_mangle]
pub unsafe fn free_rects(ptr: *mut Rect, len: usize) {
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
std::mem::forget(buf);
}
/// Draws a rect at the specified coordinates with the give ncolor
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn draw_rect(state: *mut State, rect: &Rect, color: &Color) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
let color = skia::Color::from_argb((color.a * 255.0) as u8, color.r, color.g, color.b);
render_rect(&mut state.surface, r, color);
}
#[no_mangle]
pub unsafe extern "C" fn flush(state: *mut State) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
state
.gpu_state
.context
.flush_and_submit_surface(&mut state.surface, None);
}
#[no_mangle]
pub unsafe extern "C" fn translate(state: *mut State, dx: f32, dy: f32) {
(*state).surface.canvas().translate((dx, dy));
}
#[no_mangle]
pub unsafe extern "C" fn scale(state: *mut State, sx: f32, sy: f32) {
(*state).surface.canvas().scale((sx, sy));
}
#[no_mangle]
pub unsafe extern "C" fn reset_canvas(state: *mut State) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
state.surface.canvas().clear(skia_safe::Color::TRANSPARENT);
state.surface.canvas().reset_matrix();
flush(state);
}
#[no_mangle]
pub unsafe extern "C" fn draw_shapes(
state: *mut State,
ptr: *mut Rect,
len: usize,
zoom: f32,
dx: f32,
dy: f32,
) {
let state = unsafe { state.as_mut() }.expect("got an invalid state pointer");
reset_canvas(state);
scale(state, zoom, zoom);
translate(state, dx, dy);
// create a `Vec<Rect>` from the pointer to the linear memory and length
let buf = Vec::<Rect>::from_raw_parts(ptr, len, len);
// let mut text_paint = skia::Paint::default();
// text_paint.set_anti_alias(true);
// text_paint.set_style(skia_safe::paint::Style::StrokeAndFill);
// text_paint.set_stroke_width(1.0);
// let mut path_paint = skia::Paint::default();
// path_paint.set_color(skia_safe::Color::BLACK);
// path_paint.set_anti_alias(true);
// path_paint.set_stroke_width(1.0);
// path_paint.set_style(PaintStyle::Stroke);
// let svg_canvas = skia_safe::svg::Canvas::new(skia_safe::Rect::from_size((10000, 10000)), None);
// let mut memory = Vec::new();
// let mut document =
// skia_safe::pdf::new_document(&mut memory, None).begin_page((10000, 10000), None);
// // let pdf_canvas = document.canvas();
for rect in buf.iter() {
let r = skia::Rect::new(rect.left, rect.top, rect.right, rect.bottom);
let color = skia::Color::from_argb(
(rect.a * 255.0) as u8,
rect.r as u8,
rect.g as u8,
rect.b as u8,
);
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Fill);
paint.set_color(color);
// render_rect_ref(&mut state.surface, &r, &paint);
state.surface.canvas().draw_rect(&r, &paint);
// render_rect(&mut state.surface, r, color);
// paint.set_anti_alias(true);
// state.surface.canvas().draw_rect(r, &paint);
// state.surface.canvas().draw_text_align(
// String::from("Lorem ipsum"),
// (rect.left, rect.top),
// &state.default_font,
// &paint,
// skia::utils::text_utils::Align::Left,
// );
// let mut paint = skia::Paint::default();
// paint.set_style(skia::PaintStyle::Fill);
// paint.set_color(color);
// paint.set_anti_alias(true);
// svg_canvas.draw_rect(r, &paint);
// pdf_canvas.draw_rect(r, &paint);
// text_paint.set_color(color);
// state.surface.canvas().draw_str(
// "SKIA TEXT",
// (rect.left, rect.top),
// &state.default_font,
// &text_paint,
// );
// svg_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
// pdf_canvas.draw_str("SKIA TEXT", (rect.left, rect.top), &state.default_font, &text_paint);
// let mut path = Path::new();
// path.move_to((rect.left, rect.top));
// path.line_to((rect.right, rect.bottom));
// state.surface.canvas().draw_path(&path, &path_paint);
// svg_canvas.draw_path(&path, &path_paint);
// pdf_canvas.draw_path(&path, &path_paint);
// https://github.com/rust-skia/rust-skia/blob/02c89a87649af8d2870fb631aae4a5e171887367/skia-org/src/skparagraph_example.rs#L18
// let mut font_collection = FontCollection::new();
// font_collection
// .set_default_font_manager(Some(state.typeface_font_provider.clone().into()), None);
// let paragraph_style = ParagraphStyle::new();
// let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
// let mut ts = TextStyle::new();
// ts.set_foreground_paint(&Paint::default())
// .set_font_families(&[TYPEFACE_ALIAS]);
// paragraph_builder.push_style(&ts);
// paragraph_builder.add_text("Other skia text");
// let mut paragraph = paragraph_builder.build();
// paragraph.layout(256.0);
// paragraph.paint(state.surface.canvas(), (rect.left, rect.top));
// paragraph.paint(&svg_canvas, (rect.left, rect.top));
}
/*
// base64 image of the canvas
let image = state.surface.image_snapshot();
let mut context = state.surface.direct_context();
let encoded_image = image.encode(context.as_mut(), EncodedImageFormat::PNG, None).unwrap();
let base64_image = base64::encode(&encoded_image.as_bytes());
println!("data:image/png;base64,{}", base64_image);
// SVG representation
let svg_data = svg_canvas.end();
let svg = String::from_utf8_lossy(svg_data.as_bytes());
println!("svg: {}", svg.replace('\n', ""));
// PDF
document.end_page().close();
println!("PDF: ");
print!("echo ");
for byte in &memory {
print!("{:02x}", byte);
}
println!("| xxd -r -p > output.pdf");
*/
flush(state);
std::mem::forget(buf);
}
fn main() {
init_gl();
}

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

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Penpot - Challenge</title>
<link rel="icon" href="images/favicon.png" />
<script>
var params = new URL(document.location.toString()).searchParams;
var redirectPath = params.get("redirect");
setTimeout(() => {
location.href = "/#" + redirectPath;
}, 100);
</script>
</head>
<body>
</body>
</html>

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);
}
@@ -415,6 +425,13 @@ async function generateTemplates() {
await fs.writeFile("./resources/public/index.html", content);
content = await renderTemplate(
"resources/templates/challenge.mustache",
{},
partials,
);
await fs.writeFile("./resources/public/challenge.html", content);
content = await renderTemplate(
"resources/templates/preview-body.mustache",
{

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

@@ -143,4 +143,3 @@
(reinit))))
(set! (.-stackTraceLimit js/Error) 50)

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]

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