mirror of
https://github.com/penpot/penpot.git
synced 2026-01-02 19:38:48 -05:00
Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e369b70aeb | ||
|
|
c3970255e6 | ||
|
|
7823eaf890 | ||
|
|
ec0079461e | ||
|
|
70a1a7a5ea | ||
|
|
c3dc165c4c | ||
|
|
5a3619c737 | ||
|
|
227f06c1ec | ||
|
|
946dac3c9f | ||
|
|
b160ba1793 | ||
|
|
33d51a51d1 | ||
|
|
ab4be85669 | ||
|
|
6c0dce580d | ||
|
|
59050a7bc6 | ||
|
|
3334fb0e99 | ||
|
|
24268bbf33 | ||
|
|
cd3f8f0c43 | ||
|
|
d3a8954605 | ||
|
|
1cda61e230 | ||
|
|
aca3e3db4f | ||
|
|
74f9166f3d | ||
|
|
977a2090fb | ||
|
|
14e6ea9393 | ||
|
|
3eb35f0aa6 | ||
|
|
92b7a35c58 | ||
|
|
99807b4cd4 | ||
|
|
bff415c7cd | ||
|
|
1d84835fd5 | ||
|
|
88296480ec | ||
|
|
4f5bc77379 | ||
|
|
3932054ea6 | ||
|
|
243fd17305 | ||
|
|
4b8febd7dc | ||
|
|
7c73e44ab8 | ||
|
|
e533762f33 | ||
|
|
40c118df55 | ||
|
|
f43fc282d3 | ||
|
|
8616e2f25c | ||
|
|
4299fd28f0 | ||
|
|
302ff92b31 | ||
|
|
b62cc9c8e9 | ||
|
|
225c2ca6e6 | ||
|
|
e5bdd852ca | ||
|
|
591788403a | ||
|
|
f1b82e289d | ||
|
|
f4ae8ea5ac | ||
|
|
d9310d651a | ||
|
|
6b817d102b | ||
|
|
08a9371322 | ||
|
|
f96da090d6 | ||
|
|
8d8f203b8a | ||
|
|
f40ffacfbd | ||
|
|
ae435f67a5 | ||
|
|
d89dfc5e30 | ||
|
|
cd586c81ee | ||
|
|
16e1e01234 | ||
|
|
fe6c9f24d3 | ||
|
|
fe314cf146 | ||
|
|
97a880c946 | ||
|
|
df66955594 | ||
|
|
07f055bd49 | ||
|
|
22d5b125bd | ||
|
|
ef3b4a5895 | ||
|
|
02611029fb | ||
|
|
14e4e6d6ea | ||
|
|
9170c70f2a | ||
|
|
83d8bf37a6 | ||
|
|
1fb21d537c | ||
|
|
ac80e9a1ac | ||
|
|
dbbb8e76ab | ||
|
|
916f055aec | ||
|
|
6d8c183160 | ||
|
|
9d2f484aa3 | ||
|
|
2dc0cfdee3 | ||
|
|
a25abd0ca4 | ||
|
|
3a9119cf29 | ||
|
|
c236e0765b | ||
|
|
f8fad95fef | ||
|
|
97ae295cb9 | ||
|
|
bd888dcde2 | ||
|
|
784274f8ae | ||
|
|
eda6c6a4c3 | ||
|
|
7d7594818c | ||
|
|
7cc8f67e24 | ||
|
|
87fc3bbb8e | ||
|
|
bbb2cc972f | ||
|
|
6a07e6ae01 | ||
|
|
87dfd2b3c8 | ||
|
|
b0bfb8006d | ||
|
|
d46274abf2 | ||
|
|
23f7889cff | ||
|
|
534659cdc6 | ||
|
|
1e68d4ec87 | ||
|
|
1779fd3e8b | ||
|
|
3c496ddd9d | ||
|
|
47bc9d8ef1 | ||
|
|
a3a5fe056d | ||
|
|
fbb3271c81 | ||
|
|
ecc93d9246 | ||
|
|
302672f5b0 | ||
|
|
4f16ea2d2d | ||
|
|
b7a0b7d629 | ||
|
|
bd6f1bef10 | ||
|
|
c4941bb102 | ||
|
|
b8a606a35f | ||
|
|
370eebeb64 | ||
|
|
35bcb082a0 | ||
|
|
dd220e228e | ||
|
|
7b63aa4a4f | ||
|
|
33a07346dd | ||
|
|
abd77559ab | ||
|
|
28878caca9 | ||
|
|
74f3379b5d | ||
|
|
379770343a | ||
|
|
6327286328 | ||
|
|
3a2677a91a | ||
|
|
fcd232aa35 | ||
|
|
f194e2c1c6 | ||
|
|
ea6731e22b | ||
|
|
002b1679c3 | ||
|
|
45f3a67950 | ||
|
|
c6917bb0cf | ||
|
|
f777845d14 | ||
|
|
a1f5bcae80 | ||
|
|
3e11b4aa74 | ||
|
|
4f48236fee | ||
|
|
ffadf29ad7 | ||
|
|
352efcb610 | ||
|
|
334e83479f | ||
|
|
476eedbd2c | ||
|
|
ae7e28b71b | ||
|
|
be30174a49 | ||
|
|
8373654f80 | ||
|
|
471c636580 | ||
|
|
635c6efe42 | ||
|
|
d570048f78 | ||
|
|
dcc49dafd3 | ||
|
|
7398f7ce0d | ||
|
|
76479a2486 | ||
|
|
31f62dcc12 | ||
|
|
3d7df5b005 | ||
|
|
c16a116707 | ||
|
|
f7f06f59ce | ||
|
|
d1277afee6 | ||
|
|
a510d01136 | ||
|
|
0e651df65f | ||
|
|
758e0458bc | ||
|
|
e18b4666ba | ||
|
|
864088eecd | ||
|
|
0b39318b33 | ||
|
|
d5a9961ec8 | ||
|
|
7dac7de365 | ||
|
|
dd0721e91e | ||
|
|
21fde2e991 | ||
|
|
ca1893164d | ||
|
|
b619ac3e08 | ||
|
|
d7eb86c86d | ||
|
|
6c4f216da8 | ||
|
|
f786a00e89 | ||
|
|
47cecb2ac4 | ||
|
|
5d6ceec803 | ||
|
|
bec11220e3 | ||
|
|
9b802e1c7d | ||
|
|
16cf16c422 | ||
|
|
4e1eee197e | ||
|
|
91c8af9e38 | ||
|
|
58593a9428 | ||
|
|
21aa8b0703 | ||
|
|
17cf57f7ca | ||
|
|
f7cfe36f37 | ||
|
|
c26f909565 | ||
|
|
6db7fe5f7b | ||
|
|
a207114d95 | ||
|
|
b8299a5ea5 | ||
|
|
1fa461e996 | ||
|
|
2e3745099b | ||
|
|
03ebeb0657 | ||
|
|
6892cffe54 | ||
|
|
19a613e90c | ||
|
|
7fe95f218b | ||
|
|
a1fc785771 | ||
|
|
e0034dc205 | ||
|
|
bd9eab08b7 | ||
|
|
b5121657ee | ||
|
|
4f04dbc294 | ||
|
|
2b2a84da64 | ||
|
|
21dd9a260c | ||
|
|
7b9b5bafc1 | ||
|
|
41ebba6ce0 | ||
|
|
61446592b3 | ||
|
|
b82c6326cf | ||
|
|
a2f466810b | ||
|
|
1bd1782d66 | ||
|
|
ea6a1c05fa | ||
|
|
4f84e77b10 | ||
|
|
fa75a3539f | ||
|
|
fa12d9785a | ||
|
|
c578e31ae2 | ||
|
|
749c369080 | ||
|
|
4ad4057878 | ||
|
|
2dea0b52ed | ||
|
|
ca257d1caf | ||
|
|
e164692391 | ||
|
|
b58edea544 | ||
|
|
9a587c91a8 | ||
|
|
7590a7ce4d | ||
|
|
aae1571a5c | ||
|
|
ebaf30727c | ||
|
|
884ceb052b | ||
|
|
cc7ed497e8 | ||
|
|
cd6a739abb | ||
|
|
f0cecfd517 | ||
|
|
f5f255e2d5 | ||
|
|
5ffa56be3d | ||
|
|
e65c0d9f48 | ||
|
|
076cb0e35b | ||
|
|
2a90ca6546 | ||
|
|
a26deafa75 | ||
|
|
cf705e352b | ||
|
|
86c5ca4213 | ||
|
|
179d534237 | ||
|
|
162507264c | ||
|
|
7e0a8b6227 | ||
|
|
b50fcee079 | ||
|
|
9bca42c14a | ||
|
|
475d14edf4 | ||
|
|
214733c880 | ||
|
|
979828ffe3 | ||
|
|
65bb795199 | ||
|
|
a0546b2e63 | ||
|
|
d6f6d78b1e | ||
|
|
8c1fba5160 | ||
|
|
fb39dd5440 | ||
|
|
dd0c5b7806 | ||
|
|
9e94cf7b99 | ||
|
|
b882b9e283 | ||
|
|
cdcff62232 | ||
|
|
c8caca77a3 | ||
|
|
f291125377 | ||
|
|
0ce981a68c | ||
|
|
a8814dcaba | ||
|
|
229eeae6db | ||
|
|
d03788af93 | ||
|
|
017aad6454 | ||
|
|
042b3a71d8 | ||
|
|
767ec37b83 | ||
|
|
89f64e0c49 | ||
|
|
eadae5e2cd | ||
|
|
d108ad904e | ||
|
|
6564736d3e | ||
|
|
7f9c4df284 | ||
|
|
d01cd70c6b | ||
|
|
ea7768117c | ||
|
|
5bfb39cdf6 | ||
|
|
29f1c2bdad | ||
|
|
e79f9ba40f | ||
|
|
452aabdec6 | ||
|
|
9e3f8e7827 | ||
|
|
3a4e9ccc5a | ||
|
|
860e32d965 | ||
|
|
495f9dfa84 | ||
|
|
133ca33cb5 | ||
|
|
1c69a9fd8a | ||
|
|
15faa57e01 | ||
|
|
f5510234cf | ||
|
|
eb720b053a | ||
|
|
efc61241a0 | ||
|
|
c99102e49b | ||
|
|
cfad1d178f | ||
|
|
c24b2dadec | ||
|
|
5e89cd1cb3 | ||
|
|
bf202473e9 | ||
|
|
9a3b5337d7 | ||
|
|
396cbb27b2 | ||
|
|
b4e6f8bc73 | ||
|
|
d88f28f5c2 | ||
|
|
e36cf1d963 | ||
|
|
a0bb5e5ef3 | ||
|
|
34cc211912 | ||
|
|
e95713c1df | ||
|
|
e189dc965d | ||
|
|
53f580ad40 | ||
|
|
cf0045681e | ||
|
|
762a883b39 | ||
|
|
a63ded1ba1 | ||
|
|
f812b28892 | ||
|
|
873c9b1903 | ||
|
|
edeb16bc26 | ||
|
|
90d947391a | ||
|
|
47cc80a93f | ||
|
|
1f8cfde1cf | ||
|
|
5f2ec595cb | ||
|
|
37a6446e32 | ||
|
|
be84b1cb01 | ||
|
|
298db46722 | ||
|
|
0c6b0598fa | ||
|
|
f2a2d772b0 |
@@ -111,7 +111,7 @@ jobs:
|
||||
yarn run build:app:assets
|
||||
clojure -M:dev:shadow-cljs release main
|
||||
yarn playwright install --with-deps chromium
|
||||
yarn e2e:test
|
||||
yarn test:e2e
|
||||
|
||||
- run:
|
||||
name: "backend tests"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -74,3 +74,5 @@ node_modules
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/.yarn/*
|
||||
|
||||
89
CHANGES.md
89
CHANGES.md
@@ -1,11 +1,93 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.3.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix null pointer exception on number checking functions
|
||||
- Fix problem with grid layout ordering after moving [Taiga #9179](https://tree.taiga.io/project/penpot/issue/9179)
|
||||
|
||||
### :books: Documentation
|
||||
|
||||
- Add initial documentation for Kubernetes
|
||||
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected issue on interaction between plugins sandbox and
|
||||
internal impl of promise
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- **New plugin system.**
|
||||
|
||||
Penpot now supports custom plugins. Read everything about developing your plugins [HERE](https://help.penpot.app/plugins/)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- All our plugins beta testers :heart:.
|
||||
- Fix problem when translating multiple path points by @eeropic [#4459](https://github.com/penpot/penpot/issues/4459)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
|
||||
|
||||
This refactor adds better IME support, more performant text editing
|
||||
experience and a better clipboard support while keeping full
|
||||
retrocompatibility with previous editor.
|
||||
|
||||
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
- Fix problem with go back button on error page [Taiga #8887](https://tree.taiga.io/project/penpot/issue/8887)
|
||||
- Fix problem with shadows in text for Safari [Taiga #8770](https://tree.taiga.io/project/penpot/issue/8770)
|
||||
- Fix a regression with feedback form subject and content limits [Taiga #8908](https://tree.taiga.io/project/penpot/issue/8908)
|
||||
- Fix problem with stroke and filter ordering in frames [Github #5058](https://github.com/penpot/penpot/issues/5058)
|
||||
- Fix problem with hover layers when hidden/blocked [Github #5074](https://github.com/penpot/penpot/issues/5074)
|
||||
- Fix problem with precision on boolean calculation [Taiga #8482](https://tree.taiga.io/project/penpot/issue/8482)
|
||||
- Fix problem when translating multiple path points [Github #4459](https://github.com/penpot/penpot/issues/4459)
|
||||
- Fix problem on importing (and exporting) files with flows [Taiga #8914](https://tree.taiga.io/project/penpot/issue/8914)
|
||||
- Fix Internal Error page: "go to your penpot" wrong design [Taiga #8922](https://tree.taiga.io/project/penpot/issue/8922)
|
||||
- Fix problem updating layout when toggle visibility in component copy [Github #5143](https://github.com/penpot/penpot/issues/5143)
|
||||
- Fix "Done" button on toolbar on inspect mode should go to design mode [Taiga #8933](https://tree.taiga.io/project/penpot/issue/8933)
|
||||
- Fix problem with shortcuts in text editor [Github #5078](https://github.com/penpot/penpot/issues/5078)
|
||||
- Fix problems with show in viewer and interactions [Github #4868](https://github.com/penpot/penpot/issues/4868)
|
||||
- Add visual feedback when moving an element into a board [Github #3210](https://github.com/penpot/penpot/issues/3210)
|
||||
- Fix percent calculation on grid layout tracks [Github #4688](https://github.com/penpot/penpot/issues/4688)
|
||||
- Fix problem with caps and inner shadows [Github #4517](https://github.com/penpot/penpot/issues/4517)
|
||||
- Fix problem with horizontal/vertical lines and shadows [Github #4516](https://github.com/penpot/penpot/issues/4516)
|
||||
- Fix problem with layers overflowing panel [Taiga #9021](https://tree.taiga.io/project/penpot/issue/9021)
|
||||
- Fix in workspace you can manage rulers on view mode [Taiga #8966](https://tree.taiga.io/project/penpot/issue/8966)
|
||||
- Fix problem with swap components in grid layout [Taiga #9066](https://tree.taiga.io/project/penpot/issue/9066)
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
|
||||
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
|
||||
- Add limits for invitation RPC methods (hard limit 25 emails per request)
|
||||
|
||||
## 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)
|
||||
@@ -81,6 +163,11 @@
|
||||
- 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
|
||||
|
||||
@@ -127,7 +214,7 @@
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
### :heart: Communityq contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -8,10 +8,12 @@
|
||||
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
|
||||
</picture>
|
||||
|
||||
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||
<p align="center">
|
||||
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img alt="Gitter" src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
@@ -58,6 +60,9 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
|
||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||
|
||||
### Plugin system ###
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
|
||||
### Designed for developers ###
|
||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,29 +171,43 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We received a request to change your current email to {{ pending-email }}.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We received a request to change your current email to {{ pending-email }}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Click to the link below to confirm the change:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Click to the link below to confirm the change:</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Confirm email change </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Confirm email change </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -194,17 +215,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, please consider changing your password for security reasons.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, please consider changing your password for security
|
||||
reasons.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -221,258 +249,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
323
backend/resources/app/email/includes/footer.html
Normal file
323
backend/resources/app/email/includes/footer.html
Normal file
@@ -0,0 +1,323 @@
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for
|
||||
cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://x.com/penpotapp" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-x.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.linkedin.com/company/penpotdesign/"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-linkedin.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://fosstodon.org/@penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-mastodon.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,36 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Accept invite </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Accept invite </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +208,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +234,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -235,283 +235,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We have received a request to reset your password. Click the link below to choose a new one:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We have received a request to reset your password. Click the link below to choose a new one:
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Reset password </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Reset password </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,17 +209,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, you can safely ignore it. Your password won't be changed.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, you can safely ignore it. Your password won't be changed.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -216,258 +243,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the link below and
|
||||
get started building mockups and prototypes today!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Verify email </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Verify email </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +209,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +235,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -245,283 +245,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -268,283 +268,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -285,283 +285,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -243,283 +243,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,42 +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/refs/heads/main/Wireframing%20kit%20v1.1.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/refs/heads/main/Prototype%20examples%20v1.1.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/refs/heads/main/Penpot%20-%20Design%20System%20v2.1.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/refs/heads/main/Flex%20Layout%20Playground%20v2.0.penpot"}
|
||||
{:id "welcome"
|
||||
:name "Welcome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/welcome.penpot"}]
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]
|
||||
|
||||
@@ -7,7 +7,7 @@ Debug Main Page
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>ADMIN DEBUG INTERFACE</h1>
|
||||
<h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="dashboard">
|
||||
|
||||
@@ -23,6 +23,7 @@ export PENPOT_FLAGS="\
|
||||
enable-urepl-server \
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-quotes \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
@@ -67,6 +68,7 @@ export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
|
||||
@@ -17,6 +17,7 @@ export PENPOT_FLAGS="\
|
||||
disable-secure-session-cookies \
|
||||
enable-rpc-climit \
|
||||
enable-smtp \
|
||||
enable-quotes \
|
||||
enable-file-snapshot \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
|
||||
@@ -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)})]
|
||||
|
||||
@@ -315,15 +315,13 @@
|
||||
(l/dbg :hint "sendmail"
|
||||
:id (:id params)
|
||||
:to (:to params)
|
||||
:subject (str/trim (:subject params))
|
||||
:body (str/join "," (map :type (:body params))))
|
||||
:subject (str/trim (:subject params)))
|
||||
|
||||
(.sendMessage ^Transport transport
|
||||
^MimeMessage message
|
||||
(.getAllRecipients message))))))
|
||||
|
||||
(when (or (contains? cf/flags :log-emails)
|
||||
(not (contains? cf/flags :smtp)))
|
||||
(when (contains? cf/flags :log-emails)
|
||||
(send-to-logger! cfg params))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/html"}
|
||||
::rres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {}))})
|
||||
(tmpl/render {:version (:full cf/version)}))})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FILE CHANGES
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
(try
|
||||
(let [result (handler)]
|
||||
(events/tap :end result))
|
||||
|
||||
(catch java.io.EOFException cause
|
||||
(events/tap :error (errors/handle' cause request)))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected error on processing sse response"
|
||||
:cause cause)
|
||||
|
||||
@@ -278,18 +278,18 @@
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
(def ^:private schema:params
|
||||
(sm/define
|
||||
[:map {:title "params"}
|
||||
[:session-id ::sm/uuid]]))
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [{:keys [session-id]} (sm/conform! schema:params params)]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
:hint "missing or invalid session-id found"))
|
||||
|
||||
(cond
|
||||
(not profile-id)
|
||||
(ex/raise :type :authentication
|
||||
:hint "Authentication required.")
|
||||
:hint "authentication required")
|
||||
|
||||
;; WORKAROUND: we use the adapter specific predicate for
|
||||
;; performance reasons; for now, the ring default impl for
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
|
||||
|
||||
(when-let [params (or (:request/params context) (:params context))]
|
||||
{:params (pp/pprint-str params :length 30 :level 12)})
|
||||
{:params (pp/pprint-str params :length 30 :level 13)})
|
||||
|
||||
(when-let [value (:value context)]
|
||||
{:value (pp/pprint-str value :length 30 :level 12)})
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
(l/dbg :hint "run webhook"
|
||||
:event-name (:name event)
|
||||
:webhook-id (:id whook)
|
||||
:webhook-id (str (:id whook))
|
||||
:webhook-uri (:uri whook)
|
||||
:webhook-mtype (:mtype whook))
|
||||
|
||||
|
||||
@@ -475,7 +475,8 @@
|
||||
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
|
||||
(cf/get :objects-storage-s3-bucket))
|
||||
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
|
||||
(cf/get :objects-storage-s3-io-threads))}
|
||||
(cf/get :objects-storage-s3-io-threads))
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
:app.storage.fs/backend
|
||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||
|
||||
@@ -149,6 +149,13 @@
|
||||
:hint "authentication required for this endpoint")
|
||||
(f cfg params)))))
|
||||
|
||||
(defn- wrap-db-transaction
|
||||
[_ f mdata]
|
||||
(if (::db/transaction mdata)
|
||||
(fn [cfg params]
|
||||
(db/tx-run! cfg f params))
|
||||
f))
|
||||
|
||||
(defn- wrap-audit
|
||||
[_ f mdata]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
@@ -196,6 +203,7 @@
|
||||
(defn- wrap-all
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
(wrap-db-transaction cfg $ mdata)
|
||||
(cond/wrap cfg $ mdata)
|
||||
(retry/wrap-retry cfg $ mdata)
|
||||
(climit/wrap cfg $ mdata)
|
||||
|
||||
@@ -30,18 +30,17 @@
|
||||
:tid token-id
|
||||
:iat created-at})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)]
|
||||
|
||||
(db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})))
|
||||
|
||||
expires-at (some-> expiration dt/in-future)
|
||||
token (db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})]
|
||||
(decode-row token)))
|
||||
|
||||
(defn repl:create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
@@ -60,14 +59,12 @@
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
(-> (create-access-token cfg profile-id name expiration)
|
||||
(decode-row)))))
|
||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
|
||||
(def ^:private schema:delete-access-token
|
||||
[:map {:title "delete-access-token"}
|
||||
|
||||
@@ -71,10 +71,15 @@
|
||||
[conn comment-id & {:as opts}]
|
||||
(db/get-by-id conn :comment comment-id opts))
|
||||
|
||||
(def ^:private sql:get-next-seqn
|
||||
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
|
||||
FROM file AS f
|
||||
WHERE f.id = ?
|
||||
FOR UPDATE")
|
||||
|
||||
(defn- get-next-seqn
|
||||
[conn file-id]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
@@ -292,7 +297,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]]])
|
||||
@@ -304,38 +309,43 @@
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
|
||||
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! cfg)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/project-id project-id)
|
||||
(assoc ::quotes/file-id file-id)
|
||||
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
|
||||
{::quotes/id ::quotes/comments-per-file}))
|
||||
|
||||
(create-comment-thread conn {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id})))))
|
||||
(let [params {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}
|
||||
thread (db/tx-run! cfg create-comment-thread params)]
|
||||
|
||||
(vary-meta thread assoc ::audit/props thread))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query
|
||||
;; because we need to lock the file for avoid race conditions
|
||||
|
||||
;; FIXME: this method touches and locks the file table,which
|
||||
;; is already heavy-update tablel; we need to think on move
|
||||
;; the sequence state management to a different table or
|
||||
;; different storage (example: redis) for alivate the update
|
||||
;; pression on the file table
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query because the whole
|
||||
;; operation can be retried on conflict, and in this case the new seq shold be
|
||||
;; retrieved from the database.
|
||||
seqn (get-next-seqn conn file-id)
|
||||
thread-id (uuid/next)
|
||||
thread (db/insert! conn :comment-thread
|
||||
@@ -364,7 +374,8 @@
|
||||
;; Optimistic update of current seq number on file.
|
||||
(db/update! conn :file
|
||||
{:comment-thread-seqn seqn}
|
||||
{:id file-id})
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(-> thread
|
||||
(select-keys [:id :file-id :page-id])
|
||||
@@ -387,7 +398,6 @@
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
|
||||
(def ^:private
|
||||
@@ -432,12 +442,11 @@
|
||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
(def ^:private schema:send-user-feedback
|
||||
[:map {:title "send-user-feedback"}
|
||||
[:subject [:string {:max 250}]]
|
||||
[:content [:string {:max 250}]]])
|
||||
[:subject [:string {:max 400}]]
|
||||
[:content [:string {:max 2500}]]])
|
||||
|
||||
(sv/defmethod ::send-user-feedback
|
||||
{::doc/added "1.18"
|
||||
|
||||
@@ -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
|
||||
@@ -360,7 +356,7 @@
|
||||
f.name,
|
||||
f.revn,
|
||||
f.is_shared,
|
||||
ft.media_id
|
||||
ft.media_id AS thumbnail_id
|
||||
from file as f
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id
|
||||
and ft.revn = f.revn
|
||||
@@ -371,13 +367,7 @@
|
||||
|
||||
(defn get-project-files
|
||||
[conn project-id]
|
||||
(->> (db/exec! conn [sql:project-files project-id])
|
||||
(mapv (fn [row]
|
||||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))))
|
||||
(db/exec! conn [sql:project-files project-id]))
|
||||
|
||||
(def schema:get-project-files
|
||||
[:map {:title "get-project-files"}
|
||||
@@ -487,7 +477,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 +904,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 +933,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 +966,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"
|
||||
|
||||
@@ -98,46 +98,49 @@
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-file}
|
||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
::sm/params schema:create-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
team-id (:id team)
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> (:features params #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union features))
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> (:features params #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id}))
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id})
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
;; FIXME: IMPORTANT: this code can have race
|
||||
;; conditions, because we have no locks for updating
|
||||
;; team so, creating two files concurrently can lead
|
||||
;; to lost team features updating
|
||||
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))))
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id team-id})))
|
||||
|
||||
(-> (create-file cfg params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id}))))
|
||||
|
||||
@@ -45,37 +45,38 @@
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-temp-file}
|
||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
::sm/params schema:create-temp-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features
|
||||
(:features params #{})
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features (:features params #{})
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features
|
||||
(-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))))
|
||||
(files.create/create-file cfg params)))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-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
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]])
|
||||
|
||||
;; FIXME: IMPORTANT: refactor this, we should not hold a whole db
|
||||
;; connection around the font creation
|
||||
|
||||
(sv/defmethod ::create-font-variant
|
||||
{::doc/added "1.18"
|
||||
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
||||
@@ -96,9 +99,9 @@
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn create-font-variant
|
||||
|
||||
@@ -88,10 +88,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:duplicate-file
|
||||
(sm/define
|
||||
[:map {:title "duplicate-file"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "duplicate-file"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::duplicate-file
|
||||
"Duplicate a single file in the same team."
|
||||
@@ -150,10 +149,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:duplicate-project
|
||||
(sm/define
|
||||
[:map {:title "duplicate-project"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "duplicate-project"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::duplicate-project
|
||||
"Duplicate an entire project with all the files"
|
||||
@@ -327,10 +325,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:move-files
|
||||
(sm/define
|
||||
[:map {:title "move-files"}
|
||||
[:ids ::sm/set-of-uuid]
|
||||
[:project-id ::sm/uuid]]))
|
||||
[:map {:title "move-files"}
|
||||
[:ids ::sm/set-of-uuid]
|
||||
[:project-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::move-files
|
||||
"Move a set of files from one project to other."
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.plugins :refer [schema:plugin-registry]]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -40,6 +41,33 @@
|
||||
(declare strip-private-attrs)
|
||||
(declare verify-password)
|
||||
|
||||
(def schema:props
|
||||
[:map {:title "ProfileProps"}
|
||||
[:plugins {:optional true} schema:plugin-registry]
|
||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||
[:newsletter-news {:optional true} ::sm/boolean]
|
||||
[:onboarding-team-id {:optional true} ::sm/uuid]
|
||||
[:onboarding-viewed {:optional true} ::sm/boolean]
|
||||
[:v2-info-shown {:optional true} ::sm/boolean]
|
||||
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
|
||||
[:release-notes-viewed {:optional true}
|
||||
[::sm/text {:max 100}]]])
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} ::sm/boolean]
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true} schema:props]])
|
||||
|
||||
(defn clean-email
|
||||
"Clean and normalizes email address string"
|
||||
[email]
|
||||
@@ -53,24 +81,6 @@
|
||||
email)]
|
||||
email))
|
||||
|
||||
(def ^:private
|
||||
schema:profile
|
||||
(sm/define
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} ::sm/boolean]
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true}
|
||||
[:map-of {:title "ProfileProps"} :keyword :any]]]))
|
||||
|
||||
;; --- QUERY: Get profile (own)
|
||||
|
||||
(sv/defmethod ::get-profile
|
||||
@@ -99,11 +109,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile
|
||||
(sm/define
|
||||
[:map {:title "update-profile"}
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:lang {:optional true} [:string {:max 8}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]]))
|
||||
[:map {:title "update-profile"}
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:lang {:optional true} [:string {:max 8}]]
|
||||
[:theme {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::update-profile
|
||||
{::doc/added "1.0"
|
||||
@@ -144,11 +153,10 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-password
|
||||
(sm/define
|
||||
[:map {:title "update-profile-password"}
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
;; Social registered users don't have old-password
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
|
||||
[:map {:title "update-profile-password"}
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
;; Social registered users don't have old-password
|
||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
|
||||
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::doc/added "1.0"
|
||||
@@ -199,9 +207,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-photo
|
||||
(sm/define
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]]))
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]])
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{:doc/added "1.1"
|
||||
@@ -268,9 +275,8 @@
|
||||
|
||||
(def ^:private
|
||||
schema:request-email-change
|
||||
(sm/define
|
||||
[:map {:title "request-email-change"}
|
||||
[:email ::sm/email]]))
|
||||
[:map {:title "request-email-change"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(sv/defmethod ::request-email-change
|
||||
{::doc/added "1.0"
|
||||
@@ -351,14 +357,12 @@
|
||||
:extra-data ptoken})
|
||||
nil))
|
||||
|
||||
|
||||
;; --- MUTATION: Update Profile Props
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-props
|
||||
(sm/define
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]]))
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props schema:props]])
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
|
||||
@@ -168,6 +168,17 @@
|
||||
|
||||
;; --- MUTATION: Create Project
|
||||
|
||||
(defn- create-project
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(let [project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
(db/insert! conn :team-project-profile-rel
|
||||
{:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:team-id team-id
|
||||
:is-pinned false})
|
||||
(assoc project :is-pinned false)))
|
||||
|
||||
(def ^:private schema:create-project
|
||||
[:map {:title "create-project"}
|
||||
[:team-id ::sm/uuid]
|
||||
@@ -178,23 +189,15 @@
|
||||
{::doc/added "1.18"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-project}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
|
||||
(let [params (assoc params :profile-id profile-id)
|
||||
project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
(db/insert! conn :team-project-profile-rel
|
||||
{:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:team-id team-id
|
||||
:is-pinned false})
|
||||
(assoc project :is-pinned false))))
|
||||
(teams/check-edition-permissions! cfg profile-id team-id)
|
||||
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
|
||||
(let [params (assoc params :profile-id profile-id)]
|
||||
(db/tx-run! cfg create-project params)))
|
||||
|
||||
;; --- MUTATION: Toggle Project Pin
|
||||
|
||||
|
||||
@@ -82,19 +82,17 @@
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||
|
||||
|
||||
|
||||
(defn- check-valid-email-muted
|
||||
"Check if the member's email is part of the global bounce report."
|
||||
(defn- check-profile-muted
|
||||
"Check if the member's email is part of the global bounce report"
|
||||
[conn member]
|
||||
(let [email (profile/clean-email (:email member))]
|
||||
(let [email (profile/clean-email (:email member))]
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:email email
|
||||
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
||||
|
||||
(defn- check-valid-email-bounce
|
||||
(defn- check-email-bounce
|
||||
"Check if the email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
@@ -103,7 +101,7 @@
|
||||
:email (if show? email "private")
|
||||
:hint "this email has been repeatedly reported as bounce")))
|
||||
|
||||
(defn- check-valid-email-spam
|
||||
(defn- check-email-spam
|
||||
"Check if the member email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-complaint-reports? conn email)
|
||||
@@ -227,16 +225,16 @@
|
||||
;; --- Query: Team Members
|
||||
|
||||
(def sql:team-members
|
||||
"select tp.*,
|
||||
"SELECT tp.*,
|
||||
p.id,
|
||||
p.email,
|
||||
p.fullname as name,
|
||||
p.fullname as fullname,
|
||||
p.fullname AS name,
|
||||
p.fullname AS fullname,
|
||||
p.photo_id,
|
||||
p.is_active
|
||||
from team_profile_rel as tp
|
||||
join profile as p on (p.id = tp.profile_id)
|
||||
where tp.team_id = ?")
|
||||
FROM team_profile_rel AS tp
|
||||
JOIN profile AS p ON (p.id = tp.profile_id)
|
||||
WHERE tp.team_id = ?")
|
||||
|
||||
(defn get-team-members
|
||||
[conn team-id]
|
||||
@@ -403,17 +401,19 @@
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
team (create-team cfg (assoc params
|
||||
:profile-id profile-id
|
||||
:features features))]
|
||||
(with-meta team
|
||||
{::audit/props {:id (:id team)}})))))
|
||||
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
team (db/tx-run! cfg create-team params)]
|
||||
|
||||
(with-meta team
|
||||
{::audit/props {:id (:id team)}})))
|
||||
|
||||
(defn create-team
|
||||
"This is a complete team creation process, it creates the team
|
||||
@@ -767,21 +767,51 @@
|
||||
:member-id member-id}))
|
||||
|
||||
(defn- create-profile-identity-token
|
||||
[cfg profile]
|
||||
[cfg profile-id]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid uuid for profile-id"
|
||||
(uuid? profile-id))
|
||||
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)
|
||||
:profile-id profile-id
|
||||
:exp (dt/in-future {:days 30})}))
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:map {:title "params:create-invitation"}
|
||||
[::rpc/profile-id ::sm/uuid]
|
||||
[:team
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
[:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:fullname :string]]]
|
||||
[:role [::sm/one-of valid-roles]]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private check-create-invitation-params!
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn- create-invitation
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid connection on cfg parameter"
|
||||
(db/connection? conn))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params for `create-invitation` fn"
|
||||
(check-create-invitation-params! params))
|
||||
|
||||
(let [email (profile/clean-email email)
|
||||
member (profile/get-profile-by-email conn email)]
|
||||
|
||||
(check-valid-email-muted conn member)
|
||||
(check-valid-email-bounce conn email true)
|
||||
(check-valid-email-spam conn email true)
|
||||
|
||||
(check-profile-muted conn member)
|
||||
(check-email-bounce conn email true)
|
||||
(check-email-spam conn email true)
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
;; already present in the database, we proceed to add it to the
|
||||
@@ -815,7 +845,8 @@
|
||||
(name role) expire
|
||||
(name role) expire])
|
||||
updated? (not= id (:id invitation))
|
||||
tprops {:profile-id (:id profile)
|
||||
profile-id (:id profile)
|
||||
tprops {:profile-id profile-id
|
||||
:invitation-id (:id invitation)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
@@ -823,12 +854,11 @@
|
||||
:member-id (:id member)
|
||||
:role role}
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile)]
|
||||
ptoken (create-profile-identity-token cfg profile-id)]
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
|
||||
|
||||
(let [props (-> (dissoc tprops :profile-id)
|
||||
(audit/clean-props))
|
||||
evname (if updated?
|
||||
@@ -851,26 +881,27 @@
|
||||
itoken))))
|
||||
|
||||
(defn- add-user-to-team
|
||||
[conn profile team email role]
|
||||
[conn profile team role email]
|
||||
|
||||
(let [team-id (:id team)
|
||||
member (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{::sql/columns [:id :email]})
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(role->params role))]
|
||||
member (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{::sql/columns [:id :email]})
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(role->params role))]
|
||||
|
||||
;; Do not allow blocked users to join teams.
|
||||
(when (:is-blocked member)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check!
|
||||
{::db/conn conn
|
||||
::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the member to the team
|
||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||
@@ -902,68 +933,89 @@
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:valid-requests-email team-id]))
|
||||
|
||||
(def ^:private xf:map-email
|
||||
(map :email))
|
||||
|
||||
(defn- create-team-invitations
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||
(let [join-requests (into #{} xf:map-email
|
||||
(get-valid-requests-email conn (:id team)))
|
||||
team-members (into #{} xf:map-email
|
||||
(get-team-members conn (:id team)))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to
|
||||
;; already existing members
|
||||
(remove team-members)
|
||||
;; We don't send invitations to
|
||||
;; join-requested members
|
||||
(remove join-requests)
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
|
||||
;; For requested invitations, do not send invitation emails, add
|
||||
;; the user directly to the team
|
||||
(->> (filter join-requests emails)
|
||||
(run! (partial add-user-to-team conn profile team role)))
|
||||
|
||||
invitations))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role schema:role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
"The number of invitations can be sent in a single rpc request"
|
||||
25)
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-invitations}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (get-permissions conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||
(let [perms (get-permissions cfg profile-id team-id)
|
||||
profile (db/get-by-id cfg :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
;; Check if the current profile is allowed to send emails.
|
||||
(check-valid-email-muted conn profile)
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
|
||||
;; Check if the current profile is allowed to send emails
|
||||
(check-profile-muted cfg profile)
|
||||
|
||||
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
|
||||
emails-to-add (filter #(contains? requested %) emails)
|
||||
emails (remove #(contains? requested %) emails)
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
members (->> (db/exec! conn [sql:team-members team-id])
|
||||
(into #{} (map :email)))
|
||||
(let [team (db/get-by-id cfg :team team-id)
|
||||
;; NOTE: Is important pass RPC method params down to the
|
||||
;; `create-team-invitations` because it uses the implicit
|
||||
;; RPC properties from params for fill necessary data on
|
||||
;; emiting an entry to the audit-log
|
||||
invitations (db/tx-run! cfg create-team-invitations
|
||||
(-> params
|
||||
(assoc :profile profile)
|
||||
(assoc :team team)
|
||||
(assoc :emails emails)))]
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to already existing members
|
||||
(remove (partial contains? members))
|
||||
(map (fn [email]
|
||||
(-> params
|
||||
(assoc :email email)
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :role role))))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
;; For requested invitations, do not send invitation emails, add the user directly to the team
|
||||
(doseq [email emails-to-add]
|
||||
(add-user-to-team conn profile team email role))
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}}))))
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
@@ -977,52 +1029,50 @@
|
||||
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-with-invitations}
|
||||
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
|
||||
::sm/params schema:create-team-with-invitations
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
team (create-team cfg params)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
team (create-team cfg params)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id (:id team))
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
|
||||
{::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(->> emails
|
||||
(map (fn [email]
|
||||
(-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :email email)
|
||||
(assoc :role role))))
|
||||
(run! (partial create-invitation cfg)))
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id}
|
||||
{::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
;; Create invitations for all provided emails.
|
||||
(let [profile (db/get-by-id conn :profile profile-id)
|
||||
params (-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :role role))
|
||||
invitations (->> emails
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(map (partial create-invitation cfg)))]
|
||||
|
||||
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
|
||||
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
|
||||
|
||||
;; --- Query: get-team-invitation-token
|
||||
|
||||
@@ -1215,11 +1265,11 @@
|
||||
:code :invalid-parameters))
|
||||
|
||||
;; Check that the requester is not muted
|
||||
(check-valid-email-muted conn requester)
|
||||
(check-profile-muted conn requester)
|
||||
|
||||
;; Check that the owner is not marked as bounce nor spam
|
||||
(check-valid-email-bounce conn (:email team-owner) false)
|
||||
(check-valid-email-spam conn (:email team-owner) true)
|
||||
(check-email-bounce conn (:email team-owner) false)
|
||||
(check-email-spam conn (:email team-owner) true)
|
||||
|
||||
(let [request (create-team-access-request
|
||||
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
||||
|
||||
@@ -30,21 +30,19 @@
|
||||
|
||||
(def ^:private schema:verify-token
|
||||
[:map {:title "verify-token"}
|
||||
[:token [:string {:max 1000}]]])
|
||||
[:token [:string {:max 5000}]]])
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth
|
||||
::sm/params schema:verify-token}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})
|
||||
cfg (assoc cfg :conn conn)]
|
||||
(process-token cfg params claims))))
|
||||
[cfg {:keys [token] :as params}]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
|
||||
(db/tx-run! cfg process-token params claims)))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
(let [email (profile/clean-email email)]
|
||||
(when (profile/get-profile-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
@@ -60,7 +58,7 @@
|
||||
::audit/profile-id profile-id})))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
[{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
@@ -81,22 +79,14 @@
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
(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))))
|
||||
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(defn- accept-invitation
|
||||
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||
(let [;; Update the role if there is an invitation
|
||||
role (or (some-> invitation :role keyword) role)
|
||||
params (merge
|
||||
@@ -109,10 +99,9 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the invited member to the team
|
||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||
@@ -148,7 +137,7 @@
|
||||
(sm/lazy-validator schema:team-invitation-claims))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [conn] :as cfg}
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [::rpc/profile-id token] :as params}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
|
||||
@@ -7,16 +7,13 @@
|
||||
(ns app.rpc.quotes
|
||||
"Penpot resource usage quotes."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti check-quote ::id)
|
||||
@@ -26,14 +23,16 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:quote
|
||||
(sm/define
|
||||
[:map {:title "Quote"}
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]]))
|
||||
[:map {:title "Quote"}
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]])
|
||||
|
||||
(def valid-quote?
|
||||
(sm/lazy-validator schema:quote))
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
@@ -47,20 +46,31 @@
|
||||
[]
|
||||
(vswap! enabled (constantly false)))
|
||||
|
||||
(defn check-quote!
|
||||
[ds quote]
|
||||
(dm/assert!
|
||||
"expected valid quote map"
|
||||
(sm/validate schema:quote quote))
|
||||
(defn- check
|
||||
[cfg quote]
|
||||
(let [quote (merge cfg quote)
|
||||
id (::id quote)]
|
||||
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
;; This approach add flexibility on how and where the
|
||||
;; check-quote! can be called (in or out of transaction)
|
||||
(db/run! ds (fn [cfg]
|
||||
(-> (merge cfg quote)
|
||||
(assoc ::target (name (::id quote)))
|
||||
(check-quote)))))))
|
||||
(when-not (valid-quote? quote)
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-quote-definition
|
||||
:hint "found invalid data for quote schema"
|
||||
:quote (name id)))
|
||||
|
||||
(-> (assoc quote ::target (name id))
|
||||
(check-quote))))
|
||||
|
||||
(defn check!
|
||||
([cfg]
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(db/run! cfg check {}))))
|
||||
|
||||
([cfg & others]
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(db/run! cfg (fn [cfg]
|
||||
(run! (partial check cfg) others)))))))
|
||||
|
||||
(defn- send-notification!
|
||||
[{:keys [::db/conn] :as params}]
|
||||
@@ -101,7 +111,7 @@
|
||||
(map :quote)
|
||||
(reduce max (- Integer/MAX_VALUE)))
|
||||
quote (if (pos? quote) quote default)
|
||||
total (->> (db/exec! conn count-sql) first :total)]
|
||||
total (:total (db/exec-one! conn count-sql))]
|
||||
|
||||
(when (> (+ total incr) quote)
|
||||
(if (contains? cf/flags :soft-quotes)
|
||||
@@ -113,72 +123,81 @@
|
||||
:count total)))))
|
||||
|
||||
(def ^:private sql:get-quotes-1
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and profile_id = ?
|
||||
and team_id is null
|
||||
and project_id is null
|
||||
and file_id is null;")
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND profile_id = ?
|
||||
AND team_id IS NULL
|
||||
AND project_id IS NULL
|
||||
AND file_id IS NULL;")
|
||||
|
||||
(def ^:private sql:get-quotes-2
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
|
||||
(def ^:private sql:get-quotes-3
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
|
||||
(def ^:private sql:get-quotes-4
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
"SELECT id, quote
|
||||
FROM usage_quote
|
||||
WHERE target = ?
|
||||
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAMS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-teams-per-profile
|
||||
"select count(*) as total
|
||||
from team_profile_rel
|
||||
where profile_id = ?")
|
||||
(def ^:private schema:teams-per-profile
|
||||
[:map [::profile-id ::sm/uuid]])
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::teams-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
(def ^:private valid-teams-per-profile-quote?
|
||||
(sm/lazy-validator schema:teams-per-profile))
|
||||
|
||||
(def ^:private sql:get-teams-per-profile
|
||||
"SELECT count(*) AS total
|
||||
FROM team_profile_rel
|
||||
WHERE profile_id = ?")
|
||||
|
||||
(defmethod check-quote ::teams-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(us/assert! ::teams-per-profile quote)
|
||||
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-access-tokens-per-profile
|
||||
"select count(*) as total
|
||||
from access_token
|
||||
where profile_id = ?")
|
||||
(def ^:private schema:access-tokens-per-profile
|
||||
[:map [::profile-id ::sm/uuid]])
|
||||
|
||||
(s/def ::access-tokens-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
(def ^:private valid-access-tokens-per-profile-quote?
|
||||
(sm/lazy-validator schema:access-tokens-per-profile))
|
||||
|
||||
(def ^:private sql:get-access-tokens-per-profile
|
||||
"SELECT count(*) AS total
|
||||
FROM access_token
|
||||
WHERE profile_id = ?")
|
||||
|
||||
(defmethod check-quote ::access-tokens-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(us/assert! ::access-tokens-per-profile quote)
|
||||
(assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
@@ -189,40 +208,51 @@
|
||||
;; QUOTE: PROJECTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-projects-per-team
|
||||
"select count(*) as total
|
||||
from project as p
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null")
|
||||
(def ^:private schema:projects-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::projects-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
(def ^:private valid-projects-per-team-quote?
|
||||
(sm/lazy-validator schema:projects-per-team))
|
||||
|
||||
(def ^:private sql:get-projects-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM project AS p
|
||||
WHERE p.team_id = ?
|
||||
AND p.deleted_at IS NULL")
|
||||
|
||||
(defmethod check-quote ::projects-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-font-variants-per-team
|
||||
"select count(*) as total
|
||||
from team_font_variant as v
|
||||
where v.team_id = ?")
|
||||
(def ^:private schema:font-variants-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::font-variants-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
(def ^:private valid-font-variant-per-team-quote?
|
||||
(sm/lazy-validator schema:font-variants-per-team))
|
||||
|
||||
(def ^:private sql:get-font-variants-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM team_font_variant AS v
|
||||
WHERE v.team_id = ?")
|
||||
|
||||
(defmethod check-quote ::font-variants-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::font-variants-per-team quote)
|
||||
(assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
@@ -234,70 +264,86 @@
|
||||
;; QUOTE: INVITATIONS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-invitations-per-team
|
||||
"select count(*) as total
|
||||
from team_invitation
|
||||
where team_id = ?")
|
||||
(def ^:private schema:invitations-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::invitations-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
(def ^:private valid-invitations-per-team-quote?
|
||||
(sm/lazy-validator schema:invitations-per-team))
|
||||
|
||||
(def ^:private sql:get-invitations-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?")
|
||||
|
||||
(defmethod check-quote ::invitations-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::invitations-per-team quote)
|
||||
(assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROFILES-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:profiles-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-profiles-per-team-quote?
|
||||
(sm/lazy-validator schema:profiles-per-team))
|
||||
|
||||
(def ^:private sql:get-profiles-per-team
|
||||
"select (select count(*)
|
||||
from team_profile_rel
|
||||
where team_id = ?) +
|
||||
(select count(*)
|
||||
from team_invitation
|
||||
where team_id = ?
|
||||
and valid_until > now()) as total;")
|
||||
"SELECT (SELECT count(*)
|
||||
FROM team_profile_rel
|
||||
WHERE team_id = ?) +
|
||||
(SELECT count(*)
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
AND valid_until > now()) AS total;")
|
||||
|
||||
;; NOTE: the total number of profiles is determined by the number of
|
||||
;; effective members plus ongoing valid invitations.
|
||||
|
||||
(s/def ::profiles-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::profiles-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::profiles-per-team quote)
|
||||
(assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FILES-PER-PROJECT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-files-per-project
|
||||
"select count(*) as total
|
||||
from file as f
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null")
|
||||
(def ^:private schema:files-per-project
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::files-per-project
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
(def ^:private valid-files-per-project-quote?
|
||||
(sm/lazy-validator schema:files-per-project))
|
||||
|
||||
(def ^:private sql:get-files-per-project
|
||||
"SELECT count(*) AS total
|
||||
FROM file AS f
|
||||
WHERE f.project_id = ?
|
||||
AND f.deleted_at IS NULL")
|
||||
|
||||
(defmethod check-quote ::files-per-project
|
||||
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(assert (valid-files-per-project-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
|
||||
@@ -308,17 +354,24 @@
|
||||
;; QUOTE: COMMENT-THREADS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comment-threads-per-file
|
||||
"select count(*) as total
|
||||
from comment_thread as ct
|
||||
where ct.file_id = ?")
|
||||
(def ^:private schema:comment-threads-per-file
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::comment-threads-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
(def ^:private valid-comment-threads-per-file-quote?
|
||||
(sm/lazy-validator schema:comment-threads-per-file))
|
||||
|
||||
(def ^:private sql:get-comment-threads-per-file
|
||||
"SELECT count(*) AS total
|
||||
FROM comment_thread AS ct
|
||||
WHERE ct.file_id = ?")
|
||||
|
||||
(defmethod check-quote ::comment-threads-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
|
||||
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
@@ -326,23 +379,28 @@
|
||||
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENTS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comments-per-file
|
||||
"select count(*) as total
|
||||
from comment as c
|
||||
join comment_thread as ct on (ct.id = c.thread_id)
|
||||
where ct.file_id = ?")
|
||||
(def ^:private schema:comments-per-file
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::project-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(s/def ::comments-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
(def ^:private valid-comments-per-file-quote?
|
||||
(sm/lazy-validator schema:comments-per-file))
|
||||
|
||||
(def ^:private sql:get-comments-per-file
|
||||
"SELECT count(*) AS total
|
||||
FROM comment AS c
|
||||
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
|
||||
WHERE ct.file_id = ?")
|
||||
|
||||
(defmethod check-quote ::comments-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.http.client :as http]
|
||||
@@ -19,28 +20,26 @@
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private
|
||||
schema:template
|
||||
(sm/define
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]]))
|
||||
(def ^:private schema:template
|
||||
[:map {:title "Template"}
|
||||
[:id ::sm/word-string]
|
||||
[:name ::sm/word-string]
|
||||
[:file-uri ::sm/word-string]])
|
||||
|
||||
(def ^:private
|
||||
schema:templates
|
||||
(sm/define
|
||||
[:vector schema:template]))
|
||||
(def ^:private schema:templates
|
||||
[:vector schema:template])
|
||||
|
||||
(def check-templates!
|
||||
(sm/check-fn schema:templates
|
||||
:code :invalid-templates
|
||||
:hint "invalid templates"))
|
||||
|
||||
(defmethod ig/init-key ::setup/templates
|
||||
[_ _]
|
||||
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
|
||||
templates (check-templates! templates)
|
||||
dest (fs/join fs/*cwd* "builtin-templates")]
|
||||
|
||||
(dm/verify!
|
||||
"expected a valid templates file"
|
||||
(sm/check! schema:templates templates))
|
||||
|
||||
(doseq [{:keys [id path] :as template} templates]
|
||||
(let [path (or path (fs/join dest id))]
|
||||
(if (fs/exists? path)
|
||||
@@ -60,9 +59,9 @@
|
||||
(let [resp (http/req! cfg
|
||||
{:method :get :uri (:file-uri template)}
|
||||
{:response-type :input-stream :sync? true})]
|
||||
|
||||
(dm/verify!
|
||||
"unexpected response found on fetching template"
|
||||
(= 200 (:status resp)))
|
||||
(when-not (= 200 (:status resp))
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-status-code
|
||||
:hint (str "unable to download template, recevied status " (:status resp))))
|
||||
|
||||
(io/input-stream (:body resp)))))))
|
||||
|
||||
@@ -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)})))
|
||||
|
||||
|
||||
@@ -155,9 +155,10 @@
|
||||
|
||||
(defn enable-team-feature!
|
||||
[team-id feature]
|
||||
(dm/verify!
|
||||
"feature should be supported"
|
||||
(contains? cfeat/supported-features feature))
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
|
||||
(let [team-id (h/parse-uuid team-id)]
|
||||
(db/tx-run! main/system
|
||||
@@ -173,9 +174,11 @@
|
||||
|
||||
(defn disable-team-feature!
|
||||
[team-id feature]
|
||||
(dm/verify!
|
||||
"feature should be supported"
|
||||
(contains? cfeat/supported-features feature))
|
||||
|
||||
(when-not (contains? cfeat/supported-features feature)
|
||||
(ex/raise :type :assertion
|
||||
:code :feature-not-supported
|
||||
:hint (str "feature '" feature "' not supported")))
|
||||
|
||||
(let [team-id (h/parse-uuid team-id)]
|
||||
(db/tx-run! main/system
|
||||
@@ -203,9 +206,11 @@
|
||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
(dm/verify!
|
||||
["invalid level %" level]
|
||||
(contains? #{:success :error :info :warning} level))
|
||||
|
||||
(when-not (contains? #{:success :error :info :warning} level)
|
||||
(ex/raise :type :assertion
|
||||
:code :incorrect-level
|
||||
:hint (str "level '" level "' not supported")))
|
||||
|
||||
(letfn [(send [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -19,6 +20,7 @@
|
||||
[app.storage.s3 :as ss3]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
@@ -30,7 +32,7 @@
|
||||
(case name
|
||||
:assets-fs :fs
|
||||
:assets-s3 :s3
|
||||
:fs)))
|
||||
nil)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Storage Module State
|
||||
@@ -52,11 +54,19 @@
|
||||
|
||||
(defmethod ig/init-key ::storage
|
||||
[_ {:keys [::backends ::db/pool] :as cfg}]
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc ::backends (d/without-nils backends))
|
||||
(assoc ::backend (or (get-legacy-backend)
|
||||
(cf/get :objects-storage-backend :fs)))
|
||||
(assoc ::db/connectable pool)))
|
||||
(let [backend (or (get-legacy-backend)
|
||||
(cf/get :objects-storage-backend)
|
||||
:fs)
|
||||
backends (d/without-nils backends)]
|
||||
|
||||
(l/dbg :hint "initialize"
|
||||
:default (d/name backend)
|
||||
:available (str/join "," (map d/name (keys backends))))
|
||||
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc ::backends backends)
|
||||
(assoc ::backend backend)
|
||||
(assoc ::db/connectable pool))))
|
||||
|
||||
(s/def ::backend keyword?)
|
||||
(s/def ::storage
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
@@ -27,17 +28,15 @@
|
||||
java.io.FilterInputStream
|
||||
java.io.InputStream
|
||||
java.net.URI
|
||||
java.nio.ByteBuffer
|
||||
java.nio.file.Path
|
||||
java.time.Duration
|
||||
java.util.Collection
|
||||
java.util.Optional
|
||||
java.util.concurrent.Semaphore
|
||||
org.reactivestreams.Subscriber
|
||||
org.reactivestreams.Subscription
|
||||
software.amazon.awssdk.core.ResponseBytes
|
||||
software.amazon.awssdk.core.async.AsyncRequestBody
|
||||
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
||||
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
|
||||
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
|
||||
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
|
||||
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
|
||||
@@ -59,6 +58,20 @@
|
||||
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
||||
|
||||
(def ^:private max-retries
|
||||
"A maximum number of retries on internal operations"
|
||||
3)
|
||||
|
||||
(def ^:private max-concurrency
|
||||
"Maximum concurrent request to S3 service"
|
||||
128)
|
||||
|
||||
(def ^:private max-pending-connection-acquires
|
||||
20000)
|
||||
|
||||
(def default-timeout
|
||||
(dt/duration {:seconds 30}))
|
||||
|
||||
(declare put-object)
|
||||
(declare get-object-bytes)
|
||||
(declare get-object-data)
|
||||
@@ -80,7 +93,7 @@
|
||||
(s/def ::io-threads ::us/integer)
|
||||
|
||||
(defmethod ig/pre-init-spec ::backend [_]
|
||||
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads]))
|
||||
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads ::wrk/executor]))
|
||||
|
||||
(defmethod ig/prep-key ::backend
|
||||
[_ {:keys [::prefix ::region] :as cfg}]
|
||||
@@ -128,18 +141,29 @@
|
||||
[backend object]
|
||||
(us/assert! ::backend backend)
|
||||
|
||||
(let [result (p/await (get-object-data backend object))]
|
||||
(if (ex/exception? result)
|
||||
(cond
|
||||
(ex/instance? NoSuchKeyException result)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "s3 object not found"
|
||||
:cause result)
|
||||
:else
|
||||
(throw result))
|
||||
(loop [result (get-object-data backend object)
|
||||
retryn 0]
|
||||
|
||||
result)))
|
||||
(let [result (p/await result)]
|
||||
(if (ex/exception? result)
|
||||
(cond
|
||||
(ex/instance? NoSuchKeyException result)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "s3 object not found"
|
||||
:object-id (:id object)
|
||||
:object-path (impl/id->path (:id object))
|
||||
:cause result)
|
||||
|
||||
(and (ex/instance? java.nio.file.FileAlreadyExistsException result)
|
||||
(< retryn max-retries))
|
||||
(recur (get-object-data backend object)
|
||||
(inc retryn))
|
||||
|
||||
:else
|
||||
(throw result))
|
||||
|
||||
result))))
|
||||
|
||||
(defmethod impl/get-object-bytes :s3
|
||||
[backend object]
|
||||
@@ -163,18 +187,14 @@
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def default-timeout
|
||||
(dt/duration {:seconds 30}))
|
||||
|
||||
(defn- lookup-region
|
||||
^Region
|
||||
[region]
|
||||
(Region/of (name region)))
|
||||
|
||||
(defn- build-s3-client
|
||||
[{:keys [::region ::endpoint ::io-threads]}]
|
||||
(let [executor (px/resolve-executor :virtual)
|
||||
aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
|
||||
(let [aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
||||
(.build))
|
||||
|
||||
@@ -190,6 +210,8 @@
|
||||
(.connectionTimeout default-timeout)
|
||||
(.readTimeout default-timeout)
|
||||
(.writeTimeout default-timeout)
|
||||
(.maxConcurrency (int max-concurrency))
|
||||
(.maxPendingConnectionAcquires (int max-pending-connection-acquires))
|
||||
(.build))
|
||||
|
||||
client (let [builder (S3AsyncClient/builder)
|
||||
@@ -223,69 +245,38 @@
|
||||
(.serviceConfiguration ^S3Configuration config)
|
||||
(.build))))
|
||||
|
||||
(defn- upload-thread
|
||||
[id subscriber sem content]
|
||||
(px/thread
|
||||
{:name "penpot/s3/uploader"
|
||||
:virtual true
|
||||
:daemon true}
|
||||
(l/trace :hint "start upload thread"
|
||||
:object-id (str id)
|
||||
:size (impl/get-size content)
|
||||
::l/sync? true)
|
||||
(let [stream (io/input-stream content)
|
||||
bsize (* 1024 64)
|
||||
tpoint (dt/tpoint)]
|
||||
(try
|
||||
(loop []
|
||||
(.acquire ^Semaphore sem 1)
|
||||
(let [buffer (byte-array bsize)
|
||||
readed (.read ^InputStream stream buffer)]
|
||||
(when (pos? readed)
|
||||
(let [data (ByteBuffer/wrap ^bytes buffer 0 readed)]
|
||||
(.onNext ^Subscriber subscriber ^ByteBuffer data)
|
||||
(when (= readed bsize)
|
||||
(recur))))))
|
||||
(.onComplete ^Subscriber subscriber)
|
||||
(catch InterruptedException _
|
||||
(l/trace :hint "interrupted upload thread"
|
||||
:object-:id (str id)
|
||||
::l/sync? true)
|
||||
nil)
|
||||
(catch Throwable cause
|
||||
(.onError ^Subscriber subscriber cause))
|
||||
(finally
|
||||
(l/trace :hint "end upload thread"
|
||||
:object-id (str id)
|
||||
:elapsed (dt/format-duration (tpoint))
|
||||
::l/sync? true)
|
||||
(.close ^InputStream stream))))))
|
||||
(defn- write-input-stream
|
||||
[delegate input]
|
||||
(try
|
||||
(.writeInputStream ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^InputStream input)
|
||||
(catch Throwable cause
|
||||
(l/error :hint "encountered error while writing input stream to service"
|
||||
:cause cause))
|
||||
(finally
|
||||
(.close ^InputStream input))))
|
||||
|
||||
(defn- make-request-body
|
||||
[id content]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
(contentLength [_]
|
||||
(Optional/of (long (impl/get-size content))))
|
||||
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [sem (Semaphore. 0)
|
||||
thr (upload-thread id subscriber sem content)]
|
||||
(.onSubscribe subscriber
|
||||
(reify Subscription
|
||||
(cancel [_]
|
||||
(px/interrupt! thr)
|
||||
(.release sem 1))
|
||||
(request [_ n]
|
||||
(.release sem (int n)))))))))
|
||||
[executor content]
|
||||
(let [size (impl/get-size content)]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
(contentLength [_]
|
||||
(Optional/of (long size)))
|
||||
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
|
||||
input (io/input-stream content)]
|
||||
(px/run! executor (partial write-input-stream delegate input))
|
||||
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^Subscriber subscriber))))))
|
||||
|
||||
(defn- put-object
|
||||
[{:keys [::client ::bucket ::prefix]} {:keys [id] :as object} content]
|
||||
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
|
||||
(let [path (dm/str prefix (impl/id->path id))
|
||||
mdata (meta object)
|
||||
mtype (:content-type mdata "application/octet-stream")
|
||||
rbody (make-request-body id content)
|
||||
rbody (make-request-body executor content)
|
||||
request (.. (PutObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(contentType mtype)
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
permanently delete these files (look at systemd-tempfiles)."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
[promesa.exec.csp :as sp])
|
||||
(:import
|
||||
java.nio.file.Files))
|
||||
|
||||
(def default-tmp-dir "/tmp/penpot")
|
||||
|
||||
@@ -76,11 +79,9 @@
|
||||
[& {:keys [suffix prefix min-age]
|
||||
:or {prefix "penpot."
|
||||
suffix ".tmp"}}]
|
||||
(let [path (fs/create-tempfile
|
||||
:perms "rw-r--r--"
|
||||
:dir default-tmp-dir
|
||||
:suffix suffix
|
||||
:prefix prefix)]
|
||||
(let [attrs (fs/make-permissions "rw-r--r--")
|
||||
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
|
||||
path (Files/createFile path attrs)]
|
||||
(fs/delete-on-exit! path)
|
||||
(sp/offer! queue [path (some-> min-age dt/duration)])
|
||||
path))
|
||||
|
||||
@@ -133,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 = ?
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px])
|
||||
(:import
|
||||
java.util.concurrent.Executor
|
||||
java.util.concurrent.ThreadPoolExecutor))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(s/def ::wrk/executor #(instance? Executor %))
|
||||
(s/def ::wrk/executor #(instance? ThreadPoolExecutor %))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXECUTOR
|
||||
@@ -36,30 +35,22 @@
|
||||
(let [factory (px/thread-factory :prefix "penpot/default/")
|
||||
executor (px/cached-executor :factory factory :keepalive 60000)]
|
||||
(l/inf :hint "executor started")
|
||||
(reify
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(l/inf :hint "stoping executor")
|
||||
(px/shutdown! executor))
|
||||
|
||||
clojure.lang.IDeref
|
||||
(deref [_]
|
||||
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||
|
||||
Executor
|
||||
(execute [_ runnable]
|
||||
(.execute ^Executor executor ^Runnable runnable)))))
|
||||
executor))
|
||||
|
||||
(defmethod ig/halt-key! ::wrk/executor
|
||||
[_ instance]
|
||||
(.close ^java.lang.AutoCloseable instance))
|
||||
(px/shutdown! instance))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MONITOR
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- get-stats
|
||||
[^ThreadPoolExecutor executor]
|
||||
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||
|
||||
(s/def ::name ::us/keyword)
|
||||
|
||||
(defmethod ig/pre-init-spec ::wrk/monitor [_]
|
||||
@@ -74,7 +65,7 @@
|
||||
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
|
||||
(letfn [(monitor! [executor prev-completed]
|
||||
(let [labels (into-array String [(d/name name)])
|
||||
stats (deref executor)
|
||||
stats (get-stats executor)
|
||||
|
||||
completed (:completed stats)
|
||||
completed-inc (- completed prev-completed)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest ttf-font-upload-1
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.transit :as transit]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -84,54 +85,56 @@
|
||||
(t/is (= (hash obj1) (hash obj2))))))
|
||||
|
||||
(t/deftest internal-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty))]
|
||||
(let [obj1 (omap/wrap data)
|
||||
obj2 (omap/create (deref obj1))
|
||||
obj3 (assoc obj2 uuid/zero 1)
|
||||
obj4 (omap/create (deref obj3))]
|
||||
;; (app.common.pprint/pprint data)
|
||||
(t/is (= (hash obj1) (hash obj2)))
|
||||
(t/is (not= (hash obj2) (hash obj3)))
|
||||
(t/is (bytes? (deref obj3)))
|
||||
(t/is (pos? (alength (deref obj3))))
|
||||
(t/is (= (hash obj3) (hash obj4)))))))
|
||||
|
||||
(and (= (hash obj1) (hash obj2))
|
||||
(not= (hash obj2) (hash obj3))
|
||||
(bytes? (deref obj3))
|
||||
(pos? (alength (deref obj3)))
|
||||
(= (hash obj3) (hash obj4)))))
|
||||
{:num 50}))
|
||||
|
||||
(t/deftest fressian-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
|
||||
(let [res (-> data fres/encode fres/decode)]
|
||||
(t/is (contains? res :objects))
|
||||
(t/is (omap/objects-map? (:objects res)))
|
||||
(t/is (= (count (:objects data))
|
||||
(count (:objects res))))
|
||||
(t/is (= (hash (:objects data))
|
||||
(hash (:objects res))))))))
|
||||
(and (contains? res :objects)
|
||||
(omap/objects-map? (:objects res))
|
||||
(= (count (:objects data))
|
||||
(count (:objects res)))
|
||||
(= (hash (:objects data))
|
||||
(hash (:objects res))))))
|
||||
{:num 50}))
|
||||
|
||||
(t/deftest transit-encode-decode
|
||||
(sg/check!
|
||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
(let [res (-> data transit/encode transit/decode)]
|
||||
;; (app.common.pprint/pprint data)
|
||||
;; (app.common.pprint/pprint res)
|
||||
(doseq [[k v] (:objects res)]
|
||||
(t/is (= v (get-in data [:objects k]))))
|
||||
|
||||
(t/is (contains? res :objects))
|
||||
(t/is (contains? data :objects))
|
||||
|
||||
(t/is (omap/objects-map? (:objects data)))
|
||||
(t/is (not (omap/objects-map? (:objects res))))
|
||||
|
||||
(t/is (= (count (:objects data))
|
||||
(count (:objects res))))))))
|
||||
(and (every? (fn [[k v]]
|
||||
(= v (get-in data [:objects k])))
|
||||
(:objects res))
|
||||
(contains? res :objects)
|
||||
(contains? data :objects)
|
||||
(omap/objects-map? (:objects data))
|
||||
(not (omap/objects-map? (:objects res)))
|
||||
(= (count (:objects data))
|
||||
(count (:objects res))))))
|
||||
{:num 50}))
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
(ns user
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
|
||||
(defn ordered-map
|
||||
([] lkm/empty-linked-map)
|
||||
([a] (conj lkm/empty-linked-map a))
|
||||
([a & xs] (apply conj lkm/empty-linked-map a xs)))
|
||||
([k a] (assoc lkm/empty-linked-map k a))
|
||||
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
|
||||
|
||||
(defn ordered-set?
|
||||
[o]
|
||||
@@ -564,6 +564,41 @@
|
||||
new-elems
|
||||
(remove p? after))))
|
||||
|
||||
(defn addm-at-index
|
||||
"Insert an element in an ordered map at an arbitrary index"
|
||||
[coll index key element]
|
||||
(assert (ordered-map? coll))
|
||||
(-> (ordered-map)
|
||||
(into (take index coll))
|
||||
(assoc key element)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn insertm-at-index
|
||||
"Insert a map {k v} of elements in an ordered map at an arbitrary index"
|
||||
[coll index new-elems]
|
||||
(assert (ordered-map? coll))
|
||||
(-> (ordered-map)
|
||||
(into (take index coll))
|
||||
(into new-elems)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn adds-at-index
|
||||
"Insert an element in an ordered set at an arbitrary index"
|
||||
[coll index element]
|
||||
(assert (ordered-set? coll))
|
||||
(-> (ordered-set)
|
||||
(into (take index coll))
|
||||
(conj element)
|
||||
(into (drop index coll))))
|
||||
|
||||
(defn inserts-at-index
|
||||
"Insert a list of elements in an ordered set at an arbitrary index"
|
||||
[coll index new-elems]
|
||||
(assert (ordered-set? coll))
|
||||
(-> (ordered-set)
|
||||
(into (take index coll))
|
||||
(into new-elems)
|
||||
(into (drop index coll))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Parsing / Conversion
|
||||
|
||||
@@ -108,14 +108,6 @@
|
||||
`(do ~@body)
|
||||
(reverse (partition 2 bindings))))
|
||||
|
||||
(defmacro check
|
||||
"Applies a predicate to the value, if result is true, return the
|
||||
value if not, returns nil."
|
||||
[pred-fn value]
|
||||
`(if (~pred-fn ~value)
|
||||
~value
|
||||
nil))
|
||||
|
||||
(defmacro get-prop
|
||||
"A macro based, optimized variant of `get` that access the property
|
||||
directly on CLJS, on CLJ works as get."
|
||||
@@ -124,47 +116,32 @@
|
||||
(list 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop))
|
||||
(list `c/get obj prop)))
|
||||
|
||||
(def ^:dynamic *assert-context* nil)
|
||||
(defn runtime-assert
|
||||
[hint f]
|
||||
(try
|
||||
(when-not (f)
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint})))
|
||||
(catch #?(:clj Throwable :cljs :default) cause
|
||||
(let [data (-> (ex-data cause)
|
||||
(assoc :type :assertion)
|
||||
(assoc :code :expr-validation)
|
||||
(assoc :hint hint))]
|
||||
(throw (ex-info hint data cause))))))
|
||||
|
||||
(defmacro assert!
|
||||
([expr]
|
||||
`(assert! nil ~expr))
|
||||
([hint expr]
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
|
||||
(some? hint)
|
||||
hint
|
||||
(some? hint)
|
||||
hint
|
||||
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
(when *assert*
|
||||
`(binding [*assert-context* ~hint]
|
||||
(when-not ~expr
|
||||
(let [hint# ~hint
|
||||
params# {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint#}]
|
||||
(throw (ex-info hint# params#)))))))))
|
||||
|
||||
(defmacro verify!
|
||||
([expr]
|
||||
`(verify! nil ~expr))
|
||||
([hint expr]
|
||||
(let [hint (cond
|
||||
(vector? hint)
|
||||
`(str/ffmt ~@hint)
|
||||
|
||||
(some? hint)
|
||||
hint
|
||||
|
||||
:else
|
||||
(str "expr assert: " (pr-str expr)))]
|
||||
`(binding [*assert-context* ~hint]
|
||||
(when-not ~expr
|
||||
(let [hint# ~hint
|
||||
params# {:type :assertion
|
||||
:code :expr-validation
|
||||
:hint hint#}]
|
||||
(throw (ex-info hint# params#))))))))
|
||||
`(runtime-assert ~hint (fn [] ~expr))))))
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
"components/v2"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"plugins/runtime"})
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"})
|
||||
|
||||
;; A set of features enabled by default
|
||||
(def default-features
|
||||
@@ -64,7 +65,8 @@
|
||||
;; team feature field
|
||||
(def frontend-only-features
|
||||
#{"styles/v2"
|
||||
"plugins/runtime"})
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"})
|
||||
|
||||
;; Features that are mainly backend only or there are a proper
|
||||
;; fallback when frontend reports no support for it
|
||||
@@ -81,7 +83,8 @@
|
||||
"fdata/pointer-map"
|
||||
"layout/grid"
|
||||
"fdata/shape-data-type"
|
||||
"plugins/runtime"}
|
||||
"plugins/runtime"
|
||||
"text-editor/v2"}
|
||||
(into frontend-only-features)))
|
||||
|
||||
(sm/register! ::features
|
||||
@@ -101,6 +104,7 @@
|
||||
:feature-fdata-objects-map "fdata/objects-map"
|
||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||
:feature-plugins "plugins/runtime"
|
||||
:feature-text-editor-v2 "text-editor/v2"
|
||||
nil))
|
||||
|
||||
(defn migrate-legacy-features
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
valid? (or (and components-v2
|
||||
(nil? (:component-id change))
|
||||
(nil? (:page-id change)))
|
||||
(ch/check-change! change))]
|
||||
(ch/valid-change? change))]
|
||||
|
||||
(when-not valid?
|
||||
(let [explain (sm/explain ::ch/change change)]
|
||||
@@ -741,46 +741,36 @@
|
||||
|
||||
(defn add-guide
|
||||
[file guide]
|
||||
|
||||
(let [guide (cond-> guide
|
||||
(nil? (:id guide))
|
||||
(assoc :id (uuid/next)))
|
||||
page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (assoc old-guides (:id guide) guide)]
|
||||
page-id (:current-page-id file)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides})
|
||||
:id (:id guide)
|
||||
:params guide})
|
||||
(assoc :last-id (:id guide)))))
|
||||
|
||||
(defn delete-guide
|
||||
[file id]
|
||||
|
||||
(let [page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (dissoc old-guides id)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides}))))
|
||||
(let [page-id (:current-page-id file)]
|
||||
(commit-change file
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params nil})))
|
||||
|
||||
(defn update-guide
|
||||
[file guide]
|
||||
|
||||
(let [page-id (:current-page-id file)
|
||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
||||
new-guides (assoc old-guides (:id guide) guide)]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option :guides
|
||||
:value new-guides}))))
|
||||
(let [page-id (:current-page-id file)]
|
||||
(commit-change file
|
||||
{:type :set-guide
|
||||
:page-id page-id
|
||||
:id (:id guide)
|
||||
:params guide})))
|
||||
|
||||
(defn strip-image-extension [filename]
|
||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||
|
||||
@@ -10,21 +10,25 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-native :as smd]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.colors-list :as ctcl]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.grid :as ctg]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -60,6 +64,111 @@
|
||||
[:type [:= :set-remote-synced]]
|
||||
[:remote-synced {:optional true} [:maybe :boolean]]]]])
|
||||
|
||||
(def schema:set-default-grid-change
|
||||
(let [gen (->> (sg/elements #{:square :column :row})
|
||||
(sg/mcat (fn [grid-type]
|
||||
(sg/fmap (fn [params]
|
||||
{:page-id (uuid/next)
|
||||
:type :set-default-grid
|
||||
:grid-type grid-type
|
||||
:params params})
|
||||
|
||||
(case grid-type
|
||||
:square (sg/generator ctg/schema:square-params)
|
||||
:column (sg/generator ctg/schema:column-params)
|
||||
:row (sg/generator ctg/schema:column-params))))))]
|
||||
|
||||
[:multi {:decode/json #(update % :grid-type keyword)
|
||||
:gen/gen gen
|
||||
:dispatch :grid-type
|
||||
::smd/simplified true}
|
||||
[:square
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :square]]
|
||||
[:params [:maybe ctg/schema:square-params]]]]
|
||||
|
||||
[:column
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :column]]
|
||||
[:params [:maybe ctg/schema:column-params]]]]
|
||||
|
||||
[:row
|
||||
[:map
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :row]]
|
||||
[:params [:maybe ctg/schema:column-params]]]]]))
|
||||
|
||||
(def schema:set-guide-change
|
||||
(let [schema [:map {:title "SetGuideChange"}
|
||||
[:type [:= :set-guide]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/guide]]]
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
(if (some? (:params change))
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
|
||||
(def schema:set-flow-change
|
||||
(let [schema [:map {:title "SetFlowChange"}
|
||||
[:type [:= :set-flow]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/flow]]]
|
||||
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
(if (some? (:params change))
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
|
||||
(def schema:set-plugin-data-change
|
||||
(let [types #{:file :page :shape :color :typography :component}
|
||||
|
||||
schema [:map {:title "SetPagePluginData"}
|
||||
[:type [:= :set-plugin-data]]
|
||||
[:object-type [::sm/one-of types]]
|
||||
;; It's optional because files don't need the id for type :file
|
||||
[:object-id {:optional true} ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:namespace {:gen/gen (sg/word-keyword)} :keyword]
|
||||
[:key {:gen/gen (sg/word-string)} :string]
|
||||
[:value [:maybe [:string {:gen/gen (sg/word-string)}]]]]
|
||||
|
||||
check1 [:fn {:error/path [:page-id]
|
||||
:error/message "missing page-id"}
|
||||
(fn [{:keys [object-type] :as change}]
|
||||
(if (= :shape object-type)
|
||||
(uuid? (:page-id change))
|
||||
true))]
|
||||
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/filter :object-id)
|
||||
(sg/filter :page-id)
|
||||
(sg/fmap (fn [{:keys [object-type] :as change}]
|
||||
(cond
|
||||
(= :file object-type)
|
||||
(-> change
|
||||
(dissoc :object-id)
|
||||
(dissoc :page-id))
|
||||
|
||||
(= :shape object-type)
|
||||
change
|
||||
|
||||
:else
|
||||
(dissoc change :page-id)))))]
|
||||
|
||||
[:and {:gen/gen gen} schema check1]))
|
||||
|
||||
(def schema:change
|
||||
[:schema
|
||||
[:multi {:dispatch :type
|
||||
@@ -67,13 +176,18 @@
|
||||
:decode/json #(update % :type keyword)
|
||||
::smd/simplified true}
|
||||
[:set-option
|
||||
[:map {:title "SetOptionChange"}
|
||||
[:type [:= :set-option]]
|
||||
|
||||
;; DEPRECATED: remove before 2.3 release
|
||||
;;
|
||||
;; Is still there for not cause error when event is received
|
||||
[:map {:title "SetOptionChange"}]]
|
||||
|
||||
[:set-comment-thread-position
|
||||
[:map
|
||||
[:comment-thread-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:option [:union
|
||||
[:keyword]
|
||||
[:vector {:gen/max 10} :keyword]]]
|
||||
[:value :any]]]
|
||||
[:frame-id [:maybe ::sm/uuid]]
|
||||
[:position [:maybe ::gpt/point]]]]
|
||||
|
||||
[:add-obj
|
||||
[:map {:title "AddObjChange"}
|
||||
@@ -103,6 +217,10 @@
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
|
||||
[:set-guide schema:set-guide-change]
|
||||
[:set-flow schema:set-flow-change]
|
||||
[:set-default-grid schema:set-default-grid-change]
|
||||
|
||||
[:fix-obj
|
||||
[:map {:title "FixObjChange"}
|
||||
[:type [:= :fix-obj]]
|
||||
@@ -143,19 +261,12 @@
|
||||
[:map {:title "ModPageChange"}
|
||||
[:type [:= :mod-page]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]]]
|
||||
;; All props are optional, background can be nil because is the
|
||||
;; way to remove already set background
|
||||
[:background {:optional true} [:maybe ::ctc/rgb-color]]
|
||||
[:name {:optional true} :string]]]
|
||||
|
||||
[:mod-plugin-data
|
||||
[:map {:title "ModPagePluginData"}
|
||||
[:type [:= :mod-plugin-data]]
|
||||
[:object-type [::sm/one-of #{:file :page :shape :color :typography :component}]]
|
||||
;; It's optional because files don't need the id for type :file
|
||||
[:object-id {:optional true} [:maybe ::sm/uuid]]
|
||||
;; Only needed in type shape
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:namespace :keyword]
|
||||
[:key :string]
|
||||
[:value [:maybe :string]]]]
|
||||
[:set-plugin-data schema:set-plugin-data-change]
|
||||
|
||||
[:del-page
|
||||
[:map {:title "DelPageChange"}
|
||||
@@ -263,11 +374,11 @@
|
||||
(sm/register! ::change schema:change)
|
||||
(sm/register! ::changes schema:changes)
|
||||
|
||||
(def check-change!
|
||||
(sm/check-fn ::change))
|
||||
(def valid-change?
|
||||
(sm/lazy-validator schema:change))
|
||||
|
||||
(def check-changes!
|
||||
(sm/check-fn ::changes))
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Specific helpers
|
||||
@@ -303,10 +414,12 @@
|
||||
;; If object has changed or is new verify is correct
|
||||
(when (and (some? shape-new)
|
||||
(not= shape-old shape-new))
|
||||
(dm/verify!
|
||||
"expected valid shape"
|
||||
(and (cts/check-shape! shape-new)
|
||||
(cts/shape? shape-new))))))]
|
||||
(when-not (and (cts/valid-shape? shape-new)
|
||||
(cts/shape? shape-new))
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint "invalid shape found after applying changes"
|
||||
::sm/explain (cts/explain-shape shape-new))))))]
|
||||
|
||||
(->> (into #{} (map :page-id) items)
|
||||
(mapcat (fn [page-id]
|
||||
@@ -339,12 +452,10 @@
|
||||
(process-changes data items true))
|
||||
|
||||
([data items verify?]
|
||||
;; When verify? false we spec the schema validation. Currently used to make just
|
||||
;; 1 validation even if the changes are applied twice
|
||||
;; When verify? false we spec the schema validation. Currently used
|
||||
;; to make just 1 validation even if the changes are applied twice
|
||||
(when verify?
|
||||
(dm/verify!
|
||||
"expected valid changes"
|
||||
(check-changes! items)))
|
||||
(check-changes! items))
|
||||
|
||||
(binding [*touched-changes* (volatile! #{})]
|
||||
(let [result (reduce #(or (process-change %1 %2) %1) data items)
|
||||
@@ -356,14 +467,71 @@
|
||||
#?(:clj (validate-shapes! data result items))
|
||||
result))))
|
||||
|
||||
;; DEPRECATED: remove after 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]}]
|
||||
@@ -545,6 +713,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)))
|
||||
@@ -603,25 +772,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)
|
||||
@@ -660,6 +838,7 @@
|
||||
[data _]
|
||||
data)
|
||||
|
||||
|
||||
;; -- Media
|
||||
|
||||
(defmethod process-change :add-media
|
||||
|
||||
@@ -135,12 +135,6 @@
|
||||
(or (contains? (meta changes) ::page-id)
|
||||
(contains? (meta changes) ::component-id))))
|
||||
|
||||
(defn- assert-page!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"Call (with-page) before using this function"
|
||||
(contains? (meta changes) ::page)))
|
||||
|
||||
(defn- assert-objects!
|
||||
[changes]
|
||||
(dm/assert!
|
||||
@@ -195,41 +189,85 @@
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn mod-page
|
||||
[changes page new-name]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-page :id (:id page) :name new-name})
|
||||
(update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)})
|
||||
(apply-changes-local)))
|
||||
([changes options]
|
||||
(let [page (::page (meta changes))]
|
||||
(mod-page changes page options)))
|
||||
|
||||
(defn mod-plugin-data
|
||||
([changes page {:keys [name background]}]
|
||||
(let [change {:type :mod-page :id (:id page)}
|
||||
redo (cond-> change
|
||||
(some? name)
|
||||
(assoc :name name)
|
||||
|
||||
(some? background)
|
||||
(assoc :background background))
|
||||
|
||||
undo (cond-> change
|
||||
(some? name)
|
||||
(assoc :name (:name page))
|
||||
|
||||
(some? background)
|
||||
(assoc :background (:background page)))]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj redo)
|
||||
(update :undo-changes conj undo)
|
||||
(apply-changes-local)))))
|
||||
|
||||
(defn set-plugin-data
|
||||
([changes namespace key value]
|
||||
(mod-plugin-data changes :file nil nil namespace key value))
|
||||
(set-plugin-data changes :file nil nil namespace key value))
|
||||
([changes type id namespace key value]
|
||||
(mod-plugin-data changes type id nil namespace key value))
|
||||
(set-plugin-data changes type id nil namespace key value))
|
||||
([changes type id page-id namespace key value]
|
||||
(let [data (::file-data (meta changes))
|
||||
old-val
|
||||
(case type
|
||||
:file
|
||||
(get-in data [:plugin-data namespace key])
|
||||
(dm/get-in data [:plugin-data namespace key])
|
||||
|
||||
:page
|
||||
(get-in data [:pages-index id :options :plugin-data namespace key])
|
||||
(dm/get-in data [:pages-index id :options :plugin-data namespace key])
|
||||
|
||||
:shape
|
||||
(get-in data [:pages-index page-id :objects id :plugin-data namespace key])
|
||||
(dm/get-in data [:pages-index page-id :objects id :plugin-data namespace key])
|
||||
|
||||
:color
|
||||
(get-in data [:colors id :plugin-data namespace key])
|
||||
(dm/get-in data [:colors id :plugin-data namespace key])
|
||||
|
||||
:typography
|
||||
(get-in data [:typographies id :plugin-data namespace key])
|
||||
(dm/get-in data [:typographies id :plugin-data namespace key])
|
||||
|
||||
:component
|
||||
(get-in data [:components id :plugin-data namespace key]))]
|
||||
(dm/get-in data [:components id :plugin-data namespace key]))
|
||||
|
||||
redo-change
|
||||
(cond-> {:type :set-plugin-data
|
||||
:object-type type
|
||||
:namespace namespace
|
||||
:key key
|
||||
:value value}
|
||||
(uuid? id)
|
||||
(assoc :object-id id)
|
||||
|
||||
(uuid? page-id)
|
||||
(assoc :page-id page-id))
|
||||
|
||||
undo-change
|
||||
(cond-> {:type :set-plugin-data
|
||||
:object-type type
|
||||
:namespace namespace
|
||||
:key key
|
||||
:value old-val}
|
||||
(uuid? id)
|
||||
(assoc :object-id id)
|
||||
|
||||
(uuid? page-id)
|
||||
(assoc :page-id page-id))]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value value})
|
||||
(update :undo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value old-val})
|
||||
(update :redo-changes conj redo-change)
|
||||
(update :undo-changes conj undo-change)
|
||||
(apply-changes-local)))))
|
||||
|
||||
(defn del-page
|
||||
@@ -246,42 +284,76 @@
|
||||
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index})
|
||||
(apply-changes-local)))
|
||||
|
||||
(defn set-page-option
|
||||
[changes option-key option-val]
|
||||
(assert-page! changes)
|
||||
(defn set-guide
|
||||
[changes id guide]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (get-in page [:options option-key])]
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:guides id])]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-option
|
||||
(update :redo-changes conj {:type :set-guide
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value option-val})
|
||||
(update :undo-changes conj {:type :set-option
|
||||
:id id
|
||||
:params guide})
|
||||
(update :undo-changes conj {:type :set-guide
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value old-val})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn update-page-option
|
||||
[changes option-key update-fn & args]
|
||||
(assert-page! changes)
|
||||
:id id
|
||||
:params old-val}))))
|
||||
(defn set-flow
|
||||
[changes id flow]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (get-in page [:options option-key])
|
||||
new-val (apply update-fn old-val args)]
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:flows id])
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-option
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value new-val})
|
||||
(update :undo-changes conj {:type :set-option
|
||||
:page-id page-id
|
||||
:option option-key
|
||||
:value old-val})
|
||||
(apply-changes-local))))
|
||||
changes (-> changes
|
||||
(update :redo-changes conj {:type :set-flow
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params flow})
|
||||
(update :undo-changes conj {:type :set-flow
|
||||
:page-id page-id
|
||||
:id id
|
||||
:params old-val}))]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
(defn set-comment-thread-position
|
||||
[changes {:keys [id frame-id position] :as thread}]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
|
||||
old-val (dm/get-in page [:comment-thread-positions id])
|
||||
|
||||
changes (-> changes
|
||||
(update :redo-changes conj {:type :set-comment-thread-position
|
||||
:comment-thread-id id
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:position position})
|
||||
(update :undo-changes conj {:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id id
|
||||
:frame-id (:frame-id old-val)
|
||||
:position (:position old-val)}))]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
(defn set-default-grid
|
||||
[changes type params]
|
||||
(let [page-id (::page-id (meta changes))
|
||||
page (::page (meta changes))
|
||||
old-val (dm/get-in page [:grids type])
|
||||
|
||||
changes (update changes :redo-changes conj {:type :set-default-grid
|
||||
:page-id page-id
|
||||
:grid-type type
|
||||
:params params})
|
||||
changes (update changes :undo-changes conj {:type :set-default-grid
|
||||
:page-id page-id
|
||||
:grid-type type
|
||||
:params old-val})]
|
||||
;; FIXME: not sure if we need this
|
||||
(apply-changes-local changes)))
|
||||
|
||||
;; Shape tree changes
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 51)
|
||||
(def version 57)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.files.defaults :as cfd]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
@@ -863,11 +864,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 +1009,127 @@
|
||||
|
||||
(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? (:background options))
|
||||
(not (contains? page :background)))
|
||||
(assoc :background (:background options))
|
||||
|
||||
(and (some? (:flows options))
|
||||
(or (not (contains? page :flows))
|
||||
(not (map? (:flows page)))))
|
||||
(assoc :flows (d/index-by :id (:flows options)))
|
||||
|
||||
(and (some? (:guides options))
|
||||
(not (contains? page :guides)))
|
||||
(assoc :guides (:guides options))
|
||||
|
||||
(and (some? (:comment-threads-position options))
|
||||
(not (contains? page :comment-thread-positions)))
|
||||
(assoc :comment-thread-positions (:comment-threads-position options))))]
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defn migrate-up-56
|
||||
[data]
|
||||
(letfn [(fix-fills [object]
|
||||
(d/update-when object :fills (partial filterv valid-fill?)))
|
||||
|
||||
(update-object [object]
|
||||
(-> object
|
||||
(fix-fills)
|
||||
|
||||
;; If shape contains shape-ref but has a nil value, we
|
||||
;; should remove it from shape object
|
||||
(cond-> (and (contains? object :shape-ref)
|
||||
(nil? (get object :shape-ref)))
|
||||
(dissoc :shape-ref))
|
||||
|
||||
;; The text shape also can has fills on the text
|
||||
;; fragments so we need to fix fills there
|
||||
(cond-> (cfh/text-shape? object)
|
||||
(update :content (partial txt/transform-nodes identity fix-fills)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
|
||||
(defn migrate-up-57
|
||||
[data]
|
||||
(letfn [(fix-thread-positions [positions]
|
||||
(reduce-kv (fn [result id {:keys [position] :as data}]
|
||||
(let [data (cond
|
||||
(gpt/point? position)
|
||||
data
|
||||
|
||||
(and (map? position)
|
||||
(gpt/valid-point-attrs? position))
|
||||
(assoc data :position (gpt/point position))
|
||||
|
||||
:else
|
||||
(assoc data :position (gpt/point 0 0)))]
|
||||
(assoc result id data)))
|
||||
positions
|
||||
positions))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :comment-thread-positions fix-thread-positions))]
|
||||
|
||||
(-> data
|
||||
(update :pages (fn [pages] (into [] (remove nil?) pages)))
|
||||
(update :pages-index dissoc nil)
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -1059,4 +1172,11 @@
|
||||
{: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}
|
||||
{:id 56 :migrate-up migrate-up-56}
|
||||
{:id 57 :migrate-up migrate-up-57}])
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
[old-page page check-attrs]
|
||||
|
||||
(let [old-objects (get old-page :objects)
|
||||
old-guides (or (get-in old-page [:options :guides]) [])
|
||||
old-guides (or (get old-page :guides) [])
|
||||
|
||||
new-objects (get page :objects)
|
||||
new-guides (or (get-in page [:options :guides]) [])
|
||||
new-guides (or (get page :guides) [])
|
||||
|
||||
changed-object?
|
||||
(fn [id]
|
||||
|
||||
@@ -57,16 +57,17 @@
|
||||
:misplaced-slot
|
||||
:missing-slot})
|
||||
|
||||
(def ^:private
|
||||
schema:error
|
||||
(sm/define
|
||||
[:map {:title "ValidationError"}
|
||||
[:code {:optional false} [::sm/one-of error-codes]]
|
||||
[:hint {:optional false} :string]
|
||||
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
||||
[:shape-id {:optional true} ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]]))
|
||||
(def ^:private schema:error
|
||||
[:map {:title "ValidationError"}
|
||||
[:code {:optional false} [::sm/one-of error-codes]]
|
||||
[:hint {:optional false} :string]
|
||||
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
||||
[:shape-id {:optional true} ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def check-error!
|
||||
(sm/check-fn schema:error))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ERROR HANDLING
|
||||
@@ -95,7 +96,7 @@
|
||||
|
||||
(dm/assert!
|
||||
"expected valid error"
|
||||
(sm/check! schema:error error))
|
||||
(check-error! error))
|
||||
|
||||
(vswap! *errors* conj error)))
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
java.time.Instant
|
||||
java.time.OffsetDateTime
|
||||
java.util.List
|
||||
linked.map.LinkedMap
|
||||
org.fressian.Reader
|
||||
org.fressian.StreamingWriter
|
||||
org.fressian.Writer
|
||||
@@ -109,6 +110,13 @@
|
||||
(clojure.lang.PersistentArrayMap. (.toArray kvs))
|
||||
(clojure.lang.PersistentHashMap/create (seq kvs)))))
|
||||
|
||||
(defn read-ordered-map
|
||||
[^Reader rdr]
|
||||
(let [kvs ^java.util.List (read-object! rdr)]
|
||||
(reduce #(assoc %1 (first %2) (second %2))
|
||||
(d/ordered-map)
|
||||
(partition-all 2 (seq kvs)))))
|
||||
|
||||
(def ^:dynamic *write-handler-lookup* nil)
|
||||
(def ^:dynamic *read-handler-lookup* nil)
|
||||
|
||||
@@ -225,6 +233,11 @@
|
||||
:wfn write-map-like
|
||||
:rfn read-map-like}
|
||||
|
||||
{:name "linked/map"
|
||||
:class LinkedMap
|
||||
:wfn write-map-like
|
||||
:rfn read-ordered-map}
|
||||
|
||||
{:name "clj/keyword"
|
||||
:class clojure.lang.Keyword
|
||||
:wfn write-named
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
[:x ::sm/safe-number]
|
||||
[:y ::sm/safe-number]])
|
||||
|
||||
(def valid-point-attrs?
|
||||
(sm/validator schema:point-attrs))
|
||||
|
||||
(def valid-point?
|
||||
(sm/validator
|
||||
[:and [:fn point?] schema:point-attrs]))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn shape-stroke-margin
|
||||
@@ -60,6 +61,7 @@
|
||||
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
|
||||
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
|
||||
filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)]
|
||||
|
||||
(grc/make-rect filter-x filter-y filter-w filter-h)))
|
||||
|
||||
(defn get-rect-filter-bounds
|
||||
@@ -96,12 +98,15 @@
|
||||
([shape ignore-margin?]
|
||||
(let [strokes (:strokes shape)
|
||||
|
||||
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||
^boolean (gsh/open-path? shape))
|
||||
|
||||
stroke-width
|
||||
(->> strokes
|
||||
(map #(case (get % :stroke-alignment :center)
|
||||
:center (/ (:stroke-width % 0) 2)
|
||||
:outer (:stroke-width % 0)
|
||||
0))
|
||||
(if open-path? (:stroke-width % 0) 0)))
|
||||
(reduce d/max 0))
|
||||
|
||||
stroke-margin
|
||||
|
||||
@@ -852,8 +852,10 @@
|
||||
|
||||
(defn ray-overlaps?
|
||||
[ray-point {selrect :selrect}]
|
||||
(and (>= (:y ray-point) (:y1 selrect))
|
||||
(<= (:y ray-point) (:y2 selrect))))
|
||||
(and (or (> (:y ray-point) (:y1 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y1 selrect))))
|
||||
(or (< (:y ray-point) (:y2 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y2 selrect))))))
|
||||
|
||||
(defn content->geom-data
|
||||
[content]
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
[(:parent-id first-shape)]
|
||||
(fn [shape objects]
|
||||
(-> shape
|
||||
(ctl/assign-cells objects)
|
||||
(ctl/push-into-cell [(:id first-shape)] row column)
|
||||
(ctl/assign-cells objects)))
|
||||
{:with-objects? true})
|
||||
@@ -1947,54 +1948,55 @@
|
||||
|
||||
(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
|
||||
:axis (:axis guide)}]
|
||||
(pcb/set-guide changes guide-id guide))
|
||||
changes))
|
||||
changes
|
||||
guides)))
|
||||
(pcb/with-page changes page)
|
||||
frames)))
|
||||
|
||||
(defn generate-duplicate-component-change
|
||||
[changes objects page component-root parent-id frame-id delta libraries library-data]
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
(ns app.common.logic.shapes
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]))
|
||||
@@ -85,7 +83,9 @@
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data file))
|
||||
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
@@ -110,30 +110,21 @@
|
||||
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]
|
||||
;; All parents of any deleted shape must be resized.
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
ids-to-delete)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
@@ -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]))
|
||||
|
||||
|
||||
@@ -389,13 +391,14 @@
|
||||
(-> (pcb/update-shapes
|
||||
[parent-id]
|
||||
(fn [frame objects]
|
||||
(-> frame
|
||||
;; Assign the cell when pushing into a specific grid cell
|
||||
(cond-> (some? cell)
|
||||
(-> (ctl/free-cell-shapes ids)
|
||||
(ctl/push-into-cell ids (:row cell) (:column cell))
|
||||
(ctl/assign-cells objects)))
|
||||
(ctl/assign-cell-positions objects)))
|
||||
(let [[row column] cell]
|
||||
(-> frame
|
||||
;; Assign the cell when pushing into a specific grid cell
|
||||
(cond-> (some? cell)
|
||||
(-> (ctl/free-cell-shapes ids)
|
||||
(ctl/push-into-cell ids row column)
|
||||
(ctl/assign-cells objects)))
|
||||
(ctl/assign-cell-positions objects))))
|
||||
{:with-objects? true})
|
||||
(pcb/reorder-grid-children [parent-id])))
|
||||
|
||||
@@ -406,17 +409,12 @@
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn change-show-in-viewer [shape hide?]
|
||||
(cond-> (assoc shape :hide-in-viewer hide?)
|
||||
;; When a frame is no longer shown in view mode, it cannot have interactions
|
||||
hide?
|
||||
(dissoc :interactions)))
|
||||
(assoc shape :hide-in-viewer hide?))
|
||||
|
||||
(defn add-new-interaction [shape interaction]
|
||||
(-> shape
|
||||
(update :interactions ctsi/add-interaction interaction)
|
||||
;; When a interaction is created, the frame must be shown in view mode
|
||||
(dissoc :hide-in-viewer)))
|
||||
(update :interactions ctsi/add-interaction interaction)))
|
||||
|
||||
(defn show-in-viewer [shape]
|
||||
(dissoc shape :hide-in-viewer))
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
@@ -29,11 +28,6 @@
|
||||
[malli.util :as mu]))
|
||||
|
||||
(defprotocol ILazySchema
|
||||
(-get-schema [_])
|
||||
(-get-validator [_])
|
||||
(-get-explainer [_])
|
||||
(-get-decoder [_])
|
||||
(-get-encoder [_])
|
||||
(-validate [_ o])
|
||||
(-explain [_ o])
|
||||
(-decode [_ o]))
|
||||
@@ -53,27 +47,21 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn lazy-schema?
|
||||
(defn- lazy-schema?
|
||||
[s]
|
||||
(satisfies? ILazySchema s))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-schema s)
|
||||
(m/schema s default-options)))
|
||||
(m/schema s default-options))
|
||||
|
||||
(defn validate
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(-validate s value)
|
||||
(m/validate s value default-options)))
|
||||
(m/validate s value default-options))
|
||||
|
||||
(defn explain
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(-explain s value)
|
||||
(m/explain s value default-options)))
|
||||
(m/explain s value default-options))
|
||||
|
||||
(defn simplify
|
||||
"Given an explain data structure, return a simplified version of it"
|
||||
@@ -171,29 +159,19 @@
|
||||
|
||||
(defn validator
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-validator s)
|
||||
(-> s schema m/validator)))
|
||||
(-> s schema m/validator))
|
||||
|
||||
(defn explainer
|
||||
[s]
|
||||
(if (lazy-schema? s)
|
||||
(-get-explainer s)
|
||||
(-> s schema m/explainer)))
|
||||
(-> s schema m/explainer))
|
||||
|
||||
(defn encoder
|
||||
([s]
|
||||
(assert (lazy-schema? s) "expected lazy schema")
|
||||
(-get-decoder s))
|
||||
([s transformer]
|
||||
(m/encoder s default-options transformer))
|
||||
([s options transformer]
|
||||
(m/encoder s options transformer)))
|
||||
|
||||
(defn decoder
|
||||
([s]
|
||||
(assert (lazy-schema? s) "expected lazy schema")
|
||||
(-get-decoder s))
|
||||
([s transformer]
|
||||
(m/decoder s default-options transformer))
|
||||
([s options transformer]
|
||||
@@ -242,6 +220,8 @@
|
||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
||||
|
||||
(defn pretty-explain
|
||||
"A helper that allows print a console-friendly output for the
|
||||
explain; should not be used for other purposes"
|
||||
[explain & {:keys [variant message]
|
||||
:or {variant ::explain
|
||||
message "Validation Error"}}]
|
||||
@@ -259,102 +239,50 @@
|
||||
([s] (lookup sr/default-registry s))
|
||||
([registry s] (schema (mr/schema registry s))))
|
||||
|
||||
(defn fast-check!
|
||||
(defn- fast-check!
|
||||
"A fast path for checking process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
[s value]
|
||||
[s type code hint value]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [hint (d/nilv dm/*assert-context* "check error")
|
||||
explain (-explain s value)]
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :data-validation
|
||||
(let [explain (-explain s value)]
|
||||
(throw (ex-info hint {:type type
|
||||
:code code
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
true)
|
||||
value)
|
||||
|
||||
(declare define)
|
||||
(declare ^:private lazy-schema)
|
||||
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (define s))]
|
||||
(partial fast-check! schema)))
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [schema (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(partial fast-check! schema type code hint)))
|
||||
|
||||
(defn check!
|
||||
"A helper intended to be used on assertions for validate/check the
|
||||
schema over provided data. Raises an assertion exception, should be
|
||||
used together with `dm/assert!` or `dm/verify!`."
|
||||
[s value]
|
||||
(if (lazy-schema? s)
|
||||
(fast-check! s value)
|
||||
(do
|
||||
(when-not ^boolean (m/validate s value default-options)
|
||||
(let [hint (d/nilv dm/*assert-context* "check error")
|
||||
explain (explain s value)]
|
||||
(throw (ex-info hint {:type :assertion
|
||||
:code :data-validation
|
||||
:hint hint
|
||||
::explain explain}))))
|
||||
true)))
|
||||
|
||||
(defn fast-validate!
|
||||
"A fast path for validation process, assumes the ILazySchema protocol
|
||||
implemented on the provided `s` schema. Sould not be used directly."
|
||||
([s value] (fast-validate! s value nil))
|
||||
([s value options]
|
||||
(when-not ^boolean (-validate s value)
|
||||
(let [explain (-explain s value)
|
||||
options (into {:type :validation
|
||||
:code :data-validation
|
||||
::explain explain}
|
||||
options)
|
||||
hint (get options :hint "schema validation error")]
|
||||
(throw (ex-info hint options))))))
|
||||
|
||||
(defn validate-fn
|
||||
"Create a predefined validate function that raises an expception"
|
||||
[s]
|
||||
(let [schema (if (lazy-schema? s) s (define s))]
|
||||
(partial fast-validate! schema)))
|
||||
|
||||
(defn validate!
|
||||
"A generic validation function for predefined schemas."
|
||||
([s value] (validate! s value nil))
|
||||
([s value options]
|
||||
(if (lazy-schema? s)
|
||||
(fast-validate! s value options)
|
||||
(when-not ^boolean (m/validate s value default-options)
|
||||
(let [explain (explain s value)
|
||||
options (into {:type :validation
|
||||
:code :data-validation
|
||||
::explain explain}
|
||||
options)
|
||||
hint (get options :hint "schema validation error")]
|
||||
(throw (ex-info hint options)))))))
|
||||
|
||||
;; FIXME: revisit
|
||||
(defn conform!
|
||||
[schema value]
|
||||
(assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol")
|
||||
(let [params (-decode schema value)]
|
||||
(fast-validate! schema params nil)
|
||||
params))
|
||||
schema over provided data. Raises an assertion exception."
|
||||
[s value & {:keys [hint type code]}]
|
||||
(let [s (if (lazy-schema? s) s (lazy-schema s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
(fast-check! s type code hint value)))
|
||||
|
||||
(defn register! [type s]
|
||||
(let [s (if (map? s) (m/-simple-schema s) s)]
|
||||
(swap! sr/registry assoc type s)
|
||||
nil))
|
||||
|
||||
(defn define
|
||||
(defn- lazy-schema
|
||||
"Create ans instance of ILazySchema"
|
||||
[s & {:keys [transformer] :or {transformer json-transformer} :as options}]
|
||||
[s]
|
||||
(let [schema (delay (schema s))
|
||||
validator (delay (m/validator @schema))
|
||||
explainer (delay (m/explainer @schema))
|
||||
|
||||
options (c/merge default-options (dissoc options :transformer))
|
||||
decoder (delay (m/decoder @schema options transformer))
|
||||
encoder (delay (m/encoder @schema options transformer))]
|
||||
explainer (delay (m/explainer @schema))]
|
||||
|
||||
(reify
|
||||
m/AST
|
||||
@@ -397,16 +325,6 @@
|
||||
(m/-form @schema))
|
||||
|
||||
ILazySchema
|
||||
(-get-schema [_]
|
||||
@schema)
|
||||
(-get-validator [_]
|
||||
@validator)
|
||||
(-get-explainer [_]
|
||||
@explainer)
|
||||
(-get-encoder [_]
|
||||
@encoder)
|
||||
(-get-decoder [_]
|
||||
@decoder)
|
||||
(-validate [_ o]
|
||||
(@validator o))
|
||||
(-explain [_ o]
|
||||
@@ -448,7 +366,7 @@
|
||||
(defn parse-email
|
||||
[s]
|
||||
(if (string? s)
|
||||
(re-matches email-re s)
|
||||
(first (re-seq email-re s))
|
||||
nil))
|
||||
|
||||
(defn email-string?
|
||||
@@ -476,7 +394,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 +407,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 +416,6 @@
|
||||
(nil? kind) any?
|
||||
:else (validator kind))
|
||||
|
||||
encode-child
|
||||
(encoder kind string-transformer)
|
||||
|
||||
pred
|
||||
(cond
|
||||
(and max min)
|
||||
@@ -531,31 +443,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 +499,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 +534,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 +567,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 +585,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 +601,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"}
|
||||
@@ -742,13 +681,13 @@
|
||||
(let [pred int?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)]
|
||||
|
||||
{:pred pred
|
||||
@@ -780,13 +719,13 @@
|
||||
(let [pred double?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)]
|
||||
|
||||
{:pred pred
|
||||
@@ -810,13 +749,13 @@
|
||||
(let [pred number?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)
|
||||
|
||||
gen (sg/one-of
|
||||
@@ -1041,6 +980,12 @@
|
||||
(def check-email!
|
||||
(check-fn ::email))
|
||||
|
||||
(def check-uuid!
|
||||
(check-fn ::uuid :hint "expected valid uuid instance"))
|
||||
|
||||
(def check-string!
|
||||
(check-fn :string :hint "expected string"))
|
||||
|
||||
(def check-coll-of-uuid!
|
||||
(check-fn ::coll-of-uuid))
|
||||
|
||||
|
||||
@@ -5,46 +5,21 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.generators
|
||||
(:refer-clojure :exclude [set subseq uuid for filter map let boolean])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.schema.registry :as sr]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.core :as c]
|
||||
[clojure.test.check :as tc]
|
||||
[clojure.test.check.generators :as tg]
|
||||
[clojure.test.check.properties :as tp]
|
||||
[cuerdas.core :as str]
|
||||
[malli.generator :as mg]))
|
||||
|
||||
(defn default-reporter-fn
|
||||
[{:keys [type result] :as args}]
|
||||
(case type
|
||||
:complete
|
||||
(prn (select-keys args [:result :num-tests :seed "time-elapsed-ms"]))
|
||||
|
||||
:failure
|
||||
(do
|
||||
(prn (select-keys args [:num-tests :seed :failed-after-ms]))
|
||||
(when #?(:clj (instance? Throwable result)
|
||||
:cljs (instance? js/Error result))
|
||||
(throw result)))
|
||||
|
||||
nil))
|
||||
|
||||
(defmacro for
|
||||
[& params]
|
||||
`(tp/for-all ~@params))
|
||||
|
||||
(defmacro let
|
||||
[& params]
|
||||
`(tg/let ~@params))
|
||||
|
||||
(defn check!
|
||||
[p & {:keys [num] :or {num 20} :as options}]
|
||||
(tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50)))
|
||||
|
||||
(defn sample
|
||||
([g]
|
||||
(mg/sample g {:registry sr/default-registry}))
|
||||
@@ -83,6 +58,11 @@
|
||||
(tg/such-that (fn [v] (>= (count v) 4)) $$ 100)
|
||||
(tg/fmap str/lower $$)))
|
||||
|
||||
(defn word-keyword
|
||||
[]
|
||||
(->> (word-string)
|
||||
(tg/fmap keyword)))
|
||||
|
||||
(defn email
|
||||
[]
|
||||
(->> (word-string)
|
||||
@@ -91,7 +71,6 @@
|
||||
(tg/fmap (fn [v]
|
||||
(str v "@example.net")))))
|
||||
|
||||
|
||||
(defn uri
|
||||
[]
|
||||
(tg/let [scheme (tg/elements ["http" "https"])
|
||||
@@ -103,8 +82,7 @@
|
||||
|
||||
(defn uuid
|
||||
[]
|
||||
(->> tg/small-integer
|
||||
(tg/fmap (fn [_] (uuid/next)))))
|
||||
(tg/fmap (fn [_] (uuid/next)) (small-int)))
|
||||
|
||||
(defn subseq
|
||||
"Given a collection, generates \"subsequences\" which are sequences
|
||||
|
||||
97
common/src/app/common/schema/test.cljc
Normal file
97
common/src/app/common/schema/test.cljc
Normal file
@@ -0,0 +1,97 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.schema.test
|
||||
(:refer-clojure :exclude [for])
|
||||
#?(:cljs (:require-macros [app.common.schema.test]))
|
||||
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[clojure.test :as ct]
|
||||
[clojure.test.check :as tc]
|
||||
[clojure.test.check.properties :as tp]))
|
||||
|
||||
(defn- get-testing-var
|
||||
[]
|
||||
(let [testing-vars #?(:clj ct/*testing-vars*
|
||||
:cljs (:testing-vars ct/*current-env*))]
|
||||
(first testing-vars)))
|
||||
|
||||
(defn- get-testing-sym
|
||||
[var]
|
||||
(let [tmeta (meta var)]
|
||||
(:name tmeta)))
|
||||
|
||||
(defn default-reporter-fn
|
||||
"Default function passed as the :reporter-fn to clojure.test.check/quick-check.
|
||||
Delegates to clojure.test/report."
|
||||
[{:keys [type] :as args}]
|
||||
(case type
|
||||
:complete
|
||||
(ct/report {:type ::complete ::params args})
|
||||
|
||||
:trial
|
||||
(ct/report {:type ::trial ::params args})
|
||||
|
||||
:failure
|
||||
(ct/report {:type ::fail ::params args})
|
||||
|
||||
:shrunk
|
||||
(ct/report {:type ::thrunk ::params args})
|
||||
|
||||
nil))
|
||||
|
||||
(defmethod ct/report #?(:clj ::complete :cljs [:cljs.test/default ::complete])
|
||||
[{:keys [::params] :as m}]
|
||||
#?(:clj (ct/inc-report-counter :pass)
|
||||
:cljs (ct/inc-report-counter! :pass))
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
time (:time-elapsed-ms params)]
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=TRUE, tests=" (:num-tests params) ", seed=" (:seed params) ", elapsed=" time "ms)"))))
|
||||
|
||||
(defmethod ct/report #?(:clj ::thrunk :cljs [:cljs.test/default ::thrunk])
|
||||
[{:keys [::params] :as m}]
|
||||
(let [smallest (-> params :shrunk :smallest vec)]
|
||||
(println)
|
||||
(println "Condition failed with the following params:")
|
||||
(println)
|
||||
(pp/pprint smallest)))
|
||||
|
||||
(defmethod ct/report #?(:clj ::trial :cljs [:cljs.test/default ::trial])
|
||||
[_]
|
||||
#?(:clj (ct/inc-report-counter :pass)
|
||||
:cljs (ct/inc-report-counter! :pass)))
|
||||
|
||||
(defmethod ct/report #?(:clj ::fail :cljs [:cljs.test/default ::fail])
|
||||
[{:keys [::params] :as m}]
|
||||
#?(:clj (ct/inc-report-counter :fail)
|
||||
:cljs (ct/inc-report-counter! :fail))
|
||||
(let [tvar (get-testing-var)
|
||||
tsym (get-testing-sym tvar)
|
||||
res (:result params)]
|
||||
(println)
|
||||
(println "Generative test:" (str "'" tsym "'")
|
||||
(str "(pass=FALSE, tests=" (:num-tests params) ", seed=" (:seed params) ")"))
|
||||
|
||||
(when (ex/exception? res)
|
||||
#?(:clj (ex/print-throwable res)
|
||||
:cljs (js/console.error res)))))
|
||||
|
||||
(defmacro for
|
||||
[bindings & body]
|
||||
`(tp/for-all ~bindings ~@body))
|
||||
|
||||
(defn check!
|
||||
[p & {:keys [num] :or {num 20} :as options}]
|
||||
(let [result (tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50))
|
||||
pass? (:pass? result)
|
||||
total-tests (:num-tests result)]
|
||||
|
||||
(ct/is (= num total-tests))
|
||||
(ct/is (true? pass?))))
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
@@ -29,12 +30,12 @@
|
||||
{:x 0 :y 0 :width 1 :height 1})
|
||||
|
||||
(defn- assert-valid-num [attr num]
|
||||
(dm/verify!
|
||||
["%1 attribute has invalid value: %2" (d/name attr) num]
|
||||
(and (d/num? num)
|
||||
(<= num max-safe-int)
|
||||
(>= num min-safe-int)))
|
||||
|
||||
(when-not (and (d/num? num)
|
||||
(<= num max-safe-int)
|
||||
(>= num min-safe-int))
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "invalid numeric value for `" attr "`: " num)))
|
||||
(cond
|
||||
(and (> num 0) (< num 1)) 1
|
||||
(and (< num 0) (> num -1)) -1
|
||||
@@ -43,19 +44,21 @@
|
||||
(defn- assert-valid-pos-num
|
||||
[attr num]
|
||||
|
||||
(dm/verify!
|
||||
["%1 attribute should be positive" (d/name attr)]
|
||||
(pos? num))
|
||||
|
||||
(when-not (pos? num)
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "invalid numeric value for `" attr "`: " num " (should be positive)")))
|
||||
num)
|
||||
|
||||
(defn- assert-valid-blend-mode
|
||||
[mode]
|
||||
(let [clean-value (-> mode str/trim str/lower keyword)]
|
||||
(dm/verify!
|
||||
["%1 is not a valid blend mode" clean-value]
|
||||
(contains? cts/blend-modes clean-value))
|
||||
clean-value))
|
||||
(let [value (-> mode str/trim str/lower keyword)]
|
||||
|
||||
(when-not (contains? cts/blend-modes value)
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint (str "unexpected blend mode: " value)))
|
||||
value))
|
||||
|
||||
(defn- svg-dimensions
|
||||
[{:keys [attrs] :as data}]
|
||||
|
||||
@@ -78,6 +78,12 @@
|
||||
|
||||
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
|
||||
|
||||
(def text-style-attrs
|
||||
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
|
||||
|
||||
(def default-root-attrs
|
||||
{:vertical-align "top"})
|
||||
|
||||
(def default-text-attrs
|
||||
{:typography-ref-file nil
|
||||
:typography-ref-id nil
|
||||
@@ -92,9 +98,13 @@
|
||||
:text-transform "none"
|
||||
:text-align "left"
|
||||
:text-decoration "none"
|
||||
:text-direction "ltr"
|
||||
:fills [{:fill-color clr/black
|
||||
:fill-opacity 1}]})
|
||||
|
||||
(def default-attrs
|
||||
(merge default-root-attrs default-text-attrs))
|
||||
|
||||
(def typography-fields
|
||||
[:font-id
|
||||
:font-family
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
(def schema:image-color
|
||||
[:map {:title "ImageColor"}
|
||||
[:name {:optional true} :string]
|
||||
[:width :int]
|
||||
[:height :int]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:id ::sm/uuid]
|
||||
[:keep-aspect-ratio {:optional true} :boolean]])
|
||||
@@ -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
|
||||
@@ -111,12 +113,13 @@
|
||||
(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 valid-color?
|
||||
(sm/lazy-validator schema:color))
|
||||
(def check-color!
|
||||
(sm/check-fn schema:color :hint "expected valid color struct"))
|
||||
|
||||
(def valid-recent-color?
|
||||
(sm/lazy-validator schema:recent-color))
|
||||
(def check-recent-color!
|
||||
(sm/check-fn schema:recent-color))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.types.grid
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as ctc]))
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
[:display :boolean]
|
||||
[:params schema:square-params]]]])
|
||||
|
||||
(def schema:saved-grids
|
||||
(def schema:default-grids
|
||||
[:map {:title "PageGrid"}
|
||||
[:square {:optional true} ::square-params]
|
||||
[:row {:optional true} ::column-params]
|
||||
@@ -63,4 +64,24 @@
|
||||
(sm/register! ::square-params schema:square-params)
|
||||
(sm/register! ::column-params schema:column-params)
|
||||
(sm/register! ::grid schema:grid)
|
||||
(sm/register! ::saved-grids schema:saved-grids)
|
||||
(sm/register! ::default-grids schema:default-grids)
|
||||
|
||||
(def ^:private default-square-params
|
||||
{:size 16
|
||||
:color {:color clr/info
|
||||
:opacity 0.4}})
|
||||
|
||||
(def ^:private default-layout-params
|
||||
{:size 12
|
||||
:type :stretch
|
||||
:item-length nil
|
||||
:gutter 8
|
||||
:margin 0
|
||||
:color {:color clr/default-layout
|
||||
:opacity 0.1}})
|
||||
|
||||
(def default-grid-params
|
||||
{:square default-square-params
|
||||
:column default-layout-params
|
||||
:row default-layout-params})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.common.types.page
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as-alias gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.color :as-alias ctc]
|
||||
[app.common.types.grid :as ctg]
|
||||
@@ -24,38 +25,56 @@
|
||||
[:name :string]
|
||||
[:starting-frame ::sm/uuid]])
|
||||
|
||||
(def schema:flows
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:flow])
|
||||
|
||||
(def schema:guide
|
||||
[:map {:title "Guide"}
|
||||
[:id ::sm/uuid]
|
||||
[:axis [::sm/one-of #{:x :y}]]
|
||||
[:position ::sm/safe-number]
|
||||
;; FIXME: remove maybe?
|
||||
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def schema:guides
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
|
||||
|
||||
(def schema:objects
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape])
|
||||
|
||||
(def schema:comment-thread-position
|
||||
[:map {:title "CommentThreadPosition"}
|
||||
[:frame-id ::sm/uuid]
|
||||
[:position ::gpt/point]])
|
||||
|
||||
(def schema:page
|
||||
[:map {:title "FilePage"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:objects
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape]]
|
||||
[:objects schema:objects]
|
||||
[:default-grids {:optional true} ::ctg/default-grids]
|
||||
[:flows {:optional true} schema:flows]
|
||||
[:guides {:optional true} schema:guides]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]
|
||||
[:background {:optional true} ::ctc/rgb-color]
|
||||
|
||||
[:comment-thread-positions {:optional true}
|
||||
[:map-of ::sm/uuid schema:comment-thread-position]]
|
||||
|
||||
[:options
|
||||
[:map {:title "PageOptions"}
|
||||
[:background {:optional true} ::ctc/rgb-color]
|
||||
[:saved-grids {:optional true} ::ctg/saved-grids]
|
||||
[:flows {:optional true}
|
||||
[:vector {:gen/max 2} schema:flow]]
|
||||
[:guides {:optional true}
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide]]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]]]])
|
||||
;; DEPERECATED: remove after 2.3 release
|
||||
[:map {:title "PageOptions"}]]])
|
||||
|
||||
(sm/register! ::page schema:page)
|
||||
(sm/register! ::guide schema:guide)
|
||||
(sm/register! ::flow schema:flow)
|
||||
|
||||
(def check-page-guide!
|
||||
(sm/check-fn ::guide))
|
||||
(def valid-guide?
|
||||
(sm/lazy-validator schema:guide))
|
||||
|
||||
;; FIXME: convert to validator
|
||||
(def check-page!
|
||||
(sm/check-fn ::page))
|
||||
(sm/check-fn schema:page))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INIT & HELPERS
|
||||
@@ -80,25 +99,6 @@
|
||||
(assoc :id (or id (uuid/next)))
|
||||
(assoc :name (or name "Page 1"))))
|
||||
|
||||
;; --- Helpers for flow
|
||||
|
||||
(defn rename-flow
|
||||
[flow name]
|
||||
(assoc flow :name name))
|
||||
|
||||
(defn add-flow
|
||||
[flows flow]
|
||||
(conj (or flows []) flow))
|
||||
|
||||
(defn remove-flow
|
||||
[flows flow-id]
|
||||
(d/removev #(= (:id %) flow-id) flows))
|
||||
|
||||
(defn update-flow
|
||||
[flows flow-id update-fn]
|
||||
(let [index (d/index-of-pred flows #(= (:id %) flow-id))]
|
||||
(update flows index update-fn)))
|
||||
|
||||
(defn get-frame-flow
|
||||
[flows frame-id]
|
||||
(d/seek #(= (:starting-frame %) frame-id) flows))
|
||||
(d/seek #(= (:starting-frame %) frame-id) (vals flows)))
|
||||
|
||||
@@ -29,3 +29,25 @@
|
||||
schema:string]])
|
||||
|
||||
(sm/register! ::plugin-data schema:plugin-data)
|
||||
|
||||
|
||||
(def ^:private schema:registry-entry
|
||||
[:map
|
||||
[:plugin-id :string]
|
||||
[:name :string]
|
||||
[:description {:optional true} :string]
|
||||
[:host :string]
|
||||
[:code :string]
|
||||
[:icon {:optional true} :string]
|
||||
[:permissions [:set :string]]])
|
||||
|
||||
(def schema:plugin-registry
|
||||
[:map
|
||||
[:ids [:vector :string]]
|
||||
[:data
|
||||
[:map-of {:gen/max 5}
|
||||
:string
|
||||
schema:registry-entry]]])
|
||||
|
||||
(sm/register! ::plugin-registry schema:plugin-registry)
|
||||
(sm/register! ::registry-entry schema:registry-entry)
|
||||
|
||||
@@ -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]
|
||||
@@ -221,8 +224,8 @@
|
||||
[:map {:title "ImageAttrs"}
|
||||
[:metadata
|
||||
[:map
|
||||
[:width {:gen/gen (sg/small-int :min 1)} :int]
|
||||
[:height {:gen/gen (sg/small-int :min 1)} :int]
|
||||
[:width {:gen/gen (sg/small-int :min 1)} ::sm/int]
|
||||
[:height {:gen/gen (sg/small-int :min 1)} ::sm/int]
|
||||
[:mtype {:optional true
|
||||
:gen/gen (sg/elements ["image/jpeg"
|
||||
"image/png"])}
|
||||
@@ -352,7 +355,14 @@
|
||||
(sm/check-fn schema:shape-attrs))
|
||||
|
||||
(def check-shape!
|
||||
(sm/check-fn schema:shape))
|
||||
(sm/check-fn schema:shape
|
||||
:hint "expected valid shape"))
|
||||
|
||||
(def valid-shape?
|
||||
(sm/lazy-validator schema:shape))
|
||||
|
||||
(def explain-shape
|
||||
(sm/lazy-explainer schema:shape))
|
||||
|
||||
(defn has-images?
|
||||
[{:keys [fills strokes]}]
|
||||
|
||||
@@ -27,3 +27,6 @@
|
||||
[:color ::ctc/color]])
|
||||
|
||||
(sm/register! ::shadow schema:shadow)
|
||||
|
||||
(def check-shadow!
|
||||
(sm/check-fn schema:shadow))
|
||||
|
||||
864
common/test/common_tests/files_changes_test.cljc
Normal file
864
common/test/common_tests/files_changes_test.cljc
Normal file
@@ -0,0 +1,864 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.files-changes-test
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]
|
||||
[common-tests.types.shape-decode-encode-test :refer [json-roundtrip]]))
|
||||
|
||||
(defn- make-file-data
|
||||
[file-id page-id]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data file-id page-id)))
|
||||
|
||||
(t/deftest add-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
id-a (uuid/custom 2 1)
|
||||
id-b (uuid/custom 2 2)
|
||||
id-c (uuid/custom 2 3)]
|
||||
|
||||
(t/testing "Adds single object"
|
||||
(let [chg {:type :add-obj
|
||||
:page-id page-id
|
||||
:id id-a
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:id id-a
|
||||
:type :rect
|
||||
:name "rect"})}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 2 (count objects)))
|
||||
(t/is (= (:obj chg) (get objects id-a)))
|
||||
|
||||
(t/is (= [id-a] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
|
||||
(t/testing "Adds several objects with different indexes"
|
||||
(let [chg (fn [id index]
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:frame-id uuid/zero
|
||||
:index index
|
||||
:obj (cts/setup-shape
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name (str id)})})
|
||||
res (ch/process-changes data [(chg id-a 0)
|
||||
(chg id-b 0)
|
||||
(chg id-c 1)])]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (clojure.pprint/pprint res)
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 4 (count objects)))
|
||||
(t/is (not (nil? (get objects id-a))))
|
||||
(t/is (not (nil? (get objects id-b))))
|
||||
(t/is (not (nil? (get objects id-c))))
|
||||
(t/is (= [id-b id-c id-a] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
(t/deftest mod-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "simple mod-obj"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id uuid/zero
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= "foobar" (get-in objects [uuid/zero :name]))))))
|
||||
|
||||
(t/testing "mod-obj for not existing shape"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id (uuid/next)
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= res data))))))
|
||||
|
||||
|
||||
(t/deftest del-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
id (uuid/custom 2 1)
|
||||
data (make-file-data file-id page-id)
|
||||
data (-> data
|
||||
(assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id])
|
||||
(assoc-in [:pages-index page-id :objects id]
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name "rect"}))]
|
||||
(t/testing "delete"
|
||||
(let [chg {:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 1 (count objects)))
|
||||
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
(t/testing "delete idempotency"
|
||||
(let [chg {:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}
|
||||
res1 (ch/process-changes data [chg])
|
||||
res2 (ch/process-changes res1 [chg])]
|
||||
|
||||
(t/is (= res1 res2))
|
||||
(let [objects (get-in res1 [:pages-index page-id :objects])]
|
||||
(t/is (= 1 (count objects)))
|
||||
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
|
||||
(t/deftest move-objects-1
|
||||
(let [frame-a-id (uuid/custom 0 1)
|
||||
frame-b-id (uuid/custom 0 2)
|
||||
group-a-id (uuid/custom 0 3)
|
||||
group-b-id (uuid/custom 0 4)
|
||||
rect-a-id (uuid/custom 0 5)
|
||||
rect-b-id (uuid/custom 0 6)
|
||||
rect-c-id (uuid/custom 0 7)
|
||||
rect-d-id (uuid/custom 0 8)
|
||||
rect-e-id (uuid/custom 0 9)
|
||||
|
||||
file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
|
||||
data (update-in data [:pages-index page-id :objects]
|
||||
#(-> %
|
||||
(assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id])
|
||||
(assoc-in [frame-a-id]
|
||||
(cts/setup-shape
|
||||
{:id frame-a-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:name "Frame a"
|
||||
:shapes [group-a-id group-b-id rect-e-id]
|
||||
:type :frame}))
|
||||
|
||||
(assoc-in [frame-b-id]
|
||||
(cts/setup-shape
|
||||
{:id frame-b-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:name "Frame b"
|
||||
:shapes []
|
||||
:type :frame}))
|
||||
|
||||
;; Groups
|
||||
(assoc-in [group-a-id]
|
||||
(cts/setup-shape
|
||||
{:id group-a-id
|
||||
:name "Group A"
|
||||
:type :group
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id
|
||||
:shapes [rect-a-id rect-b-id rect-c-id]}))
|
||||
(assoc-in [group-b-id]
|
||||
(cts/setup-shape
|
||||
{:id group-b-id
|
||||
:name "Group B"
|
||||
:type :group
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id
|
||||
:shapes [rect-d-id]}))
|
||||
|
||||
;; Shapes
|
||||
(assoc-in [rect-a-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-a-id
|
||||
:name "Rect A"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-b-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-b-id
|
||||
:name "Rect B"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-c-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-c-id
|
||||
:name "Rect C"
|
||||
:type :rect
|
||||
:parent-id group-a-id
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-d-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-d-id
|
||||
:name "Rect D"
|
||||
:parent-id group-b-id
|
||||
:type :rect
|
||||
:frame-id frame-a-id}))
|
||||
|
||||
(assoc-in [rect-e-id]
|
||||
(cts/setup-shape
|
||||
{:id rect-e-id
|
||||
:name "Rect E"
|
||||
:type :rect
|
||||
:parent-id frame-a-id
|
||||
:frame-id frame-a-id}))))]
|
||||
|
||||
(t/testing "Create new group an add objects from the same group"
|
||||
(let [new-group-id (uuid/next)
|
||||
changes [{:type :add-obj
|
||||
:page-id page-id
|
||||
:id new-group-id
|
||||
:frame-id frame-a-id
|
||||
:obj (cts/setup-shape
|
||||
{:id new-group-id
|
||||
:type :group
|
||||
:frame-id frame-a-id
|
||||
:name "Group C"})}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id new-group-id
|
||||
:shapes [rect-b-id rect-c-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (println "===============")
|
||||
;; (clojure.pprint/pprint res)
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id new-group-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [new-group-id :shapes])))
|
||||
(t/is (= [rect-a-id]
|
||||
(get-in objects [group-a-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-c-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-c-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements from group and frame to an existing group at index"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements from several groups"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-b-id
|
||||
:index 0
|
||||
:shapes [rect-a-id rect-e-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [rect-b-id rect-c-id]
|
||||
(get-in objects [group-a-id :shapes])))
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move all elements from a group"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-a-id
|
||||
:shapes [rect-d-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (empty? (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements to a group with different frame"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id frame-b-id
|
||||
:shapes [group-a-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects]))
|
||||
;; (println "==========")
|
||||
;; (pprint (get-in res [:pages-index page-id :objects]))
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes])))
|
||||
(t/is (= [group-a-id] (get-in objects [frame-b-id :shapes])))
|
||||
(t/is (= frame-b-id (get-in objects [group-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-a-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-b-id :frame-id])))
|
||||
(t/is (= frame-b-id (get-in objects [rect-c-id :frame-id]))))))
|
||||
|
||||
(t/testing "Move elements to frame zero"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:shapes [group-a-id]
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (pprint (get-in data [:objects uuid/zero]))
|
||||
;; (println "==========")
|
||||
;; (pprint (get-in objects [uuid/zero]))
|
||||
|
||||
(t/is (= [group-a-id frame-a-id frame-b-id]
|
||||
(get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
(t/testing "Don't allow to move inside self"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-a-id
|
||||
:shapes [group-a-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
(t/is (= data res))))))
|
||||
|
||||
|
||||
(t/deftest mov-objects-regression-1
|
||||
(let [shape-1-id (uuid/custom 2 1)
|
||||
shape-2-id (uuid/custom 2 2)
|
||||
shape-3-id (uuid/custom 2 3)
|
||||
frame-id (uuid/custom 1 1)
|
||||
file-id (uuid/custom 4 4)
|
||||
page-id (uuid/custom 0 1)
|
||||
|
||||
changes [{:type :add-obj
|
||||
:id frame-id
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :frame
|
||||
:name "Frame"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:parent-id frame-id
|
||||
:id shape-1-id
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 1"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-2-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 2"})}
|
||||
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-3-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:type :rect
|
||||
:name "Shape 3"})}]
|
||||
data (make-file-data file-id page-id)
|
||||
data (ch/process-changes data changes)]
|
||||
|
||||
(t/testing "preserve order on multiple shape mov 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id shape-3-id]
|
||||
:parent-id uuid/zero
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (println "==> BEFORE")
|
||||
;; (pprint (get-in data [:objects]))
|
||||
;; (println "==> AFTER")
|
||||
;; (pprint (get-in res [:objects]))
|
||||
|
||||
(t/is (= [frame-id shape-2-id shape-3-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
(t/is (= [shape-2-id shape-3-id frame-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
(t/testing "preserve order on multiple shape mov 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-3-id shape-2-id]
|
||||
:parent-id uuid/zero
|
||||
:index 0}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (println "==> BEFORE")
|
||||
;; (pprint (get-in data [:objects]))
|
||||
;; (println "==> AFTER")
|
||||
;; (pprint (get-in res [:objects]))
|
||||
|
||||
(t/is (= [frame-id shape-2-id shape-3-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
(t/is (= [shape-3-id shape-2-id frame-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
(t/testing "move inside->outside-inside"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id]
|
||||
:parent-id frame-id}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:shapes [shape-2-id]
|
||||
:parent-id uuid/zero}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
(t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id])
|
||||
(get-in data [:pages-index page-id :objects shape-1-id :frame-id])))
|
||||
(t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id])
|
||||
(get-in data [:pages-index page-id :objects shape-2-id :frame-id])))))))
|
||||
|
||||
|
||||
(t/deftest move-objects-2
|
||||
(let [shape-1-id (uuid/custom 1 1)
|
||||
shape-2-id (uuid/custom 1 2)
|
||||
shape-3-id (uuid/custom 1 3)
|
||||
shape-4-id (uuid/custom 1 4)
|
||||
group-1-id (uuid/custom 1 5)
|
||||
file-id (uuid/custom 1 6)
|
||||
page-id (uuid/custom 0 1)
|
||||
|
||||
changes [{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-1-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-1-id
|
||||
:type :rect
|
||||
:name "Shape a"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-2-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-2-id
|
||||
:type :rect
|
||||
:name "Shape b"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-3-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-3-id
|
||||
:type :rect
|
||||
:name "Shape c"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id shape-4-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-4-id
|
||||
:type :rect
|
||||
:name "Shape d"})}
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id group-1-id
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:id group-1-id
|
||||
:type :group
|
||||
:name "Group"})}
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:shapes [shape-1-id shape-2-id]}]
|
||||
|
||||
data (make-file-data file-id page-id)
|
||||
data (ch/process-changes data changes)]
|
||||
|
||||
(t/testing "case 1"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 2
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id shape-3-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
))
|
||||
|
||||
(t/testing "case 2"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 2
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After:
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id shape-3-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 3"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 1
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-3-id shape-2-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 4"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-1-id
|
||||
:index 0
|
||||
:shapes [shape-3-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-3-id shape-1-id shape-2-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
))
|
||||
|
||||
(t/testing "case 5"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 0
|
||||
:shapes [shape-2-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-2-id shape-3-id shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id]
|
||||
(get-in res [:pages-index page-id :objects group-1-id :shapes])))))
|
||||
|
||||
(t/testing "case 6"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id uuid/zero
|
||||
:index 0
|
||||
:shapes [shape-2-id shape-1-id]}]
|
||||
res (ch/process-changes data changes)]
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; Before
|
||||
|
||||
(t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
(get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (= [shape-1-id shape-2-id]
|
||||
(get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
(t/is (not= nil
|
||||
(get-in res [:pages-index page-id :objects group-1-id])))))))
|
||||
|
||||
(t/deftest set-guide-json-encode-decode
|
||||
(let [schema ch/schema:set-guide-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-guide-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-guide-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :guides (:id change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-guide-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-guide-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :guides (:id change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :guides])))))
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-plugin-data-json-encode-decode
|
||||
(let [schema ch/schema:set-plugin-data-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-plugin-data-gen-and-validate
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-plugin-data-change)]
|
||||
(sm/validate ch/schema:set-plugin-data-change change))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-json-encode-decode
|
||||
(let [schema ch/schema:set-flow-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-flow-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :flows (:id change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-flow-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-flow-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :flows (:id change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :flows])))))
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-json-encode-decode
|
||||
(let [schema ch/schema:set-default-grid-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
decode (sm/decoder schema (sm/json-transformer))]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
;; (println "==========")
|
||||
;; (app.common.pprint/pprint data-2)
|
||||
;; (app.common.pprint/pprint data-3)
|
||||
;; (println "==========")
|
||||
(= data data-3)))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-1
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (sg/generator ch/schema:set-default-grid-change)]
|
||||
(let [change (assoc change :page-id page-id)
|
||||
result (ch/process-changes data [change])]
|
||||
;; (app.common.pprint/pprint change)
|
||||
(= (:params change)
|
||||
(get-in result [:pages-index page-id :default-grids (:grid-type change)]))))
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-default-grid-2
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(smt/check!
|
||||
(smt/for [change (->> (sg/generator ch/schema:set-default-grid-change)
|
||||
(sg/filter :params))]
|
||||
(let [change1 (assoc change :page-id page-id)
|
||||
result1 (ch/process-changes data [change1])
|
||||
|
||||
change2 (assoc change1 :params nil)
|
||||
result2 (ch/process-changes result1 [change2])]
|
||||
|
||||
;; (app.common.pprint/pprint change1)
|
||||
|
||||
(and (some? (:params change1))
|
||||
(= (:params change1)
|
||||
(get-in result1 [:pages-index page-id :default-grids (:grid-type change1)]))
|
||||
|
||||
(nil? (:params change2))
|
||||
(nil? (get-in result2 [:pages-index page-id :default-grids])))))
|
||||
|
||||
{:num 1000})))
|
||||
@@ -289,42 +289,3 @@
|
||||
(t/is (= (:fill-opacity fill') 1))
|
||||
(t/is (= (:touched copy2-root') nil))
|
||||
(t/is (= (:touched copy2-child') #{:fill-group}))))
|
||||
|
||||
(t/deftest test-touched-when-changing-lower
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-nested-component-with-copy :component1
|
||||
:main1-root
|
||||
:main1-child
|
||||
:component2
|
||||
:main2-root
|
||||
:main2-nested-head
|
||||
:copy2-root
|
||||
:copy2-root-params {:children-labels [:copy2-child]}))
|
||||
page (thf/current-page file)
|
||||
copy2-child (ths/get-shape file :copy2-child)
|
||||
|
||||
;; ==== Action
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id copy2-child)}
|
||||
(fn [shape]
|
||||
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
copy2-root' (ths/get-shape file' :copy2-root)
|
||||
copy2-child' (ths/get-shape file' :copy2-child)
|
||||
fills' (:fills copy2-child')
|
||||
fill' (first fills')]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? copy2-root'))
|
||||
(t/is (some? copy2-child'))
|
||||
(t/is (= (count fills') 1))
|
||||
(t/is (= (:fill-color fill') "#fabada"))
|
||||
(t/is (= (:fill-opacity fill') 1))
|
||||
(t/is (= (:touched copy2-root') nil))
|
||||
(t/is (= (:touched copy2-child') #{:fill-group}))))
|
||||
@@ -1,75 +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.logic.hide-in-viewer-test
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
|
||||
(t/deftest test-remove-show-in-view-mode-delete-interactions
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest)
|
||||
(tho/add-frame :frame-origin)
|
||||
(ths/add-interaction :frame-origin :frame-dest))
|
||||
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/change-show-in-viewer % true)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? (:interactions frame-origin)))
|
||||
(t/is (nil? (:interactions frame-origin')))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-add-new-interaction-updates-show-in-view-mode
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest :hide-in-viewer true)
|
||||
(tho/add-frame :frame-origin :hide-in-viewer true))
|
||||
frame-dest (ths/get-shape file :frame-dest)
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination (:id frame-dest))
|
||||
(assoc :position-relative-to (:id frame-dest)))
|
||||
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/add-new-interaction % new-interaction)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (true? (:hide-in-viewer frame-origin)))
|
||||
(t/is (nil? (:hide-in-viewer frame-origin')))))
|
||||
@@ -1,740 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.pages-test
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(defn- make-file-data
|
||||
[file-id page-id]
|
||||
(binding [ffeat/*current* #{"components/v2"}]
|
||||
(ctf/make-file-data file-id page-id)))
|
||||
|
||||
(t/deftest process-change-set-option
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
(t/testing "Sets option single"
|
||||
(let [chg {:type :set-option
|
||||
:page-id page-id
|
||||
:option :test
|
||||
:value "test"}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= "test" (get-in res [:pages-index page-id :options :test])))))
|
||||
|
||||
(t/testing "Sets option nested"
|
||||
(let [chgs [{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :a]
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :b]
|
||||
:value "b"}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (= {:a "a" :b "b"}
|
||||
(get-in res [:pages-index page-id :options :values :test])))))
|
||||
|
||||
(t/testing "Remove option single"
|
||||
(let [chg {:type :set-option
|
||||
:page-id page-id
|
||||
:option :test
|
||||
:value nil}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
|
||||
|
||||
(t/testing "Remove option nested 1"
|
||||
(let [chgs [{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :a]
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test :b]
|
||||
:value "b"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test]
|
||||
:value nil}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
|
||||
|
||||
(t/testing "Remove option nested 2"
|
||||
(let [chgs [{:type :set-option
|
||||
:option [:values :test1 :a]
|
||||
:page-id page-id
|
||||
:value "a"}
|
||||
{:type :set-option
|
||||
:option [:values :test2 :b]
|
||||
:page-id page-id
|
||||
:value "b"}
|
||||
{:type :set-option
|
||||
:page-id page-id
|
||||
:option [:values :test2]
|
||||
:value nil}]
|
||||
res (ch/process-changes data chgs)]
|
||||
(t/is (= [:test1] (keys (get-in res [:pages-index page-id :options :values]))))))))
|
||||
|
||||
(t/deftest process-change-add-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)
|
||||
id-a (uuid/custom 2 1)
|
||||
id-b (uuid/custom 2 2)
|
||||
id-c (uuid/custom 2 3)]
|
||||
|
||||
(t/testing "Adds single object"
|
||||
(let [chg {:type :add-obj
|
||||
:page-id page-id
|
||||
:id id-a
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:obj (cts/setup-shape
|
||||
{:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:id id-a
|
||||
:type :rect
|
||||
:name "rect"})}
|
||||
res (ch/process-changes data [chg])]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 2 (count objects)))
|
||||
(t/is (= (:obj chg) (get objects id-a)))
|
||||
|
||||
(t/is (= [id-a] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
|
||||
(t/testing "Adds several objects with different indexes"
|
||||
(let [chg (fn [id index]
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:frame-id uuid/zero
|
||||
:index index
|
||||
:obj (cts/setup-shape
|
||||
{:id id
|
||||
:frame-id uuid/zero
|
||||
:type :rect
|
||||
:name (str id)})})
|
||||
res (ch/process-changes data [(chg id-a 0)
|
||||
(chg id-b 0)
|
||||
(chg id-c 1)])]
|
||||
|
||||
;; (clojure.pprint/pprint data)
|
||||
;; (clojure.pprint/pprint res)
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= 4 (count objects)))
|
||||
(t/is (not (nil? (get objects id-a))))
|
||||
(t/is (not (nil? (get objects id-b))))
|
||||
(t/is (not (nil? (get objects id-c))))
|
||||
(t/is (= [id-b id-c id-a] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
(t/deftest process-change-mod-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "simple mod-obj"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id uuid/zero
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= "foobar" (get-in objects [uuid/zero :name]))))))
|
||||
|
||||
(t/testing "mod-obj for not existing shape"
|
||||
(let [chg {:type :mod-obj
|
||||
:page-id page-id
|
||||
:id (uuid/next)
|
||||
:operations [{:type :set
|
||||
:attr :name
|
||||
:val "foobar"}]}
|
||||
res (ch/process-changes data [chg])]
|
||||
(t/is (= res data))))))
|
||||
|
||||
|
||||
;; (t/deftest process-change-del-obj
|
||||
;; (let [file-id (uuid/custom 2 2)
|
||||
;; page-id (uuid/custom 1 1)
|
||||
;; id (uuid/custom 2 1)
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (-> data
|
||||
;; (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id])
|
||||
;; (assoc-in [:pages-index page-id :objects id]
|
||||
;; {:id id
|
||||
;; :frame-id uuid/zero
|
||||
;; :type :rect
|
||||
;; :name "rect"}))]
|
||||
;; (t/testing "delete"
|
||||
;; (let [chg {:type :del-obj
|
||||
;; :page-id page-id
|
||||
;; :id id}
|
||||
;; res (ch/process-changes data [chg])]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= 1 (count objects)))
|
||||
;; (t/is (= [] (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
;; (t/testing "delete idempotency"
|
||||
;; (let [chg {:type :del-obj
|
||||
;; :page-id page-id
|
||||
;; :id id}
|
||||
;; res1 (ch/process-changes data [chg])
|
||||
;; res2 (ch/process-changes res1 [chg])]
|
||||
|
||||
;; (t/is (= res1 res2))
|
||||
;; (let [objects (get-in res1 [:pages-index page-id :objects])]
|
||||
;; (t/is (= 1 (count objects)))
|
||||
;; (t/is (= [] (get-in objects [uuid/zero :shapes]))))))))
|
||||
|
||||
|
||||
;; (t/deftest process-change-move-objects
|
||||
;; (let [frame-a-id (uuid/custom 0 1)
|
||||
;; frame-b-id (uuid/custom 0 2)
|
||||
;; group-a-id (uuid/custom 0 3)
|
||||
;; group-b-id (uuid/custom 0 4)
|
||||
;; rect-a-id (uuid/custom 0 5)
|
||||
;; rect-b-id (uuid/custom 0 6)
|
||||
;; rect-c-id (uuid/custom 0 7)
|
||||
;; rect-d-id (uuid/custom 0 8)
|
||||
;; rect-e-id (uuid/custom 0 9)
|
||||
|
||||
;; file-id (uuid/custom 2 2)
|
||||
;; page-id (uuid/custom 1 1)
|
||||
;; data (make-file-data file-id page-id)
|
||||
|
||||
;; data (update-in data [:pages-index page-id :objects]
|
||||
;; #(-> %
|
||||
;; (assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id])
|
||||
;; (assoc-in [frame-a-id]
|
||||
;; {:id frame-a-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :name "Frame a"
|
||||
;; :shapes [group-a-id group-b-id rect-e-id]
|
||||
;; :type :frame})
|
||||
|
||||
;; (assoc-in [frame-b-id]
|
||||
;; {:id frame-b-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :name "Frame b"
|
||||
;; :shapes []
|
||||
;; :type :frame})
|
||||
|
||||
;; ;; Groups
|
||||
;; (assoc-in [group-a-id]
|
||||
;; {:id group-a-id
|
||||
;; :name "Group A"
|
||||
;; :type :group
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :shapes [rect-a-id rect-b-id rect-c-id]})
|
||||
;; (assoc-in [group-b-id]
|
||||
;; {:id group-b-id
|
||||
;; :name "Group B"
|
||||
;; :type :group
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :shapes [rect-d-id]})
|
||||
|
||||
;; ;; Shapes
|
||||
;; (assoc-in [rect-a-id]
|
||||
;; {:id rect-a-id
|
||||
;; :name "Rect A"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-b-id]
|
||||
;; {:id rect-b-id
|
||||
;; :name "Rect B"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-c-id]
|
||||
;; {:id rect-c-id
|
||||
;; :name "Rect C"
|
||||
;; :type :rect
|
||||
;; :parent-id group-a-id
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-d-id]
|
||||
;; {:id rect-d-id
|
||||
;; :name "Rect D"
|
||||
;; :parent-id group-b-id
|
||||
;; :type :rect
|
||||
;; :frame-id frame-a-id})
|
||||
|
||||
;; (assoc-in [rect-e-id]
|
||||
;; {:id rect-e-id
|
||||
;; :name "Rect E"
|
||||
;; :type :rect
|
||||
;; :parent-id frame-a-id
|
||||
;; :frame-id frame-a-id})))]
|
||||
|
||||
;; (t/testing "Create new group an add objects from the same group"
|
||||
;; (let [new-group-id (uuid/next)
|
||||
;; changes [{:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id new-group-id
|
||||
;; :frame-id frame-a-id
|
||||
;; :obj {:id new-group-id
|
||||
;; :type :group
|
||||
;; :frame-id frame-a-id
|
||||
;; :name "Group C"}}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id new-group-id
|
||||
;; :shapes [rect-b-id rect-c-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (clojure.pprint/pprint data)
|
||||
;; ;; (println "===============")
|
||||
;; ;; (clojure.pprint/pprint res)
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id new-group-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [new-group-id :shapes])))
|
||||
;; (t/is (= [rect-a-id]
|
||||
;; (get-in objects [group-a-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements to an existing group at index"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-c-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-c-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements from group and frame to an existing group at index"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-e-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements from several groups"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-b-id
|
||||
;; :index 0
|
||||
;; :shapes [rect-a-id rect-e-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [rect-b-id rect-c-id]
|
||||
;; (get-in objects [group-a-id :shapes])))
|
||||
;; (t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
;; (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move all elements from a group"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-a-id
|
||||
;; :shapes [rect-d-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-a-id group-b-id rect-e-id]
|
||||
;; (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (empty? (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
;; (t/testing "Move elements to a group with different frame"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id frame-b-id
|
||||
;; :shapes [group-a-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects]))
|
||||
;; ;; (println "==========")
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects]))
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; (t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes])))
|
||||
;; (t/is (= [group-a-id] (get-in objects [frame-b-id :shapes])))
|
||||
;; (t/is (= frame-b-id (get-in objects [group-a-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-a-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-b-id :frame-id])))
|
||||
;; (t/is (= frame-b-id (get-in objects [rect-c-id :frame-id]))))))
|
||||
|
||||
;; (t/testing "Move elements to frame zero"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :shapes [group-a-id]
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (let [objects (get-in res [:pages-index page-id :objects])]
|
||||
;; ;; (pprint (get-in data [:objects uuid/zero]))
|
||||
;; ;; (println "==========")
|
||||
;; ;; (pprint (get-in objects [uuid/zero]))
|
||||
|
||||
;; (t/is (= [group-a-id frame-a-id frame-b-id]
|
||||
;; (get-in objects [uuid/zero :shapes]))))))
|
||||
|
||||
;; (t/testing "Don't allow to move inside self"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-a-id
|
||||
;; :shapes [group-a-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
;; (t/is (= data res))))
|
||||
;; ))
|
||||
|
||||
|
||||
;; (t/deftest process-change-mov-objects-regression
|
||||
;; (let [shape-1-id (uuid/custom 2 1)
|
||||
;; shape-2-id (uuid/custom 2 2)
|
||||
;; shape-3-id (uuid/custom 2 3)
|
||||
;; frame-id (uuid/custom 1 1)
|
||||
;; file-id (uuid/custom 4 4)
|
||||
;; page-id (uuid/custom 0 1)
|
||||
|
||||
;; changes [{:type :add-obj
|
||||
;; :id frame-id
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :frame
|
||||
;; :name "Frame"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :frame-id frame-id
|
||||
;; :parent-id frame-id
|
||||
;; :id shape-1-id
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 1"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-2-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 2"}}
|
||||
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-3-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:type :rect
|
||||
;; :name "Shape 3"}}
|
||||
;; ]
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (ch/process-changes data changes)]
|
||||
|
||||
;; (t/testing "preserve order on multiple shape mov 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id shape-3-id]
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (println "==> BEFORE")
|
||||
;; ;; (pprint (get-in data [:objects]))
|
||||
;; ;; (println "==> AFTER")
|
||||
;; ;; (pprint (get-in res [:objects]))
|
||||
|
||||
;; (t/is (= [frame-id shape-2-id shape-3-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
;; (t/is (= [shape-2-id shape-3-id frame-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
;; (t/testing "preserve order on multiple shape mov 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-3-id shape-2-id]
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (println "==> BEFORE")
|
||||
;; ;; (pprint (get-in data [:objects]))
|
||||
;; ;; (println "==> AFTER")
|
||||
;; ;; (pprint (get-in res [:objects]))
|
||||
|
||||
;; (t/is (= [frame-id shape-2-id shape-3-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
;; (t/is (= [shape-3-id shape-2-id frame-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
|
||||
|
||||
;; (t/testing "move inside->outside-inside"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id]
|
||||
;; :parent-id frame-id}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :shapes [shape-2-id]
|
||||
;; :parent-id uuid/zero}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; (t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id])
|
||||
;; (get-in data [:pages-index page-id :objects shape-1-id :frame-id])))
|
||||
;; (t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id])
|
||||
;; (get-in data [:pages-index page-id :objects shape-2-id :frame-id])))))
|
||||
|
||||
;; ))
|
||||
|
||||
|
||||
;; (t/deftest process-change-move-objects-2
|
||||
;; (let [shape-1-id (uuid/custom 1 1)
|
||||
;; shape-2-id (uuid/custom 1 2)
|
||||
;; shape-3-id (uuid/custom 1 3)
|
||||
;; shape-4-id (uuid/custom 1 4)
|
||||
;; group-1-id (uuid/custom 1 5)
|
||||
;; file-id (uuid/custom 1 6)
|
||||
;; page-id (uuid/custom 0 1)
|
||||
|
||||
;; changes [{:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-1-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-1-id
|
||||
;; :type :rect
|
||||
;; :name "Shape a"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-2-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-2-id
|
||||
;; :type :rect
|
||||
;; :name "Shape b"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-3-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-3-id
|
||||
;; :type :rect
|
||||
;; :name "Shape c"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id shape-4-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id shape-4-id
|
||||
;; :type :rect
|
||||
;; :name "Shape d"}}
|
||||
;; {:type :add-obj
|
||||
;; :page-id page-id
|
||||
;; :id group-1-id
|
||||
;; :frame-id uuid/zero
|
||||
;; :obj {:id group-1-id
|
||||
;; :type :group
|
||||
;; :name "Group"}}
|
||||
;; {:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :shapes [shape-1-id shape-2-id]}]
|
||||
|
||||
;; data (make-file-data file-id page-id)
|
||||
;; data (ch/process-changes data changes)]
|
||||
|
||||
;; (t/testing "case 1"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 2
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id shape-3-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 2"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 2
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After:
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id shape-3-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 3"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 1
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-3-id shape-2-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 4"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id group-1-id
|
||||
;; :index 0
|
||||
;; :shapes [shape-3-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-3-id shape-1-id shape-2-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 5"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0
|
||||
;; :shapes [shape-2-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-2-id shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ))
|
||||
|
||||
;; (t/testing "case 6"
|
||||
;; (let [changes [{:type :mov-objects
|
||||
;; :page-id page-id
|
||||
;; :parent-id uuid/zero
|
||||
;; :index 0
|
||||
;; :shapes [shape-2-id shape-1-id]}]
|
||||
;; res (ch/process-changes data changes)]
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects uuid/zero]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects uuid/zero]))
|
||||
|
||||
;; ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
|
||||
;; ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
|
||||
|
||||
;; ;; Before
|
||||
|
||||
;; (t/is (= [shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (= [shape-1-id shape-2-id]
|
||||
;; (get-in data [:pages-index page-id :objects group-1-id :shapes])))
|
||||
|
||||
;; ;; After
|
||||
|
||||
;; (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
|
||||
;; (get-in res [:pages-index page-id :objects uuid/zero :shapes])))
|
||||
|
||||
;; (t/is (not= nil
|
||||
;; (get-in res [:pages-index page-id :objects group-1-id])))
|
||||
|
||||
;; ))
|
||||
|
||||
;; ))
|
||||
@@ -4,12 +4,13 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types.decoder-test
|
||||
(ns common-tests.types.shape-decode-encode-test
|
||||
(:require
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.types.color :refer [schema:color schema:gradient]]
|
||||
[app.common.types.plugins :refer [schema:plugin-data]]
|
||||
[app.common.types.shape :as tsh]
|
||||
@@ -49,102 +50,102 @@
|
||||
(t/deftest gradient-json-roundtrip
|
||||
(let [encode (sm/encoder schema:gradient (sm/json-transformer))
|
||||
decode (sm/decoder schema:gradient (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [gradient (sg/generator schema:gradient)]
|
||||
(smt/check!
|
||||
(smt/for [gradient (sg/generator schema:gradient)]
|
||||
(let [gradient-1 (encode gradient)
|
||||
gradient-2 (json-roundtrip gradient-1)
|
||||
gradient-3 (decode gradient-2)]
|
||||
;; (app.common.pprint/pprint gradient)
|
||||
;; (app.common.pprint/pprint gradient-3)
|
||||
(t/is (= gradient gradient-3))))
|
||||
(= gradient gradient-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest color-json-roundtrip
|
||||
(let [encode (sm/encoder schema:color (sm/json-transformer))
|
||||
decode (sm/decoder schema:color (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [color (sg/generator schema:color)]
|
||||
(smt/check!
|
||||
(smt/for [color (sg/generator schema:color)]
|
||||
(let [color-1 (encode color)
|
||||
color-2 (json-roundtrip color-1)
|
||||
color-3 (decode color-2)]
|
||||
;; (app.common.pprint/pprint color)
|
||||
;; (app.common.pprint/pprint color-3)
|
||||
(t/is (= color color-3))))
|
||||
(= color color-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-shadow-json-roundtrip
|
||||
(let [encode (sm/encoder schema:shadow (sm/json-transformer))
|
||||
decode (sm/decoder schema:shadow (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [shadow (sg/generator schema:shadow)]
|
||||
(smt/check!
|
||||
(smt/for [shadow (sg/generator schema:shadow)]
|
||||
(let [shadow-1 (encode shadow)
|
||||
shadow-2 (json-roundtrip shadow-1)
|
||||
shadow-3 (decode shadow-2)]
|
||||
;; (app.common.pprint/pprint shadow)
|
||||
;; (app.common.pprint/pprint shadow-3)
|
||||
(t/is (= shadow shadow-3))))
|
||||
(= shadow shadow-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-animation-json-roundtrip
|
||||
(let [encode (sm/encoder schema:animation (sm/json-transformer))
|
||||
decode (sm/decoder schema:animation (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [animation (sg/generator schema:animation)]
|
||||
(smt/check!
|
||||
(smt/for [animation (sg/generator schema:animation)]
|
||||
(let [animation-1 (encode animation)
|
||||
animation-2 (json-roundtrip animation-1)
|
||||
animation-3 (decode animation-2)]
|
||||
;; (app.common.pprint/pprint animation)
|
||||
;; (app.common.pprint/pprint animation-3)
|
||||
(t/is (= animation animation-3))))
|
||||
(= animation animation-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-interaction-json-roundtrip
|
||||
(let [encode (sm/encoder schema:interaction (sm/json-transformer))
|
||||
decode (sm/decoder schema:interaction (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [interaction (sg/generator schema:interaction)]
|
||||
(smt/check!
|
||||
(smt/for [interaction (sg/generator schema:interaction)]
|
||||
(let [interaction-1 (encode interaction)
|
||||
interaction-2 (json-roundtrip interaction-1)
|
||||
interaction-3 (decode interaction-2)]
|
||||
;; (app.common.pprint/pprint interaction)
|
||||
;; (app.common.pprint/pprint interaction-3)
|
||||
(t/is (= interaction interaction-3))))
|
||||
(= interaction interaction-3)))
|
||||
{:num 500})))
|
||||
|
||||
|
||||
(t/deftest shape-path-content-json-roundtrip
|
||||
(let [encode (sm/encoder schema:path-content (sm/json-transformer))
|
||||
decode (sm/decoder schema:path-content (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [path-content (sg/generator schema:path-content)]
|
||||
(smt/check!
|
||||
(smt/for [path-content (sg/generator schema:path-content)]
|
||||
(let [path-content-1 (encode path-content)
|
||||
path-content-2 (json-roundtrip path-content-1)
|
||||
path-content-3 (decode path-content-2)]
|
||||
;; (app.common.pprint/pprint path-content)
|
||||
;; (app.common.pprint/pprint path-content-3)
|
||||
(t/is (= path-content path-content-3))))
|
||||
(= path-content path-content-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest plugin-data-json-roundtrip
|
||||
(let [encode (sm/encoder schema:plugin-data (sm/json-transformer))
|
||||
decode (sm/decoder schema:plugin-data (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [data (sg/generator schema:plugin-data)]
|
||||
(smt/check!
|
||||
(smt/for [data (sg/generator schema:plugin-data)]
|
||||
(let [data-1 (encode data)
|
||||
data-2 (json-roundtrip data-1)
|
||||
data-3 (decode data-2)]
|
||||
(t/is (= data data-3))))
|
||||
(= data data-3)))
|
||||
{:num 500})))
|
||||
|
||||
(t/deftest shape-json-roundtrip
|
||||
(let [encode (sm/encoder ::tsh/shape (sm/json-transformer))
|
||||
decode (sm/decoder ::tsh/shape (sm/json-transformer))]
|
||||
(sg/check!
|
||||
(sg/for [shape (sg/generator ::tsh/shape)]
|
||||
(smt/check!
|
||||
(smt/for [shape (sg/generator ::tsh/shape)]
|
||||
(let [shape-1 (encode shape)
|
||||
shape-2 (json-roundtrip shape-1)
|
||||
shape-3 (decode shape-2)]
|
||||
;; (app.common.pprint/pprint shape)
|
||||
;; (app.common.pprint/pprint shape-3)
|
||||
(t/is (= shape shape-3))))
|
||||
(= shape shape-3)))
|
||||
{:num 1000})))
|
||||
@@ -1,33 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.transit :as transit]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape :as cts]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest transit-encode-decode-with-shape
|
||||
(sg/check!
|
||||
(sg/for [fdata (sg/generator ::cts/shape)]
|
||||
(let [res (-> fdata transit/encode-str transit/decode-str)]
|
||||
(t/is (= res fdata))))
|
||||
{:num 18 :seed 1683548002439}))
|
||||
|
||||
(t/deftest types-shape-spec
|
||||
(sg/check!
|
||||
(sg/for [fdata (sg/generator ::cts/shape)]
|
||||
(binding [app.common.data.macros/*assert-context* true]
|
||||
(t/is (sm/validate ::cts/shape fdata))))))
|
||||
|
||||
(t/deftest types-page-spec
|
||||
(-> (sg/for [fdata (sg/generator ::ctp/page)]
|
||||
(t/is (sm/validate ::ctp/page fdata)))
|
||||
(sg/check! {:num 30})))
|
||||
@@ -1,18 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.uuid-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest non-repeating-uuid-next-1-schema
|
||||
(sg/check!
|
||||
(sg/for [uuid1 (sg/generator ::sm/uuid)
|
||||
uuid2 (sg/generator ::sm/uuid)]
|
||||
(t/is (not= uuid1 uuid2)))
|
||||
{:num 100}))
|
||||
@@ -8,6 +8,8 @@ ENV NODE_VERSION=v20.11.1 \
|
||||
CLJKONDO_VERSION=2024.03.13 \
|
||||
BABASHKA_VERSION=1.3.189 \
|
||||
CLJFMT_VERSION=0.12.0 \
|
||||
RUSTUP_VERSION=1.27.1 \
|
||||
RUST_VERSION=1.81.0 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
@@ -242,6 +244,27 @@ RUN set -ex; \
|
||||
mv /tmp/mc /usr/local/bin/; \
|
||||
chmod +x /usr/local/bin/mc;
|
||||
|
||||
# Install Rust toolchain
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH;
|
||||
|
||||
RUN set -eux; \
|
||||
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
|
||||
dpkgArch="$(dpkg --print-architecture)"; \
|
||||
case "${dpkgArch##*-}" in \
|
||||
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
|
||||
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
|
||||
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
|
||||
esac; \
|
||||
url="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \
|
||||
wget "$url"; \
|
||||
echo "${rustupSha256} *rustup-init" | sha256sum -c -; \
|
||||
chmod +x rustup-init; \
|
||||
./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \
|
||||
rm rustup-init; \
|
||||
chmod -R a+w $RUSTUP_HOME $CARGO_HOME;
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
export JAVA_OPTS="-Xmx1000m -Xms50m"
|
||||
export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms200m"};
|
||||
|
||||
alias l='ls --color -GFlh'
|
||||
alias rm='rm -r'
|
||||
@@ -9,6 +9,9 @@ alias ls='ls --color -F'
|
||||
alias lsd='ls -d *(/)'
|
||||
alias lsf='ls -h *(.)'
|
||||
|
||||
# init Cargo / Rust env
|
||||
. "/usr/local/cargo/env"
|
||||
|
||||
# include .bashrc if it exists
|
||||
if [ -f "$HOME/.bashrc.local" ]; then
|
||||
. "$HOME/.bashrc.local"
|
||||
|
||||
9
docs/.editorconfig
Normal file
9
docs/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
144
docs/.eleventy.js
Normal file
144
docs/.eleventy.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const { DateTime } = require("luxon");
|
||||
const fs = require("fs");
|
||||
const pluginNavigation = require("@11ty/eleventy-navigation");
|
||||
const pluginRss = require("@11ty/eleventy-plugin-rss");
|
||||
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||
const pluginAncestry = require("@tigersway/eleventy-plugin-ancestry");
|
||||
const metagen = require('eleventy-plugin-metagen');
|
||||
const pluginTOC = require('eleventy-plugin-nesting-toc');
|
||||
const markdownIt = require("markdown-it");
|
||||
const markdownItAnchor = require("markdown-it-anchor");
|
||||
const markdownItPlantUML = require("markdown-it-plantuml");
|
||||
const elasticlunr = require("elasticlunr");
|
||||
|
||||
|
||||
module.exports = function(eleventyConfig) {
|
||||
eleventyConfig.addPlugin(pluginNavigation);
|
||||
eleventyConfig.addPlugin(pluginRss);
|
||||
eleventyConfig.addPlugin(pluginSyntaxHighlight);
|
||||
eleventyConfig.addPlugin(pluginAncestry);
|
||||
eleventyConfig.addPlugin(metagen);
|
||||
eleventyConfig.addPlugin(pluginTOC, {
|
||||
tags: ['h1', 'h2', 'h3']
|
||||
});
|
||||
|
||||
eleventyConfig.setDataDeepMerge(true);
|
||||
|
||||
eleventyConfig.addLayoutAlias("post", "layouts/post.njk");
|
||||
|
||||
eleventyConfig.addFilter("readableDate", dateObj => {
|
||||
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy");
|
||||
});
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
|
||||
eleventyConfig.addFilter('htmlDateString', (dateObj) => {
|
||||
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
|
||||
});
|
||||
|
||||
// Remove trailing # in automatic generated toc, because of
|
||||
// anchors added at the end of the titles.
|
||||
eleventyConfig.addFilter('stripHash', (toc) => {
|
||||
return toc.replace(/ #\<\/a\>/g, "</a>");
|
||||
});
|
||||
|
||||
// Get the first `n` elements of a collection.
|
||||
eleventyConfig.addFilter("head", (array, n) => {
|
||||
if( n < 0 ) {
|
||||
return array.slice(n);
|
||||
}
|
||||
|
||||
return array.slice(0, n);
|
||||
});
|
||||
|
||||
// Get the lowest in a list of numbers.
|
||||
eleventyConfig.addFilter("min", (...numbers) => {
|
||||
return Math.min.apply(null, numbers);
|
||||
});
|
||||
|
||||
// Build a search index
|
||||
eleventyConfig.addFilter("search", (collection) => {
|
||||
// What fields we'd like our index to consist of
|
||||
// TODO: remove html tags from content
|
||||
var index = elasticlunr(function () {
|
||||
this.addField("title");
|
||||
this.addField("content");
|
||||
this.setRef("id");
|
||||
});
|
||||
|
||||
// loop through each page and add it to the index
|
||||
collection.forEach((page) => {
|
||||
index.addDoc({
|
||||
id: page.url,
|
||||
title: page.template.frontMatter.data.title,
|
||||
content: page.template.frontMatter.content,
|
||||
});
|
||||
});
|
||||
|
||||
return index.toJSON();
|
||||
});
|
||||
|
||||
eleventyConfig.addPassthroughCopy("img");
|
||||
eleventyConfig.addPassthroughCopy("css");
|
||||
eleventyConfig.addPassthroughCopy("js");
|
||||
|
||||
/* Markdown Overrides */
|
||||
let markdownLibrary = markdownIt({
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true
|
||||
}).use(markdownItAnchor, {
|
||||
permalink: true,
|
||||
permalinkClass: "direct-link",
|
||||
permalinkSymbol: "#"
|
||||
}).use(markdownItPlantUML, {
|
||||
});
|
||||
eleventyConfig.setLibrary("md", markdownLibrary);
|
||||
|
||||
// Browsersync Overrides
|
||||
eleventyConfig.setBrowserSyncConfig({
|
||||
callbacks: {
|
||||
ready: function(err, browserSync) {
|
||||
const content_404 = fs.readFileSync('_dist/404.html');
|
||||
|
||||
browserSync.addMiddleware("*", (req, res) => {
|
||||
// Provides the 404 content without redirect.
|
||||
res.write(content_404);
|
||||
res.end();
|
||||
});
|
||||
},
|
||||
},
|
||||
ui: false,
|
||||
ghostMode: false
|
||||
});
|
||||
|
||||
return {
|
||||
templateFormats: [
|
||||
"md",
|
||||
"njk",
|
||||
"html",
|
||||
"liquid"
|
||||
],
|
||||
|
||||
// If your site lives in a different subdirectory, change this.
|
||||
// Leading or trailing slashes are all normalized away, so don’t worry about those.
|
||||
|
||||
// If you don’t have a subdirectory, use "" or "/" (they do the same thing)
|
||||
// This is only used for link URLs (it does not affect your file structure)
|
||||
// Best paired with the `url` filter: https://www.11ty.dev/docs/filters/url/
|
||||
|
||||
// You can also pass this in on the command line using `--pathprefix`
|
||||
// pathPrefix: "/",
|
||||
|
||||
markdownTemplateEngine: "liquid",
|
||||
htmlTemplateEngine: "njk",
|
||||
dataTemplateEngine: "njk",
|
||||
|
||||
// These are all optional, defaults are shown:
|
||||
dir: {
|
||||
input: ".",
|
||||
includes: "_includes",
|
||||
data: "_data",
|
||||
output: "_dist"
|
||||
}
|
||||
};
|
||||
};
|
||||
1
docs/.eleventyignore
Normal file
1
docs/.eleventyignore
Normal file
@@ -0,0 +1 @@
|
||||
README.md
|
||||
118
docs/.gitignore
vendored
Normal file
118
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# Distribution files
|
||||
_dist/*
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
.idea
|
||||
5
docs/.vscode/settings.json
vendored
Normal file
5
docs/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.rulers": [
|
||||
80
|
||||
]
|
||||
}
|
||||
11
docs/.yarnrc.yml
Normal file
11
docs/.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
enableGlobalCache: true
|
||||
|
||||
enableImmutableCache: false
|
||||
|
||||
enableImmutableInstalls: false
|
||||
|
||||
enableTelemetry: false
|
||||
|
||||
httpTimeout: 600000
|
||||
|
||||
nodeLinker: node-modules
|
||||
17
docs/404.md
Normal file
17
docs/404.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
layout: layouts/home.njk
|
||||
permalink: 404.html
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
# Content not found.
|
||||
|
||||
Go <a href="{{ '/' | url }}">home</a>.
|
||||
|
||||
{% comment %}
|
||||
Read more: https://www.11ty.dev/docs/quicktips/not-found/
|
||||
|
||||
This will work for both GitHub pages and Netlify:
|
||||
|
||||
* https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/
|
||||
* https://www.netlify.com/docs/redirects/#custom-404
|
||||
{% endcomment %}
|
||||
38
docs/README.md
Normal file
38
docs/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Penpot Docs
|
||||
|
||||
Penpot documentation website.
|
||||
|
||||
## Usage
|
||||
|
||||
To view this site locally, first set up the environment:
|
||||
|
||||
```sh
|
||||
# only if necessary
|
||||
nvm install
|
||||
nvm use
|
||||
# only if necessary
|
||||
corepack enable
|
||||
|
||||
yarn install
|
||||
```
|
||||
|
||||
And launch a development server:
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
You can then point a browser to [http://localhost:8080](http://localhost:8080).
|
||||
|
||||
## Tooling
|
||||
|
||||
* [Eleventy (11ty)](https://www.11ty.dev/docs)
|
||||
* [Diagrams](https://github.com/gmunguia/markdown-it-plantuml) with
|
||||
[plantuml](https://plantuml.com). See also
|
||||
[real-world-plantuml](https://real-world-plantuml.com).
|
||||
* [Diagrams](https://github.com/agoose77/markdown-it-diagrams) with
|
||||
[svgbob](https://github.com/ivanceras/svgbob) and
|
||||
[mermaid](https://github.com/mermaid-js/mermaid).
|
||||
* [arc42](https://arc42.org/overview) template.
|
||||
* [c4model](https://c4model.com) for software architecture, and an
|
||||
[implementation in plantuml](https://github.com/plantuml-stdlib/C4-PlantUML).
|
||||
21
docs/_data/metadata.json
Executable file
21
docs/_data/metadata.json
Executable file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Help center",
|
||||
"url": "https://docs.penpot.app/",
|
||||
"description": "Design freedom for teams.",
|
||||
"feed": {
|
||||
"subtitle": "Penpot: design freedom for teams.",
|
||||
"filename": "feed.xml",
|
||||
"path": "/feed/feed.xml",
|
||||
"id": "https://docs.penpot.app/"
|
||||
},
|
||||
"jsonfeed": {
|
||||
"path": "/feed/feed.json",
|
||||
"url": "https://docs.penpot.app/feed/feed.json"
|
||||
},
|
||||
"author": {
|
||||
"name": "Penpot",
|
||||
"email": "hello@penpot.app",
|
||||
"url": "https://penpot.app"
|
||||
},
|
||||
"twitter": "@penpotapp"
|
||||
}
|
||||
165
docs/_includes/layouts/base.njk
Normal file
165
docs/_includes/layouts/base.njk
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user