mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 11:48:42 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5b0331611d | ||
|
|
f8f1c58f61 | ||
|
|
3f34aa92fa | ||
|
|
c99102e49b | ||
|
|
d583661e58 | ||
|
|
cfad1d178f | ||
|
|
ffa326e08f | ||
|
|
c24b2dadec | ||
|
|
03040ed40b | ||
|
|
5e89cd1cb3 | ||
|
|
bf202473e9 | ||
|
|
9a3b5337d7 | ||
|
|
396cbb27b2 | ||
|
|
b4e6f8bc73 | ||
|
|
d88f28f5c2 | ||
|
|
886c0c596f | ||
|
|
b15b394c65 | ||
|
|
e36cf1d963 | ||
|
|
caf78a6b4d | ||
|
|
6a161267ba | ||
|
|
a0bb5e5ef3 | ||
|
|
34cc211912 | ||
|
|
e95713c1df | ||
|
|
e189dc965d | ||
|
|
a180c33a32 | ||
|
|
ea8febdb7d | ||
|
|
f765cc8dbc | ||
|
|
81b7972347 | ||
|
|
1281670c61 | ||
|
|
b8c6103858 | ||
|
|
b2c0bed84c | ||
|
|
9619fcbc1f | ||
|
|
e9c55e9eb4 | ||
|
|
53f580ad40 | ||
|
|
cf0045681e | ||
|
|
488d034a58 | ||
|
|
762a883b39 | ||
|
|
a63ded1ba1 | ||
|
|
8d66275187 | ||
|
|
f812b28892 | ||
|
|
59063e861c | ||
|
|
873c9b1903 | ||
|
|
9da891e9b0 | ||
|
|
a6de12323e | ||
|
|
edeb16bc26 | ||
|
|
90d947391a | ||
|
|
47cc80a93f | ||
|
|
1f8cfde1cf | ||
|
|
5f2ec595cb | ||
|
|
37a6446e32 | ||
|
|
be84b1cb01 | ||
|
|
298db46722 | ||
|
|
0c6b0598fa | ||
|
|
f2a2d772b0 |
No files matched your search
@@ -111,7 +111,7 @@ jobs:
|
|||||||
yarn run build:app:assets
|
yarn run build:app:assets
|
||||||
clojure -M:dev:shadow-cljs release main
|
clojure -M:dev:shadow-cljs release main
|
||||||
yarn playwright install --with-deps chromium
|
yarn playwright install --with-deps chromium
|
||||||
yarn e2e:test
|
yarn test:e2e
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: "backend tests"
|
name: "backend tests"
|
||||||
|
|||||||
+82
-1
@@ -1,11 +1,81 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 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
|
## 2.2.0
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :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!)
|
### :heart: Community contributions (Thank you!)
|
||||||
|
|
||||||
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
|
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
|
||||||
@@ -71,10 +141,21 @@
|
|||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
|
||||||
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
|
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
|
||||||
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
|
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
|
||||||
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
|
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
|
||||||
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
|
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
|
||||||
|
- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888)
|
||||||
|
- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464)
|
||||||
|
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
|
||||||
|
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
|
||||||
|
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
|
||||||
|
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
|
||||||
|
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
|
||||||
|
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
|
||||||
|
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
|
||||||
|
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
|
||||||
|
|
||||||
## 2.1.5
|
## 2.1.5
|
||||||
|
|
||||||
@@ -121,7 +202,7 @@
|
|||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
### :heart: Community contributions (Thank you!)
|
### :heart: Communityq contributions (Thank you!)
|
||||||
|
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,12 @@
|
|||||||
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
|
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
|
||||||
</picture>
|
</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>
|
<p align="center">
|
||||||
<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://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://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://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://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>
|
<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">
|
<p align="center">
|
||||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
<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.
|
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 ###
|
### 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".
|
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>
|
<!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>
|
<head>
|
||||||
<title>
|
<title>
|
||||||
@@ -110,15 +111,20 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:97px;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -151,7 +157,8 @@
|
|||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
@@ -164,29 +171,43 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" vertical-align="middle"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
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>
|
<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">
|
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||||
<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>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -194,17 +215,24 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -221,258 +249,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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>
|
<!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>
|
<head>
|
||||||
<title>
|
<title>
|
||||||
@@ -110,15 +111,20 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:97px;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -151,7 +157,8 @@
|
|||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
@@ -164,24 +171,36 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" vertical-align="middle"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
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>
|
<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">
|
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||||
<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>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -189,12 +208,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -211,258 +234,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -235,283 +235,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!doctype html>
|
<!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>
|
<head>
|
||||||
<title>
|
<title>
|
||||||
@@ -110,15 +111,20 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:97px;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -151,7 +157,8 @@
|
|||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
@@ -164,24 +171,37 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" vertical-align="middle"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
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>
|
<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">
|
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||||
<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>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -189,17 +209,24 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -216,258 +243,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<!doctype html>
|
<!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>
|
<head>
|
||||||
<title>
|
<title>
|
||||||
@@ -110,15 +111,20 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:97px;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -151,7 +157,8 @@
|
|||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
<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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
@@ -164,24 +171,37 @@
|
|||||||
class="" style="vertical-align:top;width:600px;"
|
class="" style="vertical-align:top;width:600px;"
|
||||||
>
|
>
|
||||||
<![endif]-->
|
<![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%;">
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
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>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" vertical-align="middle"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
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>
|
<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">
|
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||||
<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>
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -189,12 +209,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -211,258 +235,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -245,283 +245,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -268,283 +268,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -285,283 +285,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -243,283 +243,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
{% include "app/email/includes/footer.html" %}
|
||||||
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://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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,42 @@
|
|||||||
[{:id "wireframing-kit"
|
[{:id "wireframing-kit"
|
||||||
:name "Wireframe library"
|
: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"
|
{:id "prototype-examples"
|
||||||
:name "Prototype template"
|
: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"
|
{:id "plants-app"
|
||||||
:name "UI mockup example"
|
: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"
|
{:id "penpot-design-system"
|
||||||
:name "Design system example"
|
: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"
|
{:id "tutorial-for-beginners"
|
||||||
:name "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"
|
{:id "lucide-icons"
|
||||||
:name "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"
|
{:id "font-awesome"
|
||||||
:name "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"
|
{:id "black-white-mobile-templates"
|
||||||
:name "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"
|
{:id "avataaars"
|
||||||
:name "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"
|
{:id "ux-notes"
|
||||||
:name "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"
|
{:id "whiteboarding-kit"
|
||||||
:name "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"
|
{:id "open-color-scheme"
|
||||||
:name "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"
|
{:id "flex-layout-playground"
|
||||||
:name "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/main/welcome.penpot"}]
|
||||||
@@ -7,7 +7,7 @@ Debug Main Page
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<nav>
|
<nav>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>ADMIN DEBUG INTERFACE</h1>
|
<h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="dashboard">
|
<main class="dashboard">
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ export PENPOT_FLAGS="\
|
|||||||
enable-urepl-server \
|
enable-urepl-server \
|
||||||
enable-rpc-climit \
|
enable-rpc-climit \
|
||||||
enable-rpc-rlimit \
|
enable-rpc-rlimit \
|
||||||
|
enable-quotes \
|
||||||
enable-soft-rpc-rlimit \
|
enable-soft-rpc-rlimit \
|
||||||
enable-file-snapshot \
|
enable-auto-file-snapshot \
|
||||||
enable-webhooks \
|
enable-webhooks \
|
||||||
enable-access-tokens \
|
enable-access-tokens \
|
||||||
enable-tiered-file-data-storage \
|
enable-tiered-file-data-storage \
|
||||||
@@ -67,6 +68,7 @@ export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
|||||||
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||||
|
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||||
|
|
||||||
export OPTIONS="
|
export OPTIONS="
|
||||||
-A:jmx-remote -A:dev \
|
-A:jmx-remote -A:dev \
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export PENPOT_FLAGS="\
|
|||||||
disable-secure-session-cookies \
|
disable-secure-session-cookies \
|
||||||
enable-rpc-climit \
|
enable-rpc-climit \
|
||||||
enable-smtp \
|
enable-smtp \
|
||||||
|
enable-quotes \
|
||||||
enable-file-snapshot \
|
enable-file-snapshot \
|
||||||
enable-access-tokens \
|
enable-access-tokens \
|
||||||
enable-tiered-file-data-storage \
|
enable-tiered-file-data-storage \
|
||||||
|
|||||||
@@ -567,7 +567,6 @@
|
|||||||
(tokens/generate (::setup/props cfg)
|
(tokens/generate (::setup/props cfg)
|
||||||
{:iss :auth
|
{:iss :auth
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:props (:props info)
|
|
||||||
:profile-id (:id profile)}))
|
:profile-id (:id profile)}))
|
||||||
props (audit/profile->props profile)
|
props (audit/profile->props profile)
|
||||||
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
||||||
|
|||||||
@@ -315,15 +315,13 @@
|
|||||||
(l/dbg :hint "sendmail"
|
(l/dbg :hint "sendmail"
|
||||||
:id (:id params)
|
:id (:id params)
|
||||||
:to (:to params)
|
:to (:to params)
|
||||||
:subject (str/trim (:subject params))
|
:subject (str/trim (:subject params)))
|
||||||
:body (str/join "," (map :type (:body params))))
|
|
||||||
|
|
||||||
(.sendMessage ^Transport transport
|
(.sendMessage ^Transport transport
|
||||||
^MimeMessage message
|
^MimeMessage message
|
||||||
(.getAllRecipients message))))))
|
(.getAllRecipients message))))))
|
||||||
|
|
||||||
(when (or (contains? cf/flags :log-emails)
|
(when (contains? cf/flags :log-emails)
|
||||||
(not (contains? cf/flags :smtp)))
|
|
||||||
(send-to-logger! cfg params))))
|
(send-to-logger! cfg params))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
{::rres/status 200
|
{::rres/status 200
|
||||||
::rres/headers {"content-type" "text/html"}
|
::rres/headers {"content-type" "text/html"}
|
||||||
::rres/body (-> (io/resource "app/templates/debug.tmpl")
|
::rres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||||
(tmpl/render {}))})
|
(tmpl/render {:version (:full cf/version)}))})
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; FILE CHANGES
|
;; FILE CHANGES
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
(try
|
(try
|
||||||
(let [result (handler)]
|
(let [result (handler)]
|
||||||
(events/tap :end result))
|
(events/tap :end result))
|
||||||
|
|
||||||
|
(catch java.io.EOFException cause
|
||||||
|
(events/tap :error (errors/handle' cause request)))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/err :hint "unexpected error on processing sse response"
|
(l/err :hint "unexpected error on processing sse response"
|
||||||
:cause cause)
|
:cause cause)
|
||||||
|
|||||||
@@ -278,18 +278,18 @@
|
|||||||
:inc 1)
|
:inc 1)
|
||||||
message)
|
message)
|
||||||
|
|
||||||
(def ^:private schema:params
|
|
||||||
(sm/define
|
|
||||||
[:map {:title "params"}
|
|
||||||
[:session-id ::sm/uuid]]))
|
|
||||||
|
|
||||||
(defn- http-handler
|
(defn- http-handler
|
||||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
[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
|
(cond
|
||||||
(not profile-id)
|
(not profile-id)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:hint "Authentication required.")
|
:hint "authentication required")
|
||||||
|
|
||||||
;; WORKAROUND: we use the adapter specific predicate for
|
;; WORKAROUND: we use the adapter specific predicate for
|
||||||
;; performance reasons; for now, the ring default impl 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))}
|
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
|
||||||
|
|
||||||
(when-let [params (or (:request/params context) (:params context))]
|
(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)]
|
(when-let [value (:value context)]
|
||||||
{:value (pp/pprint-str value :length 30 :level 12)})
|
{:value (pp/pprint-str value :length 30 :level 12)})
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
|
|
||||||
(l/dbg :hint "run webhook"
|
(l/dbg :hint "run webhook"
|
||||||
:event-name (:name event)
|
:event-name (:name event)
|
||||||
:webhook-id (:id whook)
|
:webhook-id (str (:id whook))
|
||||||
:webhook-uri (:uri whook)
|
:webhook-uri (:uri whook)
|
||||||
:webhook-mtype (:mtype whook))
|
:webhook-mtype (:mtype whook))
|
||||||
|
|
||||||
|
|||||||
@@ -475,7 +475,8 @@
|
|||||||
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
|
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
|
||||||
(cf/get :objects-storage-s3-bucket))
|
(cf/get :objects-storage-s3-bucket))
|
||||||
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
|
::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
|
:app.storage.fs/backend
|
||||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||||
|
|||||||
@@ -149,6 +149,13 @@
|
|||||||
:hint "authentication required for this endpoint")
|
:hint "authentication required for this endpoint")
|
||||||
(f cfg params)))))
|
(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
|
(defn- wrap-audit
|
||||||
[_ f mdata]
|
[_ f mdata]
|
||||||
(if (or (contains? cf/flags :webhooks)
|
(if (or (contains? cf/flags :webhooks)
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
(defn- wrap-all
|
(defn- wrap-all
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(as-> f $
|
(as-> f $
|
||||||
|
(wrap-db-transaction cfg $ mdata)
|
||||||
(cond/wrap cfg $ mdata)
|
(cond/wrap cfg $ mdata)
|
||||||
(retry/wrap-retry cfg $ mdata)
|
(retry/wrap-retry cfg $ mdata)
|
||||||
(climit/wrap cfg $ mdata)
|
(climit/wrap cfg $ mdata)
|
||||||
|
|||||||
@@ -30,18 +30,17 @@
|
|||||||
:tid token-id
|
:tid token-id
|
||||||
:iat created-at})
|
:iat created-at})
|
||||||
|
|
||||||
expires-at (some-> expiration dt/in-future)]
|
expires-at (some-> expiration dt/in-future)
|
||||||
|
token (db/insert! conn :access-token
|
||||||
(db/insert! conn :access-token
|
{:id token-id
|
||||||
{:id token-id
|
:name name
|
||||||
:name name
|
:token token
|
||||||
:token token
|
:profile-id profile-id
|
||||||
:profile-id profile-id
|
:created-at created-at
|
||||||
:created-at created-at
|
:updated-at created-at
|
||||||
:updated-at created-at
|
:expires-at expires-at
|
||||||
:expires-at expires-at
|
:perms (db/create-array conn "text" [])})]
|
||||||
:perms (db/create-array conn "text" [])})))
|
(decode-row token)))
|
||||||
|
|
||||||
|
|
||||||
(defn repl:create-access-token
|
(defn repl:create-access-token
|
||||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||||
@@ -60,14 +59,12 @@
|
|||||||
(sv/defmethod ::create-access-token
|
(sv/defmethod ::create-access-token
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
::sm/params schema:create-access-token}
|
::sm/params schema:create-access-token}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [cfg (assoc cfg ::db/conn conn)]
|
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||||
(quotes/check-quote! conn
|
::quotes/profile-id profile-id})
|
||||||
{::quotes/id ::quotes/access-tokens-per-profile
|
|
||||||
::quotes/profile-id profile-id})
|
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||||
(-> (create-access-token cfg profile-id name expiration)
|
|
||||||
(decode-row)))))
|
|
||||||
|
|
||||||
(def ^:private schema:delete-access-token
|
(def ^:private schema:delete-access-token
|
||||||
[:map {:title "delete-access-token"}
|
[:map {:title "delete-access-token"}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@
|
|||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
|
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(def schema:password
|
(def schema:password
|
||||||
@@ -241,6 +243,7 @@
|
|||||||
|
|
||||||
params (d/without-nils params)
|
params (d/without-nils params)
|
||||||
token (tokens/generate (::setup/props cfg) params)]
|
token (tokens/generate (::setup/props cfg) params)]
|
||||||
|
|
||||||
(with-meta {:token token}
|
(with-meta {:token token}
|
||||||
{::audit/profile-id uuid/zero})))
|
{::audit/profile-id uuid/zero})))
|
||||||
|
|
||||||
@@ -350,7 +353,7 @@
|
|||||||
:extra-data ptoken})))
|
:extra-data ptoken})))
|
||||||
|
|
||||||
(defn register-profile
|
(defn register-profile
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
|
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
|
||||||
(let [theme (when (= theme "light") theme)
|
(let [theme (when (= theme "light") theme)
|
||||||
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
||||||
params (-> claims
|
params (-> claims
|
||||||
@@ -380,8 +383,13 @@
|
|||||||
invitation (when-let [token (:invitation-token params)]
|
invitation (when-let [token (:invitation-token params)]
|
||||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
||||||
|
|
||||||
props (audit/profile->props profile)]
|
props (audit/profile->props profile)
|
||||||
|
|
||||||
|
create-welcome-file-when-needed
|
||||||
|
(fn []
|
||||||
|
(when (:create-welcome-file params)
|
||||||
|
(let [cfg (dissoc cfg ::db/conn)]
|
||||||
|
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
||||||
(cond
|
(cond
|
||||||
;; When profile is blocked, we just ignore it and return plain data
|
;; When profile is blocked, we just ignore it and return plain data
|
||||||
(:is-blocked profile)
|
(:is-blocked profile)
|
||||||
@@ -418,6 +426,7 @@
|
|||||||
(if (:is-active profile)
|
(if (:is-active profile)
|
||||||
(-> (profile/strip-private-attrs profile)
|
(-> (profile/strip-private-attrs profile)
|
||||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||||
|
(rph/with-defer create-welcome-file-when-needed)
|
||||||
(rph/with-meta
|
(rph/with-meta
|
||||||
{::audit/replace-props props
|
{::audit/replace-props props
|
||||||
::audit/context {:action "login"}
|
::audit/context {:action "login"}
|
||||||
@@ -427,10 +436,12 @@
|
|||||||
(when-not (eml/has-reports? conn (:email profile))
|
(when-not (eml/has-reports? conn (:email profile))
|
||||||
(send-email-verification! cfg profile))
|
(send-email-verification! cfg profile))
|
||||||
|
|
||||||
(rph/with-meta {:email (:email profile)}
|
(-> {:email (:email profile)}
|
||||||
{::audit/replace-props props
|
(rph/with-defer create-welcome-file-when-needed)
|
||||||
::audit/context {:action "email-verification"}
|
(rph/with-meta
|
||||||
::audit/profile-id (:id profile)})))
|
{::audit/replace-props props
|
||||||
|
::audit/context {:action "email-verification"}
|
||||||
|
::audit/profile-id (:id profile)}))))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||||
@@ -462,7 +473,8 @@
|
|||||||
[:map {:title "register-profile"}
|
[:map {:title "register-profile"}
|
||||||
[:token schema:token]
|
[:token schema:token]
|
||||||
[:fullname [::sm/word-string {:max 100}]]
|
[:fullname [::sm/word-string {:max 100}]]
|
||||||
[:theme {:optional true} [:string {:max 10}]]])
|
[:theme {:optional true} [:string {:max 10}]]
|
||||||
|
[:create-welcome-file {:optional true} :boolean]])
|
||||||
|
|
||||||
(sv/defmethod ::register-profile
|
(sv/defmethod ::register-profile
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
|
|||||||
@@ -71,10 +71,15 @@
|
|||||||
[conn comment-id & {:as opts}]
|
[conn comment-id & {:as opts}]
|
||||||
(db/get-by-id conn :comment comment-id 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
|
(defn- get-next-seqn
|
||||||
[conn file-id]
|
[conn file-id]
|
||||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
|
||||||
res (db/exec-one! conn [sql file-id])]
|
|
||||||
(:next-seqn res)))
|
(:next-seqn res)))
|
||||||
|
|
||||||
(def sql:upsert-comment-thread-status
|
(def sql:upsert-comment-thread-status
|
||||||
@@ -292,7 +297,7 @@
|
|||||||
[:map {:title "create-comment-thread"}
|
[:map {:title "create-comment-thread"}
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:position ::gpt/point]
|
[:position ::gpt/point]
|
||||||
[:content [:string {:max 250}]]
|
[:content [:string {:max 750}]]
|
||||||
[:page-id ::sm/uuid]
|
[:page-id ::sm/uuid]
|
||||||
[:frame-id ::sm/uuid]
|
[:frame-id ::sm/uuid]
|
||||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||||
@@ -304,38 +309,43 @@
|
|||||||
::rtry/when rtry/conflict-exception?
|
::rtry/when rtry/conflict-exception?
|
||||||
::sm/params schema:create-comment-thread}
|
::sm/params schema:create-comment-thread}
|
||||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
[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}]
|
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
|
||||||
(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)]
|
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! cfg)
|
(-> cfg
|
||||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
(assoc ::quotes/profile-id profile-id)
|
||||||
::quotes/profile-id profile-id
|
(assoc ::quotes/team-id team-id)
|
||||||
::quotes/team-id team-id
|
(assoc ::quotes/project-id project-id)
|
||||||
::quotes/project-id project-id
|
(assoc ::quotes/file-id file-id)
|
||||||
::quotes/file-id file-id}
|
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
|
||||||
{::quotes/id ::quotes/comments-per-file
|
{::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}))
|
|
||||||
|
|
||||||
(create-comment-thread conn {:created-at request-at
|
(let [params {:created-at request-at
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:page-name page-name
|
:page-name page-name
|
||||||
:position position
|
:position position
|
||||||
:content content
|
:content content
|
||||||
:frame-id frame-id})))))
|
:frame-id frame-id}
|
||||||
|
thread (db/tx-run! cfg create-comment-thread params)]
|
||||||
|
|
||||||
|
(vary-meta thread assoc ::audit/props thread))))
|
||||||
|
|
||||||
(defn- create-comment-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)
|
seqn (get-next-seqn conn file-id)
|
||||||
thread-id (uuid/next)
|
thread-id (uuid/next)
|
||||||
thread (db/insert! conn :comment-thread
|
thread (db/insert! conn :comment-thread
|
||||||
@@ -364,7 +374,8 @@
|
|||||||
;; Optimistic update of current seq number on file.
|
;; Optimistic update of current seq number on file.
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:comment-thread-seqn seqn}
|
{:comment-thread-seqn seqn}
|
||||||
{:id file-id})
|
{:id file-id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
(-> thread
|
(-> thread
|
||||||
(select-keys [:id :file-id :page-id])
|
(select-keys [:id :file-id :page-id])
|
||||||
@@ -387,7 +398,6 @@
|
|||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(upsert-comment-thread-status! conn profile-id id)))))
|
(upsert-comment-thread-status! conn profile-id id)))))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND: Update Comment Thread
|
;; --- COMMAND: Update Comment Thread
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
@@ -432,12 +442,11 @@
|
|||||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
{: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)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(quotes/check-quote! conn
|
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||||
{::quotes/id ::quotes/comments-per-file
|
::quotes/profile-id profile-id
|
||||||
::quotes/profile-id profile-id
|
::quotes/team-id team-id
|
||||||
::quotes/team-id team-id
|
::quotes/project-id project-id
|
||||||
::quotes/project-id project-id
|
::quotes/file-id file-id})
|
||||||
::quotes/file-id file-id})
|
|
||||||
|
|
||||||
;; Update the page-name cached attribute on comment thread table.
|
;; Update the page-name cached attribute on comment thread table.
|
||||||
(when (not= page-name (:page-name thread))
|
(when (not= page-name (:page-name thread))
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
(def ^:private schema:send-user-feedback
|
(def ^:private schema:send-user-feedback
|
||||||
[:map {:title "send-user-feedback"}
|
[:map {:title "send-user-feedback"}
|
||||||
[:subject [:string {:max 250}]]
|
[:subject [:string {:max 400}]]
|
||||||
[:content [:string {:max 250}]]])
|
[:content [:string {:max 2500}]]])
|
||||||
|
|
||||||
(sv/defmethod ::send-user-feedback
|
(sv/defmethod ::send-user-feedback
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
|||||||
@@ -174,38 +174,34 @@
|
|||||||
;; --- COMMAND QUERY: get-file (by id)
|
;; --- COMMAND QUERY: get-file (by id)
|
||||||
|
|
||||||
(def schema:file
|
(def schema:file
|
||||||
(sm/define
|
[:map {:title "File"}
|
||||||
[:map {:title "File"}
|
[:id ::sm/uuid]
|
||||||
[:id ::sm/uuid]
|
[:features ::cfeat/features]
|
||||||
[:features ::cfeat/features]
|
[:has-media-trimmed ::sm/boolean]
|
||||||
[:has-media-trimmed ::sm/boolean]
|
[:comment-thread-seqn [::sm/int {:min 0}]]
|
||||||
[:comment-thread-seqn [::sm/int {:min 0}]]
|
[:name [:string {:max 250}]]
|
||||||
[:name [:string {:max 250}]]
|
[:revn [::sm/int {:min 0}]]
|
||||||
[:revn [::sm/int {:min 0}]]
|
[:modified-at ::dt/instant]
|
||||||
[:modified-at ::dt/instant]
|
[:is-shared ::sm/boolean]
|
||||||
[:is-shared ::sm/boolean]
|
[:project-id ::sm/uuid]
|
||||||
[:project-id ::sm/uuid]
|
[:created-at ::dt/instant]
|
||||||
[:created-at ::dt/instant]
|
[:data {:optional true} :any]])
|
||||||
[:data {:optional true} :any]]))
|
|
||||||
|
|
||||||
(def schema:permissions-mixin
|
(def schema:permissions-mixin
|
||||||
(sm/define
|
[:map {:title "PermissionsMixin"}
|
||||||
[:map {:title "PermissionsMixin"}
|
[:permissions ::perms/permissions]])
|
||||||
[:permissions ::perms/permissions]]))
|
|
||||||
|
|
||||||
(def schema:file-with-permissions
|
(def schema:file-with-permissions
|
||||||
(sm/define
|
[:merge {:title "FileWithPermissions"}
|
||||||
[:merge {:title "FileWithPermissions"}
|
schema:file
|
||||||
schema:file
|
schema:permissions-mixin])
|
||||||
schema:permissions-mixin]))
|
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:get-file
|
schema:get-file
|
||||||
(sm/define
|
[:map {:title "get-file"}
|
||||||
[:map {:title "get-file"}
|
[:features {:optional true} ::cfeat/features]
|
||||||
[:features {:optional true} ::cfeat/features]
|
[:id ::sm/uuid]
|
||||||
[:id ::sm/uuid]
|
[:project-id {:optional true} ::sm/uuid]])
|
||||||
[:project-id {:optional true} ::sm/uuid]]))
|
|
||||||
|
|
||||||
(defn- migrate-file
|
(defn- migrate-file
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||||
@@ -273,7 +269,7 @@
|
|||||||
|
|
||||||
(defn get-minimal-file
|
(defn get-minimal-file
|
||||||
[cfg id & {:as opts}]
|
[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)))
|
(db/get cfg :file {:id id} opts)))
|
||||||
|
|
||||||
(defn get-file-etag
|
(defn get-file-etag
|
||||||
@@ -360,7 +356,7 @@
|
|||||||
f.name,
|
f.name,
|
||||||
f.revn,
|
f.revn,
|
||||||
f.is_shared,
|
f.is_shared,
|
||||||
ft.media_id
|
ft.media_id AS thumbnail_id
|
||||||
from file as f
|
from file as f
|
||||||
left join file_thumbnail as ft on (ft.file_id = f.id
|
left join file_thumbnail as ft on (ft.file_id = f.id
|
||||||
and ft.revn = f.revn
|
and ft.revn = f.revn
|
||||||
@@ -371,13 +367,7 @@
|
|||||||
|
|
||||||
(defn get-project-files
|
(defn get-project-files
|
||||||
[conn project-id]
|
[conn project-id]
|
||||||
(->> (db/exec! conn [sql:project-files 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))))))
|
|
||||||
|
|
||||||
(def schema:get-project-files
|
(def schema:get-project-files
|
||||||
[:map {:title "get-project-files"}
|
[:map {:title "get-project-files"}
|
||||||
@@ -487,7 +477,7 @@
|
|||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:page-id {:optional true} ::sm/uuid]
|
[:page-id {:optional true} ::sm/uuid]
|
||||||
[:share-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]])
|
[:features {:optional true} ::cfeat/features]])
|
||||||
|
|
||||||
(sv/defmethod ::get-page
|
(sv/defmethod ::get-page
|
||||||
@@ -914,10 +904,9 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:set-file-shared
|
schema:set-file-shared
|
||||||
(sm/define
|
[:map {:title "set-file-shared"}
|
||||||
[:map {:title "set-file-shared"}
|
[:id ::sm/uuid]
|
||||||
[:id ::sm/uuid]
|
[:is-shared ::sm/boolean]])
|
||||||
[:is-shared ::sm/boolean]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::set-file-shared
|
(sv/defmethod ::set-file-shared
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
@@ -944,9 +933,8 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:delete-file
|
schema:delete-file
|
||||||
(sm/define
|
[:map {:title "delete-file"}
|
||||||
[:map {:title "delete-file"}
|
[:id ::sm/uuid]])
|
||||||
[:id ::sm/uuid]]))
|
|
||||||
|
|
||||||
(defn- delete-file
|
(defn- delete-file
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
@@ -978,10 +966,9 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:link-file-to-library
|
schema:link-file-to-library
|
||||||
(sm/define
|
[:map {:title "link-file-to-library"}
|
||||||
[:map {:title "link-file-to-library"}
|
[:file-id ::sm/uuid]
|
||||||
[:file-id ::sm/uuid]
|
[:library-id ::sm/uuid]])
|
||||||
[:library-id ::sm/uuid]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::link-file-to-library
|
(sv/defmethod ::link-file-to-library
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
@@ -1058,7 +1045,7 @@
|
|||||||
(def ^:private schema:ignore-file-library-sync-status
|
(def ^:private schema:ignore-file-library-sync-status
|
||||||
[:map {:title "ignore-file-library-sync-status"}
|
[:map {:title "ignore-file-library-sync-status"}
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:date ::dt/duration]])
|
[:date ::dt/instant]])
|
||||||
|
|
||||||
;; TODO: improve naming
|
;; TODO: improve naming
|
||||||
(sv/defmethod ::ignore-file-library-sync-status
|
(sv/defmethod ::ignore-file-library-sync-status
|
||||||
|
|||||||
@@ -98,46 +98,49 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::doc/module :files
|
::doc/module :files
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:create-file}
|
::sm/params schema:create-file
|
||||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
::db/transaction true}
|
||||||
(db/tx-run! cfg
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||||
(fn [{:keys [::db/conn] :as cfg}]
|
(projects/check-edition-permissions! conn profile-id project-id)
|
||||||
(projects/check-edition-permissions! conn profile-id project-id)
|
(let [team (teams/get-team conn
|
||||||
(let [team (teams/get-team conn
|
:profile-id profile-id
|
||||||
:profile-id profile-id
|
:project-id project-id)
|
||||||
:project-id project-id)
|
team-id (:id team)
|
||||||
team-id (:id team)
|
|
||||||
|
|
||||||
;; When we create files, we only need to respect the team
|
;; When we create files, we only need to respect the team
|
||||||
;; features, because some features can be enabled
|
;; features, because some features can be enabled
|
||||||
;; globally, but the team is still not migrated properly.
|
;; globally, but the team is still not migrated properly.
|
||||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||||
(cfeat/check-client-features! (:features params)))
|
(cfeat/check-client-features! (:features params)))
|
||||||
|
|
||||||
;; We also include all no migration features declared by
|
;; We also include all no migration features declared by
|
||||||
;; client; that enables the ability to enable a runtime
|
;; client; that enables the ability to enable a runtime
|
||||||
;; feature on frontend and make it permanent on file
|
;; feature on frontend and make it permanent on file
|
||||||
features (-> (:features params #{})
|
features (-> (:features params #{})
|
||||||
(set/intersection cfeat/no-migration-features)
|
(set/intersection cfeat/no-migration-features)
|
||||||
(set/union features))
|
(set/union features))
|
||||||
|
|
||||||
params (-> params
|
params (-> params
|
||||||
(assoc :profile-id profile-id)
|
(assoc :profile-id profile-id)
|
||||||
(assoc :features features))]
|
(assoc :features features))]
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! conn)
|
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||||
(list {::quotes/id ::quotes/files-per-project
|
::quotes/team-id team-id
|
||||||
::quotes/team-id team-id
|
::quotes/profile-id profile-id
|
||||||
::quotes/profile-id profile-id
|
::quotes/project-id project-id})
|
||||||
::quotes/project-id project-id}))
|
|
||||||
|
|
||||||
;; When newly computed features does not match exactly with
|
;; FIXME: IMPORTANT: this code can have race
|
||||||
;; the features defined on team row, we update it.
|
;; conditions, because we have no locks for updating
|
||||||
(when (not= features (:features team))
|
;; team so, creating two files concurrently can lead
|
||||||
(let [features (db/create-array conn "text" features)]
|
;; to lost team features updating
|
||||||
(db/update! conn :team
|
|
||||||
{:features features}
|
|
||||||
{:id team-id})))
|
|
||||||
|
|
||||||
(-> (create-file cfg params)
|
;; When newly computed features does not match exactly with
|
||||||
(vary-meta assoc ::audit/props {:team-id team-id}))))))
|
;; 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
|
(sv/defmethod ::create-temp-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::doc/module :files
|
::doc/module :files
|
||||||
::sm/params schema:create-temp-file}
|
::sm/params schema:create-temp-file
|
||||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
::db/transaction true}
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||||
(projects/check-edition-permissions! conn profile-id project-id)
|
(projects/check-edition-permissions! conn profile-id project-id)
|
||||||
(let [team (teams/get-team conn :profile-id profile-id :project-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
|
;; If the imported project doesn't contain v2 we need to remove it
|
||||||
;; features, because some features can be enabled
|
team-features
|
||||||
;; globally, but the team is still not migrated properly.
|
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||||
input-features (:features params #{})
|
(not (contains? input-features "components/v2"))
|
||||||
|
(disj "components/v2"))
|
||||||
|
|
||||||
;; If the imported project doesn't contain v2 we need to remove it
|
;; We also include all no migration features declared by
|
||||||
team-features
|
;; client; that enables the ability to enable a runtime
|
||||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
;; feature on frontend and make it permanent on file
|
||||||
(not (contains? input-features "components/v2"))
|
features
|
||||||
(disj "components/v2"))
|
(-> 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
|
(files.create/create-file cfg params)))
|
||||||
;; 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)))))
|
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: update-temp-file
|
;; --- MUTATION COMMAND: update-temp-file
|
||||||
|
|
||||||
|
|||||||
@@ -179,18 +179,16 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:get-file-data-for-thumbnail
|
schema:get-file-data-for-thumbnail
|
||||||
(sm/define
|
[:map {:title "get-file-data-for-thumbnail"}
|
||||||
[:map {:title "get-file-data-for-thumbnail"}
|
[:file-id ::sm/uuid]
|
||||||
[:file-id ::sm/uuid]
|
[:features {:optional true} ::cfeat/features]])
|
||||||
[:features {:optional true} ::cfeat/features]]))
|
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:partial-file
|
schema:partial-file
|
||||||
(sm/define
|
[:map {:title "PartialFile"}
|
||||||
[:map {:title "PartialFile"}
|
[:id ::sm/uuid]
|
||||||
[:id ::sm/uuid]
|
[:revn {:min 0} ::sm/int]
|
||||||
[:revn {:min 0} ::sm/int]
|
[:page :any]])
|
||||||
[:page :any]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::get-file-data-for-thumbnail
|
(sv/defmethod ::get-file-data-for-thumbnail
|
||||||
"Retrieves the data for generate the thumbnail of the file. Used
|
"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)
|
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT (file_id, object_id, tag)
|
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 *")
|
RETURNING *")
|
||||||
|
|
||||||
(defn- persist-thumbnail!
|
(defn- persist-thumbnail!
|
||||||
@@ -251,17 +249,19 @@
|
|||||||
:content-type mtype
|
:content-type mtype
|
||||||
:bucket "file-object-thumbnail"})))
|
:bucket "file-object-thumbnail"})))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn- create-file-object-thumbnail!
|
(defn- create-file-object-thumbnail!
|
||||||
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
|
[{:keys [::sto/storage] :as cfg} file object-id media tag]
|
||||||
(let [tsnow (dt/now)
|
(let [file-id (:id file)
|
||||||
media (persist-thumbnail! storage media tsnow)
|
timestamp (dt/now)
|
||||||
|
media (persist-thumbnail! storage media timestamp)
|
||||||
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
[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])
|
(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
|
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
|
||||||
file-id object-id tag (:id media)
|
file-id object-id tag
|
||||||
tsnow (:id media)])]
|
(:id media)
|
||||||
|
timestamp
|
||||||
|
(:id media)
|
||||||
|
(:deleted-at file)])]
|
||||||
[th1 th2])))]
|
[th1 th2])))]
|
||||||
|
|
||||||
(when (and (some? th1)
|
(when (and (some? th1)
|
||||||
@@ -294,8 +294,8 @@
|
|||||||
(media/validate-media-size! media)
|
(media/validate-media-size! media)
|
||||||
|
|
||||||
(db/run! cfg files/check-edition-permissions! profile-id file-id)
|
(db/run! cfg files/check-edition-permissions! profile-id file-id)
|
||||||
|
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
|
||||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))
|
(create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,20 @@
|
|||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(declare ^:private get-lagged-changes)
|
||||||
|
(declare ^:private send-notifications!)
|
||||||
|
(declare ^:private update-file)
|
||||||
|
(declare ^:private update-file*)
|
||||||
|
(declare ^:private process-changes-and-validate)
|
||||||
|
(declare ^:private take-snapshot?)
|
||||||
|
(declare ^:private delete-old-snapshots!)
|
||||||
|
|
||||||
|
;; PUBLIC API; intended to be used outside of this module
|
||||||
|
(declare update-file!)
|
||||||
|
(declare update-file-data!)
|
||||||
|
(declare persist-file!)
|
||||||
|
(declare get-file)
|
||||||
|
|
||||||
;; --- SCHEMA
|
;; --- SCHEMA
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
@@ -97,41 +111,6 @@
|
|||||||
(or (contains? library-change-types type)
|
(or (contains? library-change-types type)
|
||||||
(contains? file-change-types type)))
|
(contains? file-change-types type)))
|
||||||
|
|
||||||
(def ^:private sql:get-file
|
|
||||||
"SELECT f.*, p.team_id
|
|
||||||
FROM file AS f
|
|
||||||
JOIN project AS p ON (p.id = f.project_id)
|
|
||||||
WHERE f.id = ?
|
|
||||||
AND (f.deleted_at IS NULL OR
|
|
||||||
f.deleted_at > now())
|
|
||||||
FOR KEY SHARE")
|
|
||||||
|
|
||||||
(defn get-file
|
|
||||||
[conn id]
|
|
||||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
|
||||||
(when-not file
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :object-not-found
|
|
||||||
:hint (format "file with id '%s' does not exists" id)))
|
|
||||||
(update file :features db/decode-pgarray #{})))
|
|
||||||
|
|
||||||
(defn- wrap-with-pointer-map-context
|
|
||||||
[f]
|
|
||||||
(fn [cfg {:keys [id] :as file}]
|
|
||||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
|
||||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
|
||||||
(let [result (f cfg file)]
|
|
||||||
(feat.fdata/persist-pointers! cfg id)
|
|
||||||
result))))
|
|
||||||
|
|
||||||
(declare ^:private delete-old-snapshots!)
|
|
||||||
(declare ^:private get-lagged-changes)
|
|
||||||
(declare ^:private send-notifications!)
|
|
||||||
(declare ^:private take-snapshot?)
|
|
||||||
(declare ^:private update-file)
|
|
||||||
(declare ^:private update-file*)
|
|
||||||
(declare ^:private update-file-data)
|
|
||||||
|
|
||||||
;; If features are specified from params and the final feature
|
;; If features are specified from params and the final feature
|
||||||
;; set is different than the persisted one, update it on the
|
;; set is different than the persisted one, update it on the
|
||||||
;; database.
|
;; database.
|
||||||
@@ -147,7 +126,8 @@
|
|||||||
::sm/result schema:update-file-result
|
::sm/result schema:update-file-result
|
||||||
::doc/module :files
|
::doc/module :files
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::mtx/metrics] :as cfg}
|
||||||
|
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(files/check-edition-permissions! conn profile-id id)
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
(db/xact-lock! conn id)
|
(db/xact-lock! conn id)
|
||||||
@@ -161,14 +141,30 @@
|
|||||||
(cfeat/check-client-features! (:features params))
|
(cfeat/check-client-features! (:features params))
|
||||||
(cfeat/check-file-features! (:features file) (:features params)))
|
(cfeat/check-file-features! (:features file) (:features params)))
|
||||||
|
|
||||||
params (assoc params
|
changes (if changes-with-metadata
|
||||||
:profile-id profile-id
|
(->> changes-with-metadata (mapcat :changes) vec)
|
||||||
:features features
|
(vec changes))
|
||||||
:team team
|
|
||||||
:file file)
|
params (-> params
|
||||||
|
(assoc :profile-id profile-id)
|
||||||
|
(assoc :features features)
|
||||||
|
(assoc :team team)
|
||||||
|
(assoc :file file)
|
||||||
|
(assoc :changes changes))
|
||||||
|
|
||||||
|
cfg (assoc cfg ::timestamp (dt/now))
|
||||||
|
|
||||||
tpoint (dt/tpoint)]
|
tpoint (dt/tpoint)]
|
||||||
|
|
||||||
|
|
||||||
|
(when (> (:revn params)
|
||||||
|
(:revn file))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :revn-conflict
|
||||||
|
:hint "The incoming revision number is greater that stored version."
|
||||||
|
:context {:incoming-revn (:revn params)
|
||||||
|
:stored-revn (:revn file)}))
|
||||||
|
|
||||||
;; When newly computed features does not match exactly with
|
;; When newly computed features does not match exactly with
|
||||||
;; the features defined on team row, we update it.
|
;; the features defined on team row, we update it.
|
||||||
(when (not= features (:features team))
|
(when (not= features (:features team))
|
||||||
@@ -177,90 +173,126 @@
|
|||||||
{:features features}
|
{:features features}
|
||||||
{:id (:id team)})))
|
{:id (:id team)})))
|
||||||
|
|
||||||
|
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||||
|
|
||||||
(binding [l/*context* (some-> (meta params)
|
(binding [l/*context* (some-> (meta params)
|
||||||
(get :app.http/request)
|
(get :app.http/request)
|
||||||
(errors/request->context))]
|
(errors/request->context))]
|
||||||
(-> (update-file cfg params)
|
(-> (update-file* cfg params)
|
||||||
(rph/with-defer #(let [elapsed (tpoint)]
|
(rph/with-defer #(let [elapsed (tpoint)]
|
||||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
||||||
|
|
||||||
(defn update-file
|
|
||||||
[{:keys [::mtx/metrics] :as cfg}
|
|
||||||
{:keys [file features changes changes-with-metadata] :as params}]
|
|
||||||
(let [features (-> features
|
|
||||||
(set/difference cfeat/frontend-only-features)
|
|
||||||
(set/union (:features file)))
|
|
||||||
|
|
||||||
update-fn (cond-> update-file*
|
|
||||||
(contains? features "fdata/pointer-map")
|
|
||||||
(wrap-with-pointer-map-context))
|
|
||||||
|
|
||||||
changes (if changes-with-metadata
|
|
||||||
(->> changes-with-metadata (mapcat :changes) vec)
|
|
||||||
(vec changes))]
|
|
||||||
|
|
||||||
(when (> (:revn params)
|
|
||||||
(:revn file))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :revn-conflict
|
|
||||||
:hint "The incoming revision number is greater that stored version."
|
|
||||||
:context {:incoming-revn (:revn params)
|
|
||||||
:stored-revn (:revn file)}))
|
|
||||||
|
|
||||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
|
||||||
|
|
||||||
(binding [cfeat/*current* features
|
|
||||||
cfeat/*previous* (:features file)]
|
|
||||||
(let [file (assoc file :features features)
|
|
||||||
params (-> params
|
|
||||||
(assoc :file file)
|
|
||||||
(assoc :changes changes)
|
|
||||||
(assoc ::created-at (dt/now)))]
|
|
||||||
|
|
||||||
(-> (update-fn cfg params)
|
|
||||||
(vary-meta assoc ::audit/replace-props
|
|
||||||
{:id (:id file)
|
|
||||||
:name (:name file)
|
|
||||||
:features (:features file)
|
|
||||||
:project-id (:project-id file)
|
|
||||||
:team-id (:team-id file)}))))))
|
|
||||||
|
|
||||||
(defn- update-file*
|
(defn- update-file*
|
||||||
[{:keys [::db/conn ::wrk/executor] :as cfg}
|
"Internal function, part of the update-file process, that encapsulates
|
||||||
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
|
the changes application offload to a separated thread and emit all
|
||||||
|
corresponding notifications.
|
||||||
|
|
||||||
|
Follow the inner implementation to `update-file-data!` function.
|
||||||
|
|
||||||
|
Only intended for internal use on this module."
|
||||||
|
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||||
|
{:keys [profile-id file features changes session-id skip-validate] :as params}]
|
||||||
|
|
||||||
(let [;; Retrieve the file data
|
(let [;; Retrieve the file data
|
||||||
file (feat.fdata/resolve-file-data cfg file)
|
file (feat.fdata/resolve-file-data cfg file)
|
||||||
|
|
||||||
|
file (assoc file :features
|
||||||
|
(-> features
|
||||||
|
(set/difference cfeat/frontend-only-features)
|
||||||
|
(set/union (:features file))))
|
||||||
|
|
||||||
;; Process the file data on separated thread for avoid to do
|
;; Process the file data on separated thread for avoid to do
|
||||||
;; the CPU intensive operation on vthread.
|
;; the CPU intensive operation on vthread.
|
||||||
|
file (px/invoke! executor
|
||||||
|
(fn []
|
||||||
|
(binding [cfeat/*current* features
|
||||||
|
cfeat/*previous* (:features file)]
|
||||||
|
(update-file-data! cfg file
|
||||||
|
process-changes-and-validate
|
||||||
|
changes skip-validate))))]
|
||||||
|
|
||||||
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
|
(when (feat.fdata/offloaded? file)
|
||||||
features (db/create-array conn "text" (:features file))]
|
|
||||||
|
|
||||||
;; NOTE: if file was offloaded, we need to touch the referenced
|
|
||||||
;; storage object because on this update operation the data will
|
|
||||||
;; be overwritted.
|
|
||||||
(when (= "objects-storage" (:data-backend file))
|
|
||||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||||
(sto/touch-object! storage (:data-ref-id file))))
|
(some->> (:data-ref-id file) (sto/touch-object! storage))))
|
||||||
|
|
||||||
(db/insert! conn :file-change
|
|
||||||
{:id (uuid/next)
|
|
||||||
:session-id session-id
|
|
||||||
:profile-id profile-id
|
|
||||||
:created-at created-at
|
|
||||||
:file-id (:id file)
|
|
||||||
:revn (:revn file)
|
|
||||||
:version (:version file)
|
|
||||||
:label (::snapshot-label file)
|
|
||||||
:data (::snapshot-data file)
|
|
||||||
:features (db/create-array conn "text" (:features file))
|
|
||||||
:changes (blob/encode changes)}
|
|
||||||
{::db/return-keys false})
|
|
||||||
|
|
||||||
|
;; TODO: move this to asynchronous task
|
||||||
(when (::snapshot-data file)
|
(when (::snapshot-data file)
|
||||||
(delete-old-snapshots! cfg file))
|
(delete-old-snapshots! cfg file))
|
||||||
|
|
||||||
|
(persist-file! cfg file)
|
||||||
|
|
||||||
|
(let [params (assoc params :file file)
|
||||||
|
response {:revn (:revn file)
|
||||||
|
:lagged (get-lagged-changes conn params)}
|
||||||
|
features (db/create-array conn "text" (:features file))]
|
||||||
|
|
||||||
|
;; Insert change (xlog)
|
||||||
|
(db/insert! conn :file-change
|
||||||
|
{:id (uuid/next)
|
||||||
|
:session-id session-id
|
||||||
|
:profile-id profile-id
|
||||||
|
:created-at timestamp
|
||||||
|
:file-id (:id file)
|
||||||
|
:revn (:revn file)
|
||||||
|
:version (:version file)
|
||||||
|
:features features
|
||||||
|
:label (::snapshot-label file)
|
||||||
|
:data (::snapshot-data file)
|
||||||
|
:changes (blob/encode changes)}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
|
;; Send asynchronous notifications
|
||||||
|
(send-notifications! cfg params)
|
||||||
|
|
||||||
|
(vary-meta response assoc ::audit/replace-props
|
||||||
|
{:id (:id file)
|
||||||
|
:name (:name file)
|
||||||
|
:features (:features file)
|
||||||
|
:project-id (:project-id file)
|
||||||
|
:team-id (:team-id file)}))))
|
||||||
|
|
||||||
|
(defn update-file!
|
||||||
|
"A public api that allows apply a transformation to a file with all context setup."
|
||||||
|
[cfg file-id update-fn & args]
|
||||||
|
(let [file (get-file cfg file-id)
|
||||||
|
file (apply update-file-data! cfg file update-fn args)]
|
||||||
|
(persist-file! cfg file)))
|
||||||
|
|
||||||
|
(def ^:private sql:get-file
|
||||||
|
"SELECT f.*, p.team_id
|
||||||
|
FROM file AS f
|
||||||
|
JOIN project AS p ON (p.id = f.project_id)
|
||||||
|
WHERE f.id = ?
|
||||||
|
AND (f.deleted_at IS NULL OR
|
||||||
|
f.deleted_at > now())
|
||||||
|
FOR KEY SHARE")
|
||||||
|
|
||||||
|
(defn get-file
|
||||||
|
"Get not-decoded file, only decodes the features set."
|
||||||
|
[conn id]
|
||||||
|
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||||
|
(when-not file
|
||||||
|
(ex/raise :type :not-found
|
||||||
|
:code :object-not-found
|
||||||
|
:hint (format "file with id '%s' does not exists" id)))
|
||||||
|
(update file :features db/decode-pgarray #{})))
|
||||||
|
|
||||||
|
(defn persist-file!
|
||||||
|
"Function responsible of persisting already encoded file. Should be
|
||||||
|
used together with `get-file` and `update-file-data!`.
|
||||||
|
|
||||||
|
It also updates the project modified-at attr."
|
||||||
|
[{:keys [::db/conn ::timestamp]} file]
|
||||||
|
(let [features (db/create-array conn "text" (:features file))
|
||||||
|
;; The timestamp can be nil because this function is also
|
||||||
|
;; intended to be used outside of this module
|
||||||
|
modified-at (or timestamp (dt/now))]
|
||||||
|
|
||||||
|
(db/update! conn :project
|
||||||
|
{:modified-at modified-at}
|
||||||
|
{:id (:project-id file)}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:revn (:revn file)
|
{:revn (:revn file)
|
||||||
:data (:data file)
|
:data (:data file)
|
||||||
@@ -268,20 +300,95 @@
|
|||||||
:features features
|
:features features
|
||||||
:data-backend nil
|
:data-backend nil
|
||||||
:data-ref-id nil
|
:data-ref-id nil
|
||||||
:modified-at created-at
|
:modified-at modified-at
|
||||||
:has-media-trimmed false}
|
:has-media-trimmed false}
|
||||||
{:id (:id file)})
|
{:id (:id file)}
|
||||||
|
{::db/return-keys false})))
|
||||||
|
|
||||||
(db/update! conn :project
|
(defn- update-file-data!
|
||||||
{:modified-at created-at}
|
"Perform a file data transformation in with all update context setup.
|
||||||
{:id (:project-id file)})
|
|
||||||
|
|
||||||
(let [params (assoc params :file file)]
|
This function expected not-decoded file and transformation function. Returns
|
||||||
;; Send asynchronous notifications
|
an encoded file.
|
||||||
(send-notifications! cfg params)
|
|
||||||
|
|
||||||
{:revn (:revn file)
|
This function is not responsible of saving the file. It only saves
|
||||||
:lagged (get-lagged-changes conn params)})))
|
fdata/pointer-map modified fragments."
|
||||||
|
|
||||||
|
[cfg {:keys [id] :as file} update-fn & args]
|
||||||
|
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||||
|
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||||
|
(let [file (update file :data (fn [data]
|
||||||
|
(-> data
|
||||||
|
(blob/decode)
|
||||||
|
(assoc :id (:id file)))))
|
||||||
|
|
||||||
|
;; For avoid unnecesary overhead of creating multiple pointers
|
||||||
|
;; and handly internally with objects map in their worst
|
||||||
|
;; case (when probably all shapes and all pointers will be
|
||||||
|
;; readed in any case), we just realize/resolve them before
|
||||||
|
;; applying the migration to the file
|
||||||
|
file (if (fmg/need-migration? file)
|
||||||
|
(-> file
|
||||||
|
(update :data feat.fdata/process-pointers deref)
|
||||||
|
(update :data feat.fdata/process-objects (partial into {}))
|
||||||
|
(fmg/migrate-file))
|
||||||
|
file)
|
||||||
|
|
||||||
|
file (apply update-fn cfg file args)
|
||||||
|
|
||||||
|
;; TODO: reuse operations if file is migrated
|
||||||
|
;; TODO: move encoding to a separated thread
|
||||||
|
file (if (take-snapshot? file)
|
||||||
|
(let [tpoint (dt/tpoint)
|
||||||
|
snapshot (-> (:data file)
|
||||||
|
(feat.fdata/process-pointers deref)
|
||||||
|
(feat.fdata/process-objects (partial into {}))
|
||||||
|
(blob/encode))
|
||||||
|
elapsed (tpoint)
|
||||||
|
label (str "internal/snapshot/" (:revn file))]
|
||||||
|
|
||||||
|
(l/trc :hint "take snapshot"
|
||||||
|
:file-id (str (:id file))
|
||||||
|
:revn (:revn file)
|
||||||
|
:label label
|
||||||
|
:elapsed (dt/format-duration elapsed))
|
||||||
|
|
||||||
|
(-> file
|
||||||
|
(assoc ::snapshot-data snapshot)
|
||||||
|
(assoc ::snapshot-label label)))
|
||||||
|
file)
|
||||||
|
|
||||||
|
file (cond-> file
|
||||||
|
(contains? cfeat/*current* "fdata/objects-map")
|
||||||
|
(feat.fdata/enable-objects-map)
|
||||||
|
|
||||||
|
(contains? cfeat/*current* "fdata/pointer-map")
|
||||||
|
(feat.fdata/enable-pointer-map)
|
||||||
|
|
||||||
|
:always
|
||||||
|
(update :data blob/encode))]
|
||||||
|
|
||||||
|
(feat.fdata/persist-pointers! cfg id)
|
||||||
|
|
||||||
|
file)))
|
||||||
|
|
||||||
|
(defn- get-file-libraries
|
||||||
|
"A helper for preload file libraries, mainly used for perform file
|
||||||
|
semantical and structural validation"
|
||||||
|
[{:keys [::db/conn] :as cfg} file]
|
||||||
|
(->> (files/get-file-libraries conn (:id file))
|
||||||
|
(into [file] (map (fn [{:keys [id]}]
|
||||||
|
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||||
|
pmap/*tracked* nil]
|
||||||
|
;; We do not resolve the objects maps here
|
||||||
|
;; because there is a lower probability that all
|
||||||
|
;; shapes needed to be loded into memory, so we
|
||||||
|
;; leeave it on lazy status
|
||||||
|
(-> (files/get-file cfg id :migrate? false)
|
||||||
|
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||||
|
(update :data feat.fdata/process-objects (partial into {}))
|
||||||
|
(fmg/migrate-file))))))
|
||||||
|
(d/index-by :id)))
|
||||||
|
|
||||||
(defn- soft-validate-file-schema!
|
(defn- soft-validate-file-schema!
|
||||||
[file]
|
[file]
|
||||||
@@ -298,68 +405,19 @@
|
|||||||
(l/error :hint "file validation error"
|
(l/error :hint "file validation error"
|
||||||
:cause cause))))
|
:cause cause))))
|
||||||
|
|
||||||
(defn- update-file-data
|
(defn- process-changes-and-validate
|
||||||
[{:keys [::db/conn] :as cfg} file changes skip-validate]
|
[cfg file changes skip-validate]
|
||||||
(let [file (update file :data (fn [data]
|
(let [;; WARNING: this ruins performance; maybe we need to find
|
||||||
(-> data
|
|
||||||
(blob/decode)
|
|
||||||
(assoc :id (:id file)))))
|
|
||||||
;; For avoid unnecesary overhead of creating multiple pointers
|
|
||||||
;; and handly internally with objects map in their worst
|
|
||||||
;; case (when probably all shapes and all pointers will be
|
|
||||||
;; readed in any case), we just realize/resolve them before
|
|
||||||
;; applying the migration to the file
|
|
||||||
file (if (fmg/need-migration? file)
|
|
||||||
(-> file
|
|
||||||
(update :data feat.fdata/process-pointers deref)
|
|
||||||
(update :data feat.fdata/process-objects (partial into {}))
|
|
||||||
(fmg/migrate-file))
|
|
||||||
file)
|
|
||||||
|
|
||||||
;; WARNING: this ruins performance; maybe we need to find
|
|
||||||
;; some other way to do general validation
|
;; some other way to do general validation
|
||||||
libs (when (and (or (contains? cf/flags :file-validation)
|
libs (when (and (or (contains? cf/flags :file-validation)
|
||||||
(contains? cf/flags :soft-file-validation))
|
(contains? cf/flags :soft-file-validation))
|
||||||
(not skip-validate))
|
(not skip-validate))
|
||||||
(->> (files/get-file-libraries conn (:id file))
|
(get-file-libraries cfg file))
|
||||||
(into [file] (map (fn [{:keys [id]}]
|
|
||||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
|
||||||
pmap/*tracked* nil]
|
|
||||||
;; We do not resolve the objects maps here
|
|
||||||
;; because there is a lower probability that all
|
|
||||||
;; shapes needed to be loded into memory, so we
|
|
||||||
;; leeave it on lazy status
|
|
||||||
(-> (files/get-file cfg id :migrate? false)
|
|
||||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
|
||||||
(update :data feat.fdata/process-objects (partial into {}))
|
|
||||||
(fmg/migrate-file))))))
|
|
||||||
(d/index-by :id)))
|
|
||||||
|
|
||||||
|
|
||||||
file (-> (files/check-version! file)
|
file (-> (files/check-version! file)
|
||||||
(update :revn inc)
|
(update :revn inc)
|
||||||
(update :data cpc/process-changes changes)
|
(update :data cpc/process-changes changes)
|
||||||
(update :data d/without-nils))
|
(update :data d/without-nils))]
|
||||||
|
|
||||||
file (if (take-snapshot? file)
|
|
||||||
(let [tpoint (dt/tpoint)
|
|
||||||
snapshot (-> (:data file)
|
|
||||||
(feat.fdata/process-pointers deref)
|
|
||||||
(feat.fdata/process-objects (partial into {}))
|
|
||||||
(blob/encode))
|
|
||||||
elapsed (tpoint)
|
|
||||||
label (str "internal/snapshot/" (:revn file))]
|
|
||||||
|
|
||||||
(l/trc :hint "take snapshot"
|
|
||||||
:file-id (str (:id file))
|
|
||||||
:revn (:revn file)
|
|
||||||
:label label
|
|
||||||
:elapsed (dt/format-duration elapsed))
|
|
||||||
|
|
||||||
(-> file
|
|
||||||
(assoc ::snapshot-data snapshot)
|
|
||||||
(assoc ::snapshot-label label)))
|
|
||||||
file)]
|
|
||||||
|
|
||||||
(binding [pmap/*tracked* nil]
|
(binding [pmap/*tracked* nil]
|
||||||
(when (contains? cf/flags :soft-file-validation)
|
(when (contains? cf/flags :soft-file-validation)
|
||||||
@@ -376,15 +434,7 @@
|
|||||||
(not skip-validate))
|
(not skip-validate))
|
||||||
(val/validate-file-schema! file)))
|
(val/validate-file-schema! file)))
|
||||||
|
|
||||||
(cond-> file
|
file))
|
||||||
(contains? cfeat/*current* "fdata/objects-map")
|
|
||||||
(feat.fdata/enable-objects-map)
|
|
||||||
|
|
||||||
(contains? cfeat/*current* "fdata/pointer-map")
|
|
||||||
(feat.fdata/enable-pointer-map)
|
|
||||||
|
|
||||||
:always
|
|
||||||
(update :data blob/encode))))
|
|
||||||
|
|
||||||
(defn- take-snapshot?
|
(defn- take-snapshot?
|
||||||
"Defines the rule when file `data` snapshot should be saved."
|
"Defines the rule when file `data` snapshot should be saved."
|
||||||
@@ -426,8 +476,7 @@
|
|||||||
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
|
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
|
||||||
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
|
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private sql:lagged-changes
|
||||||
sql:lagged-changes
|
|
||||||
"select s.id, s.revn, s.file_id,
|
"select s.id, s.revn, s.file_id,
|
||||||
s.session_id, s.changes
|
s.session_id, s.changes
|
||||||
from file_change as s
|
from file_change as s
|
||||||
|
|||||||
@@ -86,6 +86,9 @@
|
|||||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||||
[:font-style [::sm/one-of {:format "string"} valid-style]]])
|
[: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
|
(sv/defmethod ::create-font-variant
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
::climit/id [[:process-font/by-profile ::rpc/profile-id]
|
||||||
@@ -96,9 +99,9 @@
|
|||||||
(db/tx-run! cfg
|
(db/tx-run! cfg
|
||||||
(fn [{:keys [::db/conn] :as cfg}]
|
(fn [{:keys [::db/conn] :as cfg}]
|
||||||
(teams/check-edition-permissions! conn profile-id team-id)
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
|
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
|
||||||
::quotes/profile-id profile-id
|
::quotes/profile-id profile-id
|
||||||
::quotes/team-id team-id})
|
::quotes/team-id team-id})
|
||||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||||
|
|
||||||
(defn create-font-variant
|
(defn create-font-variant
|
||||||
|
|||||||
@@ -88,10 +88,9 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:duplicate-file
|
schema:duplicate-file
|
||||||
(sm/define
|
[:map {:title "duplicate-file"}
|
||||||
[:map {:title "duplicate-file"}
|
[:file-id ::sm/uuid]
|
||||||
[:file-id ::sm/uuid]
|
[:name {:optional true} [:string {:max 250}]]])
|
||||||
[:name {:optional true} [:string {:max 250}]]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-file
|
(sv/defmethod ::duplicate-file
|
||||||
"Duplicate a single file in the same team."
|
"Duplicate a single file in the same team."
|
||||||
@@ -150,10 +149,9 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:duplicate-project
|
schema:duplicate-project
|
||||||
(sm/define
|
[:map {:title "duplicate-project"}
|
||||||
[:map {:title "duplicate-project"}
|
[:project-id ::sm/uuid]
|
||||||
[:project-id ::sm/uuid]
|
[:name {:optional true} [:string {:max 250}]]])
|
||||||
[:name {:optional true} [:string {:max 250}]]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-project
|
(sv/defmethod ::duplicate-project
|
||||||
"Duplicate an entire project with all the files"
|
"Duplicate an entire project with all the files"
|
||||||
@@ -327,10 +325,9 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:move-files
|
schema:move-files
|
||||||
(sm/define
|
[:map {:title "move-files"}
|
||||||
[:map {:title "move-files"}
|
[:ids ::sm/set-of-uuid]
|
||||||
[:ids ::sm/set-of-uuid]
|
[:project-id ::sm/uuid]])
|
||||||
[:project-id ::sm/uuid]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::move-files
|
(sv/defmethod ::move-files
|
||||||
"Move a set of files from one project to other."
|
"Move a set of files from one project to other."
|
||||||
@@ -396,8 +393,8 @@
|
|||||||
|
|
||||||
;; --- COMMAND: Clone Template
|
;; --- COMMAND: Clone Template
|
||||||
|
|
||||||
(defn- clone-template
|
(defn clone-template
|
||||||
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
[cfg {:keys [project-id profile-id] :as params} template]
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||||
;; NOTE: the importation process performs some operations that
|
;; NOTE: the importation process performs some operations that
|
||||||
;; are not very friendly with virtual threads, and for avoid
|
;; are not very friendly with virtual threads, and for avoid
|
||||||
@@ -416,6 +413,7 @@
|
|||||||
(doseq [file-id result]
|
(doseq [file-id result]
|
||||||
(let [props (assoc props :id file-id)
|
(let [props (assoc props :id file-id)
|
||||||
event (-> (audit/event-from-rpc-params params)
|
event (-> (audit/event-from-rpc-params params)
|
||||||
|
(assoc ::audit/profile-id profile-id)
|
||||||
(assoc ::audit/name "create-file")
|
(assoc ::audit/name "create-file")
|
||||||
(assoc ::audit/props props))]
|
(assoc ::audit/props props))]
|
||||||
(audit/submit! cfg event))))
|
(audit/submit! cfg event))))
|
||||||
@@ -437,7 +435,8 @@
|
|||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
||||||
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
||||||
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
||||||
template (tmpl/get-template-stream cfg template-id)]
|
template (tmpl/get-template-stream cfg template-id)
|
||||||
|
params (assoc params :profile-id profile-id)]
|
||||||
|
|
||||||
(when-not template
|
(when-not template
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
|
[app.common.types.plugins :refer [schema:plugin-registry]]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
@@ -40,6 +41,33 @@
|
|||||||
(declare strip-private-attrs)
|
(declare strip-private-attrs)
|
||||||
(declare verify-password)
|
(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
|
(defn clean-email
|
||||||
"Clean and normalizes email address string"
|
"Clean and normalizes email address string"
|
||||||
[email]
|
[email]
|
||||||
@@ -53,24 +81,6 @@
|
|||||||
email)]
|
email)]
|
||||||
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)
|
;; --- QUERY: Get profile (own)
|
||||||
|
|
||||||
(sv/defmethod ::get-profile
|
(sv/defmethod ::get-profile
|
||||||
@@ -99,11 +109,10 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:update-profile
|
schema:update-profile
|
||||||
(sm/define
|
[:map {:title "update-profile"}
|
||||||
[:map {:title "update-profile"}
|
[:fullname [::sm/word-string {:max 250}]]
|
||||||
[:fullname [::sm/word-string {:max 250}]]
|
[:lang {:optional true} [:string {:max 8}]]
|
||||||
[:lang {:optional true} [:string {:max 8}]]
|
[:theme {:optional true} [:string {:max 250}]]])
|
||||||
[:theme {:optional true} [:string {:max 250}]]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::update-profile
|
(sv/defmethod ::update-profile
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
@@ -144,11 +153,10 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:update-profile-password
|
schema:update-profile-password
|
||||||
(sm/define
|
[:map {:title "update-profile-password"}
|
||||||
[:map {:title "update-profile-password"}
|
[:password [::sm/word-string {:max 500}]]
|
||||||
[:password [::sm/word-string {:max 500}]]
|
;; Social registered users don't have old-password
|
||||||
;; Social registered users don't have old-password
|
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
|
||||||
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-password
|
(sv/defmethod ::update-profile-password
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
@@ -199,9 +207,8 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:update-profile-photo
|
schema:update-profile-photo
|
||||||
(sm/define
|
[:map {:title "update-profile-photo"}
|
||||||
[:map {:title "update-profile-photo"}
|
[:file ::media/upload]])
|
||||||
[:file ::media/upload]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-photo
|
(sv/defmethod ::update-profile-photo
|
||||||
{:doc/added "1.1"
|
{:doc/added "1.1"
|
||||||
@@ -268,9 +275,8 @@
|
|||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:request-email-change
|
schema:request-email-change
|
||||||
(sm/define
|
[:map {:title "request-email-change"}
|
||||||
[:map {:title "request-email-change"}
|
[:email ::sm/email]])
|
||||||
[:email ::sm/email]]))
|
|
||||||
|
|
||||||
(sv/defmethod ::request-email-change
|
(sv/defmethod ::request-email-change
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
@@ -351,36 +357,38 @@
|
|||||||
:extra-data ptoken})
|
:extra-data ptoken})
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile Props
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:update-profile-props
|
schema:update-profile-props
|
||||||
(sm/define
|
[:map {:title "update-profile-props"}
|
||||||
[:map {:title "update-profile-props"}
|
[:props schema:props]])
|
||||||
[:props [:map-of :keyword :any]]]))
|
|
||||||
|
(defn update-profile-props
|
||||||
|
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||||
|
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||||
|
props (reduce-kv (fn [props k v]
|
||||||
|
;; We don't accept namespaced keys
|
||||||
|
(if (simple-ident? k)
|
||||||
|
(if (nil? v)
|
||||||
|
(dissoc props k)
|
||||||
|
(assoc props k v))
|
||||||
|
props))
|
||||||
|
(:props profile)
|
||||||
|
props)]
|
||||||
|
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:props (db/tjson props)}
|
||||||
|
{:id profile-id})
|
||||||
|
|
||||||
|
(filter-props props)))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-props
|
(sv/defmethod ::update-profile-props
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
::sm/params schema:update-profile-props}
|
::sm/params schema:update-profile-props}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
|
[cfg {:keys [::rpc/profile-id props]}]
|
||||||
(db/with-atomic [conn pool]
|
(db/tx-run! cfg (fn [cfg]
|
||||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
(update-profile-props cfg profile-id props))))
|
||||||
props (reduce-kv (fn [props k v]
|
|
||||||
;; We don't accept namespaced keys
|
|
||||||
(if (simple-ident? k)
|
|
||||||
(if (nil? v)
|
|
||||||
(dissoc props k)
|
|
||||||
(assoc props k v))
|
|
||||||
props))
|
|
||||||
(:props profile)
|
|
||||||
props)]
|
|
||||||
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:props (db/tjson props)}
|
|
||||||
{:id profile-id})
|
|
||||||
|
|
||||||
(filter-props props))))
|
|
||||||
|
|
||||||
;; --- MUTATION: Delete Profile
|
;; --- MUTATION: Delete Profile
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,17 @@
|
|||||||
|
|
||||||
;; --- MUTATION: Create Project
|
;; --- 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
|
(def ^:private schema:create-project
|
||||||
[:map {:title "create-project"}
|
[:map {:title "create-project"}
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
@@ -178,23 +189,15 @@
|
|||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:create-project}
|
::sm/params schema:create-project}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
[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})
|
|
||||||
|
|
||||||
(let [params (assoc params :profile-id profile-id)
|
(teams/check-edition-permissions! cfg profile-id team-id)
|
||||||
project (teams/create-project conn params)]
|
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
|
||||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
::quotes/profile-id profile-id
|
||||||
(db/insert! conn :team-project-profile-rel
|
::quotes/team-id team-id})
|
||||||
{:project-id (:id project)
|
|
||||||
:profile-id profile-id
|
|
||||||
:team-id team-id
|
|
||||||
:is-pinned false})
|
|
||||||
(assoc project :is-pinned false))))
|
|
||||||
|
|
||||||
|
(let [params (assoc params :profile-id profile-id)]
|
||||||
|
(db/tx-run! cfg create-project params)))
|
||||||
|
|
||||||
;; --- MUTATION: Toggle Project Pin
|
;; --- MUTATION: Toggle Project Pin
|
||||||
|
|
||||||
|
|||||||
@@ -82,19 +82,17 @@
|
|||||||
(cond-> row
|
(cond-> row
|
||||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||||
|
|
||||||
|
(defn- check-profile-muted
|
||||||
|
"Check if the member's email is part of the global bounce report"
|
||||||
(defn- check-valid-email-muted
|
|
||||||
"Check if the member's email is part of the global bounce report."
|
|
||||||
[conn member]
|
[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)))
|
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :member-is-muted
|
:code :member-is-muted
|
||||||
:email email
|
:email email
|
||||||
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
: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"
|
"Check if the email is part of the global complain report"
|
||||||
[conn email show?]
|
[conn email show?]
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
@@ -103,7 +101,7 @@
|
|||||||
:email (if show? email "private")
|
:email (if show? email "private")
|
||||||
:hint "this email has been repeatedly reported as bounce")))
|
: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"
|
"Check if the member email is part of the global complain report"
|
||||||
[conn email show?]
|
[conn email show?]
|
||||||
(when (eml/has-complaint-reports? conn email)
|
(when (eml/has-complaint-reports? conn email)
|
||||||
@@ -227,16 +225,16 @@
|
|||||||
;; --- Query: Team Members
|
;; --- Query: Team Members
|
||||||
|
|
||||||
(def sql:team-members
|
(def sql:team-members
|
||||||
"select tp.*,
|
"SELECT tp.*,
|
||||||
p.id,
|
p.id,
|
||||||
p.email,
|
p.email,
|
||||||
p.fullname as name,
|
p.fullname AS name,
|
||||||
p.fullname as fullname,
|
p.fullname AS fullname,
|
||||||
p.photo_id,
|
p.photo_id,
|
||||||
p.is_active
|
p.is_active
|
||||||
from team_profile_rel as tp
|
FROM team_profile_rel AS tp
|
||||||
join profile as p on (p.id = tp.profile_id)
|
JOIN profile AS p ON (p.id = tp.profile_id)
|
||||||
where tp.team_id = ?")
|
WHERE tp.team_id = ?")
|
||||||
|
|
||||||
(defn get-team-members
|
(defn get-team-members
|
||||||
[conn team-id]
|
[conn team-id]
|
||||||
@@ -403,17 +401,19 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:create-team}
|
::sm/params schema:create-team}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[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)
|
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
|
||||||
(cfeat/check-client-features! (:features params)))
|
::quotes/profile-id profile-id})
|
||||||
team (create-team cfg (assoc params
|
|
||||||
:profile-id profile-id
|
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||||
:features features))]
|
(cfeat/check-client-features! (:features params)))
|
||||||
(with-meta team
|
params (-> params
|
||||||
{::audit/props {:id (:id team)}})))))
|
(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
|
(defn create-team
|
||||||
"This is a complete team creation process, it creates the team
|
"This is a complete team creation process, it creates the team
|
||||||
@@ -767,21 +767,51 @@
|
|||||||
:member-id member-id}))
|
:member-id member-id}))
|
||||||
|
|
||||||
(defn- create-profile-identity-token
|
(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)
|
(tokens/generate (::setup/props cfg)
|
||||||
{:iss :profile-identity
|
{:iss :profile-identity
|
||||||
:profile-id (:id profile)
|
:profile-id profile-id
|
||||||
:exp (dt/in-future {:days 30})}))
|
: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
|
(defn- create-invitation
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
[{: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)
|
(let [email (profile/clean-email email)
|
||||||
member (profile/get-profile-by-email conn email)]
|
member (profile/get-profile-by-email conn email)]
|
||||||
|
|
||||||
(check-valid-email-muted conn member)
|
(check-profile-muted conn member)
|
||||||
(check-valid-email-bounce conn email true)
|
(check-email-bounce conn email true)
|
||||||
(check-valid-email-spam conn email true)
|
(check-email-spam conn email true)
|
||||||
|
|
||||||
|
|
||||||
;; When we have email verification disabled and invitation user is
|
;; When we have email verification disabled and invitation user is
|
||||||
;; already present in the database, we proceed to add it to the
|
;; already present in the database, we proceed to add it to the
|
||||||
@@ -815,7 +845,8 @@
|
|||||||
(name role) expire
|
(name role) expire
|
||||||
(name role) expire])
|
(name role) expire])
|
||||||
updated? (not= id (:id invitation))
|
updated? (not= id (:id invitation))
|
||||||
tprops {:profile-id (:id profile)
|
profile-id (:id profile)
|
||||||
|
tprops {:profile-id profile-id
|
||||||
:invitation-id (:id invitation)
|
:invitation-id (:id invitation)
|
||||||
:valid-until expire
|
:valid-until expire
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
@@ -823,12 +854,11 @@
|
|||||||
:member-id (:id member)
|
:member-id (:id member)
|
||||||
:role role}
|
:role role}
|
||||||
itoken (create-invitation-token cfg tprops)
|
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)
|
(when (contains? cf/flags :log-invitation-tokens)
|
||||||
(l/info :hint "invitation token" :token itoken))
|
(l/info :hint "invitation token" :token itoken))
|
||||||
|
|
||||||
|
|
||||||
(let [props (-> (dissoc tprops :profile-id)
|
(let [props (-> (dissoc tprops :profile-id)
|
||||||
(audit/clean-props))
|
(audit/clean-props))
|
||||||
evname (if updated?
|
evname (if updated?
|
||||||
@@ -851,26 +881,27 @@
|
|||||||
itoken))))
|
itoken))))
|
||||||
|
|
||||||
(defn- add-user-to-team
|
(defn- add-user-to-team
|
||||||
[conn profile team email role]
|
[conn profile team role email]
|
||||||
|
|
||||||
(let [team-id (:id team)
|
(let [team-id (:id team)
|
||||||
member (db/get* conn :profile
|
member (db/get* conn :profile
|
||||||
{:email (str/lower email)}
|
{:email (str/lower email)}
|
||||||
{::sql/columns [:id :email]})
|
{::sql/columns [:id :email]})
|
||||||
params (merge
|
params (merge
|
||||||
{:team-id team-id
|
{:team-id team-id
|
||||||
:profile-id (:id member)}
|
:profile-id (:id member)}
|
||||||
(role->params role))]
|
(role->params role))]
|
||||||
|
|
||||||
;; Do not allow blocked users to join teams.
|
;; Do not allow blocked users to join teams.
|
||||||
(when (:is-blocked member)
|
(when (:is-blocked member)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
|
|
||||||
(quotes/check-quote! conn
|
(quotes/check!
|
||||||
{::quotes/id ::quotes/profiles-per-team
|
{::db/conn conn
|
||||||
::quotes/profile-id (:id member)
|
::quotes/id ::quotes/profiles-per-team
|
||||||
::quotes/team-id team-id})
|
::quotes/profile-id (:id member)
|
||||||
|
::quotes/team-id team-id})
|
||||||
|
|
||||||
;; Insert the member to the team
|
;; Insert the member to the team
|
||||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||||
@@ -902,68 +933,89 @@
|
|||||||
[conn team-id]
|
[conn team-id]
|
||||||
(db/exec! conn [sql:valid-requests-email 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
|
(def ^:private schema:create-team-invitations
|
||||||
[:map {:title "create-team-invitations"}
|
[:map {:title "create-team-invitations"}
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
[:role schema:role]
|
[:role schema:role]
|
||||||
[:emails [::sm/set ::sm/email]]])
|
[: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
|
(sv/defmethod ::create-team-invitations
|
||||||
"A rpc call that allow to send a single or multiple invitations to
|
"A rpc call that allow to send a single or multiple invitations to
|
||||||
join the team."
|
join the team."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:create-team-invitations}
|
::sm/params schema:create-team-invitations}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
|
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(let [perms (get-permissions cfg profile-id team-id)
|
||||||
(let [perms (get-permissions conn profile-id team-id)
|
profile (db/get-by-id cfg :profile profile-id)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
emails (into #{} (map profile/clean-email) emails)]
|
||||||
team (db/get-by-id conn :team team-id)
|
|
||||||
emails (into #{} (map profile/clean-email) emails)]
|
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! conn)
|
(when-not (:is-admin perms)
|
||||||
(list {::quotes/id ::quotes/invitations-per-team
|
(ex/raise :type :validation
|
||||||
::quotes/profile-id profile-id
|
:code :insufficient-permissions))
|
||||||
::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)
|
(when (> (count emails) max-invitations-by-request-threshold)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :insufficient-permissions))
|
: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.
|
(-> cfg
|
||||||
(check-valid-email-muted conn profile)
|
(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))
|
(let [team (db/get-by-id cfg :team team-id)
|
||||||
emails-to-add (filter #(contains? requested %) emails)
|
;; NOTE: Is important pass RPC method params down to the
|
||||||
emails (remove #(contains? requested %) emails)
|
;; `create-team-invitations` because it uses the implicit
|
||||||
cfg (assoc cfg ::db/conn conn)
|
;; RPC properties from params for fill necessary data on
|
||||||
members (->> (db/exec! conn [sql:team-members team-id])
|
;; emiting an entry to the audit-log
|
||||||
(into #{} (map :email)))
|
invitations (db/tx-run! cfg create-team-invitations
|
||||||
|
(-> params
|
||||||
|
(assoc :profile profile)
|
||||||
|
(assoc :team team)
|
||||||
|
(assoc :emails emails)))]
|
||||||
|
|
||||||
invitations (into #{}
|
(with-meta {:total (count invitations)
|
||||||
(comp
|
:invitations invitations}
|
||||||
;; We don't re-send inviation to already existing members
|
{::audit/props {:invitations (count invitations)}}))))
|
||||||
(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)}})))))
|
|
||||||
|
|
||||||
;; --- Mutation: Create Team & Invite Members
|
;; --- Mutation: Create Team & Invite Members
|
||||||
|
|
||||||
@@ -977,52 +1029,50 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-team-with-invitations
|
(sv/defmethod ::create-team-with-invitations
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:create-team-with-invitations}
|
::sm/params schema:create-team-with-invitations
|
||||||
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
|
::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
|
params (-> params
|
||||||
(fn [{:keys [::db/conn] :as cfg}]
|
(assoc :profile-id profile-id)
|
||||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
(assoc :features features))
|
||||||
(cfeat/check-client-features! (:features params)))
|
|
||||||
|
|
||||||
params (-> params
|
team (create-team cfg params)
|
||||||
(assoc :profile-id profile-id)
|
emails (into #{} (map profile/clean-email) emails)]
|
||||||
(assoc :features features))
|
|
||||||
|
|
||||||
cfg (assoc cfg ::db/conn conn)
|
(-> cfg
|
||||||
team (create-team cfg params)
|
(assoc ::quotes/profile-id profile-id)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
(assoc ::quotes/team-id (:id team))
|
||||||
emails (into #{} (map profile/clean-email) emails)]
|
(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}
|
(when (> (count emails) max-invitations-by-request-threshold)
|
||||||
event (-> (audit/event-from-rpc-params params)
|
(ex/raise :type :validation
|
||||||
(assoc ::audit/name "create-team")
|
:code :max-invitations-by-request
|
||||||
(assoc ::audit/props props))]
|
:hint "the maximum of invitation on single request is reached"
|
||||||
(audit/submit! cfg event))
|
:threshold max-invitations-by-request-threshold))
|
||||||
|
|
||||||
;; Create invitations for all provided emails.
|
(let [props {:name name :features features}
|
||||||
(->> emails
|
event (-> (audit/event-from-rpc-params params)
|
||||||
(map (fn [email]
|
(assoc ::audit/name "create-team")
|
||||||
(-> params
|
(assoc ::audit/props props))]
|
||||||
(assoc :team team)
|
(audit/submit! cfg event))
|
||||||
(assoc :profile profile)
|
|
||||||
(assoc :email email)
|
|
||||||
(assoc :role role))))
|
|
||||||
(run! (partial create-invitation cfg)))
|
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! conn)
|
;; Create invitations for all provided emails.
|
||||||
(list {::quotes/id ::quotes/teams-per-profile
|
(let [profile (db/get-by-id conn :profile profile-id)
|
||||||
::quotes/profile-id profile-id}
|
params (-> params
|
||||||
{::quotes/id ::quotes/invitations-per-team
|
(assoc :team team)
|
||||||
::quotes/profile-id profile-id
|
(assoc :profile profile)
|
||||||
::quotes/team-id (:id team)
|
(assoc :role role))
|
||||||
::quotes/incr (count emails)}
|
invitations (->> emails
|
||||||
{::quotes/id ::quotes/profiles-per-team
|
(map (fn [email] (assoc params :email email)))
|
||||||
::quotes/profile-id profile-id
|
(map (partial create-invitation cfg)))]
|
||||||
::quotes/team-id (:id team)
|
|
||||||
::quotes/incr (count emails)}))
|
|
||||||
|
|
||||||
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
|
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
|
||||||
|
|
||||||
;; --- Query: get-team-invitation-token
|
;; --- Query: get-team-invitation-token
|
||||||
|
|
||||||
@@ -1215,11 +1265,11 @@
|
|||||||
:code :invalid-parameters))
|
:code :invalid-parameters))
|
||||||
|
|
||||||
;; Check that the requester is not muted
|
;; 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 that the owner is not marked as bounce nor spam
|
||||||
(check-valid-email-bounce conn (:email team-owner) false)
|
(check-email-bounce conn (:email team-owner) false)
|
||||||
(check-valid-email-spam conn (:email team-owner) true)
|
(check-email-spam conn (:email team-owner) true)
|
||||||
|
|
||||||
(let [request (create-team-access-request
|
(let [request (create-team-access-request
|
||||||
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
||||||
|
|||||||
@@ -30,21 +30,19 @@
|
|||||||
|
|
||||||
(def ^:private schema:verify-token
|
(def ^:private schema:verify-token
|
||||||
[:map {:title "verify-token"}
|
[:map {:title "verify-token"}
|
||||||
[:token [:string {:max 1000}]]])
|
[:token [:string {:max 5000}]]])
|
||||||
|
|
||||||
(sv/defmethod ::verify-token
|
(sv/defmethod ::verify-token
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::doc/added "1.15"
|
::doc/added "1.15"
|
||||||
::doc/module :auth
|
::doc/module :auth
|
||||||
::sm/params schema:verify-token}
|
::sm/params schema:verify-token}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
[cfg {:keys [token] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
|
||||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})
|
(db/tx-run! cfg process-token params claims)))
|
||||||
cfg (assoc cfg :conn conn)]
|
|
||||||
(process-token cfg params claims))))
|
|
||||||
|
|
||||||
(defmethod process-token :change-email
|
(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)]
|
(let [email (profile/clean-email email)]
|
||||||
(when (profile/get-profile-by-email conn email)
|
(when (profile/get-profile-by-email conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@@ -60,7 +58,7 @@
|
|||||||
::audit/profile-id profile-id})))
|
::audit/profile-id profile-id})))
|
||||||
|
|
||||||
(defmethod process-token :verify-email
|
(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)
|
(let [profile (profile/get-profile conn profile-id)
|
||||||
claims (assoc claims :profile profile)]
|
claims (assoc claims :profile profile)]
|
||||||
|
|
||||||
@@ -81,22 +79,14 @@
|
|||||||
::audit/profile-id (:id profile)}))))
|
::audit/profile-id (:id profile)}))))
|
||||||
|
|
||||||
(defmethod process-token :auth
|
(defmethod process-token :auth
|
||||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||||
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
|
(let [profile (profile/get-profile conn profile-id)]
|
||||||
props (merge (:props profile)
|
(assoc claims :profile 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))))
|
|
||||||
|
|
||||||
;; --- Team Invitation
|
;; --- Team Invitation
|
||||||
|
|
||||||
(defn- accept-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
|
(let [;; Update the role if there is an invitation
|
||||||
role (or (some-> invitation :role keyword) role)
|
role (or (some-> invitation :role keyword) role)
|
||||||
params (merge
|
params (merge
|
||||||
@@ -109,10 +99,9 @@
|
|||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
|
|
||||||
(quotes/check-quote! conn
|
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
||||||
{::quotes/id ::quotes/profiles-per-team
|
::quotes/profile-id (:id member)
|
||||||
::quotes/profile-id (:id member)
|
::quotes/team-id team-id})
|
||||||
::quotes/team-id team-id})
|
|
||||||
|
|
||||||
;; Insert the invited member to the team
|
;; Insert the invited member to the team
|
||||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||||
@@ -148,7 +137,7 @@
|
|||||||
(sm/lazy-validator schema:team-invitation-claims))
|
(sm/lazy-validator schema:team-invitation-claims))
|
||||||
|
|
||||||
(defmethod process-token :team-invitation
|
(defmethod process-token :team-invitation
|
||||||
[{:keys [conn] :as cfg}
|
[{:keys [::db/conn] :as cfg}
|
||||||
{:keys [::rpc/profile-id token] :as params}
|
{:keys [::rpc/profile-id token] :as params}
|
||||||
{:keys [member-id team-id member-email] :as claims}]
|
{:keys [member-id team-id member-email] :as claims}]
|
||||||
|
|
||||||
|
|||||||
+181
-123
@@ -7,16 +7,13 @@
|
|||||||
(ns app.rpc.quotes
|
(ns app.rpc.quotes
|
||||||
"Penpot resource usage quotes."
|
"Penpot resource usage quotes."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defmulti check-quote ::id)
|
(defmulti check-quote ::id)
|
||||||
@@ -26,14 +23,16 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private schema:quote
|
(def ^:private schema:quote
|
||||||
(sm/define
|
[:map {:title "Quote"}
|
||||||
[:map {:title "Quote"}
|
[::team-id {:optional true} ::sm/uuid]
|
||||||
[::team-id {:optional true} ::sm/uuid]
|
[::project-id {:optional true} ::sm/uuid]
|
||||||
[::project-id {:optional true} ::sm/uuid]
|
[::file-id {:optional true} ::sm/uuid]
|
||||||
[::file-id {:optional true} ::sm/uuid]
|
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
[::id :keyword]
|
||||||
[::id :keyword]
|
[::profile-id ::sm/uuid]])
|
||||||
[::profile-id ::sm/uuid]]))
|
|
||||||
|
(def valid-quote?
|
||||||
|
(sm/lazy-validator schema:quote))
|
||||||
|
|
||||||
(def ^:private enabled (volatile! true))
|
(def ^:private enabled (volatile! true))
|
||||||
|
|
||||||
@@ -47,20 +46,31 @@
|
|||||||
[]
|
[]
|
||||||
(vswap! enabled (constantly false)))
|
(vswap! enabled (constantly false)))
|
||||||
|
|
||||||
(defn check-quote!
|
(defn- check
|
||||||
[ds quote]
|
[cfg quote]
|
||||||
(dm/assert!
|
(let [quote (merge cfg quote)
|
||||||
"expected valid quote map"
|
id (::id quote)]
|
||||||
(sm/validate schema:quote quote))
|
|
||||||
|
|
||||||
(when (contains? cf/flags :quotes)
|
(when-not (valid-quote? quote)
|
||||||
(when @enabled
|
(ex/raise :type :internal
|
||||||
;; This approach add flexibility on how and where the
|
:code :invalid-quote-definition
|
||||||
;; check-quote! can be called (in or out of transaction)
|
:hint "found invalid data for quote schema"
|
||||||
(db/run! ds (fn [cfg]
|
:quote (name id)))
|
||||||
(-> (merge cfg quote)
|
|
||||||
(assoc ::target (name (::id quote)))
|
(-> (assoc quote ::target (name id))
|
||||||
(check-quote)))))))
|
(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!
|
(defn- send-notification!
|
||||||
[{:keys [::db/conn] :as params}]
|
[{:keys [::db/conn] :as params}]
|
||||||
@@ -101,7 +111,7 @@
|
|||||||
(map :quote)
|
(map :quote)
|
||||||
(reduce max (- Integer/MAX_VALUE)))
|
(reduce max (- Integer/MAX_VALUE)))
|
||||||
quote (if (pos? quote) quote default)
|
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)
|
(when (> (+ total incr) quote)
|
||||||
(if (contains? cf/flags :soft-quotes)
|
(if (contains? cf/flags :soft-quotes)
|
||||||
@@ -113,72 +123,81 @@
|
|||||||
:count total)))))
|
:count total)))))
|
||||||
|
|
||||||
(def ^:private sql:get-quotes-1
|
(def ^:private sql:get-quotes-1
|
||||||
"select id, quote from usage_quote
|
"SELECT id, quote
|
||||||
where target = ?
|
FROM usage_quote
|
||||||
and profile_id = ?
|
WHERE target = ?
|
||||||
and team_id is null
|
AND profile_id = ?
|
||||||
and project_id is null
|
AND team_id IS NULL
|
||||||
and file_id is null;")
|
AND project_id IS NULL
|
||||||
|
AND file_id IS NULL;")
|
||||||
|
|
||||||
(def ^:private sql:get-quotes-2
|
(def ^:private sql:get-quotes-2
|
||||||
"select id, quote from usage_quote
|
"SELECT id, quote
|
||||||
where target = ?
|
FROM usage_quote
|
||||||
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
|
WHERE target = ?
|
||||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
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
|
(def ^:private sql:get-quotes-3
|
||||||
"select id, quote from usage_quote
|
"SELECT id, quote
|
||||||
where target = ?
|
FROM usage_quote
|
||||||
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
|
WHERE target = ?
|
||||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
AND ((project_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));")
|
(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
|
(def ^:private sql:get-quotes-4
|
||||||
"select id, quote from usage_quote
|
"SELECT id, quote
|
||||||
where target = ?
|
FROM usage_quote
|
||||||
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
|
WHERE target = ?
|
||||||
(project_id = ? and (profile_id = ? or profile_id is null)) or
|
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
|
||||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
(project_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));")
|
(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
|
;; QUOTE: TEAMS-PER-PROFILE
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-teams-per-profile
|
(def ^:private schema:teams-per-profile
|
||||||
"select count(*) as total
|
[:map [::profile-id ::sm/uuid]])
|
||||||
from team_profile_rel
|
|
||||||
where profile_id = ?")
|
|
||||||
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
(def ^:private valid-teams-per-profile-quote?
|
||||||
(s/def ::teams-per-profile
|
(sm/lazy-validator schema:teams-per-profile))
|
||||||
(s/keys :req [::profile-id ::target]))
|
|
||||||
|
(def ^:private sql:get-teams-per-profile
|
||||||
|
"SELECT count(*) AS total
|
||||||
|
FROM team_profile_rel
|
||||||
|
WHERE profile_id = ?")
|
||||||
|
|
||||||
(defmethod check-quote ::teams-per-profile
|
(defmethod check-quote ::teams-per-profile
|
||||||
[{:keys [::profile-id ::target] :as quote}]
|
[{:keys [::profile-id ::target] :as quote}]
|
||||||
(us/assert! ::teams-per-profile quote)
|
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
|
||||||
(-> quote
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
||||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||||
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
||||||
(generic-check!)))
|
(generic-check!)))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
|
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-access-tokens-per-profile
|
(def ^:private schema:access-tokens-per-profile
|
||||||
"select count(*) as total
|
[:map [::profile-id ::sm/uuid]])
|
||||||
from access_token
|
|
||||||
where profile_id = ?")
|
|
||||||
|
|
||||||
(s/def ::access-tokens-per-profile
|
(def ^:private valid-access-tokens-per-profile-quote?
|
||||||
(s/keys :req [::profile-id ::target]))
|
(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
|
(defmethod check-quote ::access-tokens-per-profile
|
||||||
[{:keys [::profile-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
|
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
|
||||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||||
@@ -189,40 +208,51 @@
|
|||||||
;; QUOTE: PROJECTS-PER-TEAM
|
;; QUOTE: PROJECTS-PER-TEAM
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-projects-per-team
|
(def ^:private schema:projects-per-team
|
||||||
"select count(*) as total
|
[:map
|
||||||
from project as p
|
[::profile-id ::sm/uuid]
|
||||||
where p.team_id = ?
|
[::team-id ::sm/uuid]])
|
||||||
and p.deleted_at is null")
|
|
||||||
|
|
||||||
(s/def ::team-id ::us/uuid)
|
(def ^:private valid-projects-per-team-quote?
|
||||||
(s/def ::projects-per-team
|
(sm/lazy-validator schema:projects-per-team))
|
||||||
(s/keys :req [::profile-id ::team-id ::target]))
|
|
||||||
|
(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
|
(defmethod check-quote ::projects-per-team
|
||||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||||
|
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
|
||||||
|
|
||||||
(-> quote
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
|
(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 ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||||
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
||||||
(generic-check!)))
|
(generic-check!)))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-font-variants-per-team
|
(def ^:private schema:font-variants-per-team
|
||||||
"select count(*) as total
|
[:map
|
||||||
from team_font_variant as v
|
[::profile-id ::sm/uuid]
|
||||||
where v.team_id = ?")
|
[::team-id ::sm/uuid]])
|
||||||
|
|
||||||
(s/def ::font-variants-per-team
|
(def ^:private valid-font-variant-per-team-quote?
|
||||||
(s/keys :req [::profile-id ::team-id ::target]))
|
(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
|
(defmethod check-quote ::font-variants-per-team
|
||||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
|
(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])
|
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||||
@@ -234,70 +264,86 @@
|
|||||||
;; QUOTE: INVITATIONS-PER-TEAM
|
;; QUOTE: INVITATIONS-PER-TEAM
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-invitations-per-team
|
(def ^:private schema:invitations-per-team
|
||||||
"select count(*) as total
|
[:map
|
||||||
from team_invitation
|
[::profile-id ::sm/uuid]
|
||||||
where team_id = ?")
|
[::team-id ::sm/uuid]])
|
||||||
|
|
||||||
(s/def ::invitations-per-team
|
(def ^:private valid-invitations-per-team-quote?
|
||||||
(s/keys :req [::profile-id ::team-id ::target]))
|
(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
|
(defmethod check-quote ::invitations-per-team
|
||||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
|
(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 ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||||
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
||||||
(generic-check!)))
|
(generic-check!)))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUOTE: PROFILES-PER-TEAM
|
;; 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
|
(def ^:private sql:get-profiles-per-team
|
||||||
"select (select count(*)
|
"SELECT (SELECT count(*)
|
||||||
from team_profile_rel
|
FROM team_profile_rel
|
||||||
where team_id = ?) +
|
WHERE team_id = ?) +
|
||||||
(select count(*)
|
(SELECT count(*)
|
||||||
from team_invitation
|
FROM team_invitation
|
||||||
where team_id = ?
|
WHERE team_id = ?
|
||||||
and valid_until > now()) as total;")
|
AND valid_until > now()) AS total;")
|
||||||
|
|
||||||
;; NOTE: the total number of profiles is determined by the number of
|
;; NOTE: the total number of profiles is determined by the number of
|
||||||
;; effective members plus ongoing valid invitations.
|
;; 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
|
(defmethod check-quote ::profiles-per-team
|
||||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
|
(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 ::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])
|
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
|
||||||
(generic-check!)))
|
(generic-check!)))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUOTE: FILES-PER-PROJECT
|
;; QUOTE: FILES-PER-PROJECT
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-files-per-project
|
(def ^:private schema:files-per-project
|
||||||
"select count(*) as total
|
[:map
|
||||||
from file as f
|
[::profile-id ::sm/uuid]
|
||||||
where f.project_id = ?
|
[::project-id ::sm/uuid]
|
||||||
and f.deleted_at is null")
|
[::team-id ::sm/uuid]])
|
||||||
|
|
||||||
(s/def ::project-id ::us/uuid)
|
(def ^:private valid-files-per-project-quote?
|
||||||
(s/def ::files-per-project
|
(sm/lazy-validator schema:files-per-project))
|
||||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
|
||||||
|
(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
|
(defmethod check-quote ::files-per-project
|
||||||
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
|
(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])
|
(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
|
;; QUOTE: COMMENT-THREADS-PER-FILE
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-comment-threads-per-file
|
(def ^:private schema:comment-threads-per-file
|
||||||
"select count(*) as total
|
[:map
|
||||||
from comment_thread as ct
|
[::profile-id ::sm/uuid]
|
||||||
where ct.file_id = ?")
|
[::project-id ::sm/uuid]
|
||||||
|
[::team-id ::sm/uuid]])
|
||||||
|
|
||||||
(s/def ::comment-threads-per-file
|
(def ^:private valid-comment-threads-per-file-quote?
|
||||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
(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
|
(defmethod check-quote ::comment-threads-per-file
|
||||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
|
(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
|
(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])
|
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
|
||||||
(generic-check!)))
|
(generic-check!)))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; QUOTE: COMMENTS-PER-FILE
|
;; QUOTE: COMMENTS-PER-FILE
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(def ^:private sql:get-comments-per-file
|
(def ^:private schema:comments-per-file
|
||||||
"select count(*) as total
|
[:map
|
||||||
from comment as c
|
[::profile-id ::sm/uuid]
|
||||||
join comment_thread as ct on (ct.id = c.thread_id)
|
[::project-id ::sm/uuid]
|
||||||
where ct.file_id = ?")
|
[::team-id ::sm/uuid]])
|
||||||
|
|
||||||
(s/def ::comments-per-file
|
(def ^:private valid-comments-per-file-quote?
|
||||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
(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
|
(defmethod check-quote ::comments-per-file
|
||||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
[{: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
|
(-> quote
|
||||||
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
|
(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
|
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
@@ -19,28 +20,26 @@
|
|||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private schema:template
|
||||||
schema:template
|
[:map {:title "Template"}
|
||||||
(sm/define
|
[:id ::sm/word-string]
|
||||||
[:map {:title "Template"}
|
[:name ::sm/word-string]
|
||||||
[:id ::sm/word-string]
|
[:file-uri ::sm/word-string]])
|
||||||
[:name ::sm/word-string]
|
|
||||||
[:file-uri ::sm/word-string]]))
|
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private schema:templates
|
||||||
schema:templates
|
[:vector schema:template])
|
||||||
(sm/define
|
|
||||||
[:vector schema:template]))
|
(def check-templates!
|
||||||
|
(sm/check-fn schema:templates
|
||||||
|
:code :invalid-templates
|
||||||
|
:hint "invalid templates"))
|
||||||
|
|
||||||
(defmethod ig/init-key ::setup/templates
|
(defmethod ig/init-key ::setup/templates
|
||||||
[_ _]
|
[_ _]
|
||||||
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
|
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
|
||||||
|
templates (check-templates! templates)
|
||||||
dest (fs/join fs/*cwd* "builtin-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]
|
(doseq [{:keys [id path] :as template} templates]
|
||||||
(let [path (or path (fs/join dest id))]
|
(let [path (or path (fs/join dest id))]
|
||||||
(if (fs/exists? path)
|
(if (fs/exists? path)
|
||||||
@@ -60,9 +59,9 @@
|
|||||||
(let [resp (http/req! cfg
|
(let [resp (http/req! cfg
|
||||||
{:method :get :uri (:file-uri template)}
|
{:method :get :uri (:file-uri template)}
|
||||||
{:response-type :input-stream :sync? true})]
|
{:response-type :input-stream :sync? true})]
|
||||||
|
(when-not (= 200 (:status resp))
|
||||||
(dm/verify!
|
(ex/raise :type :internal
|
||||||
"unexpected response found on fetching template"
|
:code :unexpected-status-code
|
||||||
(= 200 (:status resp)))
|
:hint (str "unable to download template, recevied status " (:status resp))))
|
||||||
|
|
||||||
(io/input-stream (:body resp)))))))
|
(io/input-stream (:body resp)))))))
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.setup.welcome-file
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit :as-alias climit]
|
||||||
|
[app.rpc.commands.files-update :as fupdate]
|
||||||
|
[app.rpc.commands.management :as management]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.setup :as-alias setup]
|
||||||
|
[app.setup.templates :as tmpl]
|
||||||
|
[app.worker :as-alias wrk]))
|
||||||
|
|
||||||
|
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
|
||||||
|
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
|
||||||
|
|
||||||
|
(def ^:private update-path
|
||||||
|
[:data :pages-index page-id :objects shape-id
|
||||||
|
:content :children 0 :children 0 :children 0])
|
||||||
|
|
||||||
|
(def ^:private sql:mark-file-object-thumbnails-deleted
|
||||||
|
"UPDATE file_tagged_object_thumbnail
|
||||||
|
SET deleted_at = now()
|
||||||
|
WHERE file_id = ?")
|
||||||
|
|
||||||
|
(def ^:private sql:mark-file-thumbnail-deleted
|
||||||
|
"UPDATE file_thumbnail
|
||||||
|
SET deleted_at = now()
|
||||||
|
WHERE file_id = ?")
|
||||||
|
|
||||||
|
(defn- update-welcome-shape
|
||||||
|
[_ file name]
|
||||||
|
(let [text (str "Welcome to Penpot, " name "!")]
|
||||||
|
(-> file
|
||||||
|
(update-in update-path assoc :text text)
|
||||||
|
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
|
||||||
|
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
|
||||||
|
|
||||||
|
(defn create-welcome-file
|
||||||
|
[cfg {:keys [id fullname] :as profile}]
|
||||||
|
(try
|
||||||
|
(let [cfg (dissoc cfg ::db/conn)
|
||||||
|
params {:profile-id (:id profile)
|
||||||
|
:project-id (:default-project-id profile)}
|
||||||
|
template-stream (tmpl/get-template-stream cfg "welcome")
|
||||||
|
file-id (-> (management/clone-template cfg params template-stream)
|
||||||
|
first)]
|
||||||
|
|
||||||
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
|
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
|
||||||
|
(profile/update-profile-props cfg id {:welcome-file-id file-id})
|
||||||
|
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
|
||||||
|
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
|
||||||
|
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "unexpected error on create welcome file " :cause cause))))
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
:created-at (:created-at file)
|
:created-at (:created-at file)
|
||||||
:modified-at (:modified-at file)
|
:modified-at (:modified-at file)
|
||||||
:data-backend nil
|
:data-backend nil
|
||||||
|
:data-ref-id nil
|
||||||
:has-media-trimmed false}
|
:has-media-trimmed false}
|
||||||
{:id (:id file)})))
|
{:id (:id file)})))
|
||||||
|
|
||||||
|
|||||||
@@ -155,9 +155,10 @@
|
|||||||
|
|
||||||
(defn enable-team-feature!
|
(defn enable-team-feature!
|
||||||
[team-id feature]
|
[team-id feature]
|
||||||
(dm/verify!
|
(when-not (contains? cfeat/supported-features feature)
|
||||||
"feature should be supported"
|
(ex/raise :type :assertion
|
||||||
(contains? cfeat/supported-features feature))
|
:code :feature-not-supported
|
||||||
|
:hint (str "feature '" feature "' not supported")))
|
||||||
|
|
||||||
(let [team-id (h/parse-uuid team-id)]
|
(let [team-id (h/parse-uuid team-id)]
|
||||||
(db/tx-run! main/system
|
(db/tx-run! main/system
|
||||||
@@ -173,9 +174,11 @@
|
|||||||
|
|
||||||
(defn disable-team-feature!
|
(defn disable-team-feature!
|
||||||
[team-id feature]
|
[team-id feature]
|
||||||
(dm/verify!
|
|
||||||
"feature should be supported"
|
(when-not (contains? cfeat/supported-features feature)
|
||||||
(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)]
|
(let [team-id (h/parse-uuid team-id)]
|
||||||
(db/tx-run! main/system
|
(db/tx-run! main/system
|
||||||
@@ -203,9 +206,11 @@
|
|||||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||||
:or {code :generic level :info}
|
:or {code :generic level :info}
|
||||||
:as params}]
|
:as params}]
|
||||||
(dm/verify!
|
|
||||||
["invalid level %" level]
|
(when-not (contains? #{:success :error :info :warning} level)
|
||||||
(contains? #{:success :error :info :warning} level))
|
(ex/raise :type :assertion
|
||||||
|
:code :incorrect-level
|
||||||
|
:hint (str "level '" level "' not supported")))
|
||||||
|
|
||||||
(letfn [(send [dest]
|
(letfn [(send [dest]
|
||||||
(l/inf :hint "sending notification" :dest (str dest))
|
(l/inf :hint "sending notification" :dest (str dest))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
[app.storage.s3 :as ss3]
|
[app.storage.s3 :as ss3]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
(case name
|
(case name
|
||||||
:assets-fs :fs
|
:assets-fs :fs
|
||||||
:assets-s3 :s3
|
:assets-s3 :s3
|
||||||
:fs)))
|
nil)))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Storage Module State
|
;; Storage Module State
|
||||||
@@ -52,11 +54,19 @@
|
|||||||
|
|
||||||
(defmethod ig/init-key ::storage
|
(defmethod ig/init-key ::storage
|
||||||
[_ {:keys [::backends ::db/pool] :as cfg}]
|
[_ {:keys [::backends ::db/pool] :as cfg}]
|
||||||
(-> (d/without-nils cfg)
|
(let [backend (or (get-legacy-backend)
|
||||||
(assoc ::backends (d/without-nils backends))
|
(cf/get :objects-storage-backend)
|
||||||
(assoc ::backend (or (get-legacy-backend)
|
:fs)
|
||||||
(cf/get :objects-storage-backend :fs)))
|
backends (d/without-nils backends)]
|
||||||
(assoc ::db/connectable pool)))
|
|
||||||
|
(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 ::backend keyword?)
|
||||||
(s/def ::storage
|
(s/def ::storage
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
[app.storage.impl :as impl]
|
[app.storage.impl :as impl]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
@@ -27,17 +28,15 @@
|
|||||||
java.io.FilterInputStream
|
java.io.FilterInputStream
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
java.net.URI
|
java.net.URI
|
||||||
java.nio.ByteBuffer
|
|
||||||
java.nio.file.Path
|
java.nio.file.Path
|
||||||
java.time.Duration
|
java.time.Duration
|
||||||
java.util.Collection
|
java.util.Collection
|
||||||
java.util.Optional
|
java.util.Optional
|
||||||
java.util.concurrent.Semaphore
|
|
||||||
org.reactivestreams.Subscriber
|
org.reactivestreams.Subscriber
|
||||||
org.reactivestreams.Subscription
|
|
||||||
software.amazon.awssdk.core.ResponseBytes
|
software.amazon.awssdk.core.ResponseBytes
|
||||||
software.amazon.awssdk.core.async.AsyncRequestBody
|
software.amazon.awssdk.core.async.AsyncRequestBody
|
||||||
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
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.ClientAsyncConfiguration
|
||||||
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
|
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
|
||||||
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
|
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.GetObjectPresignRequest
|
||||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
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 put-object)
|
||||||
(declare get-object-bytes)
|
(declare get-object-bytes)
|
||||||
(declare get-object-data)
|
(declare get-object-data)
|
||||||
@@ -80,7 +93,7 @@
|
|||||||
(s/def ::io-threads ::us/integer)
|
(s/def ::io-threads ::us/integer)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::backend [_]
|
(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
|
(defmethod ig/prep-key ::backend
|
||||||
[_ {:keys [::prefix ::region] :as cfg}]
|
[_ {:keys [::prefix ::region] :as cfg}]
|
||||||
@@ -128,18 +141,29 @@
|
|||||||
[backend object]
|
[backend object]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
|
|
||||||
(let [result (p/await (get-object-data backend object))]
|
(loop [result (get-object-data backend object)
|
||||||
(if (ex/exception? result)
|
retryn 0]
|
||||||
(cond
|
|
||||||
(ex/instance? NoSuchKeyException result)
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :object-not-found
|
|
||||||
:hint "s3 object not found"
|
|
||||||
:cause result)
|
|
||||||
:else
|
|
||||||
(throw result))
|
|
||||||
|
|
||||||
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
|
(defmethod impl/get-object-bytes :s3
|
||||||
[backend object]
|
[backend object]
|
||||||
@@ -163,18 +187,14 @@
|
|||||||
|
|
||||||
;; --- HELPERS
|
;; --- HELPERS
|
||||||
|
|
||||||
(def default-timeout
|
|
||||||
(dt/duration {:seconds 30}))
|
|
||||||
|
|
||||||
(defn- lookup-region
|
(defn- lookup-region
|
||||||
^Region
|
^Region
|
||||||
[region]
|
[region]
|
||||||
(Region/of (name region)))
|
(Region/of (name region)))
|
||||||
|
|
||||||
(defn- build-s3-client
|
(defn- build-s3-client
|
||||||
[{:keys [::region ::endpoint ::io-threads]}]
|
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
|
||||||
(let [executor (px/resolve-executor :virtual)
|
(let [aconfig (-> (ClientAsyncConfiguration/builder)
|
||||||
aconfig (-> (ClientAsyncConfiguration/builder)
|
|
||||||
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
||||||
(.build))
|
(.build))
|
||||||
|
|
||||||
@@ -190,6 +210,8 @@
|
|||||||
(.connectionTimeout default-timeout)
|
(.connectionTimeout default-timeout)
|
||||||
(.readTimeout default-timeout)
|
(.readTimeout default-timeout)
|
||||||
(.writeTimeout default-timeout)
|
(.writeTimeout default-timeout)
|
||||||
|
(.maxConcurrency (int max-concurrency))
|
||||||
|
(.maxPendingConnectionAcquires (int max-pending-connection-acquires))
|
||||||
(.build))
|
(.build))
|
||||||
|
|
||||||
client (let [builder (S3AsyncClient/builder)
|
client (let [builder (S3AsyncClient/builder)
|
||||||
@@ -223,69 +245,38 @@
|
|||||||
(.serviceConfiguration ^S3Configuration config)
|
(.serviceConfiguration ^S3Configuration config)
|
||||||
(.build))))
|
(.build))))
|
||||||
|
|
||||||
(defn- upload-thread
|
(defn- write-input-stream
|
||||||
[id subscriber sem content]
|
[delegate input]
|
||||||
(px/thread
|
(try
|
||||||
{:name "penpot/s3/uploader"
|
(.writeInputStream ^BlockingInputStreamAsyncRequestBody delegate
|
||||||
:virtual true
|
^InputStream input)
|
||||||
:daemon true}
|
(catch Throwable cause
|
||||||
(l/trace :hint "start upload thread"
|
(l/error :hint "encountered error while writing input stream to service"
|
||||||
:object-id (str id)
|
:cause cause))
|
||||||
:size (impl/get-size content)
|
(finally
|
||||||
::l/sync? true)
|
(.close ^InputStream input))))
|
||||||
(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- make-request-body
|
(defn- make-request-body
|
||||||
[id content]
|
[executor content]
|
||||||
(reify
|
(let [size (impl/get-size content)]
|
||||||
AsyncRequestBody
|
(reify
|
||||||
(contentLength [_]
|
AsyncRequestBody
|
||||||
(Optional/of (long (impl/get-size content))))
|
(contentLength [_]
|
||||||
|
(Optional/of (long size)))
|
||||||
(^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)))))))))
|
|
||||||
|
|
||||||
|
(^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
|
(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))
|
(let [path (dm/str prefix (impl/id->path id))
|
||||||
mdata (meta object)
|
mdata (meta object)
|
||||||
mtype (:content-type mdata "application/octet-stream")
|
mtype (:content-type mdata "application/octet-stream")
|
||||||
rbody (make-request-body id content)
|
rbody (make-request-body executor content)
|
||||||
request (.. (PutObjectRequest/builder)
|
request (.. (PutObjectRequest/builder)
|
||||||
(bucket bucket)
|
(bucket bucket)
|
||||||
(contentType mtype)
|
(contentType mtype)
|
||||||
|
|||||||
@@ -11,13 +11,16 @@
|
|||||||
permanently delete these files (look at systemd-tempfiles)."
|
permanently delete these files (look at systemd-tempfiles)."
|
||||||
(:require
|
(:require
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px]
|
[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")
|
(def default-tmp-dir "/tmp/penpot")
|
||||||
|
|
||||||
@@ -76,11 +79,9 @@
|
|||||||
[& {:keys [suffix prefix min-age]
|
[& {:keys [suffix prefix min-age]
|
||||||
:or {prefix "penpot."
|
:or {prefix "penpot."
|
||||||
suffix ".tmp"}}]
|
suffix ".tmp"}}]
|
||||||
(let [path (fs/create-tempfile
|
(let [attrs (fs/make-permissions "rw-r--r--")
|
||||||
:perms "rw-r--r--"
|
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
|
||||||
:dir default-tmp-dir
|
path (Files/createFile path attrs)]
|
||||||
:suffix suffix
|
|
||||||
:prefix prefix)]
|
|
||||||
(fs/delete-on-exit! path)
|
(fs/delete-on-exit! path)
|
||||||
(sp/offer! queue [path (some-> min-age dt/duration)])
|
(sp/offer! queue [path (some-> min-age dt/duration)])
|
||||||
path))
|
path))
|
||||||
@@ -133,7 +133,13 @@
|
|||||||
file))
|
file))
|
||||||
|
|
||||||
(def ^:private sql:get-files-for-library
|
(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
|
FROM file AS f
|
||||||
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
|
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
|
||||||
WHERE fl.library_file_id = ?
|
WHERE fl.library_file_id = ?
|
||||||
|
|||||||
@@ -141,21 +141,22 @@
|
|||||||
|
|
||||||
;; --- INSTANT
|
;; --- INSTANT
|
||||||
|
|
||||||
|
(defn instant?
|
||||||
|
[v]
|
||||||
|
(instance? Instant v))
|
||||||
|
|
||||||
(defn instant
|
(defn instant
|
||||||
([s]
|
([s]
|
||||||
(if (int? s)
|
(cond
|
||||||
(Instant/ofEpochMilli s)
|
(instant? s) s
|
||||||
(Instant/parse s)))
|
(int? s) (Instant/ofEpochMilli s)
|
||||||
|
:else (Instant/parse s)))
|
||||||
([s fmt]
|
([s fmt]
|
||||||
(case fmt
|
(case fmt
|
||||||
:rfc1123 (Instant/from (.parse DateTimeFormatter/RFC_1123_DATE_TIME ^String s))
|
:rfc1123 (Instant/from (.parse DateTimeFormatter/RFC_1123_DATE_TIME ^String s))
|
||||||
:iso (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s))
|
:iso (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s))
|
||||||
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
|
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
|
||||||
|
|
||||||
(defn instant?
|
|
||||||
[v]
|
|
||||||
(instance? Instant v))
|
|
||||||
|
|
||||||
(defn is-after?
|
(defn is-after?
|
||||||
[da db]
|
[da db]
|
||||||
(.isAfter ^Instant da ^Instant db))
|
(.isAfter ^Instant da ^Instant db))
|
||||||
|
|||||||
@@ -17,12 +17,11 @@
|
|||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px])
|
[promesa.exec :as px])
|
||||||
(:import
|
(:import
|
||||||
java.util.concurrent.Executor
|
|
||||||
java.util.concurrent.ThreadPoolExecutor))
|
java.util.concurrent.ThreadPoolExecutor))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(s/def ::wrk/executor #(instance? Executor %))
|
(s/def ::wrk/executor #(instance? ThreadPoolExecutor %))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; EXECUTOR
|
;; EXECUTOR
|
||||||
@@ -36,30 +35,22 @@
|
|||||||
(let [factory (px/thread-factory :prefix "penpot/default/")
|
(let [factory (px/thread-factory :prefix "penpot/default/")
|
||||||
executor (px/cached-executor :factory factory :keepalive 60000)]
|
executor (px/cached-executor :factory factory :keepalive 60000)]
|
||||||
(l/inf :hint "executor started")
|
(l/inf :hint "executor started")
|
||||||
(reify
|
executor))
|
||||||
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)))))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::wrk/executor
|
(defmethod ig/halt-key! ::wrk/executor
|
||||||
[_ instance]
|
[_ instance]
|
||||||
(.close ^java.lang.AutoCloseable instance))
|
(px/shutdown! instance))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; MONITOR
|
;; MONITOR
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- get-stats
|
||||||
|
[^ThreadPoolExecutor executor]
|
||||||
|
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||||
|
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||||
|
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||||
|
|
||||||
(s/def ::name ::us/keyword)
|
(s/def ::name ::us/keyword)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::wrk/monitor [_]
|
(defmethod ig/pre-init-spec ::wrk/monitor [_]
|
||||||
@@ -74,7 +65,7 @@
|
|||||||
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
|
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
|
||||||
(letfn [(monitor! [executor prev-completed]
|
(letfn [(monitor! [executor prev-completed]
|
||||||
(let [labels (into-array String [(d/name name)])
|
(let [labels (into-array String [(d/name name)])
|
||||||
stats (deref executor)
|
stats (get-stats executor)
|
||||||
|
|
||||||
completed (:completed stats)
|
completed (:completed stats)
|
||||||
completed-inc (- completed prev-completed)
|
completed-inc (- completed prev-completed)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
:enable-feature-fdata-pointer-map
|
:enable-feature-fdata-pointer-map
|
||||||
:enable-feature-fdata-objets-map
|
:enable-feature-fdata-objets-map
|
||||||
:enable-feature-components-v2
|
:enable-feature-components-v2
|
||||||
:enable-file-snapshot
|
:enable-auto-file-snapshot
|
||||||
:disable-file-validation])
|
:disable-file-validation])
|
||||||
|
|
||||||
(defn state-init
|
(defn state-init
|
||||||
@@ -304,16 +304,18 @@
|
|||||||
([params] (update-file* *system* params))
|
([params] (update-file* *system* params))
|
||||||
([system {:keys [file-id changes session-id profile-id revn]
|
([system {:keys [file-id changes session-id profile-id revn]
|
||||||
:or {session-id (uuid/next) revn 0}}]
|
:or {session-id (uuid/next) revn 0}}]
|
||||||
(db/tx-run! system (fn [{:keys [::db/conn] :as system}]
|
(-> system
|
||||||
(let [file (files.update/get-file conn file-id)]
|
(assoc ::files.update/timestamp (dt/now))
|
||||||
(files.update/update-file system
|
(db/tx-run! (fn [{:keys [::db/conn] :as system}]
|
||||||
|
(let [file (files.update/get-file conn file-id)]
|
||||||
|
(#'files.update/update-file* system
|
||||||
{:id file-id
|
{:id file-id
|
||||||
:revn revn
|
:revn revn
|
||||||
:file file
|
:file file
|
||||||
:features (:features file)
|
:features (:features file)
|
||||||
:changes changes
|
:changes changes
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
:profile-id profile-id}))))))
|
:profile-id profile-id})))))))
|
||||||
|
|
||||||
(declare command!)
|
(declare command!)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
(t/deftest ttf-font-upload-1
|
(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})
|
(let [prof (th/create-profile* 1 {:is-active true})
|
||||||
team-id (:default-team-id prof)
|
team-id (:default-team-id prof)
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.fressian :as fres]
|
[app.common.fressian :as fres]
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
|
[app.common.schema.test :as smt]
|
||||||
[app.common.transit :as transit]
|
[app.common.transit :as transit]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
@@ -84,54 +85,56 @@
|
|||||||
(t/is (= (hash obj1) (hash obj2))))))
|
(t/is (= (hash obj1) (hash obj2))))))
|
||||||
|
|
||||||
(t/deftest internal-encode-decode
|
(t/deftest internal-encode-decode
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||||
(cg/not-empty))]
|
(cg/not-empty))]
|
||||||
(let [obj1 (omap/wrap data)
|
(let [obj1 (omap/wrap data)
|
||||||
obj2 (omap/create (deref obj1))
|
obj2 (omap/create (deref obj1))
|
||||||
obj3 (assoc obj2 uuid/zero 1)
|
obj3 (assoc obj2 uuid/zero 1)
|
||||||
obj4 (omap/create (deref obj3))]
|
obj4 (omap/create (deref obj3))]
|
||||||
;; (app.common.pprint/pprint data)
|
;; (app.common.pprint/pprint data)
|
||||||
(t/is (= (hash obj1) (hash obj2)))
|
|
||||||
(t/is (not= (hash obj2) (hash obj3)))
|
(and (= (hash obj1) (hash obj2))
|
||||||
(t/is (bytes? (deref obj3)))
|
(not= (hash obj2) (hash obj3))
|
||||||
(t/is (pos? (alength (deref obj3))))
|
(bytes? (deref obj3))
|
||||||
(t/is (= (hash obj3) (hash obj4)))))))
|
(pos? (alength (deref obj3)))
|
||||||
|
(= (hash obj3) (hash obj4)))))
|
||||||
|
{:num 50}))
|
||||||
|
|
||||||
(t/deftest fressian-encode-decode
|
(t/deftest fressian-encode-decode
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||||
(cg/not-empty)
|
(cg/not-empty)
|
||||||
(cg/fmap omap/wrap)
|
(cg/fmap omap/wrap)
|
||||||
(cg/fmap (fn [o] {:objects o})))]
|
(cg/fmap (fn [o] {:objects o})))]
|
||||||
|
|
||||||
(let [res (-> data fres/encode fres/decode)]
|
(let [res (-> data fres/encode fres/decode)]
|
||||||
(t/is (contains? res :objects))
|
(and (contains? res :objects)
|
||||||
(t/is (omap/objects-map? (:objects res)))
|
(omap/objects-map? (:objects res))
|
||||||
(t/is (= (count (:objects data))
|
(= (count (:objects data))
|
||||||
(count (:objects res))))
|
(count (:objects res)))
|
||||||
(t/is (= (hash (:objects data))
|
(= (hash (:objects data))
|
||||||
(hash (:objects res))))))))
|
(hash (:objects res))))))
|
||||||
|
{:num 50}))
|
||||||
|
|
||||||
(t/deftest transit-encode-decode
|
(t/deftest transit-encode-decode
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||||
(cg/not-empty)
|
(cg/not-empty)
|
||||||
(cg/fmap omap/wrap)
|
(cg/fmap omap/wrap)
|
||||||
(cg/fmap (fn [o] {:objects o})))]
|
(cg/fmap (fn [o] {:objects o})))]
|
||||||
(let [res (-> data transit/encode transit/decode)]
|
(let [res (-> data transit/encode transit/decode)]
|
||||||
;; (app.common.pprint/pprint data)
|
;; (app.common.pprint/pprint data)
|
||||||
;; (app.common.pprint/pprint res)
|
;; (app.common.pprint/pprint res)
|
||||||
(doseq [[k v] (:objects res)]
|
(and (every? (fn [[k v]]
|
||||||
(t/is (= v (get-in data [:objects k]))))
|
(= v (get-in data [:objects k])))
|
||||||
|
(:objects res))
|
||||||
(t/is (contains? res :objects))
|
(contains? res :objects)
|
||||||
(t/is (contains? data :objects))
|
(contains? data :objects)
|
||||||
|
(omap/objects-map? (:objects data))
|
||||||
(t/is (omap/objects-map? (:objects data)))
|
(not (omap/objects-map? (:objects res)))
|
||||||
(t/is (not (omap/objects-map? (:objects res))))
|
(= (count (:objects data))
|
||||||
|
(count (:objects res))))))
|
||||||
(t/is (= (count (:objects data))
|
{:num 50}))
|
||||||
(count (:objects res))))))))
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
(ns user
|
(ns user
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.fressian :as fres]
|
||||||
[app.common.json :as json]
|
[app.common.json :as json]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
|
|
||||||
(defn ordered-map
|
(defn ordered-map
|
||||||
([] lkm/empty-linked-map)
|
([] lkm/empty-linked-map)
|
||||||
([a] (conj lkm/empty-linked-map a))
|
([k a] (assoc lkm/empty-linked-map k a))
|
||||||
([a & xs] (apply conj lkm/empty-linked-map a xs)))
|
([k a & xs] (apply assoc lkm/empty-linked-map k a xs)))
|
||||||
|
|
||||||
(defn ordered-set?
|
(defn ordered-set?
|
||||||
[o]
|
[o]
|
||||||
@@ -564,6 +564,41 @@
|
|||||||
new-elems
|
new-elems
|
||||||
(remove p? after))))
|
(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
|
;; Data Parsing / Conversion
|
||||||
|
|||||||
@@ -108,14 +108,6 @@
|
|||||||
`(do ~@body)
|
`(do ~@body)
|
||||||
(reverse (partition 2 bindings))))
|
(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
|
(defmacro get-prop
|
||||||
"A macro based, optimized variant of `get` that access the property
|
"A macro based, optimized variant of `get` that access the property
|
||||||
directly on CLJS, on CLJ works as get."
|
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 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop))
|
||||||
(list `c/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!
|
(defmacro assert!
|
||||||
([expr]
|
([expr]
|
||||||
`(assert! nil ~expr))
|
`(assert! nil ~expr))
|
||||||
([hint expr]
|
([hint expr]
|
||||||
(let [hint (cond
|
(let [hint (cond
|
||||||
(vector? hint)
|
(vector? hint)
|
||||||
`(str/ffmt ~@hint)
|
`(str/ffmt ~@hint)
|
||||||
|
|
||||||
(some? hint)
|
(some? hint)
|
||||||
hint
|
hint
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(str "expr assert: " (pr-str expr)))]
|
(str "expr assert: " (pr-str expr)))]
|
||||||
(when *assert*
|
(when *assert*
|
||||||
`(binding [*assert-context* ~hint]
|
`(runtime-assert ~hint (fn [] ~expr))))))
|
||||||
(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#))))))))
|
|
||||||
@@ -49,7 +49,8 @@
|
|||||||
"components/v2"
|
"components/v2"
|
||||||
"styles/v2"
|
"styles/v2"
|
||||||
"layout/grid"
|
"layout/grid"
|
||||||
"plugins/runtime"})
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"})
|
||||||
|
|
||||||
;; A set of features enabled by default
|
;; A set of features enabled by default
|
||||||
(def default-features
|
(def default-features
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
;; team feature field
|
;; team feature field
|
||||||
(def frontend-only-features
|
(def frontend-only-features
|
||||||
#{"styles/v2"
|
#{"styles/v2"
|
||||||
"plugins/runtime"})
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"})
|
||||||
|
|
||||||
;; Features that are mainly backend only or there are a proper
|
;; Features that are mainly backend only or there are a proper
|
||||||
;; fallback when frontend reports no support for it
|
;; fallback when frontend reports no support for it
|
||||||
@@ -81,7 +83,8 @@
|
|||||||
"fdata/pointer-map"
|
"fdata/pointer-map"
|
||||||
"layout/grid"
|
"layout/grid"
|
||||||
"fdata/shape-data-type"
|
"fdata/shape-data-type"
|
||||||
"plugins/runtime"}
|
"plugins/runtime"
|
||||||
|
"text-editor/v2"}
|
||||||
(into frontend-only-features)))
|
(into frontend-only-features)))
|
||||||
|
|
||||||
(sm/register! ::features
|
(sm/register! ::features
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
:feature-fdata-objects-map "fdata/objects-map"
|
:feature-fdata-objects-map "fdata/objects-map"
|
||||||
:feature-fdata-pointer-map "fdata/pointer-map"
|
:feature-fdata-pointer-map "fdata/pointer-map"
|
||||||
:feature-plugins "plugins/runtime"
|
:feature-plugins "plugins/runtime"
|
||||||
|
:feature-text-editor-v2 "text-editor/v2"
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn migrate-legacy-features
|
(defn migrate-legacy-features
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
valid? (or (and components-v2
|
valid? (or (and components-v2
|
||||||
(nil? (:component-id change))
|
(nil? (:component-id change))
|
||||||
(nil? (:page-id change)))
|
(nil? (:page-id change)))
|
||||||
(ch/check-change! change))]
|
(ch/valid-change? change))]
|
||||||
|
|
||||||
(when-not valid?
|
(when-not valid?
|
||||||
(let [explain (sm/explain ::ch/change change)]
|
(let [explain (sm/explain ::ch/change change)]
|
||||||
@@ -741,46 +741,36 @@
|
|||||||
|
|
||||||
(defn add-guide
|
(defn add-guide
|
||||||
[file guide]
|
[file guide]
|
||||||
|
|
||||||
(let [guide (cond-> guide
|
(let [guide (cond-> guide
|
||||||
(nil? (:id guide))
|
(nil? (:id guide))
|
||||||
(assoc :id (uuid/next)))
|
(assoc :id (uuid/next)))
|
||||||
page-id (:current-page-id file)
|
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
|
(-> file
|
||||||
(commit-change
|
(commit-change
|
||||||
{:type :set-option
|
{:type :set-guide
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:option :guides
|
:id (:id guide)
|
||||||
:value new-guides})
|
:params guide})
|
||||||
(assoc :last-id (:id guide)))))
|
(assoc :last-id (:id guide)))))
|
||||||
|
|
||||||
(defn delete-guide
|
(defn delete-guide
|
||||||
[file id]
|
[file id]
|
||||||
|
|
||||||
(let [page-id (:current-page-id file)
|
(let [page-id (:current-page-id file)]
|
||||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
(commit-change file
|
||||||
new-guides (dissoc old-guides id)]
|
{:type :set-guide
|
||||||
(-> file
|
:page-id page-id
|
||||||
(commit-change
|
:id id
|
||||||
{:type :set-option
|
:params nil})))
|
||||||
:page-id page-id
|
|
||||||
:option :guides
|
|
||||||
:value new-guides}))))
|
|
||||||
|
|
||||||
(defn update-guide
|
(defn update-guide
|
||||||
[file guide]
|
[file guide]
|
||||||
|
(let [page-id (:current-page-id file)]
|
||||||
(let [page-id (:current-page-id file)
|
(commit-change file
|
||||||
old-guides (or (dm/get-in file [:data :pages-index page-id :options :guides]) {})
|
{:type :set-guide
|
||||||
new-guides (assoc old-guides (:id guide) guide)]
|
:page-id page-id
|
||||||
(-> file
|
:id (:id guide)
|
||||||
(commit-change
|
:params guide})))
|
||||||
{:type :set-option
|
|
||||||
:page-id page-id
|
|
||||||
:option :guides
|
|
||||||
:value new-guides}))))
|
|
||||||
|
|
||||||
(defn strip-image-extension [filename]
|
(defn strip-image-extension [filename]
|
||||||
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
|
||||||
|
|||||||
@@ -10,21 +10,25 @@
|
|||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.desc-native :as smd]
|
[app.common.schema.desc-native :as smd]
|
||||||
|
[app.common.schema.generators :as sg]
|
||||||
[app.common.types.color :as ctc]
|
[app.common.types.color :as ctc]
|
||||||
[app.common.types.colors-list :as ctcl]
|
[app.common.types.colors-list :as ctcl]
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.components-list :as ctkl]
|
[app.common.types.components-list :as ctkl]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.file :as ctf]
|
[app.common.types.file :as ctf]
|
||||||
|
[app.common.types.grid :as ctg]
|
||||||
[app.common.types.page :as ctp]
|
[app.common.types.page :as ctp]
|
||||||
[app.common.types.pages-list :as ctpl]
|
[app.common.types.pages-list :as ctpl]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.types.shape-tree :as ctst]
|
[app.common.types.shape-tree :as ctst]
|
||||||
[app.common.types.typographies-list :as ctyl]
|
[app.common.types.typographies-list :as ctyl]
|
||||||
[app.common.types.typography :as ctt]
|
[app.common.types.typography :as ctt]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[clojure.set :as set]))
|
[clojure.set :as set]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -60,6 +64,111 @@
|
|||||||
[:type [:= :set-remote-synced]]
|
[:type [:= :set-remote-synced]]
|
||||||
[:remote-synced {:optional true} [:maybe :boolean]]]]])
|
[: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
|
(def schema:change
|
||||||
[:schema
|
[:schema
|
||||||
[:multi {:dispatch :type
|
[:multi {:dispatch :type
|
||||||
@@ -67,13 +176,18 @@
|
|||||||
:decode/json #(update % :type keyword)
|
:decode/json #(update % :type keyword)
|
||||||
::smd/simplified true}
|
::smd/simplified true}
|
||||||
[:set-option
|
[: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]
|
[:page-id ::sm/uuid]
|
||||||
[:option [:union
|
[:frame-id [:maybe ::sm/uuid]]
|
||||||
[:keyword]
|
[:position [:maybe ::gpt/point]]]]
|
||||||
[:vector {:gen/max 10} :keyword]]]
|
|
||||||
[:value :any]]]
|
|
||||||
|
|
||||||
[:add-obj
|
[:add-obj
|
||||||
[:map {:title "AddObjChange"}
|
[:map {:title "AddObjChange"}
|
||||||
@@ -103,6 +217,10 @@
|
|||||||
[:component-id {:optional true} ::sm/uuid]
|
[:component-id {:optional true} ::sm/uuid]
|
||||||
[:ignore-touched {:optional true} :boolean]]]
|
[: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
|
[:fix-obj
|
||||||
[:map {:title "FixObjChange"}
|
[:map {:title "FixObjChange"}
|
||||||
[:type [:= :fix-obj]]
|
[:type [:= :fix-obj]]
|
||||||
@@ -143,19 +261,12 @@
|
|||||||
[:map {:title "ModPageChange"}
|
[:map {:title "ModPageChange"}
|
||||||
[:type [:= :mod-page]]
|
[:type [:= :mod-page]]
|
||||||
[:id ::sm/uuid]
|
[: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
|
[:set-plugin-data schema:set-plugin-data-change]
|
||||||
[: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]]]]
|
|
||||||
|
|
||||||
[:del-page
|
[:del-page
|
||||||
[:map {:title "DelPageChange"}
|
[:map {:title "DelPageChange"}
|
||||||
@@ -190,10 +301,9 @@
|
|||||||
[:type [:= :del-color]]
|
[:type [:= :del-color]]
|
||||||
[:id ::sm/uuid]]]
|
[:id ::sm/uuid]]]
|
||||||
|
|
||||||
|
;; DEPRECATED: remove before 2.3
|
||||||
[:add-recent-color
|
[:add-recent-color
|
||||||
[:map {:title "AddRecentColorChange"}
|
[:map {:title "AddRecentColorChange"}]]
|
||||||
[:type [:= :add-recent-color]]
|
|
||||||
[:color ::ctc/recent-color]]]
|
|
||||||
|
|
||||||
[:add-media
|
[:add-media
|
||||||
[:map {:title "AddMediaChange"}
|
[:map {:title "AddMediaChange"}
|
||||||
@@ -264,11 +374,11 @@
|
|||||||
(sm/register! ::change schema:change)
|
(sm/register! ::change schema:change)
|
||||||
(sm/register! ::changes schema:changes)
|
(sm/register! ::changes schema:changes)
|
||||||
|
|
||||||
(def check-change!
|
(def valid-change?
|
||||||
(sm/check-fn ::change))
|
(sm/lazy-validator schema:change))
|
||||||
|
|
||||||
(def check-changes!
|
(def check-changes!
|
||||||
(sm/check-fn ::changes))
|
(sm/check-fn schema:changes))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Specific helpers
|
;; Specific helpers
|
||||||
@@ -304,10 +414,12 @@
|
|||||||
;; If object has changed or is new verify is correct
|
;; If object has changed or is new verify is correct
|
||||||
(when (and (some? shape-new)
|
(when (and (some? shape-new)
|
||||||
(not= shape-old shape-new))
|
(not= shape-old shape-new))
|
||||||
(dm/verify!
|
(when-not (and (cts/valid-shape? shape-new)
|
||||||
"expected valid shape"
|
(cts/shape? shape-new))
|
||||||
(and (cts/check-shape! shape-new)
|
(ex/raise :type :assertion
|
||||||
(cts/shape? shape-new))))))]
|
:code :data-validation
|
||||||
|
:hint "invalid shape found after applying changes"
|
||||||
|
::sm/explain (cts/explain-shape shape-new))))))]
|
||||||
|
|
||||||
(->> (into #{} (map :page-id) items)
|
(->> (into #{} (map :page-id) items)
|
||||||
(mapcat (fn [page-id]
|
(mapcat (fn [page-id]
|
||||||
@@ -340,12 +452,10 @@
|
|||||||
(process-changes data items true))
|
(process-changes data items true))
|
||||||
|
|
||||||
([data items verify?]
|
([data items verify?]
|
||||||
;; When verify? false we spec the schema validation. Currently used to make just
|
;; When verify? false we spec the schema validation. Currently used
|
||||||
;; 1 validation even if the changes are applied twice
|
;; to make just 1 validation even if the changes are applied twice
|
||||||
(when verify?
|
(when verify?
|
||||||
(dm/verify!
|
(check-changes! items))
|
||||||
"expected valid changes"
|
|
||||||
(check-changes! items)))
|
|
||||||
|
|
||||||
(binding [*touched-changes* (volatile! #{})]
|
(binding [*touched-changes* (volatile! #{})]
|
||||||
(let [result (reduce #(or (process-change %1 %2) %1) data items)
|
(let [result (reduce #(or (process-change %1 %2) %1) data items)
|
||||||
@@ -357,14 +467,71 @@
|
|||||||
#?(:clj (validate-shapes! data result items))
|
#?(:clj (validate-shapes! data result items))
|
||||||
result))))
|
result))))
|
||||||
|
|
||||||
|
;; DEPRECATED: remove after 2.3 release
|
||||||
(defmethod process-change :set-option
|
(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]
|
(d/update-in-when data [:pages-index page-id]
|
||||||
(fn [data]
|
(fn [page]
|
||||||
(let [path (if (seqable? option) option [option])]
|
(if (and position frame-id)
|
||||||
(if value
|
(update page :comment-thread-positions assoc
|
||||||
(assoc-in data (into [:options] path) value)
|
comment-thread-id {:frame-id frame-id
|
||||||
(assoc data :options (d/dissoc-in (:options data) path)))))))
|
: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
|
(defmethod process-change :add-obj
|
||||||
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
|
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
|
||||||
@@ -546,6 +713,7 @@
|
|||||||
(d/update-in-when [pid :shapes] d/without-obj sid)
|
(d/update-in-when [pid :shapes] d/without-obj sid)
|
||||||
(d/update-in-when [pid :shapes] d/vec-without-nils)
|
(d/update-in-when [pid :shapes] d/vec-without-nils)
|
||||||
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
|
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
|
||||||
|
|
||||||
(update-parent-id [objects id]
|
(update-parent-id [objects id]
|
||||||
(-> objects
|
(-> objects
|
||||||
(d/update-when id assoc :parent-id parent-id)))
|
(d/update-when id assoc :parent-id parent-id)))
|
||||||
@@ -604,25 +772,34 @@
|
|||||||
(ctpl/add-page data page)))
|
(ctpl/add-page data page)))
|
||||||
|
|
||||||
(defmethod process-change :mod-page
|
(defmethod process-change :mod-page
|
||||||
[data {:keys [id name]}]
|
[data {:keys [id] :as params}]
|
||||||
(d/update-in-when data [:pages-index id] assoc :name name))
|
(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]}]
|
[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]
|
(letfn [(update-fn [data]
|
||||||
(if (some? value)
|
(if (some? value)
|
||||||
(assoc-in data [:plugin-data namespace key] 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
|
(case object-type
|
||||||
:file
|
:file
|
||||||
(update-fn data)
|
(update-fn data)
|
||||||
|
|
||||||
:page
|
: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
|
:shape
|
||||||
(d/update-in-when data [:pages-index page-id :objects object-id] update-fn)
|
(d/update-in-when data [:pages-index page-id :objects object-id] update-fn)
|
||||||
@@ -656,18 +833,11 @@
|
|||||||
[data {:keys [id]}]
|
[data {:keys [id]}]
|
||||||
(ctcl/delete-color data id))
|
(ctcl/delete-color data id))
|
||||||
|
|
||||||
|
;; DEPRECATED: remove before 2.3
|
||||||
(defmethod process-change :add-recent-color
|
(defmethod process-change :add-recent-color
|
||||||
[data {:keys [color]}]
|
[data _]
|
||||||
;; Moves the color to the top of the list and then truncates up to 15
|
data)
|
||||||
(update
|
|
||||||
data
|
|
||||||
:recent-colors
|
|
||||||
(fn [rc]
|
|
||||||
(let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color)))
|
|
||||||
rc (-> rc (conj color))]
|
|
||||||
(cond-> rc
|
|
||||||
(> (count rc) 15)
|
|
||||||
(subvec 1))))))
|
|
||||||
|
|
||||||
;; -- Media
|
;; -- Media
|
||||||
|
|
||||||
|
|||||||
@@ -135,12 +135,6 @@
|
|||||||
(or (contains? (meta changes) ::page-id)
|
(or (contains? (meta changes) ::page-id)
|
||||||
(contains? (meta changes) ::component-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!
|
(defn- assert-objects!
|
||||||
[changes]
|
[changes]
|
||||||
(dm/assert!
|
(dm/assert!
|
||||||
@@ -195,41 +189,85 @@
|
|||||||
(apply-changes-local)))
|
(apply-changes-local)))
|
||||||
|
|
||||||
(defn mod-page
|
(defn mod-page
|
||||||
[changes page new-name]
|
([changes options]
|
||||||
(-> changes
|
(let [page (::page (meta changes))]
|
||||||
(update :redo-changes conj {:type :mod-page :id (:id page) :name new-name})
|
(mod-page changes page options)))
|
||||||
(update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)})
|
|
||||||
(apply-changes-local)))
|
|
||||||
|
|
||||||
(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]
|
([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]
|
([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]
|
([changes type id page-id namespace key value]
|
||||||
(let [data (::file-data (meta changes))
|
(let [data (::file-data (meta changes))
|
||||||
old-val
|
old-val
|
||||||
(case type
|
(case type
|
||||||
:file
|
:file
|
||||||
(get-in data [:plugin-data namespace key])
|
(dm/get-in data [:plugin-data namespace key])
|
||||||
|
|
||||||
:page
|
: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
|
: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
|
:color
|
||||||
(get-in data [:colors id :plugin-data namespace key])
|
(dm/get-in data [:colors id :plugin-data namespace key])
|
||||||
|
|
||||||
:typography
|
:typography
|
||||||
(get-in data [:typographies id :plugin-data namespace key])
|
(dm/get-in data [:typographies id :plugin-data namespace key])
|
||||||
|
|
||||||
:component
|
: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
|
(-> 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 :redo-changes conj redo-change)
|
||||||
(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 :undo-changes conj undo-change)
|
||||||
(apply-changes-local)))))
|
(apply-changes-local)))))
|
||||||
|
|
||||||
(defn del-page
|
(defn del-page
|
||||||
@@ -246,42 +284,76 @@
|
|||||||
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index})
|
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index})
|
||||||
(apply-changes-local)))
|
(apply-changes-local)))
|
||||||
|
|
||||||
(defn set-page-option
|
(defn set-guide
|
||||||
[changes option-key option-val]
|
[changes id guide]
|
||||||
(assert-page! changes)
|
|
||||||
(let [page-id (::page-id (meta changes))
|
(let [page-id (::page-id (meta changes))
|
||||||
page (::page (meta changes))
|
page (::page (meta changes))
|
||||||
old-val (get-in page [:options option-key])]
|
old-val (dm/get-in page [:guides id])]
|
||||||
|
|
||||||
(-> changes
|
(-> changes
|
||||||
(update :redo-changes conj {:type :set-option
|
(update :redo-changes conj {:type :set-guide
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:option option-key
|
:id id
|
||||||
:value option-val})
|
:params guide})
|
||||||
(update :undo-changes conj {:type :set-option
|
(update :undo-changes conj {:type :set-guide
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:option option-key
|
:id id
|
||||||
:value old-val})
|
:params old-val}))))
|
||||||
(apply-changes-local))))
|
(defn set-flow
|
||||||
|
[changes id flow]
|
||||||
(defn update-page-option
|
|
||||||
[changes option-key update-fn & args]
|
|
||||||
(assert-page! changes)
|
|
||||||
(let [page-id (::page-id (meta changes))
|
(let [page-id (::page-id (meta changes))
|
||||||
page (::page (meta changes))
|
page (::page (meta changes))
|
||||||
old-val (get-in page [:options option-key])
|
old-val (dm/get-in page [:flows id])
|
||||||
new-val (apply update-fn old-val args)]
|
|
||||||
|
|
||||||
(-> changes
|
changes (-> changes
|
||||||
(update :redo-changes conj {:type :set-option
|
(update :redo-changes conj {:type :set-flow
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:option option-key
|
:id id
|
||||||
:value new-val})
|
:params flow})
|
||||||
(update :undo-changes conj {:type :set-option
|
(update :undo-changes conj {:type :set-flow
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:option option-key
|
:id id
|
||||||
:value old-val})
|
:params old-val}))]
|
||||||
(apply-changes-local))))
|
;; 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
|
;; Shape tree changes
|
||||||
|
|
||||||
@@ -607,13 +679,6 @@
|
|||||||
(reduce resize-parent changes all-parents)))
|
(reduce resize-parent changes all-parents)))
|
||||||
|
|
||||||
;; Library changes
|
;; Library changes
|
||||||
|
|
||||||
(defn add-recent-color
|
|
||||||
[changes color]
|
|
||||||
(-> changes
|
|
||||||
(update :redo-changes conj {:type :add-recent-color :color color})
|
|
||||||
(apply-changes-local)))
|
|
||||||
|
|
||||||
(defn add-color
|
(defn add-color
|
||||||
[changes color]
|
[changes color]
|
||||||
(-> changes
|
(-> changes
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
|
|
||||||
(ns app.common.files.defaults)
|
(ns app.common.files.defaults)
|
||||||
|
|
||||||
(def version 51)
|
(def version 57)
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
[app.common.files.defaults :as cfd]
|
[app.common.files.defaults :as cfd]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.rect :as grc]
|
[app.common.geom.rect :as grc]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.geom.shapes.path :as gsp]
|
[app.common.geom.shapes.path :as gsp]
|
||||||
@@ -863,11 +864,9 @@
|
|||||||
(assoc shadow :color color)))
|
(assoc shadow :color color)))
|
||||||
|
|
||||||
(update-object [object]
|
(update-object [object]
|
||||||
(d/update-when object :shadow
|
(let [xform (comp (map fix-shadow)
|
||||||
#(into []
|
(filter valid-shadow?))]
|
||||||
(comp (map fix-shadow)
|
(d/update-when object :shadow #(into [] xform %))))
|
||||||
(filter valid-shadow?))
|
|
||||||
%)))
|
|
||||||
|
|
||||||
(update-container [container]
|
(update-container [container]
|
||||||
(d/update-when container :objects update-vals update-object))]
|
(d/update-when container :objects update-vals update-object))]
|
||||||
@@ -1010,13 +1009,127 @@
|
|||||||
|
|
||||||
(defn migrate-up-51
|
(defn migrate-up-51
|
||||||
"This migration fixes library invalid colors"
|
"This migration fixes library invalid colors"
|
||||||
|
|
||||||
[data]
|
[data]
|
||||||
(let [update-colors
|
(let [update-colors
|
||||||
(fn [colors]
|
(fn [colors]
|
||||||
(into {} (filter #(-> % val valid-color?) colors)))]
|
(into {} (filter #(-> % val valid-color?) colors)))]
|
||||||
(update data :colors update-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
|
(def migrations
|
||||||
"A vector of all applicable migrations"
|
"A vector of all applicable migrations"
|
||||||
[{:id 2 :migrate-up migrate-up-2}
|
[{:id 2 :migrate-up migrate-up-2}
|
||||||
@@ -1059,4 +1172,11 @@
|
|||||||
{:id 48 :migrate-up migrate-up-48}
|
{:id 48 :migrate-up migrate-up-48}
|
||||||
{:id 49 :migrate-up migrate-up-49}
|
{:id 49 :migrate-up migrate-up-49}
|
||||||
{:id 50 :migrate-up migrate-up-50}
|
{: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]
|
[old-page page check-attrs]
|
||||||
|
|
||||||
(let [old-objects (get old-page :objects)
|
(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-objects (get page :objects)
|
||||||
new-guides (or (get-in page [:options :guides]) [])
|
new-guides (or (get page :guides) [])
|
||||||
|
|
||||||
changed-object?
|
changed-object?
|
||||||
(fn [id]
|
(fn [id]
|
||||||
|
|||||||
@@ -57,16 +57,17 @@
|
|||||||
:misplaced-slot
|
:misplaced-slot
|
||||||
:missing-slot})
|
:missing-slot})
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private schema:error
|
||||||
schema:error
|
[:map {:title "ValidationError"}
|
||||||
(sm/define
|
[:code {:optional false} [::sm/one-of error-codes]]
|
||||||
[:map {:title "ValidationError"}
|
[:hint {:optional false} :string]
|
||||||
[:code {:optional false} [::sm/one-of error-codes]]
|
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
||||||
[:hint {:optional false} :string]
|
[:shape-id {:optional true} ::sm/uuid]
|
||||||
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
|
[:file-id ::sm/uuid]
|
||||||
[:shape-id {:optional true} ::sm/uuid]
|
[:page-id {:optional true} [:maybe ::sm/uuid]]])
|
||||||
[:file-id ::sm/uuid]
|
|
||||||
[:page-id {:optional true} [:maybe ::sm/uuid]]]))
|
(def check-error!
|
||||||
|
(sm/check-fn schema:error))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; ERROR HANDLING
|
;; ERROR HANDLING
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
|
|
||||||
(dm/assert!
|
(dm/assert!
|
||||||
"expected valid error"
|
"expected valid error"
|
||||||
(sm/check! schema:error error))
|
(check-error! error))
|
||||||
|
|
||||||
(vswap! *errors* conj error)))
|
(vswap! *errors* conj error)))
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
java.time.Instant
|
java.time.Instant
|
||||||
java.time.OffsetDateTime
|
java.time.OffsetDateTime
|
||||||
java.util.List
|
java.util.List
|
||||||
|
linked.map.LinkedMap
|
||||||
org.fressian.Reader
|
org.fressian.Reader
|
||||||
org.fressian.StreamingWriter
|
org.fressian.StreamingWriter
|
||||||
org.fressian.Writer
|
org.fressian.Writer
|
||||||
@@ -109,6 +110,13 @@
|
|||||||
(clojure.lang.PersistentArrayMap. (.toArray kvs))
|
(clojure.lang.PersistentArrayMap. (.toArray kvs))
|
||||||
(clojure.lang.PersistentHashMap/create (seq 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 *write-handler-lookup* nil)
|
||||||
(def ^:dynamic *read-handler-lookup* nil)
|
(def ^:dynamic *read-handler-lookup* nil)
|
||||||
|
|
||||||
@@ -225,6 +233,11 @@
|
|||||||
:wfn write-map-like
|
:wfn write-map-like
|
||||||
:rfn read-map-like}
|
:rfn read-map-like}
|
||||||
|
|
||||||
|
{:name "linked/map"
|
||||||
|
:class LinkedMap
|
||||||
|
:wfn write-map-like
|
||||||
|
:rfn read-ordered-map}
|
||||||
|
|
||||||
{:name "clj/keyword"
|
{:name "clj/keyword"
|
||||||
:class clojure.lang.Keyword
|
:class clojure.lang.Keyword
|
||||||
:wfn write-named
|
:wfn write-named
|
||||||
|
|||||||
@@ -56,6 +56,9 @@
|
|||||||
[:x ::sm/safe-number]
|
[:x ::sm/safe-number]
|
||||||
[:y ::sm/safe-number]])
|
[:y ::sm/safe-number]])
|
||||||
|
|
||||||
|
(def valid-point-attrs?
|
||||||
|
(sm/validator schema:point-attrs))
|
||||||
|
|
||||||
(def valid-point?
|
(def valid-point?
|
||||||
(sm/validator
|
(sm/validator
|
||||||
[:and [:fn point?] schema:point-attrs]))
|
[:and [:fn point?] schema:point-attrs]))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.rect :as grc]
|
[app.common.geom.rect :as grc]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.math :as mth]))
|
[app.common.math :as mth]))
|
||||||
|
|
||||||
(defn shape-stroke-margin
|
(defn shape-stroke-margin
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
|
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
|
||||||
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
|
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
|
||||||
filter-h (+ h (mth/abs offset-y) (* 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)))
|
(grc/make-rect filter-x filter-y filter-w filter-h)))
|
||||||
|
|
||||||
(defn get-rect-filter-bounds
|
(defn get-rect-filter-bounds
|
||||||
@@ -96,12 +98,15 @@
|
|||||||
([shape ignore-margin?]
|
([shape ignore-margin?]
|
||||||
(let [strokes (:strokes shape)
|
(let [strokes (:strokes shape)
|
||||||
|
|
||||||
|
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||||
|
^boolean (gsh/open-path? shape))
|
||||||
|
|
||||||
stroke-width
|
stroke-width
|
||||||
(->> strokes
|
(->> strokes
|
||||||
(map #(case (get % :stroke-alignment :center)
|
(map #(case (get % :stroke-alignment :center)
|
||||||
:center (/ (:stroke-width % 0) 2)
|
:center (/ (:stroke-width % 0) 2)
|
||||||
:outer (:stroke-width % 0)
|
:outer (:stroke-width % 0)
|
||||||
0))
|
(if open-path? (:stroke-width % 0) 0)))
|
||||||
(reduce d/max 0))
|
(reduce d/max 0))
|
||||||
|
|
||||||
stroke-margin
|
stroke-margin
|
||||||
|
|||||||
@@ -852,8 +852,10 @@
|
|||||||
|
|
||||||
(defn ray-overlaps?
|
(defn ray-overlaps?
|
||||||
[ray-point {selrect :selrect}]
|
[ray-point {selrect :selrect}]
|
||||||
(and (>= (:y ray-point) (:y1 selrect))
|
(and (or (> (:y ray-point) (:y1 selrect))
|
||||||
(<= (:y ray-point) (:y2 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
|
(defn content->geom-data
|
||||||
[content]
|
[content]
|
||||||
|
|||||||
@@ -232,6 +232,7 @@
|
|||||||
[(:parent-id first-shape)]
|
[(:parent-id first-shape)]
|
||||||
(fn [shape objects]
|
(fn [shape objects]
|
||||||
(-> shape
|
(-> shape
|
||||||
|
(ctl/assign-cells objects)
|
||||||
(ctl/push-into-cell [(:id first-shape)] row column)
|
(ctl/push-into-cell [(:id first-shape)] row column)
|
||||||
(ctl/assign-cells objects)))
|
(ctl/assign-cells objects)))
|
||||||
{:with-objects? true})
|
{:with-objects? true})
|
||||||
@@ -1947,54 +1948,55 @@
|
|||||||
|
|
||||||
(defn generate-duplicate-flows
|
(defn generate-duplicate-flows
|
||||||
[changes shapes page ids-map]
|
[changes shapes page ids-map]
|
||||||
(let [flows (-> page :options :flows)
|
(let [flows (get page :flows)
|
||||||
unames (volatile! (into #{} (map :name flows)))
|
unames (volatile! (cfh/get-used-names (vals flows)))
|
||||||
frames-with-flow (->> shapes
|
has-flow? (partial ctp/get-frame-flow flows)]
|
||||||
(filter #(= (:type %) :frame))
|
|
||||||
(filter #(some? (ctp/get-frame-flow flows (:id %)))))]
|
(reduce (fn [changes frame-id]
|
||||||
(if-not (empty? frames-with-flow)
|
(let [name (cfh/generate-unique-name @unames "Flow 1")
|
||||||
(let [update-flows (fn [flows]
|
frame-id (get ids-map frame-id)
|
||||||
(reduce
|
flow-id (uuid/next)
|
||||||
(fn [flows frame]
|
new-flow {:id flow-id
|
||||||
(let [name (cfh/generate-unique-name @unames "Flow 1")
|
:name name
|
||||||
_ (vswap! unames conj name)
|
:starting-frame frame-id}]
|
||||||
new-flow {:id (uuid/next)
|
|
||||||
:name name
|
(vswap! unames conj name)
|
||||||
:starting-frame (get ids-map (:id frame))}]
|
(pcb/set-flow changes flow-id new-flow)))
|
||||||
(ctp/add-flow flows new-flow)))
|
|
||||||
flows
|
changes
|
||||||
frames-with-flow))]
|
(->> shapes
|
||||||
(pcb/update-page-option changes :flows update-flows))
|
(filter cfh/frame-shape?)
|
||||||
changes)))
|
(map :id)
|
||||||
|
(filter has-flow?)))))
|
||||||
|
|
||||||
(defn generate-duplicate-guides
|
(defn generate-duplicate-guides
|
||||||
[changes shapes page ids-map delta]
|
[changes shapes page ids-map delta]
|
||||||
(let [guides (get-in page [:options :guides])
|
(let [guides (get page :guides)
|
||||||
frames (->> shapes (filter cfh/frame-shape?))
|
frames (filter cfh/frame-shape? shapes)]
|
||||||
|
|
||||||
new-guides
|
;; FIXME: this can be implemented efficiently just indexing guides
|
||||||
(reduce
|
;; by frame-id instead of iterate over all guides all the time
|
||||||
(fn [g frame]
|
|
||||||
(let [new-id (ids-map (:id frame))
|
|
||||||
new-frame (-> frame (gsh/move delta))
|
|
||||||
|
|
||||||
new-guides
|
(reduce (fn [changes frame]
|
||||||
(->> guides
|
(let [new-id (get ids-map (:id frame))
|
||||||
(vals)
|
new-frame (gsh/move frame delta)]
|
||||||
(filter #(= (:frame-id %) (:id frame)))
|
|
||||||
(map #(-> %
|
(reduce-kv (fn [changes _ guide]
|
||||||
(assoc :id (uuid/next))
|
(if (= (:id frame) (:frame-id guide))
|
||||||
(assoc :frame-id new-id)
|
(let [guide-id (uuid/next)
|
||||||
(assoc :position (if (= (:axis %) :x)
|
position (if (= (:axis guide) :x)
|
||||||
(+ (:position %) (- (:x new-frame) (:x frame)))
|
(+ (:position guide) (- (:x new-frame) (:x frame)))
|
||||||
(+ (:position %) (- (:y new-frame) (:y frame))))))))]
|
(+ (:position guide) (- (:y new-frame) (:y frame))))
|
||||||
(cond-> g
|
guide {:id guide-id
|
||||||
(not-empty new-guides)
|
:frame-id new-id
|
||||||
(conj (into {} (map (juxt :id identity) new-guides))))))
|
:position position
|
||||||
guides
|
:axis (:axis guide)}]
|
||||||
frames)]
|
(pcb/set-guide changes guide-id guide))
|
||||||
(-> (pcb/with-page changes page)
|
changes))
|
||||||
(pcb/set-page-option :guides new-guides))))
|
changes
|
||||||
|
guides)))
|
||||||
|
(pcb/with-page changes page)
|
||||||
|
frames)))
|
||||||
|
|
||||||
(defn generate-duplicate-component-change
|
(defn generate-duplicate-component-change
|
||||||
[changes objects page component-root parent-id frame-id delta libraries library-data]
|
[changes objects page component-root parent-id frame-id delta libraries library-data]
|
||||||
|
|||||||
@@ -7,13 +7,11 @@
|
|||||||
(ns app.common.logic.shapes
|
(ns app.common.logic.shapes
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.common.files.changes-builder :as pcb]
|
[app.common.files.changes-builder :as pcb]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.page :as ctp]
|
|
||||||
[app.common.types.shape.interactions :as ctsi]
|
[app.common.types.shape.interactions :as ctsi]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.common.uuid :as uuid]))
|
[app.common.uuid :as uuid]))
|
||||||
@@ -85,7 +83,9 @@
|
|||||||
(pcb/with-page page)
|
(pcb/with-page page)
|
||||||
(pcb/with-objects objects)
|
(pcb/with-objects objects)
|
||||||
(pcb/with-library-data file))
|
(pcb/with-library-data file))
|
||||||
|
|
||||||
lookup (d/getf objects)
|
lookup (d/getf objects)
|
||||||
|
|
||||||
groups-to-unmask
|
groups-to-unmask
|
||||||
(reduce (fn [group-ids id]
|
(reduce (fn [group-ids id]
|
||||||
;; When the shape to delete is the mask of a masked group,
|
;; When the shape to delete is the mask of a masked group,
|
||||||
@@ -110,30 +110,21 @@
|
|||||||
interactions)))
|
interactions)))
|
||||||
(vals objects))
|
(vals objects))
|
||||||
|
|
||||||
ids-set (set ids-to-delete)
|
changes
|
||||||
guides-to-remove
|
(reduce (fn [changes {:keys [id] :as flow}]
|
||||||
(->> (dm/get-in page [:options :guides])
|
(if (contains? ids-to-delete (:starting-frame flow))
|
||||||
(vals)
|
(pcb/set-flow changes id nil)
|
||||||
(filter #(contains? ids-set (:frame-id %)))
|
changes))
|
||||||
(map :id))
|
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
|
all-parents
|
||||||
(reduce (fn [res id]
|
(reduce (fn [res id]
|
||||||
;; All parents of any deleted shape must be resized.
|
;; All parents of any deleted shape must be resized.
|
||||||
(into res (cfh/get-parent-ids objects id)))
|
(into res (cfh/get-parent-ids objects id)))
|
||||||
(d/ordered-set)
|
(d/ordered-set)
|
||||||
ids-to-delete)
|
(concat ids-to-delete ids-to-hide))
|
||||||
|
|
||||||
all-children
|
all-children
|
||||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||||
@@ -158,7 +149,11 @@
|
|||||||
|
|
||||||
empty-parents
|
empty-parents
|
||||||
;; Any parent whose children are all deleted, must be deleted too.
|
;; 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
|
components-to-delete
|
||||||
(if components-v2
|
(if components-v2
|
||||||
@@ -172,8 +167,18 @@
|
|||||||
(into ids-to-delete all-children))
|
(into ids-to-delete all-children))
|
||||||
[])
|
[])
|
||||||
|
|
||||||
changes (-> changes
|
ids-set (set ids-to-delete)
|
||||||
(pcb/set-page-option :guides guides))
|
|
||||||
|
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]
|
changes (reduce (fn [changes component-id]
|
||||||
;; It's important to delete the component before the main instance, because we
|
;; 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)))
|
(pcb/delete-component changes component-id (:id page)))
|
||||||
changes
|
changes
|
||||||
components-to-delete)
|
components-to-delete)
|
||||||
|
|
||||||
changes (-> changes
|
changes (-> changes
|
||||||
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
(generate-update-shape-flags ids-to-hide objects {:hidden true})
|
||||||
(pcb/remove-objects all-children {:ignore-touched true})
|
(pcb/remove-objects all-children {:ignore-touched true})
|
||||||
@@ -197,11 +203,7 @@
|
|||||||
(into []
|
(into []
|
||||||
(remove #(and (ctsi/has-destination %)
|
(remove #(and (ctsi/has-destination %)
|
||||||
(contains? ids-to-delete (:destination %))))
|
(contains? ids-to-delete (:destination %))))
|
||||||
interactions)))))
|
interactions))))))]
|
||||||
(cond-> (seq starting-flows)
|
|
||||||
(pcb/update-page-option :flows (fn [flows]
|
|
||||||
(->> (map :id starting-flows)
|
|
||||||
(reduce ctp/remove-flow flows))))))]
|
|
||||||
[all-parents changes]))
|
[all-parents changes]))
|
||||||
|
|
||||||
|
|
||||||
@@ -406,17 +408,12 @@
|
|||||||
;; Resize parent containers that need to
|
;; Resize parent containers that need to
|
||||||
(pcb/resize-parents parents))))
|
(pcb/resize-parents parents))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn change-show-in-viewer [shape hide?]
|
(defn change-show-in-viewer [shape hide?]
|
||||||
(cond-> (assoc shape :hide-in-viewer hide?)
|
(assoc shape :hide-in-viewer hide?))
|
||||||
;; When a frame is no longer shown in view mode, it cannot have interactions
|
|
||||||
hide?
|
|
||||||
(dissoc :interactions)))
|
|
||||||
|
|
||||||
(defn add-new-interaction [shape interaction]
|
(defn add-new-interaction [shape interaction]
|
||||||
(-> shape
|
(-> shape
|
||||||
(update :interactions ctsi/add-interaction interaction)
|
(update :interactions ctsi/add-interaction interaction)))
|
||||||
;; When a interaction is created, the frame must be shown in view mode
|
|
||||||
(dissoc :hide-in-viewer)))
|
(defn show-in-viewer [shape]
|
||||||
|
(dissoc shape :hide-in-viewer))
|
||||||
+102
-157
@@ -9,7 +9,6 @@
|
|||||||
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
[app.common.schema.openapi :as-alias oapi]
|
[app.common.schema.openapi :as-alias oapi]
|
||||||
@@ -29,11 +28,6 @@
|
|||||||
[malli.util :as mu]))
|
[malli.util :as mu]))
|
||||||
|
|
||||||
(defprotocol ILazySchema
|
(defprotocol ILazySchema
|
||||||
(-get-schema [_])
|
|
||||||
(-get-validator [_])
|
|
||||||
(-get-explainer [_])
|
|
||||||
(-get-decoder [_])
|
|
||||||
(-get-encoder [_])
|
|
||||||
(-validate [_ o])
|
(-validate [_ o])
|
||||||
(-explain [_ o])
|
(-explain [_ o])
|
||||||
(-decode [_ o]))
|
(-decode [_ o]))
|
||||||
@@ -53,27 +47,21 @@
|
|||||||
[s]
|
[s]
|
||||||
(m/type-properties s))
|
(m/type-properties s))
|
||||||
|
|
||||||
(defn lazy-schema?
|
(defn- lazy-schema?
|
||||||
[s]
|
[s]
|
||||||
(satisfies? ILazySchema s))
|
(satisfies? ILazySchema s))
|
||||||
|
|
||||||
(defn schema
|
(defn schema
|
||||||
[s]
|
[s]
|
||||||
(if (lazy-schema? s)
|
(m/schema s default-options))
|
||||||
(-get-schema s)
|
|
||||||
(m/schema s default-options)))
|
|
||||||
|
|
||||||
(defn validate
|
(defn validate
|
||||||
[s value]
|
[s value]
|
||||||
(if (lazy-schema? s)
|
(m/validate s value default-options))
|
||||||
(-validate s value)
|
|
||||||
(m/validate s value default-options)))
|
|
||||||
|
|
||||||
(defn explain
|
(defn explain
|
||||||
[s value]
|
[s value]
|
||||||
(if (lazy-schema? s)
|
(m/explain s value default-options))
|
||||||
(-explain s value)
|
|
||||||
(m/explain s value default-options)))
|
|
||||||
|
|
||||||
(defn simplify
|
(defn simplify
|
||||||
"Given an explain data structure, return a simplified version of it"
|
"Given an explain data structure, return a simplified version of it"
|
||||||
@@ -171,29 +159,19 @@
|
|||||||
|
|
||||||
(defn validator
|
(defn validator
|
||||||
[s]
|
[s]
|
||||||
(if (lazy-schema? s)
|
(-> s schema m/validator))
|
||||||
(-get-validator s)
|
|
||||||
(-> s schema m/validator)))
|
|
||||||
|
|
||||||
(defn explainer
|
(defn explainer
|
||||||
[s]
|
[s]
|
||||||
(if (lazy-schema? s)
|
(-> s schema m/explainer))
|
||||||
(-get-explainer s)
|
|
||||||
(-> s schema m/explainer)))
|
|
||||||
|
|
||||||
(defn encoder
|
(defn encoder
|
||||||
([s]
|
|
||||||
(assert (lazy-schema? s) "expected lazy schema")
|
|
||||||
(-get-decoder s))
|
|
||||||
([s transformer]
|
([s transformer]
|
||||||
(m/encoder s default-options transformer))
|
(m/encoder s default-options transformer))
|
||||||
([s options transformer]
|
([s options transformer]
|
||||||
(m/encoder s options transformer)))
|
(m/encoder s options transformer)))
|
||||||
|
|
||||||
(defn decoder
|
(defn decoder
|
||||||
([s]
|
|
||||||
(assert (lazy-schema? s) "expected lazy schema")
|
|
||||||
(-get-decoder s))
|
|
||||||
([s transformer]
|
([s transformer]
|
||||||
(m/decoder s default-options transformer))
|
(m/decoder s default-options transformer))
|
||||||
([s options transformer]
|
([s options transformer]
|
||||||
@@ -242,6 +220,8 @@
|
|||||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
||||||
|
|
||||||
(defn pretty-explain
|
(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]
|
[explain & {:keys [variant message]
|
||||||
:or {variant ::explain
|
:or {variant ::explain
|
||||||
message "Validation Error"}}]
|
message "Validation Error"}}]
|
||||||
@@ -259,102 +239,50 @@
|
|||||||
([s] (lookup sr/default-registry s))
|
([s] (lookup sr/default-registry s))
|
||||||
([registry s] (schema (mr/schema 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
|
"A fast path for checking process, assumes the ILazySchema protocol
|
||||||
implemented on the provided `s` schema. Sould not be used directly."
|
implemented on the provided `s` schema. Sould not be used directly."
|
||||||
[s value]
|
[s type code hint value]
|
||||||
(when-not ^boolean (-validate s value)
|
(when-not ^boolean (-validate s value)
|
||||||
(let [hint (d/nilv dm/*assert-context* "check error")
|
(let [explain (-explain s value)]
|
||||||
explain (-explain s value)]
|
(throw (ex-info hint {:type type
|
||||||
(throw (ex-info hint {:type :assertion
|
:code code
|
||||||
:code :data-validation
|
|
||||||
:hint hint
|
:hint hint
|
||||||
::explain explain}))))
|
::explain explain}))))
|
||||||
true)
|
value)
|
||||||
|
|
||||||
(declare define)
|
(declare ^:private lazy-schema)
|
||||||
|
|
||||||
(defn check-fn
|
(defn check-fn
|
||||||
"Create a predefined check function"
|
"Create a predefined check function"
|
||||||
[s]
|
[s & {:keys [hint type code]}]
|
||||||
(let [schema (if (lazy-schema? s) s (define s))]
|
(let [schema (if (lazy-schema? s) s (lazy-schema s))
|
||||||
(partial fast-check! schema)))
|
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!
|
(defn check!
|
||||||
"A helper intended to be used on assertions for validate/check the
|
"A helper intended to be used on assertions for validate/check the
|
||||||
schema over provided data. Raises an assertion exception, should be
|
schema over provided data. Raises an assertion exception."
|
||||||
used together with `dm/assert!` or `dm/verify!`."
|
[s value & {:keys [hint type code]}]
|
||||||
[s value]
|
(let [s (if (lazy-schema? s) s (lazy-schema s))
|
||||||
(if (lazy-schema? s)
|
hint (or ^boolean hint "check error")
|
||||||
(fast-check! s value)
|
type (or ^boolean type :assertion)
|
||||||
(do
|
code (or ^boolean code :data-validation)]
|
||||||
(when-not ^boolean (m/validate s value default-options)
|
(fast-check! s type code hint value)))
|
||||||
(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))
|
|
||||||
|
|
||||||
(defn register! [type s]
|
(defn register! [type s]
|
||||||
(let [s (if (map? s) (m/-simple-schema s) s)]
|
(let [s (if (map? s) (m/-simple-schema s) s)]
|
||||||
(swap! sr/registry assoc type s)
|
(swap! sr/registry assoc type s)
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn define
|
(defn- lazy-schema
|
||||||
"Create ans instance of ILazySchema"
|
"Create ans instance of ILazySchema"
|
||||||
[s & {:keys [transformer] :or {transformer json-transformer} :as options}]
|
[s]
|
||||||
(let [schema (delay (schema s))
|
(let [schema (delay (schema s))
|
||||||
validator (delay (m/validator @schema))
|
validator (delay (m/validator @schema))
|
||||||
explainer (delay (m/explainer @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))]
|
|
||||||
|
|
||||||
(reify
|
(reify
|
||||||
m/AST
|
m/AST
|
||||||
@@ -397,16 +325,6 @@
|
|||||||
(m/-form @schema))
|
(m/-form @schema))
|
||||||
|
|
||||||
ILazySchema
|
ILazySchema
|
||||||
(-get-schema [_]
|
|
||||||
@schema)
|
|
||||||
(-get-validator [_]
|
|
||||||
@validator)
|
|
||||||
(-get-explainer [_]
|
|
||||||
@explainer)
|
|
||||||
(-get-encoder [_]
|
|
||||||
@encoder)
|
|
||||||
(-get-decoder [_]
|
|
||||||
@decoder)
|
|
||||||
(-validate [_ o]
|
(-validate [_ o]
|
||||||
(@validator o))
|
(@validator o))
|
||||||
(-explain [_ o]
|
(-explain [_ o]
|
||||||
@@ -448,7 +366,7 @@
|
|||||||
(defn parse-email
|
(defn parse-email
|
||||||
[s]
|
[s]
|
||||||
(if (string? s)
|
(if (string? s)
|
||||||
(re-matches email-re s)
|
(first (re-seq email-re s))
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(defn email-string?
|
(defn email-string?
|
||||||
@@ -476,7 +394,7 @@
|
|||||||
::oapi/type "string"
|
::oapi/type "string"
|
||||||
::oapi/format "email"}})
|
::oapi/format "email"}})
|
||||||
|
|
||||||
(def non-empty-strings-xf
|
(def xf:filter-word-strings
|
||||||
(comp
|
(comp
|
||||||
(filter string?)
|
(filter string?)
|
||||||
(remove str/empty?)
|
(remove str/empty?)
|
||||||
@@ -489,11 +407,8 @@
|
|||||||
:min 0
|
:min 0
|
||||||
:max 1
|
:max 1
|
||||||
:compile
|
:compile
|
||||||
(fn [{:keys [coerce kind max min] :as props} children _]
|
(fn [{:keys [kind max min] :as props} children _]
|
||||||
(let [xform (if coerce
|
(let [kind (or (last children) kind)
|
||||||
(comp non-empty-strings-xf (map coerce))
|
|
||||||
non-empty-strings-xf)
|
|
||||||
kind (or (last children) kind)
|
|
||||||
|
|
||||||
pred
|
pred
|
||||||
(cond
|
(cond
|
||||||
@@ -501,9 +416,6 @@
|
|||||||
(nil? kind) any?
|
(nil? kind) any?
|
||||||
:else (validator kind))
|
:else (validator kind))
|
||||||
|
|
||||||
encode-child
|
|
||||||
(encoder kind string-transformer)
|
|
||||||
|
|
||||||
pred
|
pred
|
||||||
(cond
|
(cond
|
||||||
(and max min)
|
(and max min)
|
||||||
@@ -531,31 +443,49 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(every? pred value)))
|
(every? pred value)))
|
||||||
|
|
||||||
decode
|
|
||||||
|
decode-string-child
|
||||||
|
(decoder kind string-transformer)
|
||||||
|
|
||||||
|
decode-string
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||||
(into #{} xform 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
|
encode-json
|
||||||
(fn [o]
|
(fn [o]
|
||||||
(if (set? o)
|
(if (set? o)
|
||||||
(vec o)
|
(vec o)
|
||||||
o))
|
|
||||||
|
|
||||||
encode-string
|
|
||||||
(fn [o]
|
|
||||||
(if (set? o)
|
|
||||||
(str/join ", " (map encode-child o))
|
|
||||||
o))]
|
o))]
|
||||||
|
|
||||||
|
|
||||||
{:pred pred
|
{:pred pred
|
||||||
:type-properties
|
:type-properties
|
||||||
{:title "set"
|
{:title "set"
|
||||||
:description "Set of Strings"
|
:description "Set of Strings"
|
||||||
:error/message "should be a set of strings"
|
:error/message "should be a set of strings"
|
||||||
:gen/gen (-> kind sg/generator sg/set)
|
:gen/gen (-> kind sg/generator sg/set)
|
||||||
:decode/string decode
|
:decode/string decode-string
|
||||||
:decode/json decode
|
:decode/json decode-json
|
||||||
:encode/string encode-string
|
:encode/string encode-string
|
||||||
:encode/json encode-json
|
:encode/json encode-json
|
||||||
::oapi/type "array"
|
::oapi/type "array"
|
||||||
@@ -569,21 +499,14 @@
|
|||||||
:min 0
|
:min 0
|
||||||
:max 1
|
:max 1
|
||||||
:compile
|
:compile
|
||||||
(fn [{:keys [coerce kind max min] :as props} children _]
|
(fn [{:keys [kind max min] :as props} children _]
|
||||||
(let [xform (if coerce
|
(let [kind (or (last children) kind)
|
||||||
(comp non-empty-strings-xf (map coerce))
|
|
||||||
non-empty-strings-xf)
|
|
||||||
|
|
||||||
kind (or (last children) kind)
|
|
||||||
pred
|
pred
|
||||||
(cond
|
(cond
|
||||||
(fn? kind) kind
|
(fn? kind) kind
|
||||||
(nil? kind) any?
|
(nil? kind) any?
|
||||||
:else (validator kind))
|
:else (validator kind))
|
||||||
|
|
||||||
encode-child
|
|
||||||
(encoder kind string-transformer)
|
|
||||||
|
|
||||||
pred
|
pred
|
||||||
(cond
|
(cond
|
||||||
(and max min)
|
(and max min)
|
||||||
@@ -611,15 +534,31 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(every? pred value)))
|
(every? pred value)))
|
||||||
|
|
||||||
decode
|
decode-string-child
|
||||||
|
(decoder kind string-transformer)
|
||||||
|
|
||||||
|
decode-json-child
|
||||||
|
(decoder kind json-transformer)
|
||||||
|
|
||||||
|
decode-string
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(let [v (if (string? v) (str/split v #"[\s,]+") v)]
|
(let [v (if (string? v) (str/split v #"[\s,]+") v)
|
||||||
(into [] xform 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
|
encode-string
|
||||||
(fn [o]
|
(fn [o]
|
||||||
(if (vector? o)
|
(if (vector? o)
|
||||||
(str/join ", " (map encode-child o))
|
(str/join ", " (map encode-string-child o))
|
||||||
o))]
|
o))]
|
||||||
|
|
||||||
{:pred pred
|
{:pred pred
|
||||||
@@ -628,8 +567,8 @@
|
|||||||
:description "Set of Strings"
|
:description "Set of Strings"
|
||||||
:error/message "should be a set of strings"
|
:error/message "should be a set of strings"
|
||||||
:gen/gen (-> kind sg/generator sg/set)
|
:gen/gen (-> kind sg/generator sg/set)
|
||||||
:decode/string decode
|
:decode/string decode-string
|
||||||
:decode/json decode
|
:decode/json decode-json
|
||||||
:encode/string encode-string
|
:encode/string encode-string
|
||||||
::oapi/type "array"
|
::oapi/type "array"
|
||||||
::oapi/format "set"
|
::oapi/format "set"
|
||||||
@@ -646,7 +585,7 @@
|
|||||||
:gen/gen (-> :string sg/generator sg/set)
|
:gen/gen (-> :string sg/generator sg/set)
|
||||||
:decode/string (fn [v]
|
:decode/string (fn [v]
|
||||||
(let [v (if (string? v) (str/split v #"[\s,]+") 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/type "array"
|
||||||
::oapi/format "set"
|
::oapi/format "set"
|
||||||
::oapi/items {:type "string"}
|
::oapi/items {:type "string"}
|
||||||
@@ -662,7 +601,7 @@
|
|||||||
:gen/gen (-> :keyword sg/generator sg/set)
|
:gen/gen (-> :keyword sg/generator sg/set)
|
||||||
:decode/string (fn [v]
|
:decode/string (fn [v]
|
||||||
(let [v (if (string? v) (str/split v #"[\s,]+") 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/type "array"
|
||||||
::oapi/format "set"
|
::oapi/format "set"
|
||||||
::oapi/items {:type "string" :format "keyword"}
|
::oapi/items {:type "string" :format "keyword"}
|
||||||
@@ -747,8 +686,8 @@
|
|||||||
pred)
|
pred)
|
||||||
pred (if (some? max)
|
pred (if (some? max)
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(and (>= max v)
|
(and (pred v)
|
||||||
(pred v)))
|
(>= max v)))
|
||||||
pred)]
|
pred)]
|
||||||
|
|
||||||
{:pred pred
|
{:pred pred
|
||||||
@@ -785,8 +724,8 @@
|
|||||||
pred)
|
pred)
|
||||||
pred (if (some? max)
|
pred (if (some? max)
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(and (>= max v)
|
(and (pred v)
|
||||||
(pred v)))
|
(>= max v)))
|
||||||
pred)]
|
pred)]
|
||||||
|
|
||||||
{:pred pred
|
{:pred pred
|
||||||
@@ -815,8 +754,8 @@
|
|||||||
pred)
|
pred)
|
||||||
pred (if (some? max)
|
pred (if (some? max)
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(and (>= max v)
|
(and (pred v)
|
||||||
(pred v)))
|
(>= max v)))
|
||||||
pred)
|
pred)
|
||||||
|
|
||||||
gen (sg/one-of
|
gen (sg/one-of
|
||||||
@@ -1041,6 +980,12 @@
|
|||||||
(def check-email!
|
(def check-email!
|
||||||
(check-fn ::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!
|
(def check-coll-of-uuid!
|
||||||
(check-fn ::coll-of-uuid))
|
(check-fn ::coll-of-uuid))
|
||||||
|
|
||||||
|
|||||||
@@ -5,46 +5,21 @@
|
|||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.common.schema.generators
|
(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]))
|
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||||
(:require
|
(:require
|
||||||
[app.common.schema.registry :as sr]
|
[app.common.schema.registry :as sr]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[clojure.test.check :as tc]
|
|
||||||
[clojure.test.check.generators :as tg]
|
[clojure.test.check.generators :as tg]
|
||||||
[clojure.test.check.properties :as tp]
|
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.generator :as mg]))
|
[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
|
(defmacro let
|
||||||
[& params]
|
[& params]
|
||||||
`(tg/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
|
(defn sample
|
||||||
([g]
|
([g]
|
||||||
(mg/sample g {:registry sr/default-registry}))
|
(mg/sample g {:registry sr/default-registry}))
|
||||||
@@ -83,6 +58,11 @@
|
|||||||
(tg/such-that (fn [v] (>= (count v) 4)) $$ 100)
|
(tg/such-that (fn [v] (>= (count v) 4)) $$ 100)
|
||||||
(tg/fmap str/lower $$)))
|
(tg/fmap str/lower $$)))
|
||||||
|
|
||||||
|
(defn word-keyword
|
||||||
|
[]
|
||||||
|
(->> (word-string)
|
||||||
|
(tg/fmap keyword)))
|
||||||
|
|
||||||
(defn email
|
(defn email
|
||||||
[]
|
[]
|
||||||
(->> (word-string)
|
(->> (word-string)
|
||||||
@@ -91,7 +71,6 @@
|
|||||||
(tg/fmap (fn [v]
|
(tg/fmap (fn [v]
|
||||||
(str v "@example.net")))))
|
(str v "@example.net")))))
|
||||||
|
|
||||||
|
|
||||||
(defn uri
|
(defn uri
|
||||||
[]
|
[]
|
||||||
(tg/let [scheme (tg/elements ["http" "https"])
|
(tg/let [scheme (tg/elements ["http" "https"])
|
||||||
@@ -103,8 +82,7 @@
|
|||||||
|
|
||||||
(defn uuid
|
(defn uuid
|
||||||
[]
|
[]
|
||||||
(->> tg/small-integer
|
(tg/fmap (fn [_] (uuid/next)) (small-int)))
|
||||||
(tg/fmap (fn [_] (uuid/next)))))
|
|
||||||
|
|
||||||
(defn subseq
|
(defn subseq
|
||||||
"Given a collection, generates \"subsequences\" which are sequences
|
"Given a collection, generates \"subsequences\" which are sequences
|
||||||
|
|||||||
@@ -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.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
@@ -29,12 +30,12 @@
|
|||||||
{:x 0 :y 0 :width 1 :height 1})
|
{:x 0 :y 0 :width 1 :height 1})
|
||||||
|
|
||||||
(defn- assert-valid-num [attr num]
|
(defn- assert-valid-num [attr num]
|
||||||
(dm/verify!
|
(when-not (and (d/num? num)
|
||||||
["%1 attribute has invalid value: %2" (d/name attr) num]
|
(<= num max-safe-int)
|
||||||
(and (d/num? num)
|
(>= num min-safe-int))
|
||||||
(<= num max-safe-int)
|
(ex/raise :type :assertion
|
||||||
(>= num min-safe-int)))
|
:code :data-validation
|
||||||
|
:hint (str "invalid numeric value for `" attr "`: " num)))
|
||||||
(cond
|
(cond
|
||||||
(and (> num 0) (< num 1)) 1
|
(and (> num 0) (< num 1)) 1
|
||||||
(and (< num 0) (> num -1)) -1
|
(and (< num 0) (> num -1)) -1
|
||||||
@@ -43,19 +44,21 @@
|
|||||||
(defn- assert-valid-pos-num
|
(defn- assert-valid-pos-num
|
||||||
[attr num]
|
[attr num]
|
||||||
|
|
||||||
(dm/verify!
|
(when-not (pos? num)
|
||||||
["%1 attribute should be positive" (d/name attr)]
|
(ex/raise :type :assertion
|
||||||
(pos? num))
|
:code :data-validation
|
||||||
|
:hint (str "invalid numeric value for `" attr "`: " num " (should be positive)")))
|
||||||
num)
|
num)
|
||||||
|
|
||||||
(defn- assert-valid-blend-mode
|
(defn- assert-valid-blend-mode
|
||||||
[mode]
|
[mode]
|
||||||
(let [clean-value (-> mode str/trim str/lower keyword)]
|
(let [value (-> mode str/trim str/lower keyword)]
|
||||||
(dm/verify!
|
|
||||||
["%1 is not a valid blend mode" clean-value]
|
(when-not (contains? cts/blend-modes value)
|
||||||
(contains? cts/blend-modes clean-value))
|
(ex/raise :type :assertion
|
||||||
clean-value))
|
:code :data-validation
|
||||||
|
:hint (str "unexpected blend mode: " value)))
|
||||||
|
value))
|
||||||
|
|
||||||
(defn- svg-dimensions
|
(defn- svg-dimensions
|
||||||
[{:keys [attrs] :as data}]
|
[{: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-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
|
(def default-text-attrs
|
||||||
{:typography-ref-file nil
|
{:typography-ref-file nil
|
||||||
:typography-ref-id nil
|
:typography-ref-id nil
|
||||||
@@ -92,9 +98,13 @@
|
|||||||
:text-transform "none"
|
:text-transform "none"
|
||||||
:text-align "left"
|
:text-align "left"
|
||||||
:text-decoration "none"
|
:text-decoration "none"
|
||||||
|
:text-direction "ltr"
|
||||||
:fills [{:fill-color clr/black
|
:fills [{:fill-color clr/black
|
||||||
:fill-opacity 1}]})
|
:fill-opacity 1}]})
|
||||||
|
|
||||||
|
(def default-attrs
|
||||||
|
(merge default-root-attrs default-text-attrs))
|
||||||
|
|
||||||
(def typography-fields
|
(def typography-fields
|
||||||
[:font-id
|
[:font-id
|
||||||
:font-family
|
:font-family
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
[app.common.uri :as uri]
|
[app.common.uri :as uri]
|
||||||
[cognitect.transit :as t]
|
[cognitect.transit :as t]
|
||||||
[lambdaisland.uri :as luri]
|
[lambdaisland.uri :as luri]
|
||||||
[linked.core :as lk]
|
[linked.map :as lkm]
|
||||||
[linked.set :as lks])
|
[linked.set :as lks])
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import
|
(:import
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
java.time.Instant
|
java.time.Instant
|
||||||
java.time.OffsetDateTime
|
java.time.OffsetDateTime
|
||||||
lambdaisland.uri.URI
|
lambdaisland.uri.URI
|
||||||
|
linked.map.LinkedMap
|
||||||
linked.set.LinkedSet)))
|
linked.set.LinkedSet)))
|
||||||
|
|
||||||
(def write-handlers (atom nil))
|
(def write-handlers (atom nil))
|
||||||
@@ -118,10 +119,15 @@
|
|||||||
{:id "u"
|
{:id "u"
|
||||||
:rfn parse-uuid})
|
:rfn parse-uuid})
|
||||||
|
|
||||||
|
{:id "ordered-map"
|
||||||
|
:class #?(:clj LinkedMap :cljs lkm/LinkedMap)
|
||||||
|
:wfn vec
|
||||||
|
:rfn #(into lkm/empty-linked-map %)}
|
||||||
|
|
||||||
{:id "ordered-set"
|
{:id "ordered-set"
|
||||||
:class #?(:clj LinkedSet :cljs lks/LinkedSet)
|
:class #?(:clj LinkedSet :cljs lks/LinkedSet)
|
||||||
:wfn vec
|
:wfn vec
|
||||||
:rfn #(into (lk/set) %)}
|
:rfn #(into lks/empty-linked-set %)}
|
||||||
|
|
||||||
{:id "duration"
|
{:id "duration"
|
||||||
:class #?(:clj Duration :cljs lxn/Duration)
|
:class #?(:clj Duration :cljs lxn/Duration)
|
||||||
|
|||||||
@@ -56,8 +56,8 @@
|
|||||||
(def schema:image-color
|
(def schema:image-color
|
||||||
[:map {:title "ImageColor"}
|
[:map {:title "ImageColor"}
|
||||||
[:name {:optional true} :string]
|
[:name {:optional true} :string]
|
||||||
[:width :int]
|
[:width ::sm/int]
|
||||||
[:height :int]
|
[:height ::sm/int]
|
||||||
[:mtype {:optional true} [:maybe :string]]
|
[:mtype {:optional true} [:maybe :string]]
|
||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:keep-aspect-ratio {:optional true} :boolean]])
|
[:keep-aspect-ratio {:optional true} :boolean]])
|
||||||
@@ -80,21 +80,23 @@
|
|||||||
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
[:opacity {:optional true} [:maybe ::sm/safe-number]]
|
||||||
[:offset ::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
|
(def schema:color
|
||||||
[:and
|
[:and schema:color-attrs
|
||||||
[: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]]
|
|
||||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||||
|
|
||||||
(def schema:recent-color
|
(def schema:recent-color
|
||||||
@@ -107,14 +109,14 @@
|
|||||||
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
[::sm/contains-any {:strict true} [:color :gradient :image]]])
|
||||||
|
|
||||||
(sm/register! ::rgb-color type:rgb-color)
|
(sm/register! ::rgb-color type:rgb-color)
|
||||||
|
|
||||||
(sm/register! ::color schema:color)
|
(sm/register! ::color schema:color)
|
||||||
(sm/register! ::gradient schema:gradient)
|
(sm/register! ::gradient schema:gradient)
|
||||||
(sm/register! ::image-color schema:image-color)
|
(sm/register! ::image-color schema:image-color)
|
||||||
(sm/register! ::recent-color schema:recent-color)
|
(sm/register! ::recent-color schema:recent-color)
|
||||||
|
(sm/register! ::color-attrs schema:color-attrs)
|
||||||
|
|
||||||
(def check-color!
|
(def check-color!
|
||||||
(sm/check-fn schema:color))
|
(sm/check-fn schema:color :hint "expected valid color struct"))
|
||||||
|
|
||||||
(def check-recent-color!
|
(def check-recent-color!
|
||||||
(sm/check-fn schema:recent-color))
|
(sm/check-fn schema:recent-color))
|
||||||
@@ -392,13 +394,22 @@
|
|||||||
|
|
||||||
(process-shape-colors shape sync-color)))
|
(process-shape-colors shape sync-color)))
|
||||||
|
|
||||||
(defn eq-recent-color?
|
(defn- eq-recent-color?
|
||||||
[c1 c2]
|
[c1 c2]
|
||||||
(or (= c1 c2)
|
(or (= c1 c2)
|
||||||
(and (some? (:color c1))
|
(and (some? (:color c1))
|
||||||
(some? (:color c2))
|
(some? (:color c2))
|
||||||
(= (:color c1) (:color c2)))))
|
(= (:color c1) (:color c2)))))
|
||||||
|
|
||||||
|
(defn add-recent-color
|
||||||
|
"Moves the color to the top of the list and then truncates up to 15"
|
||||||
|
[state file-id color]
|
||||||
|
(update state file-id (fn [colors]
|
||||||
|
(let [colors (d/removev (partial eq-recent-color? color) colors)
|
||||||
|
colors (conj colors color)]
|
||||||
|
(cond-> colors
|
||||||
|
(> (count colors) 15)
|
||||||
|
(subvec 1))))))
|
||||||
|
|
||||||
(defn stroke->color-att
|
(defn stroke->color-att
|
||||||
[stroke file-id shared-libs]
|
[stroke file-id shared-libs]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.common.types.grid
|
(ns app.common.types.grid
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.colors :as clr]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.color :as ctc]))
|
[app.common.types.color :as ctc]))
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
[:display :boolean]
|
[:display :boolean]
|
||||||
[:params schema:square-params]]]])
|
[:params schema:square-params]]]])
|
||||||
|
|
||||||
(def schema:saved-grids
|
(def schema:default-grids
|
||||||
[:map {:title "PageGrid"}
|
[:map {:title "PageGrid"}
|
||||||
[:square {:optional true} ::square-params]
|
[:square {:optional true} ::square-params]
|
||||||
[:row {:optional true} ::column-params]
|
[:row {:optional true} ::column-params]
|
||||||
@@ -63,4 +64,24 @@
|
|||||||
(sm/register! ::square-params schema:square-params)
|
(sm/register! ::square-params schema:square-params)
|
||||||
(sm/register! ::column-params schema:column-params)
|
(sm/register! ::column-params schema:column-params)
|
||||||
(sm/register! ::grid schema:grid)
|
(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
|
(ns app.common.types.page
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.point :as-alias gpt]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.color :as-alias ctc]
|
[app.common.types.color :as-alias ctc]
|
||||||
[app.common.types.grid :as ctg]
|
[app.common.types.grid :as ctg]
|
||||||
@@ -24,38 +25,56 @@
|
|||||||
[:name :string]
|
[:name :string]
|
||||||
[:starting-frame ::sm/uuid]])
|
[:starting-frame ::sm/uuid]])
|
||||||
|
|
||||||
|
(def schema:flows
|
||||||
|
[:map-of {:gen/max 2} ::sm/uuid schema:flow])
|
||||||
|
|
||||||
(def schema:guide
|
(def schema:guide
|
||||||
[:map {:title "Guide"}
|
[:map {:title "Guide"}
|
||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:axis [::sm/one-of #{:x :y}]]
|
[:axis [::sm/one-of #{:x :y}]]
|
||||||
[:position ::sm/safe-number]
|
[:position ::sm/safe-number]
|
||||||
|
;; FIXME: remove maybe?
|
||||||
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
|
[: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
|
(def schema:page
|
||||||
[:map {:title "FilePage"}
|
[:map {:title "FilePage"}
|
||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:name :string]
|
[:name :string]
|
||||||
[:objects
|
[:objects schema:objects]
|
||||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape]]
|
[: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
|
[:options
|
||||||
[:map {:title "PageOptions"}
|
;; DEPERECATED: remove after 2.3 release
|
||||||
[:background {:optional true} ::ctc/rgb-color]
|
[:map {:title "PageOptions"}]]])
|
||||||
[: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]]]])
|
|
||||||
|
|
||||||
(sm/register! ::page schema:page)
|
(sm/register! ::page schema:page)
|
||||||
(sm/register! ::guide schema:guide)
|
(sm/register! ::guide schema:guide)
|
||||||
(sm/register! ::flow schema:flow)
|
(sm/register! ::flow schema:flow)
|
||||||
|
|
||||||
(def check-page-guide!
|
(def valid-guide?
|
||||||
(sm/check-fn ::guide))
|
(sm/lazy-validator schema:guide))
|
||||||
|
|
||||||
|
;; FIXME: convert to validator
|
||||||
(def check-page!
|
(def check-page!
|
||||||
(sm/check-fn ::page))
|
(sm/check-fn schema:page))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INIT & HELPERS
|
;; INIT & HELPERS
|
||||||
@@ -80,25 +99,6 @@
|
|||||||
(assoc :id (or id (uuid/next)))
|
(assoc :id (or id (uuid/next)))
|
||||||
(assoc :name (or name "Page 1"))))
|
(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
|
(defn get-frame-flow
|
||||||
[flows frame-id]
|
[flows frame-id]
|
||||||
(d/seek #(= (:starting-frame %) frame-id) flows))
|
(d/seek #(= (:starting-frame %) frame-id) (vals flows)))
|
||||||
@@ -29,3 +29,25 @@
|
|||||||
schema:string]])
|
schema:string]])
|
||||||
|
|
||||||
(sm/register! ::plugin-data schema:plugin-data)
|
(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)
|
(sm/register! ::stroke schema:stroke)
|
||||||
|
|
||||||
|
(def check-stroke!
|
||||||
|
(sm/check-fn schema:stroke))
|
||||||
|
|
||||||
(def schema:shape-base-attrs
|
(def schema:shape-base-attrs
|
||||||
[:map {:title "ShapeMinimalRecord"}
|
[:map {:title "ShapeMinimalRecord"}
|
||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
@@ -221,8 +224,8 @@
|
|||||||
[:map {:title "ImageAttrs"}
|
[:map {:title "ImageAttrs"}
|
||||||
[:metadata
|
[:metadata
|
||||||
[:map
|
[:map
|
||||||
[:width {: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)} :int]
|
[:height {:gen/gen (sg/small-int :min 1)} ::sm/int]
|
||||||
[:mtype {:optional true
|
[:mtype {:optional true
|
||||||
:gen/gen (sg/elements ["image/jpeg"
|
:gen/gen (sg/elements ["image/jpeg"
|
||||||
"image/png"])}
|
"image/png"])}
|
||||||
@@ -352,7 +355,14 @@
|
|||||||
(sm/check-fn schema:shape-attrs))
|
(sm/check-fn schema:shape-attrs))
|
||||||
|
|
||||||
(def check-shape!
|
(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?
|
(defn has-images?
|
||||||
[{:keys [fills strokes]}]
|
[{:keys [fills strokes]}]
|
||||||
|
|||||||
@@ -1622,13 +1622,17 @@
|
|||||||
(defn remap-grid-cells
|
(defn remap-grid-cells
|
||||||
"Remaps the shapes ids inside the cells"
|
"Remaps the shapes ids inside the cells"
|
||||||
[shape ids-map]
|
[shape ids-map]
|
||||||
(let [do-remap-cells
|
(let [remap-shape
|
||||||
|
(fn [id]
|
||||||
|
(get ids-map id id))
|
||||||
|
|
||||||
|
remap-cell
|
||||||
(fn [cell]
|
(fn [cell]
|
||||||
(-> cell
|
(-> cell
|
||||||
(update :shapes #(into [] (keep ids-map) %))))
|
(update :shapes #(into [] (keep remap-shape) %))))
|
||||||
shape
|
shape
|
||||||
(-> shape
|
(-> shape
|
||||||
(update :layout-grid-cells update-vals do-remap-cells))]
|
(update :layout-grid-cells update-vals remap-cell))]
|
||||||
shape))
|
shape))
|
||||||
|
|
||||||
(defn merge-cells
|
(defn merge-cells
|
||||||
|
|||||||
@@ -27,3 +27,6 @@
|
|||||||
[:color ::ctc/color]])
|
[:color ::ctc/color]])
|
||||||
|
|
||||||
(sm/register! ::shadow schema:shadow)
|
(sm/register! ::shadow schema:shadow)
|
||||||
|
|
||||||
|
(def check-shadow!
|
||||||
|
(sm/check-fn schema:shadow))
|
||||||
@@ -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 (= (:fill-opacity fill') 1))
|
||||||
(t/is (= (:touched copy2-root') nil))
|
(t/is (= (:touched copy2-root') nil))
|
||||||
(t/is (= (:touched copy2-child') #{:fill-group}))))
|
(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])))
|
|
||||||
|
|
||||||
;; ))
|
|
||||||
|
|
||||||
;; ))
|
|
||||||
+26
-25
@@ -4,12 +4,13 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns common-tests.types.decoder-test
|
(ns common-tests.types.shape-decode-encode-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.json :as json]
|
[app.common.json :as json]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.generators :as sg]
|
[app.common.schema.generators :as sg]
|
||||||
|
[app.common.schema.test :as smt]
|
||||||
[app.common.types.color :refer [schema:color schema:gradient]]
|
[app.common.types.color :refer [schema:color schema:gradient]]
|
||||||
[app.common.types.plugins :refer [schema:plugin-data]]
|
[app.common.types.plugins :refer [schema:plugin-data]]
|
||||||
[app.common.types.shape :as tsh]
|
[app.common.types.shape :as tsh]
|
||||||
@@ -49,102 +50,102 @@
|
|||||||
(t/deftest gradient-json-roundtrip
|
(t/deftest gradient-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:gradient (sm/json-transformer))
|
(let [encode (sm/encoder schema:gradient (sm/json-transformer))
|
||||||
decode (sm/decoder schema:gradient (sm/json-transformer))]
|
decode (sm/decoder schema:gradient (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [gradient (sg/generator schema:gradient)]
|
(smt/for [gradient (sg/generator schema:gradient)]
|
||||||
(let [gradient-1 (encode gradient)
|
(let [gradient-1 (encode gradient)
|
||||||
gradient-2 (json-roundtrip gradient-1)
|
gradient-2 (json-roundtrip gradient-1)
|
||||||
gradient-3 (decode gradient-2)]
|
gradient-3 (decode gradient-2)]
|
||||||
;; (app.common.pprint/pprint gradient)
|
;; (app.common.pprint/pprint gradient)
|
||||||
;; (app.common.pprint/pprint gradient-3)
|
;; (app.common.pprint/pprint gradient-3)
|
||||||
(t/is (= gradient gradient-3))))
|
(= gradient gradient-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest color-json-roundtrip
|
(t/deftest color-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:color (sm/json-transformer))
|
(let [encode (sm/encoder schema:color (sm/json-transformer))
|
||||||
decode (sm/decoder schema:color (sm/json-transformer))]
|
decode (sm/decoder schema:color (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [color (sg/generator schema:color)]
|
(smt/for [color (sg/generator schema:color)]
|
||||||
(let [color-1 (encode color)
|
(let [color-1 (encode color)
|
||||||
color-2 (json-roundtrip color-1)
|
color-2 (json-roundtrip color-1)
|
||||||
color-3 (decode color-2)]
|
color-3 (decode color-2)]
|
||||||
;; (app.common.pprint/pprint color)
|
;; (app.common.pprint/pprint color)
|
||||||
;; (app.common.pprint/pprint color-3)
|
;; (app.common.pprint/pprint color-3)
|
||||||
(t/is (= color color-3))))
|
(= color color-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest shape-shadow-json-roundtrip
|
(t/deftest shape-shadow-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:shadow (sm/json-transformer))
|
(let [encode (sm/encoder schema:shadow (sm/json-transformer))
|
||||||
decode (sm/decoder schema:shadow (sm/json-transformer))]
|
decode (sm/decoder schema:shadow (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [shadow (sg/generator schema:shadow)]
|
(smt/for [shadow (sg/generator schema:shadow)]
|
||||||
(let [shadow-1 (encode shadow)
|
(let [shadow-1 (encode shadow)
|
||||||
shadow-2 (json-roundtrip shadow-1)
|
shadow-2 (json-roundtrip shadow-1)
|
||||||
shadow-3 (decode shadow-2)]
|
shadow-3 (decode shadow-2)]
|
||||||
;; (app.common.pprint/pprint shadow)
|
;; (app.common.pprint/pprint shadow)
|
||||||
;; (app.common.pprint/pprint shadow-3)
|
;; (app.common.pprint/pprint shadow-3)
|
||||||
(t/is (= shadow shadow-3))))
|
(= shadow shadow-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest shape-animation-json-roundtrip
|
(t/deftest shape-animation-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:animation (sm/json-transformer))
|
(let [encode (sm/encoder schema:animation (sm/json-transformer))
|
||||||
decode (sm/decoder schema:animation (sm/json-transformer))]
|
decode (sm/decoder schema:animation (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [animation (sg/generator schema:animation)]
|
(smt/for [animation (sg/generator schema:animation)]
|
||||||
(let [animation-1 (encode animation)
|
(let [animation-1 (encode animation)
|
||||||
animation-2 (json-roundtrip animation-1)
|
animation-2 (json-roundtrip animation-1)
|
||||||
animation-3 (decode animation-2)]
|
animation-3 (decode animation-2)]
|
||||||
;; (app.common.pprint/pprint animation)
|
;; (app.common.pprint/pprint animation)
|
||||||
;; (app.common.pprint/pprint animation-3)
|
;; (app.common.pprint/pprint animation-3)
|
||||||
(t/is (= animation animation-3))))
|
(= animation animation-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest shape-interaction-json-roundtrip
|
(t/deftest shape-interaction-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:interaction (sm/json-transformer))
|
(let [encode (sm/encoder schema:interaction (sm/json-transformer))
|
||||||
decode (sm/decoder schema:interaction (sm/json-transformer))]
|
decode (sm/decoder schema:interaction (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [interaction (sg/generator schema:interaction)]
|
(smt/for [interaction (sg/generator schema:interaction)]
|
||||||
(let [interaction-1 (encode interaction)
|
(let [interaction-1 (encode interaction)
|
||||||
interaction-2 (json-roundtrip interaction-1)
|
interaction-2 (json-roundtrip interaction-1)
|
||||||
interaction-3 (decode interaction-2)]
|
interaction-3 (decode interaction-2)]
|
||||||
;; (app.common.pprint/pprint interaction)
|
;; (app.common.pprint/pprint interaction)
|
||||||
;; (app.common.pprint/pprint interaction-3)
|
;; (app.common.pprint/pprint interaction-3)
|
||||||
(t/is (= interaction interaction-3))))
|
(= interaction interaction-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest shape-path-content-json-roundtrip
|
(t/deftest shape-path-content-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:path-content (sm/json-transformer))
|
(let [encode (sm/encoder schema:path-content (sm/json-transformer))
|
||||||
decode (sm/decoder schema:path-content (sm/json-transformer))]
|
decode (sm/decoder schema:path-content (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [path-content (sg/generator schema:path-content)]
|
(smt/for [path-content (sg/generator schema:path-content)]
|
||||||
(let [path-content-1 (encode path-content)
|
(let [path-content-1 (encode path-content)
|
||||||
path-content-2 (json-roundtrip path-content-1)
|
path-content-2 (json-roundtrip path-content-1)
|
||||||
path-content-3 (decode path-content-2)]
|
path-content-3 (decode path-content-2)]
|
||||||
;; (app.common.pprint/pprint path-content)
|
;; (app.common.pprint/pprint path-content)
|
||||||
;; (app.common.pprint/pprint path-content-3)
|
;; (app.common.pprint/pprint path-content-3)
|
||||||
(t/is (= path-content path-content-3))))
|
(= path-content path-content-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest plugin-data-json-roundtrip
|
(t/deftest plugin-data-json-roundtrip
|
||||||
(let [encode (sm/encoder schema:plugin-data (sm/json-transformer))
|
(let [encode (sm/encoder schema:plugin-data (sm/json-transformer))
|
||||||
decode (sm/decoder schema:plugin-data (sm/json-transformer))]
|
decode (sm/decoder schema:plugin-data (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [data (sg/generator schema:plugin-data)]
|
(smt/for [data (sg/generator schema:plugin-data)]
|
||||||
(let [data-1 (encode data)
|
(let [data-1 (encode data)
|
||||||
data-2 (json-roundtrip data-1)
|
data-2 (json-roundtrip data-1)
|
||||||
data-3 (decode data-2)]
|
data-3 (decode data-2)]
|
||||||
(t/is (= data data-3))))
|
(= data data-3)))
|
||||||
{:num 500})))
|
{:num 500})))
|
||||||
|
|
||||||
(t/deftest shape-json-roundtrip
|
(t/deftest shape-json-roundtrip
|
||||||
(let [encode (sm/encoder ::tsh/shape (sm/json-transformer))
|
(let [encode (sm/encoder ::tsh/shape (sm/json-transformer))
|
||||||
decode (sm/decoder ::tsh/shape (sm/json-transformer))]
|
decode (sm/decoder ::tsh/shape (sm/json-transformer))]
|
||||||
(sg/check!
|
(smt/check!
|
||||||
(sg/for [shape (sg/generator ::tsh/shape)]
|
(smt/for [shape (sg/generator ::tsh/shape)]
|
||||||
(let [shape-1 (encode shape)
|
(let [shape-1 (encode shape)
|
||||||
shape-2 (json-roundtrip shape-1)
|
shape-2 (json-roundtrip shape-1)
|
||||||
shape-3 (decode shape-2)]
|
shape-3 (decode shape-2)]
|
||||||
;; (app.common.pprint/pprint shape)
|
;; (app.common.pprint/pprint shape)
|
||||||
;; (app.common.pprint/pprint shape-3)
|
;; (app.common.pprint/pprint shape-3)
|
||||||
(t/is (= shape shape-3))))
|
(= shape shape-3)))
|
||||||
{:num 1000})))
|
{: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 \
|
CLJKONDO_VERSION=2024.03.13 \
|
||||||
BABASHKA_VERSION=1.3.189 \
|
BABASHKA_VERSION=1.3.189 \
|
||||||
CLJFMT_VERSION=0.12.0 \
|
CLJFMT_VERSION=0.12.0 \
|
||||||
|
RUSTUP_VERSION=1.27.1 \
|
||||||
|
RUST_VERSION=1.81.0 \
|
||||||
LANG=en_US.UTF-8 \
|
LANG=en_US.UTF-8 \
|
||||||
LC_ALL=en_US.UTF-8
|
LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
@@ -242,6 +244,27 @@ RUN set -ex; \
|
|||||||
mv /tmp/mc /usr/local/bin/; \
|
mv /tmp/mc /usr/local/bin/; \
|
||||||
chmod +x /usr/local/bin/mc;
|
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
|
WORKDIR /home
|
||||||
|
|
||||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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 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 l='ls --color -GFlh'
|
||||||
alias rm='rm -r'
|
alias rm='rm -r'
|
||||||
@@ -9,6 +9,9 @@ alias ls='ls --color -F'
|
|||||||
alias lsd='ls -d *(/)'
|
alias lsd='ls -d *(/)'
|
||||||
alias lsf='ls -h *(.)'
|
alias lsf='ls -h *(.)'
|
||||||
|
|
||||||
|
# init Cargo / Rust env
|
||||||
|
. "/usr/local/cargo/env"
|
||||||
|
|
||||||
# include .bashrc if it exists
|
# include .bashrc if it exists
|
||||||
if [ -f "$HOME/.bashrc.local" ]; then
|
if [ -f "$HOME/.bashrc.local" ]; then
|
||||||
. "$HOME/.bashrc.local"
|
. "$HOME/.bashrc.local"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
README.md
|
||||||
+118
@@ -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
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"editor.rulers": [
|
||||||
|
80
|
||||||
|
]
|
||||||
|
}
|
||||||
Loaded 100 of 981 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user