mirror of
https://github.com/penpot/penpot.git
synced 2026-01-04 12:28:52 -05:00
Compare commits
13 Commits
1.17.1
...
niwinz-adm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
201e6b06b2 | ||
|
|
144336598d | ||
|
|
17b24471c2 | ||
|
|
78642cdceb | ||
|
|
0c3041e284 | ||
|
|
e4066e7f44 | ||
|
|
6cd6946c46 | ||
|
|
281ab696e6 | ||
|
|
d17fffd160 | ||
|
|
b89ad902c9 | ||
|
|
1564f589b9 | ||
|
|
93956ae384 | ||
|
|
3496463a79 |
71
CHANGES.md
71
CHANGES.md
@@ -1,19 +1,8 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.17.1
|
||||
## :rocket: Next (1.17)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
|
||||
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
|
||||
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
|
||||
- Fix button spacing on delete acount modal [Taiga #4762](https://tree.taiga.io/project/penpot/issue/4762)
|
||||
- Fix invitations input on team management and onboarding modal [Taiga #4760](https://tree.taiga.io/project/penpot/issue/4760)
|
||||
- Fix weird numeration creating new elements in dashboard [Taiga #4755](https://tree.taiga.io/project/penpot/issue/4755)
|
||||
- Fix can move shape with lens zoom active [Taiga #4787](https://tree.taiga.io/project/penpot/issue/4787)
|
||||
- Fix social links broken [Taiga #4759](https://tree.taiga.io/project/penpot/issue/4759)
|
||||
- Fix tooltips on left toolbar [Taiga #4793](https://tree.taiga.io/project/penpot/issue/4793)
|
||||
|
||||
## 1.17.0
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
@@ -21,18 +10,6 @@
|
||||
- Better overlays interactions on boards inside boards [Taiga #4386](https://tree.taiga.io/project/penpot/us/4386)
|
||||
- Show board miniature in manual overlay setting [Taiga #4475](https://tree.taiga.io/project/penpot/issue/4475)
|
||||
- Handoff visual improvements [Taiga #3124](https://tree.taiga.io/project/penpot/us/3124)
|
||||
- Dynamic alignment only in sight [Github 1971](https://github.com/penpot/penpot/issues/1971)
|
||||
- Add some accessibility to shortcut panel [Taiga #4713](https://tree.taiga.io/project/penpot/issue/4713)
|
||||
- Add shortcuts for text editing [Taiga #2052](https://tree.taiga.io/project/penpot/us/2052)
|
||||
- Second level boards treated as groups in terms of selection [Taiga #4269](https://tree.taiga.io/project/penpot/us/4269)
|
||||
- Performance improvements both for backend and frontend
|
||||
- Accessibility improvements for login area [Taiga #4353](https://tree.taiga.io/project/penpot/us/4353)
|
||||
- Outbound webhooks [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
|
||||
- Add copy invitation link to the invitation options [Taiga #4213](https://tree.taiga.io/project/penpot/us/4213)
|
||||
- Dynamic alignment only in sight [Taiga #3537](https://tree.taiga.io/project/penpot/us/3537)
|
||||
- Improve naming of layers [Taiga #4036](https://tree.taiga.io/project/penpot/us/4036)
|
||||
- Add zoom lense [Taiga #4691](https://tree.taiga.io/project/penpot/us/4691)
|
||||
- Detect potential problems with custom font vertical metrics [Taiga #4697](https://tree.taiga.io/project/penpot/us/4697)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -45,51 +22,12 @@
|
||||
- Fix adding an extra page on import [Taiga #4543](https://tree.taiga.io/project/penpot/task/4543)
|
||||
- Fix unable to select text at assets inputs in firefox [Taiga #4572](https://tree.taiga.io/project/penpot/issue/4572)
|
||||
- Fix component sync when converting to path [Taiga #3642](https://tree.taiga.io/project/penpot/issue/3642)
|
||||
- Fix style for team invite in deutsch [Taiga #4614](https://tree.taiga.io/project/penpot/issue/4614)
|
||||
- Fix problem with text edition in Safari [Taiga #4046](https://tree.taiga.io/project/penpot/issue/4046)
|
||||
- Fix show outline with rounded corners on rects [Taiga #4053](https://tree.taiga.io/project/penpot/issue/4053)
|
||||
- Fix wrong interaction between comments and panning modes [Taiga #4297](https://tree.taiga.io/project/penpot/issue/4297)
|
||||
- Fix bad element positioning on interaction with fixed scroll [Github #2660](https://github.com/penpot/penpot/issues/2660)
|
||||
- Fix display type of component library not persistent [Taiga #4512](https://tree.taiga.io/project/penpot/issue/4512)
|
||||
- Fix problem when moving texts with keyboard [#2690](https://github.com/penpot/penpot/issues/2690)
|
||||
- Fix problem when drawing boxes won't detect mouse-up [Taiga #4618](https://tree.taiga.io/project/penpot/issue/4618)
|
||||
- Fix missing loading icon on shared libraries [Taiga #4148](https://tree.taiga.io/project/penpot/issue/4148)
|
||||
- Fix selection stroke missing in properties of multiple texts [Taiga #4048](https://tree.taiga.io/project/penpot/issue/4048)
|
||||
- Fix missing create component menu for frames [Github #2670](https://github.com/penpot/penpot/issues/2670)
|
||||
- Fix "currentColor" is not converted when importing SVG [Github 2276](https://github.com/penpot/penpot/issues/2276)
|
||||
- Fix incorrect color in properties of multiple bool shapes [Taiga #4355](https://tree.taiga.io/project/penpot/issue/4355)
|
||||
- Fix pressing the enter key gives you an internal error [Github 2675](https://github.com/penpot/penpot/issues/2675) [Github 2577](https://github.com/penpot/penpot/issues/2577)
|
||||
- Fix confirm group name with enter doesn't work in assets modal [Taiga #4506](https://tree.taiga.io/project/penpot/issue/4506)
|
||||
- Fix group/ungroup shapes inside a component [Taiga #4052](https://tree.taiga.io/project/penpot/issue/4052)
|
||||
- Fix wrong update of text in components [Taiga #4646](https://tree.taiga.io/project/penpot/issue/4646)
|
||||
- Fix problem with SVG imports with style [#2605](https://github.com/penpot/penpot/issues/2605)
|
||||
- Fix ghost shapes after sync groups in components [Taiga #4649](https://tree.taiga.io/project/penpot/issue/4649)
|
||||
- Fix layer orders messed up on move, group, reparent and undo [Github #2672](https://github.com/penpot/penpot/issues/2672)
|
||||
- Fix max height in library dialog [Github #2335](https://github.com/penpot/penpot/issues/2335)
|
||||
- Fix undo ungroup (shift+g) scrambles positions [Taiga #4674](https://tree.taiga.io/project/penpot/issue/4674)
|
||||
- Fix justified text is stretched [Github #2539](https://github.com/penpot/penpot/issues/2539)
|
||||
- Fix mousewheel on viewer inspector [Taiga #4221](https://tree.taiga.io/project/penpot/issue/4221)
|
||||
- Fix path edition activated on boards [Taiga #4105](https://tree.taiga.io/project/penpot/issue/4105)
|
||||
- Fix hidden layers inside groups become visible after the group visibility is changed[Taiga #4710](https://tree.taiga.io/project/penpot/issue/4710)
|
||||
- Fix format of HSLA color on viewer [Taiga #4393](https://tree.taiga.io/project/penpot/issue/4393)
|
||||
- Fix some typos [Taiga #4724](https://tree.taiga.io/project/penpot/issue/4724)
|
||||
- Fix ctrl+c for inspect code [Taiga #4739](https://tree.taiga.io/project/penpot/issue/4739)
|
||||
- Fix text in custom font is not at the expected position at export [Taiga #4394](https://tree.taiga.io/project/penpot/issue/4394)
|
||||
- Fix unneeded popup when updating local components [Taiga #4430](https://tree.taiga.io/project/penpot/issue/4430)
|
||||
- Fix multiuser - "Shadow" element is not updating immediately [Taiga #4709](https://tree.taiga.io/project/penpot/issue/4709)
|
||||
- Fix paths not flagged as modified when resized [Taiga #4742](https://tree.taiga.io/project/penpot/issue/4742)
|
||||
- Fix resend invitation doesn't reset the expiration date [Taiga #4741](https://tree.taiga.io/project/penpot/issue/4741)
|
||||
- Fix incorrect state after undo page creation [Taiga #4690](https://tree.taiga.io/project/penpot/issue/4690)
|
||||
- Fix copy paste texts with typography assets linked [Taiga #4750](https://tree.taiga.io/project/penpot/issue/4750)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @iprithvitharun: let's make UX Writing contributions in Open Source a trend!
|
||||
|
||||
## 1.16.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix strage cursor behaviour after clicking viewport with text pool [Github #2447](https://github.com/penpot/penpot/issues/2447)
|
||||
|
||||
## 1.16.1-beta
|
||||
@@ -159,6 +97,7 @@
|
||||
- Fix grid not syncing immediately in multiuser [Taiga #4339](https://tree.taiga.io/project/penpot/issue/4339)
|
||||
- Fix custom font upload fails silently for unsupported formats [Taiga #4279](https://tree.taiga.io/project/penpot/issue/4280)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @andrewzhurov for many code contributions on this release.
|
||||
|
||||
@@ -50,7 +50,7 @@ Being web based, Penpot is not dependent on operating systems or local installat
|
||||
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/open-source.png" alt="Open Source">
|
||||
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
|
||||
</p>
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ You will find the following categories:
|
||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/cross-teams.webp" alt="Community">
|
||||
<img src="https://penpot.app/images/cross-teams.webp" alt="Community">
|
||||
</p>
|
||||
|
||||
## Contributing ##
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.11.1"}
|
||||
org.clojure/core.async {:mvn/version "1.6.673"}
|
||||
org.clojure/core.async {:mvn/version "1.5.648"}
|
||||
|
||||
;; Logging
|
||||
org.zeromq/jeromq {:mvn/version "0.5.3"}
|
||||
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-4"}
|
||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.2.1.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v9.12"
|
||||
:git/sha "51646d8"
|
||||
{:git/tag "v9.11"
|
||||
:git/sha "6f9197a"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.834"}
|
||||
metosin/reitit-core {:mvn/version "0.5.18"}
|
||||
org.postgresql/postgresql {:mvn/version "42.5.1"}
|
||||
org.postgresql/postgresql {:mvn/version "42.5.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
@@ -37,9 +37,9 @@
|
||||
buddy/buddy-hashers {:mvn/version "1.8.158"}
|
||||
buddy/buddy-sign {:mvn/version "3.4.333"}
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.1"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.15.3"}
|
||||
org.jsoup/jsoup {:mvn/version "1.15.1"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -51,12 +51,11 @@
|
||||
integrant/integrant {:mvn/version "0.8.0"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.4"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.3"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.19.8"}
|
||||
}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.278"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
@@ -70,10 +69,8 @@
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
|
||||
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
|
||||
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
|
||||
</mj-social>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 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;"
|
||||
>
|
||||
@@ -211,9 +211,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -225,7 +225,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -239,9 +239,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -257,9 +257,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -271,7 +271,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -285,9 +285,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -301,7 +301,7 @@
|
||||
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;">
|
||||
@@ -321,7 +321,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -341,7 +341,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -361,7 +361,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -370,7 +370,7 @@
|
||||
<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">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -381,7 +381,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -390,7 +390,7 @@
|
||||
<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">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -401,7 +401,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -411,9 +411,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -425,7 +425,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -439,9 +439,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -457,9 +457,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 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;"
|
||||
>
|
||||
@@ -201,9 +201,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -215,7 +215,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -229,9 +229,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -247,9 +247,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -261,7 +261,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -275,9 +275,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
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;">
|
||||
@@ -311,7 +311,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -331,7 +331,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -351,7 +351,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -360,7 +360,7 @@
|
||||
<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">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -371,7 +371,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -380,7 +380,7 @@
|
||||
<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">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -391,7 +391,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -401,9 +401,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -415,7 +415,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -429,9 +429,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -447,9 +447,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 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;"
|
||||
>
|
||||
@@ -206,9 +206,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -220,7 +220,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -234,9 +234,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -252,9 +252,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -266,7 +266,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -280,9 +280,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -296,7 +296,7 @@
|
||||
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;">
|
||||
@@ -316,7 +316,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -336,7 +336,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -356,7 +356,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -365,7 +365,7 @@
|
||||
<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">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -376,7 +376,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -385,7 +385,7 @@
|
||||
<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">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -396,7 +396,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -406,9 +406,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -420,7 +420,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -434,9 +434,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -452,9 +452,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -129,9 +129,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -143,7 +143,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -157,9 +157,9 @@
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 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;"
|
||||
>
|
||||
@@ -201,9 +201,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -215,7 +215,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -229,9 +229,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -247,9 +247,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -261,7 +261,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -275,9 +275,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -291,7 +291,7 @@
|
||||
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;">
|
||||
@@ -311,7 +311,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -331,7 +331,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -351,7 +351,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -360,7 +360,7 @@
|
||||
<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">
|
||||
<a href="https://instagram.com/penpotapp/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -371,7 +371,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
@@ -380,7 +380,7 @@
|
||||
<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">
|
||||
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
@@ -391,7 +391,7 @@
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -401,9 +401,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
@@ -415,7 +415,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
@@ -429,9 +429,9 @@
|
||||
<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;"
|
||||
>
|
||||
@@ -447,9 +447,9 @@
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
|
||||
@@ -13,9 +13,9 @@ cp ../CHANGES.md target/classes/changelog.md;
|
||||
clojure -T:build jar;
|
||||
mv target/penpot.jar target/dist/penpot.jar
|
||||
cp scripts/run.template.sh target/dist/run.sh;
|
||||
cp scripts/manage.py target/dist/manage.py
|
||||
cp scripts/manage.template.sh target/dist/manage.sh;
|
||||
chmod +x target/dist/run.sh;
|
||||
chmod +x target/dist/manage.py
|
||||
chmod +x target/dist/manage.sh;
|
||||
|
||||
# Prefetch
|
||||
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from getpass import getpass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
PREPL_URI = "tcp://localhost:6063"
|
||||
|
||||
def get_prepl_conninfo():
|
||||
uri_data = urlparse(PREPL_URI)
|
||||
if uri_data.scheme != "tcp":
|
||||
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
|
||||
if not isinstance(uri_data.netloc, str):
|
||||
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
|
||||
|
||||
host, port = uri_data.netloc.split(":", 2)
|
||||
|
||||
if port is None:
|
||||
port = 6063
|
||||
|
||||
if isinstance(port, str):
|
||||
port = int(port)
|
||||
|
||||
return host, port
|
||||
|
||||
def send_eval(expr):
|
||||
host, port = get_prepl_conninfo()
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
s.send(expr.encode("utf-8"))
|
||||
s.send(b":repl/quit\n\n")
|
||||
|
||||
with s.makefile() as f:
|
||||
result = json.load(f)
|
||||
tag = result.get("tag", None)
|
||||
if tag != "ret":
|
||||
raise RuntimeError("unexpected response from PREPL")
|
||||
return result.get("val", None), result.get("exception", None)
|
||||
|
||||
def encode(val):
|
||||
return json.dumps(json.dumps(val))
|
||||
|
||||
def print_error(res):
|
||||
for error in res["via"]:
|
||||
print("ERR:", error["message"])
|
||||
break
|
||||
|
||||
def run_cmd(params):
|
||||
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
|
||||
res, failed = send_eval(expr)
|
||||
if failed:
|
||||
print_error(res)
|
||||
sys.exit(-1)
|
||||
|
||||
return res
|
||||
|
||||
def create_profile(fullname, email, password):
|
||||
params = {
|
||||
"cmd": "create-profile",
|
||||
"params": {
|
||||
"fullname": fullname,
|
||||
"email": email,
|
||||
"password": password
|
||||
}
|
||||
}
|
||||
|
||||
res = run_cmd(params)
|
||||
print(f"Created: {res['email']} / {res['id']}")
|
||||
|
||||
def update_profile(email, fullname, password, is_active):
|
||||
params = {
|
||||
"cmd": "update-profile",
|
||||
"params": {
|
||||
"email": email,
|
||||
"fullname": fullname,
|
||||
"password": password,
|
||||
"is_active": is_active
|
||||
}
|
||||
}
|
||||
|
||||
res = run_cmd(params)
|
||||
if res is True:
|
||||
print(f"Updated")
|
||||
else:
|
||||
print(f"No profile found with email {email}")
|
||||
|
||||
def derive_password(password):
|
||||
params = {
|
||||
"cmd": "derive-password",
|
||||
"params": {
|
||||
"password": password,
|
||||
}
|
||||
}
|
||||
|
||||
res = run_cmd(params)
|
||||
print(f"Derived password: \"{res}\"")
|
||||
|
||||
available_commands = [
|
||||
"create-profile",
|
||||
"update-profile",
|
||||
"derive-password"
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Penpot Command Line Interface (CLI)"
|
||||
)
|
||||
)
|
||||
|
||||
parser.add_argument("-V", "--version", action="version", version="Penpot CLI %%develop%%")
|
||||
parser.add_argument("action", action="store", choices=available_commands)
|
||||
parser.add_argument("-n", "--fullname", help="Fullname", action="store")
|
||||
parser.add_argument("-e", "--email", help="Email", action="store")
|
||||
parser.add_argument("-p", "--password", help="Password", action="store")
|
||||
parser.add_argument("-c", "--connect", help="Connect to PREPL", action="store", default="tcp://localhost:6063")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
PREPL_URI = args.connect
|
||||
|
||||
if args.action == "create-profile":
|
||||
email = args.email
|
||||
password = args.password
|
||||
fullname = args.fullname
|
||||
|
||||
if email is None:
|
||||
email = input("Email: ")
|
||||
|
||||
if fullname is None:
|
||||
fullname = input("Fullname: ")
|
||||
|
||||
if password is None:
|
||||
password = getpass("Password: ")
|
||||
|
||||
create_profile(fullname, email, password)
|
||||
|
||||
elif args.action == "update-profile":
|
||||
email = args.email
|
||||
password = args.password
|
||||
|
||||
if email is None:
|
||||
email = input("Email: ")
|
||||
|
||||
if password is None:
|
||||
password = getpass("Password: ")
|
||||
|
||||
update_profile(email, None, password, None)
|
||||
|
||||
elif args.action == "derive-password":
|
||||
password = args.password
|
||||
|
||||
if password is None:
|
||||
password = getpass("Password: ")
|
||||
|
||||
derive_password(password)
|
||||
19
backend/scripts/manage.template.sh
Normal file
19
backend/scripts/manage.template.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
JAVA_CMD=$(type -p java)
|
||||
|
||||
set -e
|
||||
if [[ ! -n "$JAVA_CMD" ]]; then
|
||||
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
JAVA_CMD="$JAVA_HOME/bin/java"
|
||||
else
|
||||
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m app.cli.manage "$@"
|
||||
@@ -2,21 +2,7 @@
|
||||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-backend-asserts \
|
||||
enable-audit-log \
|
||||
enable-transit-readable-response \
|
||||
enable-demo-users \
|
||||
disable-secure-session-cookies \
|
||||
enable-smtp \
|
||||
enable-prepl-server \
|
||||
enable-urepl-server \
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-webhooks \
|
||||
enable-access-tokens";
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
|
||||
|
||||
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
|
||||
# export PENPOT_DATABASE_USERNAME="penpot"
|
||||
|
||||
@@ -41,18 +41,15 @@
|
||||
(reduce-kv clojure.string/replace s replacements))
|
||||
|
||||
(defn- search-user
|
||||
[{:keys [::conn base-dn] :as cfg} email]
|
||||
(let [query (replace-several (:query cfg) ":username" email)
|
||||
attrs [(:attrs-username cfg)
|
||||
(:attrs-email cfg)
|
||||
(:attrs-fullname cfg)]
|
||||
[{:keys [conn attrs base-dn] :as cfg} email]
|
||||
(let [query (replace-several (:query cfg) ":username" email)
|
||||
params {:filter query
|
||||
:sizelimit 1
|
||||
:attributes attrs}]
|
||||
(first (ldap/search conn base-dn params))))
|
||||
|
||||
(defn- retrieve-user
|
||||
[{:keys [::conn] :as cfg} {:keys [email password]}]
|
||||
[{:keys [conn] :as cfg} {:keys [email password]}]
|
||||
(when-let [{:keys [dn] :as user} (search-user cfg email)]
|
||||
(when (ldap/bind? conn dn password)
|
||||
{:fullname (get user (-> cfg :attrs-fullname keyword))
|
||||
@@ -69,7 +66,7 @@
|
||||
(defn authenticate
|
||||
[cfg params]
|
||||
(with-open [conn (connect cfg)]
|
||||
(when-let [user (-> (assoc cfg ::conn conn)
|
||||
(when-let [user (-> (assoc cfg :conn conn)
|
||||
(retrieve-user params))]
|
||||
(when-not (s/valid? ::info-data user)
|
||||
(let [explain (s/explain-str ::info-data user)]
|
||||
@@ -103,6 +100,17 @@
|
||||
:host (:host cfg) :port (:port cfg) :cause cause)
|
||||
nil))))
|
||||
|
||||
(defn- prepare-attributes
|
||||
[cfg]
|
||||
(assoc cfg :attrs [(:attrs-username cfg)
|
||||
(:attrs-email cfg)
|
||||
(:attrs-fullname cfg)]))
|
||||
|
||||
(defmethod ig/init-key ::provider
|
||||
[_ cfg]
|
||||
(when (:enabled? cfg)
|
||||
(some-> cfg try-connectivity prepare-attributes)))
|
||||
|
||||
(s/def ::enabled? ::us/boolean)
|
||||
(s/def ::host ::cf/ldap-host)
|
||||
(s/def ::port ::cf/ldap-port)
|
||||
@@ -116,7 +124,8 @@
|
||||
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
||||
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
||||
|
||||
(s/def ::provider-params
|
||||
(defmethod ig/pre-init-spec ::provider
|
||||
[_]
|
||||
(s/keys :opt-un [::host ::port
|
||||
::ssl ::tls
|
||||
::enabled?
|
||||
@@ -126,14 +135,3 @@
|
||||
::attrs-email
|
||||
::attrs-username
|
||||
::attrs-fullname]))
|
||||
(s/def ::provider
|
||||
(s/nilable ::provider-params))
|
||||
|
||||
(defmethod ig/pre-init-spec ::provider
|
||||
[_]
|
||||
(s/spec ::provider))
|
||||
|
||||
(defmethod ig/init-key ::provider
|
||||
[_ cfg]
|
||||
(when (:enabled? cfg)
|
||||
(try-connectivity cfg)))
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- discover-oidc-config
|
||||
[cfg {:keys [base-uri] :as opts}]
|
||||
[cfg {:keys [::base-uri] :as opts}]
|
||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
||||
response (ex/try! (http/req! cfg
|
||||
{:method :get :uri (str discovery-uri)}
|
||||
|
||||
@@ -61,9 +61,11 @@
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
:host "localhost"
|
||||
:tenant "default"
|
||||
:tenant "main"
|
||||
|
||||
:redis-uri "redis://redis/0"
|
||||
:srepl-host "127.0.0.1"
|
||||
:srepl-port 6062
|
||||
|
||||
:assets-storage-backend :assets-fs
|
||||
:storage-assets-fs-directory "assets"
|
||||
@@ -100,10 +102,13 @@
|
||||
(s/def ::audit-log-archive-uri ::us/string)
|
||||
(s/def ::audit-log-http-handler-concurrency ::us/integer)
|
||||
|
||||
(s/def ::admins ::us/set-of-valid-emails)
|
||||
(s/def ::admins ::us/set-of-strings)
|
||||
(s/def ::file-change-snapshot-every ::us/integer)
|
||||
(s/def ::file-change-snapshot-timeout ::dt/duration)
|
||||
|
||||
(s/def ::setup-admin-email ::us/email)
|
||||
(s/def ::setup-admin-password ::us/not-empty-string)
|
||||
|
||||
(s/def ::default-executor-parallelism ::us/integer)
|
||||
(s/def ::scheduled-executor-parallelism ::us/integer)
|
||||
|
||||
@@ -125,16 +130,6 @@
|
||||
(s/def ::database-min-pool-size ::us/integer)
|
||||
(s/def ::database-max-pool-size ::us/integer)
|
||||
|
||||
(s/def ::quotes-teams-per-profile ::us/integer)
|
||||
(s/def ::quotes-projects-per-team ::us/integer)
|
||||
(s/def ::quotes-invitations-per-team ::us/integer)
|
||||
(s/def ::quotes-profiles-per-team ::us/integer)
|
||||
(s/def ::quotes-files-per-project ::us/integer)
|
||||
(s/def ::quotes-files-per-team ::us/integer)
|
||||
(s/def ::quotes-font-variants-per-team ::us/integer)
|
||||
(s/def ::quotes-comment-threads-per-file ::us/integer)
|
||||
(s/def ::quotes-comments-per-file ::us/integer)
|
||||
|
||||
(s/def ::default-blob-version ::us/integer)
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::user-feedback-destination ::us/string)
|
||||
@@ -194,15 +189,18 @@
|
||||
(s/def ::smtp-ssl ::us/boolean)
|
||||
(s/def ::smtp-tls ::us/boolean)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
(s/def ::urepl-host ::us/string)
|
||||
(s/def ::urepl-port ::us/integer)
|
||||
(s/def ::prepl-host ::us/string)
|
||||
(s/def ::prepl-port ::us/integer)
|
||||
(s/def ::srepl-host ::us/string)
|
||||
(s/def ::srepl-port ::us/integer)
|
||||
(s/def ::assets-storage-backend ::us/keyword)
|
||||
(s/def ::fdata-storage-backend ::us/keyword)
|
||||
(s/def ::storage-assets-fs-directory ::us/string)
|
||||
(s/def ::storage-assets-s3-bucket ::us/string)
|
||||
(s/def ::storage-assets-s3-region ::us/keyword)
|
||||
(s/def ::storage-assets-s3-endpoint ::us/string)
|
||||
(s/def ::storage-fdata-s3-bucket ::us/string)
|
||||
(s/def ::storage-fdata-s3-region ::us/keyword)
|
||||
(s/def ::storage-fdata-s3-prefix ::us/string)
|
||||
(s/def ::storage-fdata-s3-endpoint ::us/string)
|
||||
(s/def ::telemetry-uri ::us/string)
|
||||
(s/def ::telemetry-with-taiga ::us/boolean)
|
||||
(s/def ::tenant ::us/string)
|
||||
@@ -279,17 +277,6 @@
|
||||
::profile-complaint-max-age
|
||||
::profile-complaint-threshold
|
||||
::public-uri
|
||||
|
||||
::quotes-teams-per-profile
|
||||
::quotes-projects-per-team
|
||||
::quotes-invitations-per-team
|
||||
::quotes-profiles-per-team
|
||||
::quotes-files-per-project
|
||||
::quotes-files-per-team
|
||||
::quotes-font-variants-per-team
|
||||
::quotes-comment-threads-per-file
|
||||
::quotes-comments-per-file
|
||||
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::rpc-rlimit-config
|
||||
@@ -308,16 +295,22 @@
|
||||
::smtp-tls
|
||||
::smtp-username
|
||||
|
||||
::urepl-host
|
||||
::urepl-port
|
||||
::prepl-host
|
||||
::prepl-port
|
||||
::srepl-host
|
||||
::srepl-port
|
||||
|
||||
::setup-admin-email
|
||||
::setup-admin-password
|
||||
|
||||
::assets-storage-backend
|
||||
::storage-assets-fs-directory
|
||||
::storage-assets-s3-bucket
|
||||
::storage-assets-s3-region
|
||||
::storage-assets-s3-endpoint
|
||||
::fdata-storage-backend
|
||||
::storage-fdata-s3-bucket
|
||||
::storage-fdata-s3-region
|
||||
::storage-fdata-s3-prefix
|
||||
::storage-fdata-s3-endpoint
|
||||
::telemetry-enabled
|
||||
::telemetry-uri
|
||||
::telemetry-referer
|
||||
|
||||
@@ -167,7 +167,6 @@
|
||||
(instance? javax.sql.DataSource v))
|
||||
|
||||
(s/def ::pool pool?)
|
||||
(s/def ::conn-or-pool some?)
|
||||
|
||||
(defn closed?
|
||||
[pool]
|
||||
|
||||
@@ -257,17 +257,15 @@
|
||||
"Schedule an already defined email to be sent using asynchronously
|
||||
using worker task."
|
||||
[{:keys [::conn ::factory] :as context}]
|
||||
(us/verify fn? factory)
|
||||
(us/verify some? conn)
|
||||
(let [email (if factory
|
||||
(factory context)
|
||||
(dissoc context ::conn))]
|
||||
(wrk/submit! (merge
|
||||
{::wrk/task :sendmail
|
||||
::wrk/delay 0
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn}
|
||||
email))))
|
||||
(let [email (factory context)]
|
||||
(wrk/submit! (assoc email
|
||||
::wrk/task :sendmail
|
||||
::wrk/delay 0
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SENDMAIL FN / TASK HANDLER
|
||||
|
||||
@@ -91,7 +91,9 @@
|
||||
(let [params (:path-params match)
|
||||
result (:result match)
|
||||
handler (or (:handler result) not-found-handler)
|
||||
request (assoc request :path-params params)]
|
||||
request (-> request
|
||||
(assoc :path-params params)
|
||||
(update :params merge params))]
|
||||
(handler request respond raise))
|
||||
(not-found-handler request respond raise)))
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Services related to the user activity (audit log)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
@@ -21,7 +20,6 @@
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.retry :as rtry]
|
||||
[app.util.time :as dt]
|
||||
@@ -173,20 +171,18 @@
|
||||
(::webhooks/event? event))
|
||||
(let [batch-key (::webhooks/batch-key event)
|
||||
batch-timeout (::webhooks/batch-timeout event)
|
||||
label (dm/str "rpc:" (:name params))
|
||||
label (cond
|
||||
(ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event)))
|
||||
(string? batch-key) (dm/str label ":" batch-key)
|
||||
:else label)
|
||||
dedupe? (boolean (and batch-key batch-timeout))]
|
||||
|
||||
label-suffix (when (ifn? batch-key)
|
||||
(str/ffmt ":%" (batch-key (:props params))))
|
||||
dedupe? (boolean
|
||||
(and batch-key batch-timeout))]
|
||||
(wrk/submit! ::wrk/conn pool
|
||||
::wrk/task :process-webhook-event
|
||||
::wrk/queue :webhooks
|
||||
::wrk/max-retries 0
|
||||
::wrk/delay (or batch-timeout 0)
|
||||
::wrk/dedupe dedupe?
|
||||
::wrk/label label
|
||||
::wrk/label
|
||||
(str/ffmt "rpc:%1%2" (:name params) label-suffix)
|
||||
|
||||
::webhooks/event
|
||||
(-> params
|
||||
@@ -247,7 +243,7 @@
|
||||
from audit_log
|
||||
where archived_at is null
|
||||
order by created_at asc
|
||||
limit 128
|
||||
limit 256
|
||||
for update skip locked;")
|
||||
|
||||
(defn archive-events
|
||||
@@ -323,7 +319,7 @@
|
||||
where archived_at is not null")
|
||||
|
||||
(defn- clean-archived
|
||||
[{:keys [::db/pool]}]
|
||||
[{:keys [pool]}]
|
||||
(let [result (db/exec-one! pool [sql:clean-archived])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :hint "delete archived audit log entries" :deleted result)
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.zmq :as lzmq]
|
||||
[app.util.async :as aa]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]))
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
@@ -27,7 +27,7 @@
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- persist-on-database!
|
||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as event}]
|
||||
[{:keys [pool] :as cfg} {:keys [id] :as event}]
|
||||
(when-not (db/read-only? pool)
|
||||
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
|
||||
|
||||
@@ -53,49 +53,41 @@
|
||||
(assoc :version (:full cf/version))
|
||||
(update :id #(or % (uuid/next)))))
|
||||
|
||||
(defn- handle-event
|
||||
[cfg event]
|
||||
(try
|
||||
(let [event (parse-event event)
|
||||
uri (cf/get :public-uri)]
|
||||
(defn handle-event
|
||||
[{:keys [executor] :as cfg} event]
|
||||
(aa/with-thread executor
|
||||
(try
|
||||
(let [event (parse-event event)
|
||||
uri (cf/get :public-uri)]
|
||||
|
||||
(l/debug :hint "registering error on database" :id (:id event)
|
||||
:uri (str uri "/dbg/error/" (:id event)))
|
||||
(l/debug :hint "registering error on database" :id (:id event)
|
||||
:uri (str uri "/dbg/error/" (:id event)))
|
||||
|
||||
(persist-on-database! cfg event))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||
(persist-on-database! cfg event))
|
||||
(catch Exception cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause)))))
|
||||
|
||||
(defn- error-event?
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
|
||||
|
||||
(defn error-event?
|
||||
[event]
|
||||
(= "error" (:logger/level event)))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req [::db/pool ::lzmq/receiver]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [::lzmq/receiver] :as cfg}]
|
||||
(px/thread
|
||||
{:name "penpot/database-reporter"}
|
||||
(l/info :hint "initializing database error persistence")
|
||||
|
||||
(let [input (a/chan (a/sliding-buffer 5)
|
||||
(filter error-event?))]
|
||||
(try
|
||||
(lzmq/sub! receiver input)
|
||||
(loop []
|
||||
(when-let [msg (a/<!! input)]
|
||||
(handle-event cfg msg))
|
||||
(recur))
|
||||
|
||||
(catch InterruptedException _
|
||||
(l/debug :hint "reporter interrupted"))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error" :cause cause))
|
||||
(finally
|
||||
(a/close! input)
|
||||
(l/info :hint "reporter terminated"))))))
|
||||
[_ {:keys [receiver] :as cfg}]
|
||||
(l/info :msg "initializing database error persistence")
|
||||
(let [output (a/chan (a/sliding-buffer 5) (filter error-event?))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! output)]
|
||||
(if (nil? msg)
|
||||
(l/info :msg "stopping error reporting loop")
|
||||
(do
|
||||
(a/<! (handle-event cfg msg))
|
||||
(recur)))))
|
||||
output))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ thread]
|
||||
(some-> thread px/interrupt!))
|
||||
[_ output]
|
||||
(a/close! output))
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
|
||||
(defn handle-event
|
||||
[cfg event]
|
||||
(when @enabled
|
||||
(try
|
||||
(let [event (ldb/parse-event event)]
|
||||
(send-mattermost-notification! cfg event))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unhandled error"
|
||||
:cause cause)))))
|
||||
(try
|
||||
(let [event (ldb/parse-event event)]
|
||||
(when @enabled
|
||||
(send-mattermost-notification! cfg event)))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unhandled error"
|
||||
:cause cause))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req [::http/client
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as uri]
|
||||
@@ -22,15 +21,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(defn key-fn
|
||||
[k & keys]
|
||||
(fn [params]
|
||||
(reduce #(dm/str %1 ":" (get params %2))
|
||||
(dm/str (get params k))
|
||||
keys)))
|
||||
|
||||
;; --- PROC
|
||||
|
||||
(defn- lookup-webhooks-by-team
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.auth.ldap :as-alias ldap]
|
||||
[app.auth.oidc :as-alias oidc]
|
||||
[app.auth.oidc.providers :as-alias oidc.providers]
|
||||
[app.common.logging :as l]
|
||||
@@ -21,7 +20,6 @@
|
||||
[app.metrics :as-alias mtx]
|
||||
[app.metrics.definition :as-alias mdef]
|
||||
[app.redis :as-alias rds]
|
||||
[app.srepl :as-alias srepl]
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
@@ -233,7 +231,7 @@
|
||||
:max-body-size (cf/get :http-server-max-body-size)
|
||||
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||
|
||||
::ldap/provider
|
||||
:app.auth.ldap/provider
|
||||
{:host (cf/get :ldap-host)
|
||||
:port (cf/get :ldap-port)
|
||||
:ssl (cf/get :ldap-ssl)
|
||||
@@ -329,7 +327,6 @@
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::props (ig/ref :app.setup/props)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
:pool (ig/ref ::db/pool)
|
||||
:session (ig/ref :app.http.session/manager)
|
||||
:sprops (ig/ref :app.setup/props)
|
||||
@@ -338,6 +335,7 @@
|
||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:redis (ig/ref ::rds/redis)
|
||||
:ldap (ig/ref :app.auth.ldap/provider)
|
||||
:http-client (ig/ref ::http.client/client)
|
||||
:climit (ig/ref :app.rpc/climit)
|
||||
:rlimit (ig/ref :app.rpc/rlimit)
|
||||
@@ -405,13 +403,12 @@
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
::props (ig/ref :app.setup/props)}
|
||||
|
||||
[::srepl/urepl ::srepl/server]
|
||||
{:port (cf/get :urepl-port 6062)
|
||||
:host (cf/get :urepl-host "localhost")}
|
||||
:app.srepl/server
|
||||
{:port (cf/get :srepl-port)
|
||||
:host (cf/get :srepl-host)}
|
||||
|
||||
[::srepl/prepl ::srepl/server]
|
||||
{:port (cf/get :prepl-port 6063)
|
||||
:host (cf/get :prepl-host "localhost")}
|
||||
:app.setup/initial-profile
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.setup/builtin-templates
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
@@ -453,8 +450,9 @@
|
||||
::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
:app.loggers.database/reporter
|
||||
{::lzmq/receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
::db/pool (ig/ref ::db/pool)}
|
||||
{:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:pool (ig/ref ::db/pool)
|
||||
:executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::sto/storage
|
||||
{:pool (ig/ref ::db/pool)
|
||||
|
||||
@@ -299,10 +299,10 @@
|
||||
{:name "0096-del-storage-pending-table"
|
||||
:fn (mg/resource "app/migrations/sql/0096-del-storage-pending-table.sql")}
|
||||
|
||||
{:name "0098-add-quotes-table"
|
||||
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
|
||||
{:name "0097-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0097-mod-profile-table.sql")}
|
||||
|
||||
])
|
||||
])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::migrations [_ _] migrations)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE profile
|
||||
ADD COLUMN is_admin boolean DEFAULT false;
|
||||
@@ -1,82 +0,0 @@
|
||||
CREATE TABLE usage_quote (
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
target text NOT NULL,
|
||||
quote bigint NOT NULL,
|
||||
|
||||
profile_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
|
||||
project_id uuid NULL REFERENCES project(id) ON DELETE CASCADE DEFERRABLE,
|
||||
team_id uuid NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
|
||||
file_id uuid NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE
|
||||
);
|
||||
|
||||
ALTER TABLE usage_quote
|
||||
ALTER COLUMN target SET STORAGE external;
|
||||
|
||||
CREATE INDEX usage_quote__profile_id__idx ON usage_quote(profile_id, target);
|
||||
CREATE INDEX usage_quote__project_id__idx ON usage_quote(project_id, target);
|
||||
CREATE INDEX usage_quote__team_id__idx ON usage_quote(team_id, target);
|
||||
|
||||
-- DROP TABLE IF EXISTS usage_quote_test;
|
||||
-- CREATE TABLE usage_quote_test (
|
||||
-- id bigserial NOT NULL PRIMARY KEY,
|
||||
-- target text NOT NULL,
|
||||
-- quote bigint NOT NULL,
|
||||
|
||||
-- profile_id bigint NULL,
|
||||
-- team_id bigint NULL,
|
||||
-- project_id bigint NULL,
|
||||
-- file_id bigint NULL
|
||||
-- );
|
||||
|
||||
-- ALTER TABLE usage_quote_test
|
||||
-- ALTER COLUMN target SET STORAGE external;
|
||||
|
||||
-- CREATE INDEX usage_quote_test__profile_id__idx ON usage_quote_test(profile_id, target);
|
||||
-- CREATE INDEX usage_quote_test__project_id__idx ON usage_quote_test(project_id, target);
|
||||
-- CREATE INDEX usage_quote_test__team_id__idx ON usage_quote_test(team_id, target);
|
||||
-- -- CREATE INDEX usage_quote_test__target__idx ON usage_quote_test(target);
|
||||
|
||||
-- DELETE FROM usage_quote_test;
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 50*RANDOM(), 2000*RANDOM(), null, null
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 200*RANDOM(), 300*RANDOM(), 300*RANDOM(), null
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), null, 300*RANDOM()
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), 300*RANDOM(), 300*RANDOM()
|
||||
-- FROM generate_series(1, 1000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 30*RANDOM(), null, 2000*RANDOM(), null
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
|
||||
-- SELECT 'files-per-project', 10*RANDOM(), null, null, 2000*RANDOM()
|
||||
-- FROM generate_series(1, 5000);
|
||||
|
||||
-- VACUUM ANALYZE usage_quote_test;
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and profile_id = 1
|
||||
-- and team_id is null
|
||||
-- and project_id is null;
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and ((team_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (profile_id = 1 and team_id is null and project_id is null));
|
||||
|
||||
-- select * from usage_quote_test
|
||||
-- where target = 'files-per-project'
|
||||
-- and ((project_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (team_id = 1 and (profile_id = 1 or profile_id is null)) or
|
||||
-- (profile_id = 1 and team_id is null and project_id is null));
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc
|
||||
(:require
|
||||
[app.auth.ldap :as-alias ldap]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
@@ -27,7 +26,7 @@
|
||||
[app.rpc.rlimit :as rlimit]
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.util.time :as ts]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
@@ -71,16 +70,14 @@
|
||||
(defn- rpc-query-handler
|
||||
"Ring handler that dispatches query requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
||||
(let [type (keyword (:type path-params))
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::http/request request))
|
||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||
(let [type (keyword (:type params))
|
||||
data (into {::http/request request} params)
|
||||
data (if profile-id
|
||||
(-> data
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(assoc ::session-id session-id))
|
||||
(assoc data
|
||||
:profile-id profile-id
|
||||
::profile-id profile-id
|
||||
::session-id session-id)
|
||||
(dissoc data :profile-id ::profile-id))
|
||||
method (get methods type default-handler)]
|
||||
|
||||
@@ -94,17 +91,16 @@
|
||||
(defn- rpc-mutation-handler
|
||||
"Ring handler that dispatches mutation requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
||||
(let [type (keyword (:type path-params))
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::http/request request))
|
||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||
(let [type (keyword (:type params))
|
||||
data (into {::http/request request} params)
|
||||
data (if profile-id
|
||||
(-> data
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(assoc ::session-id session-id))
|
||||
(assoc data
|
||||
:profile-id profile-id
|
||||
::profile-id profile-id
|
||||
::session-id session-id)
|
||||
(dissoc data :profile-id ::profile-id))
|
||||
|
||||
method (get methods type default-handler)]
|
||||
(-> (method data)
|
||||
(p/then (partial handle-response request))
|
||||
@@ -116,18 +112,13 @@
|
||||
(defn- rpc-command-handler
|
||||
"Ring handler that dispatches cmd requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
|
||||
(let [cmd (keyword (:type path-params))
|
||||
[methods {:keys [profile-id session-id params] :as request} respond raise]
|
||||
(let [cmd (keyword (:type params))
|
||||
etag (yrq/get-header request "if-none-match")
|
||||
|
||||
data (-> params
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::http/request request)
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(-> (assoc ::profile-id profile-id)
|
||||
(assoc ::session-id session-id))))
|
||||
|
||||
data (into {::http/request request ::cond/key etag} params)
|
||||
data (if profile-id
|
||||
(assoc data ::profile-id profile-id ::session-id session-id)
|
||||
(dissoc data ::profile-id))
|
||||
method (get methods cmd default-handler)]
|
||||
(binding [cond/*enabled* true]
|
||||
(-> (method data)
|
||||
@@ -142,7 +133,7 @@
|
||||
[{:keys [metrics ::metrics-id]} f mdata]
|
||||
(let [labels (into-array String [(::sv/name mdata)])]
|
||||
(fn [cfg params]
|
||||
(let [tp (dt/tpoint)]
|
||||
(let [tp (ts/tpoint)]
|
||||
(p/finally
|
||||
(f cfg params)
|
||||
(fn [_ _]
|
||||
@@ -191,12 +182,6 @@
|
||||
:profile-id profile-id
|
||||
:ip-addr (some-> request audit/parse-client-ip)
|
||||
:props props
|
||||
|
||||
;; NOTE: for batch-key lookup we need the params as-is
|
||||
;; because the rpc api does not need to know the
|
||||
;; audit/webhook specific object layout.
|
||||
::params (dissoc params ::http/request)
|
||||
|
||||
::webhooks/batch-key
|
||||
(or (::webhooks/batch-key mdata)
|
||||
(::webhooks/batch-key resultm))
|
||||
@@ -290,10 +275,10 @@
|
||||
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
||||
(->> (sv/scan-ns 'app.rpc.commands.binfile
|
||||
'app.rpc.commands.comments
|
||||
'app.rpc.commands.profile
|
||||
'app.rpc.commands.management
|
||||
'app.rpc.commands.verify-token
|
||||
'app.rpc.commands.search
|
||||
'app.rpc.commands.media
|
||||
'app.rpc.commands.teams
|
||||
'app.rpc.commands.auth
|
||||
'app.rpc.commands.ldap
|
||||
@@ -319,7 +304,6 @@
|
||||
(s/keys :req [::audit/collector
|
||||
::http.client/client
|
||||
::db/pool
|
||||
::ldap/provider
|
||||
::wrk/executor]
|
||||
:req-un [::sto/storage
|
||||
::http.session/session
|
||||
@@ -330,7 +314,8 @@
|
||||
::climit
|
||||
::wrk/executor
|
||||
::mtx/metrics
|
||||
::db/pool]))
|
||||
::db/pool
|
||||
::ldap]))
|
||||
|
||||
(defmethod ig/init-key ::methods
|
||||
[_ cfg]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
(sv/defmethod ::push-audit-events
|
||||
{::climit/queue :push-audit-events
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::climit/key-fn :profile-id
|
||||
::audit/skip true
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
;; ---- COMMAND: login with password
|
||||
|
||||
(defn login-with-password
|
||||
[{:keys [::db/pool session] :as cfg} {:keys [email password] :as params}]
|
||||
[{:keys [::db/pool session] :as cfg} {:keys [email password scope] :as params}]
|
||||
|
||||
(when-not (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password))
|
||||
@@ -119,8 +119,17 @@
|
||||
;; accept invitation with other email
|
||||
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
||||
{:invitation-token (:invitation-token params)}
|
||||
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
||||
(contains? admins (:email profile)))))]
|
||||
(update profile :is-admin (fn [admin?]
|
||||
(or admin?
|
||||
(let [admins (cf/get :admins)]
|
||||
(contains? admins (:email profile)))))))]
|
||||
|
||||
(when (and (nil? (:default-team-id profile))
|
||||
(not= scope "admin"))
|
||||
(ex/raise :type :restriction
|
||||
:code :admin-only-profile
|
||||
:hint "can't login with admin-only profile"))
|
||||
|
||||
(-> response
|
||||
(rph/with-transform (session/create-fn session (:id profile)))
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||
@@ -313,7 +322,6 @@
|
||||
(throw e)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists
|
||||
:hint "email already exists"
|
||||
:cause e)))))))
|
||||
|
||||
(defn create-profile-relations
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.features :as ffeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.spec :as us]
|
||||
@@ -29,7 +28,6 @@
|
||||
[app.tasks.file-gc]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.fressian :as fres]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
@@ -611,23 +609,12 @@
|
||||
(vswap! *state* update :index update-index files)
|
||||
(vswap! *state* assoc :version version :files files)))
|
||||
|
||||
(defn- postprocess-file
|
||||
[data]
|
||||
(let [omap-wrap ffeat/*wrap-with-objects-map-fn*
|
||||
pmap-wrap ffeat/*wrap-with-pointer-map-fn*]
|
||||
(-> data
|
||||
(update :pages-index update-vals #(update % :objects omap-wrap))
|
||||
(update :pages-index update-vals pmap-wrap)
|
||||
(update :components update-vals #(update % :objects omap-wrap))
|
||||
(update :components pmap-wrap))))
|
||||
|
||||
(defmethod read-section :v1/files
|
||||
[{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
|
||||
(doseq [expected-file-id (-> *state* deref :files)]
|
||||
(let [file (read-obj! input)
|
||||
media' (read-obj! input)
|
||||
file-id (:id file)
|
||||
features files/default-features]
|
||||
(let [file (read-obj! input)
|
||||
media' (read-obj! input)
|
||||
file-id (:id file)]
|
||||
|
||||
(when (not= file-id expected-file-id)
|
||||
(ex/raise :type :validation
|
||||
@@ -642,42 +629,33 @@
|
||||
(l/debug :hint "update media references" ::l/async false)
|
||||
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
|
||||
|
||||
(l/debug :hint "processing file" :file-id file-id ::features features ::l/async false)
|
||||
(l/debug :hint "processing file" :file-id file-id ::l/async false)
|
||||
|
||||
(binding [ffeat/*current* features
|
||||
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
|
||||
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)
|
||||
pmap/*tracked* (atom {})]
|
||||
(let [file-id' (lookup-index file-id)
|
||||
data (-> (:data file)
|
||||
(assoc :id file-id')
|
||||
(cond-> migrate? (pmg/migrate-data))
|
||||
(update :pages-index relink-shapes)
|
||||
(update :components relink-shapes)
|
||||
(update :media relink-media))
|
||||
|
||||
(let [file-id' (lookup-index file-id)
|
||||
data (-> (:data file)
|
||||
(assoc :id file-id')
|
||||
(cond-> migrate? (pmg/migrate-data))
|
||||
(update :pages-index relink-shapes)
|
||||
(update :components relink-shapes)
|
||||
(update :media relink-media)
|
||||
(postprocess-file))
|
||||
params {:id file-id'
|
||||
:project-id project-id
|
||||
:name (:name file)
|
||||
:revn (:revn file)
|
||||
:is-shared (:is-shared file)
|
||||
:data (blob/encode data)
|
||||
:created-at timestamp
|
||||
:modified-at timestamp}]
|
||||
|
||||
params {:id file-id'
|
||||
:project-id project-id
|
||||
:features (db/create-array conn "text" features)
|
||||
:name (:name file)
|
||||
:revn (:revn file)
|
||||
:is-shared (:is-shared file)
|
||||
:data (blob/encode data)
|
||||
:created-at timestamp
|
||||
:modified-at timestamp}]
|
||||
(l/debug :hint "create file" :id file-id' ::l/async false)
|
||||
|
||||
(l/debug :hint "create file" :id file-id' ::l/async false)
|
||||
(if overwrite?
|
||||
(create-or-update-file conn params)
|
||||
(db/insert! conn :file params))
|
||||
|
||||
(if overwrite?
|
||||
(create-or-update-file conn params)
|
||||
(db/insert! conn :file params))
|
||||
|
||||
(files/persist-pointers! conn file-id')
|
||||
|
||||
(when overwrite?
|
||||
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
|
||||
(when overwrite?
|
||||
(db/delete! conn :file-thumbnail {:file-id file-id'}))))))
|
||||
|
||||
(defmethod read-section :v1/rels
|
||||
[{:keys [conn ::input ::timestamp]}]
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
|
||||
(ns app.rpc.commands.comments
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
@@ -18,14 +16,16 @@
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.retry :as rtry]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- GENERAL PURPOSE INTERNAL HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUERY COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [participants position] :as row}]
|
||||
@@ -33,61 +33,9 @@
|
||||
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
|
||||
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
|
||||
|
||||
(def sql:get-file
|
||||
"select f.id, f.modified_at, f.revn, f.features,
|
||||
f.project_id, p.team_id, f.data
|
||||
from file as f
|
||||
join project as p on (p.id = f.project_id)
|
||||
where f.id = ?
|
||||
and f.deleted_at is null")
|
||||
|
||||
(defn- get-file
|
||||
"A specialized version of get-file for comments module."
|
||||
[conn file-id page-id]
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
|
||||
(if-let [{:keys [data] :as file} (some-> (db/exec-one! conn [sql:get-file file-id]) (files/decode-row))]
|
||||
(-> file
|
||||
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
|
||||
(assoc :page-id page-id))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "file not found"))))
|
||||
|
||||
(defn- get-comment-thread
|
||||
[conn thread-id & {:keys [for-update?]}]
|
||||
(-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?})
|
||||
(decode-row)))
|
||||
|
||||
(defn- get-comment
|
||||
[conn comment-id & {:keys [for-update?]}]
|
||||
(db/get-by-id conn :comment comment-id {:for-update for-update?}))
|
||||
|
||||
(defn- get-next-seqn
|
||||
[conn file-id]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
"insert into comment_thread_status (thread_id, profile_id, modified_at)
|
||||
values (?, ?, ?)
|
||||
on conflict (thread_id, profile_id)
|
||||
do update set modified_at = ?
|
||||
returning modified_at;")
|
||||
|
||||
(defn upsert-comment-thread-status!
|
||||
([conn profile-id thread-id]
|
||||
(upsert-comment-thread-status! conn profile-id thread-id (dt/now)))
|
||||
([conn profile-id thread-id mod-at]
|
||||
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id mod-at mod-at])))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUERY COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; --- COMMAND: Get Comment Threads
|
||||
|
||||
(declare ^:private get-comment-threads)
|
||||
(declare retrieve-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
@@ -100,10 +48,9 @@
|
||||
|
||||
(sv/defmethod ::get-comment-threads
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id)))
|
||||
(retrieve-comment-threads conn params)))
|
||||
|
||||
(def sql:comment-threads
|
||||
"select distinct on (ct.id)
|
||||
@@ -127,14 +74,15 @@
|
||||
where ct.file_id = ?
|
||||
window w as (partition by c.thread_id order by c.created_at asc)")
|
||||
|
||||
(defn- get-comment-threads
|
||||
[conn profile-id file-id]
|
||||
(defn retrieve-comment-threads
|
||||
[conn {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
;; --- COMMAND: Get Unread Comment Threads
|
||||
|
||||
(declare ^:private get-unread-comment-threads)
|
||||
(declare retrieve-unread-comment-threads)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::get-unread-comment-threads
|
||||
@@ -146,7 +94,7 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-unread-comment-threads conn profile-id team-id)))
|
||||
(retrieve-unread-comment-threads conn params)))
|
||||
|
||||
(def sql:comment-threads-by-team
|
||||
"select distinct on (ct.id)
|
||||
@@ -175,17 +123,19 @@
|
||||
(str "with threads as (" sql:comment-threads-by-team ")"
|
||||
"select * from threads where count_unread_comments > 0"))
|
||||
|
||||
(defn- get-unread-comment-threads
|
||||
[conn profile-id team-id]
|
||||
(defn retrieve-unread-comment-threads
|
||||
[conn {:keys [::rpc/profile-id team-id]}]
|
||||
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
|
||||
(into [] (map decode-row))))
|
||||
|
||||
|
||||
;; --- COMMAND: Get Single Comment Thread
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
(s/def ::get-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::us/id]
|
||||
:req-un [::file-id ::id]
|
||||
:opt-un [::share-id]))
|
||||
|
||||
(sv/defmethod ::get-comment-thread
|
||||
@@ -198,10 +148,19 @@
|
||||
(-> (db/exec-one! conn [sql profile-id file-id id])
|
||||
(decode-row)))))
|
||||
|
||||
(defn get-comment-thread
|
||||
[conn {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||
"select * from threads where id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id file-id id])
|
||||
(decode-row))))
|
||||
|
||||
;; --- COMMAND: Retrieve Comments
|
||||
|
||||
(declare ^:private get-comments)
|
||||
(declare get-comments)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
(s/def ::thread-id ::us/uuid)
|
||||
(s/def ::get-comments
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -212,16 +171,16 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comments conn thread-id))))
|
||||
(let [thread (db/get-by-id conn :comment-thread thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id))
|
||||
(get-comments conn thread-id)))
|
||||
|
||||
(def sql:comments
|
||||
"select c.* from comment as c
|
||||
where c.thread_id = ?
|
||||
order by c.created_at asc")
|
||||
|
||||
(defn- get-comments
|
||||
(defn get-comments
|
||||
[conn thread-id]
|
||||
(->> (db/query conn :comment
|
||||
{:thread-id thread-id}
|
||||
@@ -230,6 +189,26 @@
|
||||
|
||||
;; --- COMMAND: Get file comments users
|
||||
|
||||
(declare get-file-comments-users)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::get-profiles-for-file-comments
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::share-id]))
|
||||
|
||||
(sv/defmethod ::get-profiles-for-file-comments
|
||||
"Retrieves a list of profiles with limited set of properties of all
|
||||
participants on comment threads of the file."
|
||||
{::doc/added "1.15"
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id)))
|
||||
|
||||
;; All the profiles that had comment the file, plus the current
|
||||
;; profile.
|
||||
|
||||
@@ -252,30 +231,20 @@
|
||||
[conn file-id profile-id]
|
||||
(db/exec! conn [sql:file-comment-users file-id profile-id]))
|
||||
|
||||
(s/def ::get-profiles-for-file-comments
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]
|
||||
:opt-un [::share-id]))
|
||||
|
||||
(sv/defmethod ::get-profiles-for-file-comments
|
||||
"Retrieves a list of profiles with limited set of properties of all
|
||||
participants on comment threads of the file."
|
||||
{::doc/added "1.15"
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare ^:private create-comment-thread)
|
||||
|
||||
;; --- COMMAND: Create Comment Thread
|
||||
|
||||
(declare upsert-comment-thread-status!)
|
||||
(declare create-comment-thread)
|
||||
(declare retrieve-page-name)
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id (s/nilable ::us/uuid))
|
||||
(s/def ::position ::gpt/point)
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::frame-id ::us/uuid)
|
||||
@@ -288,77 +257,63 @@
|
||||
(sv/defmethod ::create-comment-thread
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg}
|
||||
{:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [team-id project-id page-name] :as file} (get-file conn 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)
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
||||
::rtry/max-retries 3
|
||||
::rtry/label "create-comment-thread"}
|
||||
(create-comment-thread conn params))))
|
||||
|
||||
(rtry/with-retry {::rtry/when rtry/conflict-exception?
|
||||
::rtry/max-retries 3
|
||||
::rtry/label "create-comment-thread"}
|
||||
(create-comment-thread conn
|
||||
{:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id})))))
|
||||
(defn- retrieve-next-seqn
|
||||
[conn file-id]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(defn create-comment-thread
|
||||
[conn {:keys [::rpc/profile-id file-id page-id position content frame-id] :as params}]
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
pname (retrieve-page-name conn params)
|
||||
thread (db/insert! conn :comment-thread
|
||||
{:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-name pname
|
||||
:page-id page-id
|
||||
:created-at now
|
||||
:modified-at now
|
||||
:seqn seqn
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id})]
|
||||
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn {: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 the whole
|
||||
;; operation can be retried on conflict, and in this case the new seq shold be
|
||||
;; retrieved from the database.
|
||||
seqn (get-next-seqn conn file-id)
|
||||
thread-id (uuid/next)
|
||||
thread (db/insert! conn :comment-thread
|
||||
{:id thread-id
|
||||
:file-id file-id
|
||||
:owner-id profile-id
|
||||
:participants (db/tjson #{profile-id})
|
||||
:page-name page-name
|
||||
:page-id page-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:seqn seqn
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id})
|
||||
comment (db/insert! conn :comment
|
||||
{:id (uuid/next)
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:content content})]
|
||||
;; Create a comment entry
|
||||
(db/insert! conn :comment
|
||||
{:thread-id (:id thread)
|
||||
:owner-id profile-id
|
||||
:created-at now
|
||||
:modified-at now
|
||||
:content content})
|
||||
|
||||
;; Make the current thread as read.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id created-at)
|
||||
(upsert-comment-thread-status! conn profile-id (:id thread))
|
||||
|
||||
;; Optimistic update of current seq number on file.
|
||||
(db/update! conn :file
|
||||
{:comment-thread-seqn seqn}
|
||||
{:id file-id})
|
||||
|
||||
(-> thread
|
||||
(select-keys [:id :file-id :page-id])
|
||||
(assoc :comment-id (:id comment)))))
|
||||
(select-keys thread [:id :file-id :page-id])))
|
||||
|
||||
(defn- retrieve-page-name
|
||||
[conn {:keys [file-id page-id]}]
|
||||
(let [{:keys [data]} (db/get-by-id conn :file file-id)
|
||||
data (blob/decode data)]
|
||||
(get-in data [:pages-index page-id :name])))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread Status
|
||||
|
||||
@@ -374,9 +329,23 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id))))
|
||||
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not cthr
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(files/check-comment-permissions! conn profile-id (:file-id cthr) share-id)
|
||||
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
"insert into comment_thread_status (thread_id, profile_id)
|
||||
values (?, ?)
|
||||
on conflict (thread_id, profile_id)
|
||||
do update set modified_at = clock_timestamp()
|
||||
returning modified_at;")
|
||||
|
||||
(defn upsert-comment-thread-status!
|
||||
[conn profile-id thread-id]
|
||||
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
@@ -391,8 +360,12 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not thread
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
@@ -401,7 +374,6 @@
|
||||
|
||||
;; --- COMMAND: Add Comment
|
||||
|
||||
(declare get-comment-thread)
|
||||
(declare create-comment)
|
||||
|
||||
(s/def ::create-comment
|
||||
@@ -412,52 +384,66 @@
|
||||
(sv/defmethod ::create-comment
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true)
|
||||
{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
|
||||
(create-comment conn params)))
|
||||
|
||||
(files/check-comment-permissions! conn profile-id (:id file) share-id)
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id (:id file)})
|
||||
(defn create-comment
|
||||
[conn {:keys [::rpc/profile-id thread-id content share-id] :as params}]
|
||||
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
|
||||
(decode-row))
|
||||
pname (retrieve-page-name conn thread)]
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
(db/update! conn :comment-thread
|
||||
{:page-name page-name}
|
||||
{:id thread-id}))
|
||||
;; Standard Checks
|
||||
(when-not thread (ex/raise :type :not-found))
|
||||
|
||||
(let [comment (db/insert! conn :comment
|
||||
{:id (uuid/next)
|
||||
:created-at request-at
|
||||
:modified-at request-at
|
||||
:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:content content})
|
||||
props {:file-id file-id
|
||||
:share-id nil}]
|
||||
;; Permission Checks
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
|
||||
|
||||
;; Update thread modified-at attribute and assoc the current
|
||||
;; profile to the participant set.
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:participants (-> (:participants thread #{})
|
||||
(conj profile-id)
|
||||
(db/tjson))}
|
||||
{:id thread-id})
|
||||
;; Update the page-name cachedattribute on comment thread table.
|
||||
(when (not= pname (:page-name thread))
|
||||
(db/update! conn :comment-thread
|
||||
{:page-name pname}
|
||||
{:id thread-id}))
|
||||
|
||||
;; Update the current profile status in relation to the
|
||||
;; current thread.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id request-at)
|
||||
;; NOTE: is important that all timestamptz related fields are
|
||||
;; created or updated on the database level for avoid clock
|
||||
;; inconsistencies (some user sees something read that is not
|
||||
;; read, etc...)
|
||||
(let [ppants (:participants thread #{})
|
||||
comment (db/insert! conn :comment
|
||||
{:thread-id thread-id
|
||||
:owner-id profile-id
|
||||
:content content})]
|
||||
|
||||
(vary-meta comment assoc ::audit/props props)))))
|
||||
;; NOTE: this is done in SQL instead of using db/update!
|
||||
;; helper because currently the helper does not allow pass raw
|
||||
;; function call parameters to the underlying prepared
|
||||
;; statement; in a future when we fix/improve it, this can be
|
||||
;; changed to use the helper.
|
||||
|
||||
;; Update thread modified-at attribute and assoc the current
|
||||
;; profile to the participant set.
|
||||
(let [ppants (conj ppants profile-id)
|
||||
sql "update comment_thread
|
||||
set modified_at = clock_timestamp(),
|
||||
participants = ?
|
||||
where id = ?"]
|
||||
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
|
||||
|
||||
;; Update the current profile status in relation to the
|
||||
;; current thread.
|
||||
(upsert-comment-thread-status! conn profile-id thread-id)
|
||||
|
||||
;; Return the created comment object.
|
||||
(rph/with-meta comment
|
||||
{::audit/props {:file-id (:file-id thread)
|
||||
:share-id nil}}))))
|
||||
|
||||
;; --- COMMAND: Update Comment
|
||||
|
||||
(declare update-comment)
|
||||
|
||||
(s/def ::update-comment
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::content]
|
||||
@@ -465,69 +451,71 @@
|
||||
|
||||
(sv/defmethod ::update-comment
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true)
|
||||
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)]
|
||||
(update-comment conn params)))
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(defn update-comment
|
||||
[conn {:keys [::rpc/profile-id id content share-id] :as params}]
|
||||
(let [comment (db/get-by-id conn :comment id {:for-update true})
|
||||
_ (when-not comment (ex/raise :type :not-found))
|
||||
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
|
||||
_ (when-not thread (ex/raise :type :not-found))
|
||||
pname (retrieve-page-name conn thread)]
|
||||
|
||||
;; Don't allow edit comments to not owners
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
|
||||
|
||||
(let [{:keys [page-name] :as file} (get-file conn file-id page-id)]
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at request-at}
|
||||
{:id id})
|
||||
;; Don't allow edit comments to not owners
|
||||
(when-not (= (:owner-id thread) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(db/update! conn :comment
|
||||
{:content content
|
||||
:modified-at (dt/now)}
|
||||
{:id (:id comment)})
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at (dt/now)
|
||||
:page-name pname}
|
||||
{:id (:id thread)})
|
||||
nil))
|
||||
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:page-name page-name}
|
||||
{:id thread-id})
|
||||
nil))))
|
||||
|
||||
;; --- COMMAND: Delete Comment Thread
|
||||
|
||||
(s/def ::delete-comment-thread
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::share-id]))
|
||||
:req-un [::id]))
|
||||
|
||||
(sv/defmethod ::delete-comment-thread
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(when-not (= (:owner-id thread) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
|
||||
(db/delete! conn :comment-thread {:id id})
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- COMMAND: Delete comment
|
||||
|
||||
(s/def ::delete-comment
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::share-id]))
|
||||
:req-un [::id]))
|
||||
|
||||
(sv/defmethod ::delete-comment
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true)
|
||||
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(let [comment (db/get-by-id conn :comment id {:for-update true})]
|
||||
(when-not (= (:owner-id comment) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
(db/delete! conn :comment {:id id}))))
|
||||
|
||||
(db/delete! conn :comment {:id id}))))
|
||||
|
||||
;; --- COMMAND: Update comment thread position
|
||||
|
||||
@@ -540,10 +528,10 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at (::rpc/request-at params)
|
||||
{:modified-at (dt/now)
|
||||
:position (db/pgpoint position)
|
||||
:frame-id frame-id}
|
||||
{:id (:id thread)})
|
||||
@@ -560,10 +548,10 @@
|
||||
{::doc/added "1.15"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
|
||||
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at (::rpc/request-at params)
|
||||
{:modified-at (dt/now)
|
||||
:frame-id frame-id}
|
||||
{:id id})
|
||||
{:id (:id thread)})
|
||||
nil)))
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -44,13 +43,7 @@
|
||||
"storage/pointer-map"
|
||||
"components/v2"})
|
||||
|
||||
(def default-features
|
||||
(cond-> #{}
|
||||
(contains? cf/flags :fdata-storage-pointer-map)
|
||||
(conj "storage/pointer-map")
|
||||
|
||||
(contains? cf/flags :fdata-storage-objects-map)
|
||||
(conj "storage/objects-map")))
|
||||
(def default-features #{})
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -158,14 +151,11 @@
|
||||
(def check-read-permissions!
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
;; A user has comment permissions if she has read permissions, or
|
||||
;; explicit comment permissions through the share-id
|
||||
|
||||
;; A user has comment permissions if she has read permissions, or comment permissions
|
||||
(defn check-comment-permissions!
|
||||
[conn profile-id file-id share-id]
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
||||
can-read (has-read-permissions? perms)
|
||||
can-comment (has-comment-permissions? perms)]
|
||||
(let [can-read (has-read-permissions? conn profile-id file-id)
|
||||
can-comment (has-comment-permissions? conn profile-id file-id share-id)]
|
||||
(when-not (or can-read can-comment)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
@@ -251,6 +241,7 @@
|
||||
[conn id client-features]
|
||||
;; here we check if client requested features are supported
|
||||
(check-features-compatibility! client-features)
|
||||
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> (db/get-by-id conn :file id)
|
||||
(decode-row)
|
||||
@@ -275,7 +266,7 @@
|
||||
{::doc/added "1.17"
|
||||
::cond/get-object #(get-minimal-file %1 (:id %2))
|
||||
::cond/key-fn get-file-etag}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [perms (get-permissions conn profile-id id)]
|
||||
(check-read-permissions! perms)
|
||||
@@ -303,7 +294,7 @@
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
{::doc/added "1.17"
|
||||
::rpc/:auth false}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||
(check-read-permissions! perms)
|
||||
@@ -370,7 +361,7 @@
|
||||
(sv/defmethod ::get-project-files
|
||||
"Get all files for the specified project."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(get-project-files conn project-id)))
|
||||
@@ -383,16 +374,15 @@
|
||||
(s/def ::file-id ::us/uuid)
|
||||
|
||||
(s/def ::has-file-libraries
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id]))
|
||||
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
|
||||
|
||||
(sv/defmethod ::has-file-libraries
|
||||
"Checks if the file has libraries. Returns a boolean"
|
||||
{::doc/added "1.15.1"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(get-has-file-libraries conn file-id)))
|
||||
(get-has-file-libraries conn params)))
|
||||
|
||||
(def ^:private sql:has-file-libraries
|
||||
"SELECT COUNT(*) > 0 AS has_libraries
|
||||
@@ -403,7 +393,7 @@
|
||||
fl.deleted_at > now())")
|
||||
|
||||
(defn- get-has-file-libraries
|
||||
[conn file-id]
|
||||
[conn {:keys [file-id]}]
|
||||
(let [row (db/exec-one! conn [sql:has-file-libraries file-id])]
|
||||
(:has-libraries row)))
|
||||
|
||||
@@ -482,37 +472,37 @@
|
||||
order by f.modified_at desc")
|
||||
|
||||
(defn get-team-shared-files
|
||||
[conn team-id]
|
||||
(letfn [(assets-sample [assets limit]
|
||||
(let [sorted-assets (->> (vals assets)
|
||||
(sort-by #(str/lower (:name %))))]
|
||||
{:count (count sorted-assets)
|
||||
:sample (into [] (take limit sorted-assets))}))
|
||||
[conn {:keys [team-id] :as params}]
|
||||
(let [assets-sample
|
||||
(fn [assets limit]
|
||||
(let [sorted-assets (->> (vals assets)
|
||||
(sort-by #(str/lower (:name %))))]
|
||||
|
||||
(library-summary [{:keys [id data] :as file}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
{:components (assets-sample (:components data) 4)
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
:typographies (assets-sample (:typographies data) 3)}))]
|
||||
{:count (count sorted-assets)
|
||||
:sample (into [] (take limit sorted-assets))}))
|
||||
|
||||
(->> (db/exec! conn [sql:team-shared-files team-id])
|
||||
(into #{} (comp
|
||||
(map decode-row)
|
||||
(map #(assoc % :library-summary (library-summary %)))
|
||||
(map #(dissoc % :data)))))))
|
||||
library-summary
|
||||
(fn [data]
|
||||
{:components (assets-sample (:components data) 4)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
:typographies (assets-sample (:typographies data) 3)})
|
||||
|
||||
xform (comp
|
||||
(map decode-row)
|
||||
(map #(assoc % :library-summary (library-summary (:data %))))
|
||||
(map #(dissoc % :data)))]
|
||||
|
||||
(into #{} xform (db/exec! conn [sql:team-shared-files team-id]))))
|
||||
|
||||
(s/def ::get-team-shared-files
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id]))
|
||||
(s/keys :req [::rpc/profile-id] :req-un [::team-id]))
|
||||
|
||||
(sv/defmethod ::get-team-shared-files
|
||||
"Get all file (libraries) for the specified team."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(get-team-shared-files conn team-id)))
|
||||
(get-team-shared-files conn params)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-libraries
|
||||
@@ -546,14 +536,12 @@
|
||||
[conn file-id client-features]
|
||||
(check-features-compatibility! client-features)
|
||||
(->> (db/exec! conn [sql:file-libraries file-id])
|
||||
(map decode-row)
|
||||
(map #(assoc % :is-indirect false))
|
||||
(map (fn [{:keys [id] :as row}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> row
|
||||
(update :data dissoc :pages-index)
|
||||
(handle-file-features client-features)))))
|
||||
(vec)))
|
||||
(mapv (fn [{:keys [id] :as row}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(-> (decode-row row)
|
||||
(assoc :is-indirect false)
|
||||
(update :data dissoc :pages-index)
|
||||
(handle-file-features client-features)))))))
|
||||
|
||||
(s/def ::get-file-libraries
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -563,7 +551,7 @@
|
||||
(sv/defmethod ::get-file-libraries
|
||||
"Get libraries used by the specified file."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-file-libraries conn file-id features)))
|
||||
@@ -594,6 +582,7 @@
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(get-library-file-references conn file-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-team-recent-files
|
||||
|
||||
(def sql:team-recent-files
|
||||
@@ -722,30 +711,28 @@
|
||||
|
||||
objects)))]
|
||||
|
||||
(binding [pmap/*load-fn* (partial load-pointer conn id)]
|
||||
(let [frame (get-thumbnail-frame data)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
(-> data :pages first))
|
||||
(let [frame (get-thumbnail-frame data)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
(-> data :pages first))
|
||||
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
page (cond-> page (pmap/pointer-map? page) deref)
|
||||
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
|
||||
|
||||
obj-ids (map #(str page-id %) frame-ids)
|
||||
thumbs (get-object-thumbnails conn id obj-ids)]
|
||||
obj-ids (map #(str page-id %) frame-ids)
|
||||
thumbs (get-object-thumbnails conn id obj-ids)]
|
||||
|
||||
(cond-> page
|
||||
;; If we have frame, we need to specify it on the page level
|
||||
;; and remove the all other unrelated objects.
|
||||
(some? frame-id)
|
||||
(-> (assoc :thumbnail-frame-id frame-id)
|
||||
(update :objects filter-objects frame-id))
|
||||
(cond-> page
|
||||
;; If we have frame, we need to specify it on the page level
|
||||
;; and remove the all other unrelated objects.
|
||||
(some? frame-id)
|
||||
(-> (assoc :thumbnail-frame-id frame-id)
|
||||
(update :objects filter-objects frame-id))
|
||||
|
||||
;; Assoc the available thumbnails and prune not visible shapes
|
||||
;; for avoid transfer unnecessary data.
|
||||
:always
|
||||
(update :objects assoc-thumbnails page-id thumbs))))))
|
||||
;; Assoc the available thumbnails and prune not visible shapes
|
||||
;; for avoid transfer unnecessary data.
|
||||
:always
|
||||
(update :objects assoc-thumbnails page-id thumbs)))))
|
||||
|
||||
(s/def ::get-file-data-for-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
@@ -759,15 +746,12 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
;; NOTE: we force here the "storage/pointer-map" feature, because
|
||||
;; it used internally only and is independent if user supports it
|
||||
;; or not.
|
||||
(let [feat (into #{"storage/pointer-map"} features)
|
||||
file (get-file conn file-id feat)]
|
||||
(let [file (get-file conn file-id features)]
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
:page (get-file-data-for-thumbnail conn file)})))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -775,7 +759,7 @@
|
||||
;; --- MUTATION COMMAND: rename-file
|
||||
|
||||
(defn rename-file
|
||||
[conn {:keys [id name]}]
|
||||
[conn {:keys [id name] :as params}]
|
||||
(db/update! conn :file
|
||||
{:name name
|
||||
:modified-at (dt/now)}
|
||||
@@ -909,7 +893,7 @@
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
(defn unlink-file-from-library
|
||||
[conn {:keys [file-id library-id]}]
|
||||
[conn {:keys [file-id library-id] :as params}]
|
||||
(db/delete! conn :file-library-rel
|
||||
{:file-id file-id
|
||||
:library-file-id library-id}))
|
||||
@@ -970,8 +954,7 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(-> (ignore-sync conn params)
|
||||
(update :features db/decode-pgarray #{}))))
|
||||
(ignore-sync conn params)))
|
||||
|
||||
|
||||
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
|
||||
@@ -996,8 +979,7 @@
|
||||
:opt-un [::data]))
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
@@ -1027,8 +1009,7 @@
|
||||
(sv/defmethod ::upsert-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::audit/skip true}
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
@@ -85,12 +84,6 @@
|
||||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team-id (files/get-team-id conn project-id)
|
||||
params (assoc params :profile-id profile-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/project-id project-id}))
|
||||
|
||||
(-> (create-file conn params)
|
||||
(vary-meta assoc ::audit/props {:team-id team-id})))))
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
|
||||
(files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-file
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.loggers.webhooks :as webhooks]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
@@ -130,7 +130,7 @@
|
||||
::climit/key-fn :id
|
||||
::webhooks/event? true
|
||||
::webhooks/batch-timeout (dt/duration "2m")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/batch-key :id
|
||||
::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
@@ -168,18 +168,7 @@
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
|
||||
params (-> params
|
||||
(assoc :file file)
|
||||
(assoc :changes changes)
|
||||
(assoc ::created-at (dt/now)))]
|
||||
|
||||
(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)}))
|
||||
params (assoc params :file file :changes changes)]
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
@@ -191,15 +180,24 @@
|
||||
|
||||
(-> (update-fn cfg params)
|
||||
(vary-meta assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))))
|
||||
|
||||
(defn- update-file*
|
||||
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
|
||||
(let [file (-> file
|
||||
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id] :as params}]
|
||||
(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)}))
|
||||
|
||||
(let [ts (dt/now)
|
||||
file (-> file
|
||||
(update :revn inc)
|
||||
(update :data (fn [data]
|
||||
(cond-> data
|
||||
@@ -219,7 +217,7 @@
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:created-at ts
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:features (db/create-array conn "text" (:features file))
|
||||
@@ -231,12 +229,12 @@
|
||||
{:revn (:revn file)
|
||||
:data (:data file)
|
||||
:data-backend nil
|
||||
:modified-at created-at
|
||||
:modified-at ts
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at created-at}
|
||||
{:modified-at ts}
|
||||
{:id (:project-id file)})
|
||||
|
||||
(let [params (assoc params :file file)]
|
||||
@@ -267,10 +265,13 @@
|
||||
order by s.created_at asc")
|
||||
|
||||
(defn- get-lagged-changes
|
||||
[conn {:keys [id revn] :as params}]
|
||||
(->> (db/exec! conn [sql:lagged-changes id revn])
|
||||
(map files/decode-row)
|
||||
(vec)))
|
||||
[conn params]
|
||||
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
|
||||
(into [] (comp (map files/decode-row)
|
||||
(map (fn [row]
|
||||
(cond-> row
|
||||
(= (:revn row) (:revn (:file params)))
|
||||
(assoc :changes []))))))))
|
||||
|
||||
(defn- send-notifications!
|
||||
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
|
||||
|
||||
@@ -12,13 +12,10 @@
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
@@ -37,15 +34,15 @@
|
||||
(sv/defmethod ::login-with-ldap
|
||||
"Performs the authentication using LDAP backend. Only works if LDAP
|
||||
is properly configured and enabled with `login-with-ldap` flag."
|
||||
{::rpc/auth false
|
||||
{:auth false
|
||||
::doc/added "1.15"}
|
||||
[{:keys [::main/props ::ldap/provider session] :as cfg} params]
|
||||
(when-not provider
|
||||
[{:keys [session tokens ldap] :as cfg} params]
|
||||
(when-not ldap
|
||||
(ex/raise :type :restriction
|
||||
:code :ldap-not-initialized
|
||||
:hide "ldap auth provider is not initialized"))
|
||||
|
||||
(let [info (ldap/authenticate provider params)]
|
||||
(let [info (ldap/authenticate ldap params)]
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
@@ -61,11 +58,12 @@
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged).
|
||||
(let [claims (tokens/verify props {:token token :iss :team-invitation})
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims
|
||||
:member-id (:id profile)
|
||||
:member-email (:email profile))
|
||||
token (tokens/generate props claims)]
|
||||
token (tokens :generate claims)]
|
||||
|
||||
(-> {:invitation-token token}
|
||||
(rph/with-transform (session/create-fn session (:id profile)))
|
||||
(rph/with-meta {::audit/props (:props profile)
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"Duplicate a single file in the same team."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(duplicate-file conn (assoc params :profile-id profile-id))))
|
||||
(duplicate-file conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||
|
||||
(defn- remap-id
|
||||
[item index key]
|
||||
@@ -136,7 +136,7 @@
|
||||
and so.deleted_at is null")
|
||||
|
||||
(defn duplicate-file*
|
||||
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}]
|
||||
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
|
||||
(let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
|
||||
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
|
||||
|
||||
@@ -329,9 +329,10 @@
|
||||
"Move a set of files from one project to other."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(move-files conn (assoc params :profile-id profile-id))))
|
||||
(move-files conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||
|
||||
|
||||
;; --- COMMAND: Move project
|
||||
|
||||
@@ -369,9 +370,9 @@
|
||||
"Move projects between teams."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(move-project conn (assoc params :profile-id profile-id))))
|
||||
(move-project conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
@@ -386,10 +387,10 @@
|
||||
"Clone into the specified project the template by its id."
|
||||
{::doc/added "1.16"
|
||||
::webhooks/event? true}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-template (assoc params :profile-id profile-id)))))
|
||||
(clone-template (assoc params :profile-id (::rpc/profile-id params))))))
|
||||
|
||||
(defn- clone-template
|
||||
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
|
||||
|
||||
@@ -1,274 +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 app.rpc.commands.media
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
|
||||
(def thumbnail-options
|
||||
{:width 100
|
||||
:height 100
|
||||
:quality 85
|
||||
:format :jpeg})
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
(defn validate-content-size!
|
||||
[content]
|
||||
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
||||
(ex/raise :type :restriction
|
||||
:code :media-max-file-size-reached
|
||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||
(:size content)
|
||||
default-max-file-size))))
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
|
||||
(s/def ::content ::media/upload)
|
||||
(s/def ::is-local ::us/boolean)
|
||||
|
||||
(s/def ::upload-file-media-object
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::name ::content]
|
||||
:opt-un [::id]))
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(validate-content-size! content)
|
||||
|
||||
(create-file-media-object cfg params)))
|
||||
|
||||
(defn- big-enough-for-thumbnail?
|
||||
"Checks if the provided image info is big enough for
|
||||
create a separate thumbnail storage object."
|
||||
[info]
|
||||
(or (> (:width info) (:width thumbnail-options))
|
||||
(> (:height info) (:height thumbnail-options))))
|
||||
|
||||
(defn- svg-image?
|
||||
[info]
|
||||
(= (:mtype info) "image/svg+xml"))
|
||||
|
||||
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
||||
;; because postgresql does not returns anything if no update is
|
||||
;; performed, the `do update` does the trick.
|
||||
|
||||
(def sql:create-file-media-object
|
||||
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict (id) do update set created_at=file_media_object.created_at
|
||||
returning *")
|
||||
|
||||
;; NOTE: the following function executes without a transaction, this
|
||||
;; means that if something fails in the middle of this function, it
|
||||
;; will probably leave leaked/unreferenced objects in the database and
|
||||
;; probably in the storage layer. For handle possible object leakage,
|
||||
;; we create all media objects marked as touched, this ensures that if
|
||||
;; something fails, all leaked (already created storage objects) will
|
||||
;; be eventually marked as deleted by the touched-gc task.
|
||||
;;
|
||||
;; The touched-gc task, performs periodic analysis of all touched
|
||||
;; storage objects and check references of it. This is the reason why
|
||||
;; `reference` metadata exists: it indicates the name of the table
|
||||
;; witch holds the reference to storage object (it some kind of
|
||||
;; inverse, soft referential integrity).
|
||||
|
||||
(defn create-file-media-object
|
||||
[{:keys [storage pool climit executor]}
|
||||
{:keys [id file-id is-local name content]}]
|
||||
(letfn [;; Function responsible to retrieve the file information, as
|
||||
;; it is synchronous operation it should be wrapped into
|
||||
;; with-dispatch macro.
|
||||
(get-info [content]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run {:cmd :info :input content})))
|
||||
|
||||
;; Function responsible of calculating cryptographyc hash of
|
||||
;; the provided data.
|
||||
(calculate-hash [data]
|
||||
(px/with-dispatch executor
|
||||
(sto/calculate-hash data)))
|
||||
|
||||
;; Function responsible of generating thumnail. As it is synchronous
|
||||
;; opetation, it should be wrapped into with-dispatch macro
|
||||
(generate-thumbnail [info]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run (assoc thumbnail-options
|
||||
:cmd :generic-thumbnail
|
||||
:input info))))
|
||||
|
||||
(create-thumbnail [info]
|
||||
(when (and (not (svg-image? info))
|
||||
(big-enough-for-thumbnail? info))
|
||||
(p/let [thumb (generate-thumbnail info)
|
||||
hash (calculate-hash (:data thumb))
|
||||
content (-> (sto/content (:data thumb) (:size thumb))
|
||||
(sto/wrap-with-hash hash))]
|
||||
(sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype thumb)
|
||||
:bucket "file-media-object"}))))
|
||||
|
||||
(create-image [info]
|
||||
(p/let [data (:path info)
|
||||
hash (calculate-hash data)
|
||||
content (-> (sto/content data)
|
||||
(sto/wrap-with-hash hash))]
|
||||
(sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype info)
|
||||
:bucket "file-media-object"})))
|
||||
|
||||
(insert-into-database [info image thumb]
|
||||
(px/with-dispatch executor
|
||||
(db/exec-one! pool [sql:create-file-media-object
|
||||
(or id (uuid/next))
|
||||
file-id is-local name
|
||||
(:id image)
|
||||
(:id thumb)
|
||||
(:width info)
|
||||
(:height info)
|
||||
(:mtype info)])))]
|
||||
|
||||
(p/let [info (get-info content)
|
||||
thumb (create-thumbnail info)
|
||||
image (create-image info)]
|
||||
(insert-into-database info image thumb))))
|
||||
|
||||
;; --- Create File Media Object (from URL)
|
||||
|
||||
(declare ^:private create-file-media-object-from-url)
|
||||
|
||||
(s/def ::create-file-media-object-from-url
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::url]
|
||||
:opt-un [::id ::name]))
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(create-file-media-object-from-url cfg params)))
|
||||
|
||||
(defn- create-file-media-object-from-url
|
||||
[cfg {:keys [url name] :as params}]
|
||||
(letfn [(parse-and-validate-size [headers]
|
||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||
mtype (get headers "content-type")
|
||||
format (cm/mtype->format mtype)
|
||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||
|
||||
(when-not size
|
||||
(ex/raise :type :validation
|
||||
:code :unknown-size
|
||||
:hint "seems like the url points to resource with unknown size"))
|
||||
|
||||
(when (> size max-size)
|
||||
(ex/raise :type :validation
|
||||
:code :file-too-large
|
||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||
size
|
||||
default-max-file-size)))
|
||||
|
||||
(when (nil? format)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "seems like the url points to an invalid media object"))
|
||||
|
||||
{:size size
|
||||
:mtype mtype
|
||||
:format format}))
|
||||
|
||||
(download-media [uri]
|
||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
||||
(p/then process-response)))
|
||||
|
||||
(process-response [{:keys [body headers] :as response}]
|
||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write-to-file! body path :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
|
||||
{:filename "tempfile"
|
||||
:size size
|
||||
:path path
|
||||
:mtype mtype}))]
|
||||
|
||||
(p/let [content (download-media url)]
|
||||
(->> (merge params {:content content :name (or name (:filename content))})
|
||||
(create-file-media-object cfg)))))
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
(declare clone-file-media-object)
|
||||
|
||||
(s/def ::clone-file-media-object
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::id]))
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-file-media-object params))))
|
||||
|
||||
(defn clone-file-media-object
|
||||
[{:keys [conn]} {:keys [id file-id is-local]}]
|
||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
:file-id file-id
|
||||
:is-local is-local
|
||||
:name (:name mobj)
|
||||
:media-id (:media-id mobj)
|
||||
:thumbnail-id (:thumbnail-id mobj)
|
||||
:width (:width mobj)
|
||||
:height (:height mobj)
|
||||
:mtype (:mtype mobj)})))
|
||||
75
backend/src/app/rpc/commands/profile.clj
Normal file
75
backend/src/app/rpc/commands/profile.clj
Normal file
@@ -0,0 +1,75 @@
|
||||
;; 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.rpc.commands.profile
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- MUTATION: Set profile password
|
||||
|
||||
(declare update-profile-password!)
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
|
||||
(s/def ::get-derived-password
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::password]))
|
||||
|
||||
(sv/defmethod ::get-derived-password
|
||||
"Get derived password, only ADMINS allowed to call this RPC
|
||||
methods. Designed for administration pannel integration."
|
||||
{::climit/queue :auth
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [admins (cf/get :admins)
|
||||
profile (db/get-by-id conn :profile (::rpc/profile-id params))]
|
||||
|
||||
(if (or (:is-admin profile)
|
||||
(contains? admins (:email profile)))
|
||||
{:password (auth/derive-password password)}
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed
|
||||
:hint "only admins allowed to call this RPC method")))))
|
||||
|
||||
;; --- MUTATION: Check profile password
|
||||
|
||||
(s/def ::attempt ::us/not-empty-string)
|
||||
(s/def ::check-profile-password
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::profile-id ::password]))
|
||||
|
||||
(sv/defmethod ::check-profile-password
|
||||
"Check profile password, only ADMINS allowed to call this RPC
|
||||
methods. Designed for administration pannel integration."
|
||||
{::climit/queue :auth
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [profile-id password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [admins (cf/get :admins)
|
||||
profile (db/get-by-id pool :profile (::rpc/profile-id params))]
|
||||
|
||||
(if (or (:is-admin profile)
|
||||
(contains? admins (:email profile)))
|
||||
(let [profile (if (not= (::rpc/profile-id params) profile-id)
|
||||
(db/get-by-id conn :profile profile-id)
|
||||
profile)]
|
||||
(auth/verify-password password (:password profile)))
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed
|
||||
:hint "only admins allowed to call this RPC method")))))
|
||||
@@ -48,7 +48,7 @@
|
||||
order by f.created_at asc")
|
||||
|
||||
(defn search-files
|
||||
[conn profile-id team-id search-term]
|
||||
[conn {:keys [::rpc/profile-id team-id search-term] :as params}]
|
||||
(db/exec! conn [sql:search-files
|
||||
profile-id team-id
|
||||
profile-id team-id
|
||||
@@ -64,5 +64,6 @@
|
||||
|
||||
(sv/defmethod ::search-files
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||
(some->> search-term (search-files pool profile-id team-id)))
|
||||
[{:keys [pool]} {:keys [search-term] :as params}]
|
||||
(when search-term
|
||||
(search-files pool params)))
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
@@ -298,9 +297,6 @@
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(create-team conn (assoc params :profile-id profile-id))))
|
||||
|
||||
(defn create-team
|
||||
@@ -385,8 +381,14 @@
|
||||
|
||||
(declare role->params)
|
||||
|
||||
(s/def ::reassign-to ::us/uuid)
|
||||
(s/def ::leave-team
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::reassign-to]))
|
||||
|
||||
(defn leave-team
|
||||
[conn {:keys [profile-id id reassign-to]}]
|
||||
[conn {:keys [::rpc/profile-id id reassign-to]}]
|
||||
(let [perms (get-permissions conn profile-id id)
|
||||
members (retrieve-team-members conn id)]
|
||||
|
||||
@@ -431,17 +433,12 @@
|
||||
|
||||
nil))
|
||||
|
||||
(s/def ::reassign-to ::us/uuid)
|
||||
(s/def ::leave-team
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]
|
||||
:opt-un [::reassign-to]))
|
||||
|
||||
(sv/defmethod ::leave-team
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(leave-team conn (assoc params :profile-id profile-id))))
|
||||
(leave-team conn params)))
|
||||
|
||||
;; --- Mutation: Delete Team
|
||||
|
||||
@@ -473,7 +470,7 @@
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::member-id ::us/uuid)
|
||||
;; Temporarily disabled viewer role
|
||||
;; https://tree.taiga.io/project/penpot/issue/1083
|
||||
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
||||
;; (s/def ::role #{:owner :admin :editor :viewer})
|
||||
(s/def ::role #{:owner :admin :editor})
|
||||
|
||||
@@ -538,9 +535,9 @@
|
||||
|
||||
(sv/defmethod ::update-team-member-role
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(update-team-member-role conn (assoc params :profile-id profile-id))))
|
||||
(update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||
|
||||
|
||||
;; --- Mutation: Delete Team Member
|
||||
@@ -641,7 +638,7 @@
|
||||
"insert into team_invitation(team_id, email_to, role, valid_until)
|
||||
values (?, ?, ?, ?)
|
||||
on conflict(team_id, email_to) do
|
||||
update set role = ?, valid_until = ?, updated_at = now();")
|
||||
update set role = ?, updated_at = now();")
|
||||
|
||||
(defn- create-invitation-token
|
||||
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
||||
@@ -712,7 +709,7 @@
|
||||
{:id (:id member)})))
|
||||
(do
|
||||
(db/exec-one! conn [sql:upsert-team-invitation
|
||||
(:id team) (str/lower email) (name role) expire (name role) expire])
|
||||
(:id team) (str/lower email) (name role) expire (name role)])
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (cf/get :public-uri)
|
||||
@@ -742,17 +739,6 @@
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
||||
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
@@ -786,8 +772,7 @@
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [params (assoc params :profile-id profile-id)
|
||||
team (create-team conn params)
|
||||
(let [team (create-team conn params)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
cfg (assoc cfg ::conn conn)]
|
||||
|
||||
@@ -800,18 +785,6 @@
|
||||
:role role}))
|
||||
(run! (partial create-invitation cfg)))
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/teams-per-profile
|
||||
::quotes/profile-id profile-id}
|
||||
{::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id (:id team)
|
||||
::quotes/incr (count emails)}))
|
||||
|
||||
(-> team
|
||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
||||
(rph/with-defer
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.tokens :as tokens]
|
||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||
[app.util.services :as sv]
|
||||
@@ -97,11 +96,6 @@
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the invited member to the team
|
||||
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
|
||||
|
||||
|
||||
@@ -84,6 +84,6 @@
|
||||
::cond/key-fn files/get-file-etag
|
||||
::cond/reuse-key? true
|
||||
::doc/added "1.17"}
|
||||
[{:keys [pool]} {:keys [::rpc/profile-id] :as params}]
|
||||
[{:keys [pool]} params]
|
||||
(with-open [conn (db/open pool)]
|
||||
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
||||
(get-view-only-bundle conn (assoc params :profile-id (::rpc/profile-id params)))))
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
@@ -23,18 +22,14 @@
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [uri] :as row}]
|
||||
(cond-> row
|
||||
(string? uri) (assoc :uri (u/uri uri))))
|
||||
|
||||
;; --- Mutation: Create Webhook
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::is-active ::us/boolean)
|
||||
(s/def ::mtype
|
||||
#{"application/json"
|
||||
"application/x-www-form-urlencoded"
|
||||
"application/transit+json"})
|
||||
|
||||
(s/def ::create-webhook
|
||||
@@ -65,7 +60,7 @@
|
||||
|
||||
(if (not= (:uri whook) (:uri params))
|
||||
(->> (http/req! cfg {:method :head
|
||||
:uri (str (:uri params))
|
||||
:uri (:uri params)
|
||||
:timeout (dt/duration "3s")})
|
||||
(p/hmap (fn [response exception]
|
||||
(if exception
|
||||
@@ -85,24 +80,22 @@
|
||||
|
||||
(defn- insert-webhook!
|
||||
[{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}]
|
||||
(-> (db/insert! pool :webhook
|
||||
{:id (uuid/next)
|
||||
:team-id team-id
|
||||
:uri (str uri)
|
||||
:is-active is-active
|
||||
:mtype mtype})
|
||||
(decode-row)))
|
||||
(db/insert! pool :webhook
|
||||
{:id (uuid/next)
|
||||
:team-id team-id
|
||||
:uri uri
|
||||
:is-active is-active
|
||||
:mtype mtype}))
|
||||
|
||||
(defn- update-webhook!
|
||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as wook} {:keys [uri mtype is-active] :as params}]
|
||||
(-> (db/update! pool :webhook
|
||||
{:uri (str uri)
|
||||
:is-active is-active
|
||||
:mtype mtype
|
||||
:error-code nil
|
||||
:error-count 0}
|
||||
{:id id})
|
||||
(decode-row)))
|
||||
(db/update! pool :webhook
|
||||
{:uri uri
|
||||
:is-active is-active
|
||||
:mtype mtype
|
||||
:error-code nil
|
||||
:error-count 0}
|
||||
{:id id}))
|
||||
|
||||
(sv/defmethod ::create-webhook
|
||||
{::doc/added "1.17"}
|
||||
@@ -118,7 +111,7 @@
|
||||
(sv/defmethod ::update-webhook
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
||||
(let [whook (db/get pool :webhook {:id id})]
|
||||
(check-edition-permissions! pool profile-id (:team-id whook))
|
||||
(->> (validate-webhook! cfg whook params)
|
||||
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
|
||||
@@ -131,7 +124,7 @@
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
(let [whook (db/get conn :webhook {:id id})]
|
||||
(check-edition-permissions! conn profile-id (:team-id whook))
|
||||
(db/delete! conn :webhook {:id id})
|
||||
nil)))
|
||||
@@ -151,5 +144,4 @@
|
||||
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||
(mapv decode-row))))
|
||||
(db/exec! conn [sql:get-webhooks team-id])))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files :as cmd.files]
|
||||
[app.rpc.commands.files.create :as cmd.files.create]
|
||||
@@ -176,8 +176,7 @@
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.13"
|
||||
::doc/deprecated "1.17"
|
||||
::audit/skip true}
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
||||
@@ -193,8 +192,7 @@
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.13"
|
||||
::doc/deprecated "1.17"
|
||||
::audit/skip true}
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(cmd.files/check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
@@ -50,9 +49,6 @@
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(teams/check-edition-permissions! pool profile-id team-id)
|
||||
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg params)))
|
||||
|
||||
(defn create-font-variant
|
||||
|
||||
@@ -6,49 +6,280 @@
|
||||
|
||||
(ns app.rpc.mutations.media
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.media :as media]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.media :as cmd.media]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def default-max-file-size (* 1024 1024 10)) ; 10 MiB
|
||||
|
||||
(def thumbnail-options
|
||||
{:width 100
|
||||
:height 100
|
||||
:quality 85
|
||||
:format :jpeg})
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
|
||||
(declare create-file-media-object)
|
||||
(declare select-file)
|
||||
|
||||
(s/def ::content ::media/upload)
|
||||
(s/def ::is-local ::us/boolean)
|
||||
|
||||
(s/def ::upload-file-media-object
|
||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::name ::content]
|
||||
:opt-un [::id]))
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.2"
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(let [file (select-file pool file-id)
|
||||
cfg (update cfg :storage media/configure-assets-storage)]
|
||||
|
||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
||||
(media/validate-media-type! content)
|
||||
(cmd.media/validate-content-size! content)
|
||||
(cmd.media/create-file-media-object cfg params)))
|
||||
|
||||
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
||||
(ex/raise :type :restriction
|
||||
:code :media-max-file-size-reached
|
||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||
(:size content)
|
||||
default-max-file-size)))
|
||||
|
||||
(create-file-media-object cfg params)))
|
||||
|
||||
(defn- big-enough-for-thumbnail?
|
||||
"Checks if the provided image info is big enough for
|
||||
create a separate thumbnail storage object."
|
||||
[info]
|
||||
(or (> (:width info) (:width thumbnail-options))
|
||||
(> (:height info) (:height thumbnail-options))))
|
||||
|
||||
(defn- svg-image?
|
||||
[info]
|
||||
(= (:mtype info) "image/svg+xml"))
|
||||
|
||||
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
||||
;; because postgresql does not returns anything if no update is
|
||||
;; performed, the `do update` does the trick.
|
||||
|
||||
(def sql:create-file-media-object
|
||||
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict (id) do update set created_at=file_media_object.created_at
|
||||
returning *")
|
||||
|
||||
;; NOTE: the following function executes without a transaction, this
|
||||
;; means that if something fails in the middle of this function, it
|
||||
;; will probably leave leaked/unreferenced objects in the database and
|
||||
;; probably in the storage layer. For handle possible object leakage,
|
||||
;; we create all media objects marked as touched, this ensures that if
|
||||
;; something fails, all leaked (already created storage objects) will
|
||||
;; be eventually marked as deleted by the touched-gc task.
|
||||
;;
|
||||
;; The touched-gc task, performs periodic analysis of all touched
|
||||
;; storage objects and check references of it. This is the reason why
|
||||
;; `reference` metadata exists: it indicates the name of the table
|
||||
;; witch holds the reference to storage object (it some kind of
|
||||
;; inverse, soft referential integrity).
|
||||
|
||||
(defn create-file-media-object
|
||||
[{:keys [storage pool climit executor] :as cfg}
|
||||
{:keys [id file-id is-local name content] :as params}]
|
||||
(letfn [;; Function responsible to retrieve the file information, as
|
||||
;; it is synchronous operation it should be wrapped into
|
||||
;; with-dispatch macro.
|
||||
(get-info [content]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run {:cmd :info :input content})))
|
||||
|
||||
;; Function responsible of calculating cryptographyc hash of
|
||||
;; the provided data.
|
||||
(calculate-hash [data]
|
||||
(px/with-dispatch executor
|
||||
(sto/calculate-hash data)))
|
||||
|
||||
;; Function responsible of generating thumnail. As it is synchronous
|
||||
;; opetation, it should be wrapped into with-dispatch macro
|
||||
(generate-thumbnail [info]
|
||||
(climit/with-dispatch (:process-image climit)
|
||||
(media/run (assoc thumbnail-options
|
||||
:cmd :generic-thumbnail
|
||||
:input info))))
|
||||
|
||||
(create-thumbnail [info]
|
||||
(when (and (not (svg-image? info))
|
||||
(big-enough-for-thumbnail? info))
|
||||
(p/let [thumb (generate-thumbnail info)
|
||||
hash (calculate-hash (:data thumb))
|
||||
content (-> (sto/content (:data thumb) (:size thumb))
|
||||
(sto/wrap-with-hash hash))]
|
||||
(sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype thumb)
|
||||
:bucket "file-media-object"}))))
|
||||
|
||||
(create-image [info]
|
||||
(p/let [data (:path info)
|
||||
hash (calculate-hash data)
|
||||
content (-> (sto/content data)
|
||||
(sto/wrap-with-hash hash))]
|
||||
(sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (dt/now)
|
||||
:content-type (:mtype info)
|
||||
:bucket "file-media-object"})))
|
||||
|
||||
(insert-into-database [info image thumb]
|
||||
(px/with-dispatch executor
|
||||
(db/exec-one! pool [sql:create-file-media-object
|
||||
(or id (uuid/next))
|
||||
file-id is-local name
|
||||
(:id image)
|
||||
(:id thumb)
|
||||
(:width info)
|
||||
(:height info)
|
||||
(:mtype info)])))]
|
||||
|
||||
(p/let [info (get-info content)
|
||||
thumb (create-thumbnail info)
|
||||
image (create-image info)]
|
||||
(insert-into-database info image thumb))))
|
||||
|
||||
;; --- Create File Media Object (from URL)
|
||||
|
||||
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
|
||||
(declare ^:private create-file-media-object-from-url)
|
||||
|
||||
(s/def ::create-file-media-object-from-url
|
||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::url]
|
||||
:opt-un [::id ::name]))
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.3"
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
||||
(let [file (select-file pool file-id)
|
||||
cfg (update cfg :storage media/configure-assets-storage)]
|
||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
||||
(create-file-media-object-from-url cfg params)))
|
||||
|
||||
(defn- create-file-media-object-from-url
|
||||
[cfg {:keys [url name] :as params}]
|
||||
(letfn [(parse-and-validate-size [headers]
|
||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||
mtype (get headers "content-type")
|
||||
format (cm/mtype->format mtype)
|
||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||
|
||||
(when-not size
|
||||
(ex/raise :type :validation
|
||||
:code :unknown-size
|
||||
:hint "seems like the url points to resource with unknown size"))
|
||||
|
||||
(when (> size max-size)
|
||||
(ex/raise :type :validation
|
||||
:code :file-too-large
|
||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||
size
|
||||
default-max-file-size)))
|
||||
|
||||
(when (nil? format)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "seems like the url points to an invalid media object"))
|
||||
|
||||
{:size size
|
||||
:mtype mtype
|
||||
:format format}))
|
||||
|
||||
(download-media [uri]
|
||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
||||
(p/then process-response)))
|
||||
|
||||
(process-response [{:keys [body headers] :as response}]
|
||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write-to-file! body path :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
|
||||
{:filename "tempfile"
|
||||
:size size
|
||||
:path path
|
||||
:mtype mtype}))]
|
||||
|
||||
(p/let [content (download-media url)]
|
||||
(->> (merge params {:content content :name (or name (:filename content))})
|
||||
(create-file-media-object cfg)))))
|
||||
|
||||
;; --- Clone File Media object (Upload and create from url)
|
||||
|
||||
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
|
||||
(declare clone-file-media-object)
|
||||
|
||||
(s/def ::clone-file-media-object
|
||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::id]))
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.2"
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(-> (assoc cfg :conn conn)
|
||||
(cmd.media/clone-file-media-object params))))
|
||||
(let [file (select-file conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
(-> (assoc cfg :conn conn)
|
||||
(clone-file-media-object params)))))
|
||||
|
||||
(defn clone-file-media-object
|
||||
[{:keys [conn] :as cfg} {:keys [id file-id is-local]}]
|
||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
:file-id file-id
|
||||
:is-local is-local
|
||||
:name (:name mobj)
|
||||
:media-id (:media-id mobj)
|
||||
:thumbnail-id (:thumbnail-id mobj)
|
||||
:width (:width mobj)
|
||||
:height (:height mobj)
|
||||
:mtype (:mtype mobj)})))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def ^:private
|
||||
sql:select-file
|
||||
"select file.*,
|
||||
project.team_id as team_id
|
||||
from file
|
||||
inner join project on (project.id = file.project_id)
|
||||
where file.id = ?")
|
||||
|
||||
(defn- select-file
|
||||
[conn id]
|
||||
(let [row (db/exec-one! conn [sql:select-file id])]
|
||||
(when-not row
|
||||
(ex/raise :type :not-found))
|
||||
row))
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -38,10 +37,6 @@
|
||||
[{:keys [pool] :as cfg} {:keys [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 [project (teams/create-project conn params)]
|
||||
(teams/create-project-role conn profile-id (:id project) :owner)
|
||||
|
||||
|
||||
@@ -179,5 +179,6 @@
|
||||
(sv/defmethod ::search-files
|
||||
{::doc/added "1.0"
|
||||
::doc/deprecated "1.17"}
|
||||
[{:keys [pool]} {:keys [profile-id team-id search-term]}]
|
||||
(some->> search-term (search/search-files pool profile-id team-id)))
|
||||
[{:keys [pool]} {:keys [search-term] :as params}]
|
||||
(when search-term
|
||||
(search/search-files pool params)))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
;; --- Query: Font Variants
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::font-variants
|
||||
|
||||
@@ -1,339 +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 app.rpc.quotes
|
||||
"Penpot resource usage quotes."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti check-quote ::id)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::conn ::db/conn-or-pool)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::incr (s/and int? pos?))
|
||||
(s/def ::target ::us/string)
|
||||
|
||||
(s/def ::quote
|
||||
(s/keys :req [::id ::profile-id]
|
||||
:opt [::conn
|
||||
::team-id
|
||||
::project-id
|
||||
::file-id
|
||||
::incr]))
|
||||
|
||||
(def ^:private enabled (volatile! true))
|
||||
|
||||
(defn enable!
|
||||
"Enable quotes checking at runtime (from server REPL)."
|
||||
[]
|
||||
(vswap! enabled (constantly true)))
|
||||
|
||||
(defn disable!
|
||||
"Disable quotes checking at runtime (from server REPL)."
|
||||
[]
|
||||
(vswap! enabled (constantly false)))
|
||||
|
||||
(defn check-quote!
|
||||
[conn quote]
|
||||
(us/assert! ::db/conn-or-pool conn)
|
||||
(us/assert! ::quote quote)
|
||||
(when (contains? cf/flags :quotes)
|
||||
(when @enabled
|
||||
(check-quote (assoc quote ::conn conn ::target (name (::id quote)))))))
|
||||
|
||||
(defn- send-notification!
|
||||
[{:keys [::conn] :as params}]
|
||||
(l/warn :hint "max quote reached"
|
||||
:target (::target params)
|
||||
:profile-id (some-> params ::profile-id str)
|
||||
:team-id (some-> params ::team-id str)
|
||||
:project-id (some-> params ::project-id str)
|
||||
:file-id (some-> params ::file-id str)
|
||||
:quote (::quote params)
|
||||
:total (::total params)
|
||||
:incr (::inc params 1))
|
||||
|
||||
(when-let [admins (seq (cf/get :admins))]
|
||||
(let [subject (str/istr "[quotes:notification]: max quote reached ~(::target params)")
|
||||
content (str/istr "- Param: profile-id '~(::profile-id params)}'\n"
|
||||
"- Param: team-id '~(::team-id params)'\n"
|
||||
"- Param: project-id '~(::project-id params)'\n"
|
||||
"- Param: file-id '~(::file-id params)'\n"
|
||||
"- Quote ID: '~(::target params)'\n"
|
||||
"- Max: ~(::quote params)\n"
|
||||
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
|
||||
(wrk/submit! {::wrk/task :sendmail
|
||||
::wrk/delay (dt/duration "30s")
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn
|
||||
::wrk/dedupe true
|
||||
::wrk/label "quotes-notification"
|
||||
:to (vec admins)
|
||||
:subject subject
|
||||
:body [{:type "text/plain"
|
||||
:content content}]}))))
|
||||
|
||||
(defn- generic-check!
|
||||
[{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
|
||||
(let [quote (->> (db/exec! conn quote-sql)
|
||||
(map :quote)
|
||||
(reduce max (- Integer/MAX_VALUE)))
|
||||
quote (if (pos? quote) quote default)
|
||||
total (->> (db/exec! conn count-sql) first :total)]
|
||||
|
||||
(when (> (+ total incr) quote)
|
||||
(if (contains? cf/flags :soft-quotes)
|
||||
(send-notification! (assoc params ::quote quote ::total total))
|
||||
(ex/raise :type :restriction
|
||||
:code :max-quote-reached
|
||||
:target target
|
||||
:quote quote
|
||||
:count total)))))
|
||||
|
||||
(def ^:private sql:get-quotes-1
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and profile_id = ?
|
||||
and team_id is null
|
||||
and project_id is null
|
||||
and file_id is null;")
|
||||
|
||||
(def ^:private sql:get-quotes-2
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-3
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
(def ^:private sql:get-quotes-4
|
||||
"select id, quote from usage_quote
|
||||
where target = ?
|
||||
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(project_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(team_id = ? and (profile_id = ? or profile_id is null)) or
|
||||
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAMS-PER-PROFILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-teams-per-profile
|
||||
"select count(*) as total
|
||||
from team_profile_rel
|
||||
where profile_id = ?")
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::teams-per-profile
|
||||
(s/keys :req [::profile-id ::target]))
|
||||
|
||||
(defmethod check-quote ::teams-per-profile
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(us/assert! ::teams-per-profile quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
|
||||
(generic-check!)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROJECTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-projects-per-team
|
||||
"select count(*) as total
|
||||
from project as p
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null")
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::projects-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::projects-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-projects-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FONT-VARIANTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-font-variants-per-team
|
||||
"select count(*) as total
|
||||
from team_font_variant as v
|
||||
where v.team_id = ?")
|
||||
|
||||
(s/def ::font-variants-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::font-variants-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::font-variants-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-font-variants-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: INVITATIONS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-invitations-per-team
|
||||
"select count(*) as total
|
||||
from team_invitation
|
||||
where team_id = ?")
|
||||
|
||||
(s/def ::invitations-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::invitations-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::invitations-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-invitations-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: PROFILES-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-profiles-per-team
|
||||
"select (select count(*)
|
||||
from team_profile_rel
|
||||
where team_id = ?) +
|
||||
(select count(*)
|
||||
from team_invitation
|
||||
where team_id = ?
|
||||
and valid_until > now()) as total;")
|
||||
|
||||
;; NOTE: the total number of profiles is determined by the number of
|
||||
;; effective members plus ongoing valid invitations.
|
||||
|
||||
(s/def ::profiles-per-team
|
||||
(s/keys :req [::profile-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::profiles-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::profiles-per-team quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: FILES-PER-PROJECT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-files-per-project
|
||||
"select count(*) as total
|
||||
from file as f
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null")
|
||||
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::files-per-project
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::files-per-project
|
||||
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-files-per-project project-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENT-THREADS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comment-threads-per-file
|
||||
"select count(*) as total
|
||||
from comment_thread as ct
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comment-threads-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comment-threads-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: COMMENTS-PER-FILE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-comments-per-file
|
||||
"select count(*) as total
|
||||
from comment as c
|
||||
join comment_thread as ct on (ct.id = c.thread_id)
|
||||
where ct.file_id = ?")
|
||||
|
||||
(s/def ::comments-per-file
|
||||
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
|
||||
|
||||
(defmethod check-quote ::comments-per-file
|
||||
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
|
||||
(us/assert! ::files-per-project quote)
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
|
||||
profile-id team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-comments-per-file file-id])
|
||||
(generic-check!)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: DEFAULT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod check-quote :default
|
||||
[{:keys [::id]}]
|
||||
(ex/raise :type :internal
|
||||
:code :quote-not-defined
|
||||
:quote id
|
||||
:hint "backend using a quote identifier not defined"))
|
||||
@@ -182,94 +182,78 @@
|
||||
(assoc ::lresult/remaining remaining)
|
||||
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
|
||||
|
||||
(defn- process-limits!
|
||||
(defn- process-limits
|
||||
[redis user-id limits now]
|
||||
(->> (p/all (map (partial process-limit redis user-id now) limits))
|
||||
(p/fmap (fn [results]
|
||||
(let [remaining (->> results
|
||||
(d/index-by ::name ::lresult/remaining)
|
||||
(uri/map->query-string))
|
||||
reset (->> results
|
||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||
(uri/map->query-string))
|
||||
rejected (->> results
|
||||
(filter (complement ::lresult/allowed?))
|
||||
(first))]
|
||||
(-> (p/all (map (partial process-limit redis user-id now) limits))
|
||||
(p/then (fn [results]
|
||||
(let [remaining (->> results
|
||||
(d/index-by ::name ::lresult/remaining)
|
||||
(uri/map->query-string))
|
||||
reset (->> results
|
||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||
(uri/map->query-string))
|
||||
rejected (->> results
|
||||
(filter (complement ::lresult/allowed?))
|
||||
(first))]
|
||||
|
||||
(when rejected
|
||||
(l/warn :hint "rejected rate limit"
|
||||
:user-id (str user-id)
|
||||
:limit-service (-> rejected ::service name)
|
||||
:limit-name (-> rejected ::name name)
|
||||
:limit-strategy (-> rejected ::strategy name)))
|
||||
(when rejected
|
||||
(l/warn :hint "rejected rate limit"
|
||||
:user-id (str user-id)
|
||||
:limit-service (-> rejected ::service name)
|
||||
:limit-name (-> rejected ::name name)
|
||||
:limit-strategy (-> rejected ::strategy name)))
|
||||
|
||||
{:enabled? true
|
||||
:allowed? (not (some? rejected))
|
||||
:headers {"x-rate-limit-remaining" remaining
|
||||
"x-rate-limit-reset" reset}})))))
|
||||
{:enabled? true
|
||||
:allowed? (not (some? rejected))
|
||||
:headers {"x-rate-limit-remaining" remaining
|
||||
"x-rate-limit-reset" reset}})))))
|
||||
|
||||
(defn- handle-response
|
||||
[f cfg params result]
|
||||
(if (:enabled? result)
|
||||
(let [headers (:headers result)]
|
||||
(if (:allowed? result)
|
||||
(->> (f cfg params)
|
||||
(p/fmap (fn [response]
|
||||
(vary-meta response update ::http/headers merge headers))))
|
||||
(p/rejected
|
||||
(ex/error :type :rate-limit
|
||||
:code :request-blocked
|
||||
:hint "rate limit reached"
|
||||
::http/headers headers))))
|
||||
(when-not (:allowed? result)
|
||||
(ex/raise :type :rate-limit
|
||||
:code :request-blocked
|
||||
:hint "rate limit reached"
|
||||
::http/headers headers))
|
||||
(-> (f cfg params)
|
||||
(p/then (fn [response]
|
||||
(vary-meta response update ::http/headers merge headers)))))
|
||||
(f cfg params)))
|
||||
|
||||
(defn- get-limits
|
||||
[state skey sname]
|
||||
(some->> (or (get-in @state [::limits skey])
|
||||
(get-in @state [::limits :default]))
|
||||
(map #(assoc % ::service sname))
|
||||
(seq)))
|
||||
|
||||
(defn- get-uid
|
||||
[{:keys [::http/request] :as params}]
|
||||
(or (::rpc/profile-id params)
|
||||
(some-> request parse-client-ip)
|
||||
uuid/zero))
|
||||
|
||||
(defn wrap
|
||||
[{:keys [rlimit redis] :as cfg} f mdata]
|
||||
(if rlimit
|
||||
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
||||
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
|
||||
(fn [cfg {:keys [::http/request] :as params}]
|
||||
(let [uid (or (:profile-id params)
|
||||
(some-> request parse-client-ip)
|
||||
uuid/zero)
|
||||
|
||||
(fn [cfg params]
|
||||
(if @enabled?
|
||||
(try
|
||||
(let [uid (get-uid params)
|
||||
rsp (when-let [limits (get-limits rlimit skey sname)]
|
||||
(let [redis (redis/get-or-connect redis ::rpc/rlimit default-options)
|
||||
rsp (->> (process-limits! redis uid limits (dt/now))
|
||||
(p/merr (fn [cause]
|
||||
;; If we have an error on processing the rate-limit we just skip
|
||||
;; it for do not cause service interruption because of redis
|
||||
;; downtime or similar situation.
|
||||
(l/error :hint "error on processing rate-limit" :cause cause)
|
||||
(p/resolved {:enabled? false}))))]
|
||||
rsp (when (and uid @enabled?)
|
||||
(when-let [limits (or (get-in @rlimit [::limits skey])
|
||||
(get-in @rlimit [::limits :default]))]
|
||||
(let [redis (redis/get-or-connect redis ::rlimit default-options)
|
||||
limits (map #(assoc % ::service sname) limits)
|
||||
resp (-> (process-limits redis uid limits (dt/now))
|
||||
(p/catch (fn [cause]
|
||||
;; If we have an error on processing the rate-limit we just skip
|
||||
;; it for do not cause service interruption because of redis
|
||||
;; downtime or similar situation.
|
||||
(l/error :hint "error on processing rate-limit" :cause cause)
|
||||
{:enabled? false})))]
|
||||
|
||||
;; If soft rate are enabled, we process the rate-limit but return unprotected
|
||||
;; response.
|
||||
(if (contains? cf/flags :soft-rpc-rlimit)
|
||||
{:enabled? false}
|
||||
rsp)))]
|
||||
;; If soft rate are enabled, we process the rate-limit but return unprotected
|
||||
;; response.
|
||||
(if (contains? cf/flags :soft-rpc-rlimit)
|
||||
(p/resolved {:enabled? false})
|
||||
resp))))
|
||||
|
||||
(->> (p/promise rsp)
|
||||
(p/fmap #(or % {:enabled? false}))
|
||||
(p/mcat #(handle-response f cfg params %))))
|
||||
rsp (or rsp (p/resolved {:enabled? false}))]
|
||||
|
||||
(catch Throwable cause
|
||||
(p/rejected cause)))
|
||||
|
||||
(f cfg params))))
|
||||
(p/then rsp (partial handle-response f cfg params)))))
|
||||
f))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
[app.setup.builtin-templates]
|
||||
[app.setup.initial-user]
|
||||
[app.setup.keys :as keys]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
@@ -68,5 +69,5 @@
|
||||
(let [secret (or key (generate-random-key))]
|
||||
(-> (retrieve-all conn)
|
||||
(assoc :secret-key secret)
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||
(assoc :tokens-key (keys/derive secret :salt "tokens" :size 32))
|
||||
(update :instance-id handle-instance-id conn (db/read-only? pool))))))
|
||||
|
||||
41
backend/src/app/setup/initial_user.clj
Normal file
41
backend/src/app/setup/initial_user.clj
Normal file
@@ -0,0 +1,41 @@
|
||||
;; 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.initial-user
|
||||
"Initial data setup of instance."
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.setup :as-alias setup]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:insert-profile
|
||||
"insert into profile (id, fullname, email, password, is_active, is_admin, created_at, modified_at)
|
||||
values ('00000000-0000-0000-0000-000000000000', 'Admin', ?, ?, true, true, now(), now())
|
||||
on conflict (id)
|
||||
do update set email = ?, password = ?")
|
||||
|
||||
(defmethod ig/pre-init-spec ::setup/initial-profile [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::setup/initial-profile
|
||||
[_ {:keys [::db/pool]}]
|
||||
(let [email (cf/get :setup-admin-email)
|
||||
password (cf/get :setup-admin-password)]
|
||||
(when (and email password)
|
||||
(db/with-atomic [conn pool]
|
||||
(let [pwd (auth/derive-password password)]
|
||||
(db/exec-one! conn [sql:insert-profile email pwd email pwd])
|
||||
(l/info :hint "setting initial user (admin)"
|
||||
:email email
|
||||
:password "********"))))
|
||||
nil))
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
(defn derive
|
||||
"Derive a key from secret-key"
|
||||
[secret-key & {:keys [salt size] :or {size 32}}]
|
||||
[secret-key & {:keys [salt size]}]
|
||||
(us/assert! ::us/not-empty-string secret-key)
|
||||
(let [engine (bk/engine {:key secret-key
|
||||
:salt salt
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.srepl.ext]
|
||||
[app.srepl.main]
|
||||
[app.util.json :as json]
|
||||
[app.util.locks :as locks]
|
||||
[clojure.core.server :as ccs]
|
||||
[clojure.main :as cm]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -24,59 +20,39 @@
|
||||
(ccs/repl-init)
|
||||
(in-ns 'app.srepl.main))
|
||||
|
||||
(defn user-repl
|
||||
(defn repl
|
||||
[]
|
||||
(cm/repl
|
||||
:init repl-init
|
||||
:read ccs/repl-read))
|
||||
|
||||
(defn json-repl
|
||||
[]
|
||||
(let [out *out*
|
||||
lock (locks/create)]
|
||||
(ccs/prepl *in*
|
||||
(fn [m]
|
||||
(binding [*out* out, *flush-on-newline* true, *print-readably* true]
|
||||
(locks/locking lock
|
||||
(println (json/encode-str m))))))))
|
||||
|
||||
;; --- State initialization
|
||||
|
||||
(s/def ::port ::us/integer)
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::port int?)
|
||||
(s/def ::host ::us/not-empty-string)
|
||||
(s/def ::flag #{:urepl-server :prepl-server})
|
||||
(s/def ::type #{::prepl ::urepl})
|
||||
(s/def ::key (s/tuple ::type ::us/keyword))
|
||||
|
||||
(defmethod ig/pre-init-spec ::server
|
||||
[_]
|
||||
(s/keys :req [::flag]
|
||||
:req-un [::port ::host]))
|
||||
(s/keys :opt-un [::port ::host ::name]))
|
||||
|
||||
(defmethod ig/prep-key ::server
|
||||
[[type _] cfg]
|
||||
(assoc cfg ::flag (keyword (str (name type) "-server"))))
|
||||
[_ cfg]
|
||||
(merge {:name "main"} cfg))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[[type _] {:keys [::flag port host] :as cfg}]
|
||||
(when (contains? cf/flags flag)
|
||||
(let [accept (case type
|
||||
::prepl 'app.srepl/json-repl
|
||||
::urepl 'app.srepl/user-repl)
|
||||
params {:address host
|
||||
:port port
|
||||
:name (name type)
|
||||
:accept accept}]
|
||||
|
||||
(l/info :msg "initializing repl server"
|
||||
:name (name type)
|
||||
:port port
|
||||
:host host)
|
||||
|
||||
(ccs/start-server params)
|
||||
|
||||
params)))
|
||||
[_ {:keys [port host name] :as cfg}]
|
||||
(when (and port host name)
|
||||
(l/info :msg "initializing server repl" :port port :host host :name name)
|
||||
(ccs/start-server {:address host
|
||||
:port port
|
||||
:name name
|
||||
:accept 'app.srepl/repl})
|
||||
cfg))
|
||||
|
||||
(defmethod ig/halt-key! ::server
|
||||
[_ params]
|
||||
(some-> params :name ccs/stop-server))
|
||||
[_ cfg]
|
||||
(when cfg
|
||||
(ccs/stop-server (:name cfg))))
|
||||
|
||||
|
||||
|
||||
@@ -1,77 +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 app.srepl.ext
|
||||
"PREPL API for external usage (CLI or ADMIN)"
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.util.json :as json]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- get-current-system
|
||||
[]
|
||||
(or (deref (requiring-resolve 'app.main/system))
|
||||
(deref (requiring-resolve 'user/system))))
|
||||
|
||||
(defmulti ^:private run-json-cmd* ::cmd)
|
||||
|
||||
(defn run-json-cmd
|
||||
"Entry point with external tools integrations that uses PREPL
|
||||
interface for interacting with running penpot backend."
|
||||
[data]
|
||||
(let [data (json/decode data)
|
||||
params (merge {::cmd (keyword (:cmd data "default"))}
|
||||
(:params data))]
|
||||
(run-json-cmd* params)))
|
||||
|
||||
(defmethod run-json-cmd* :create-profile
|
||||
[{:keys [fullname email password is-active]
|
||||
:or {is-active true}}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile conn params)
|
||||
(cmd.auth/create-profile-relations conn))))))
|
||||
|
||||
(defmethod run-json-cmd* :update-profile
|
||||
[{:keys [fullname email password is-active]}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [params (cond-> {}
|
||||
(some? fullname)
|
||||
(assoc :fullname fullname)
|
||||
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
(when (seq params)
|
||||
(let [res (db/update! conn :profile
|
||||
params
|
||||
{:email email
|
||||
:deleted-at nil}
|
||||
{:return-keys false})]
|
||||
(pos? (:next.jdbc/update-count res))))))))
|
||||
|
||||
(defmethod run-json-cmd* :derive-password
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
|
||||
(defmethod run-json-cmd* :default
|
||||
[{:keys [::cmd]}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/ffmt "command '%' not implemented" (name cmd))))
|
||||
@@ -41,35 +41,3 @@
|
||||
([file state]
|
||||
(repair-orphaned-shapes (:data file))
|
||||
(update state :total (fnil inc 0))))
|
||||
|
||||
(defn rename-layout-attrs
|
||||
([file]
|
||||
(let [found? (volatile! false)]
|
||||
(letfn [(update-shape
|
||||
[shape]
|
||||
(when (or (= (:layout-flex-dir shape) :reverse-row)
|
||||
(= (:layout-flex-dir shape) :reverse-column)
|
||||
(= (:layout-wrap-type shape) :no-wrap))
|
||||
(vreset! found? true))
|
||||
(cond-> shape
|
||||
(= (:layout-flex-dir shape) :reverse-row)
|
||||
(assoc :layout-flex-dir :row-reverse)
|
||||
(= (:layout-flex-dir shape) :reverse-column)
|
||||
(assoc :layout-flex-dir :column-reverse)
|
||||
(= (:layout-wrap-type shape) :no-wrap)
|
||||
(assoc :layout-wrap-type :nowrap)))
|
||||
|
||||
(update-page
|
||||
[page]
|
||||
(h/update-shapes page update-shape))]
|
||||
|
||||
(let [new-file (update file :data h/update-pages update-page)]
|
||||
(when @found?
|
||||
(l/info :hint "Found attrs to rename in file"
|
||||
:id (:id file)
|
||||
:name (:name file)))
|
||||
new-file))))
|
||||
|
||||
([file state]
|
||||
(rename-layout-attrs file)
|
||||
(update state :total (fnil inc 0))))
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.auth :refer [derive-password]]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.features :as ffeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
@@ -22,11 +21,8 @@
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.main :refer [system]]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.queries.profile :as prof]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.stacktrace :as strace]
|
||||
@@ -70,33 +66,23 @@
|
||||
[system & {:keys [update-fn id save? migrate? inc-revn?]
|
||||
:or {save? false migrate? true inc-revn? true}}]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [file (-> (db/get-by-id conn :file id {:for-update true})
|
||||
(update :features db/decode-pgarray #{}))]
|
||||
(binding [*conn* conn
|
||||
pmap/*tracked* (atom {})
|
||||
pmap/*load-fn* (partial files/load-pointer conn id)
|
||||
ffeat/*wrap-with-pointer-map-fn*
|
||||
(if (contains? (:features file) "storage/pointer-map") pmap/wrap identity)
|
||||
ffeat/*wrap-with-objects-map-fn*
|
||||
(if (contains? (:features file) "storage/objectd-map") omap/wrap identity)]
|
||||
(let [file (-> file
|
||||
(update :data blob/decode)
|
||||
(cond-> migrate? (update :data pmg/migrate-data))
|
||||
(update-fn)
|
||||
(cond-> inc-revn? (update :revn inc)))]
|
||||
(when save?
|
||||
(let [features (db/create-array conn "text" (:features file))
|
||||
data (blob/encode (:data file))]
|
||||
(db/update! conn :file
|
||||
{:data data
|
||||
:revn (:revn file)
|
||||
:features features}
|
||||
{:id id})
|
||||
|
||||
(when (contains? (:features file) "storage/pointer-map")
|
||||
(files/persist-pointers! conn id))))
|
||||
|
||||
(dissoc file :data))))))
|
||||
(let [file (db/get-by-id conn :file id {:for-update true})
|
||||
file (-> file
|
||||
(update :features db/decode-pgarray #{})
|
||||
(update :data blob/decode)
|
||||
(cond-> migrate? (update :data pmg/migrate-data)))
|
||||
file (binding [*conn* conn]
|
||||
(-> (update-fn file)
|
||||
(cond-> inc-revn? (update :revn inc))))]
|
||||
(when save?
|
||||
(let [features (db/create-array conn "text" (:features file))
|
||||
data (blob/encode (:data file))]
|
||||
(db/update! conn :file
|
||||
{:data data
|
||||
:revn (:revn file)
|
||||
:features features}
|
||||
{:id id})))
|
||||
file)))
|
||||
|
||||
(def ^:private sql:retrieve-files-chunk
|
||||
"SELECT id, name, created_at, data FROM file
|
||||
|
||||
@@ -110,10 +110,10 @@
|
||||
(if (contains? features "storage/objects-map")
|
||||
file
|
||||
(-> file
|
||||
(update :data migrate)
|
||||
(update :data migrate-to-omap)
|
||||
(update :features conj "storage/objects-map"))))
|
||||
|
||||
(migrate [data]
|
||||
(migrate-to-omap [data]
|
||||
(-> data
|
||||
(update :pages-index update-vals #(update % :objects omap/wrap))
|
||||
(update :components update-vals #(update % :objects omap/wrap))))]
|
||||
@@ -125,17 +125,24 @@
|
||||
|
||||
(defn enable-pointer-map-feature-on-file!
|
||||
[system & {:keys [save? id]}]
|
||||
(letfn [(update-file [{:keys [features] :as file}]
|
||||
(letfn [(update-file [{:keys [features id] :as file}]
|
||||
(if (contains? features "storage/pointer-map")
|
||||
file
|
||||
(-> file
|
||||
(update :data migrate)
|
||||
(update :data migrate-to-omap id)
|
||||
(update :features conj "storage/pointer-map"))))
|
||||
|
||||
(migrate [data]
|
||||
(-> data
|
||||
(update :pages-index update-vals pmap/wrap)
|
||||
(update :components pmap/wrap)))]
|
||||
(migrate-to-omap [data file-id]
|
||||
(binding [pmap/*tracked* (atom {})]
|
||||
(let [data (-> data
|
||||
(update :pages-index update-vals pmap/wrap)
|
||||
(update :components pmap/wrap))]
|
||||
(doseq [[id item] @pmap/*tracked*]
|
||||
(db/insert! h/*conn* :file-data-fragment
|
||||
{:id id
|
||||
:file-id file-id
|
||||
:content (-> item deref blob/encode)}))
|
||||
data)))]
|
||||
|
||||
(h/update-file! system
|
||||
:id id
|
||||
|
||||
@@ -24,9 +24,7 @@
|
||||
(:import
|
||||
java.io.FilterInputStream
|
||||
java.io.InputStream
|
||||
java.net.URI
|
||||
java.nio.ByteBuffer
|
||||
java.nio.file.Path
|
||||
java.time.Duration
|
||||
java.util.Collection
|
||||
java.util.Optional
|
||||
@@ -42,7 +40,6 @@
|
||||
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
|
||||
software.amazon.awssdk.regions.Region
|
||||
software.amazon.awssdk.services.s3.S3AsyncClient
|
||||
software.amazon.awssdk.services.s3.S3Configuration
|
||||
software.amazon.awssdk.services.s3.model.Delete
|
||||
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
||||
@@ -154,51 +151,46 @@
|
||||
|
||||
(defn build-s3-client
|
||||
[{:keys [region endpoint executor]}]
|
||||
(let [aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
||||
(.build))
|
||||
(let [hclient (.. (NettyNioAsyncHttpClient/builder)
|
||||
(eventLoopGroupBuilder (.. (SdkEventLoopGroup/builder)
|
||||
(numberOfThreads (int default-eventloop-threads))))
|
||||
(connectionAcquisitionTimeout default-timeout)
|
||||
(connectionTimeout default-timeout)
|
||||
(readTimeout default-timeout)
|
||||
(writeTimeout default-timeout)
|
||||
(build))
|
||||
client (.. (S3AsyncClient/builder)
|
||||
(asyncConfiguration (.. (ClientAsyncConfiguration/builder)
|
||||
(advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR
|
||||
executor)
|
||||
(build)))
|
||||
(httpClient hclient)
|
||||
(region (lookup-region region)))]
|
||||
|
||||
sconfig (-> (S3Configuration/builder)
|
||||
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
|
||||
(.build))
|
||||
(when-let [uri (some-> endpoint (java.net.URI.))]
|
||||
(.endpointOverride client uri))
|
||||
|
||||
hclient (-> (NettyNioAsyncHttpClient/builder)
|
||||
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
|
||||
(.numberOfThreads (int default-eventloop-threads))))
|
||||
(.connectionAcquisitionTimeout default-timeout)
|
||||
(.connectionTimeout default-timeout)
|
||||
(.readTimeout default-timeout)
|
||||
(.writeTimeout default-timeout)
|
||||
(.build))
|
||||
(let [client (.build client)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] client)
|
||||
|
||||
client (-> (S3AsyncClient/builder)
|
||||
(.serviceConfiguration ^S3Configuration sconfig)
|
||||
(.asyncConfiguration ^ClientAsyncConfiguration aconfig)
|
||||
(.httpClient ^NettyNioAsyncHttpClient hclient)
|
||||
(.region (lookup-region region))
|
||||
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint)))
|
||||
(.build))]
|
||||
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] client)
|
||||
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(.close ^NettyNioAsyncHttpClient hclient)
|
||||
(.close ^S3AsyncClient client)))))
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(.close hclient)
|
||||
(.close client))))))
|
||||
|
||||
(defn build-s3-presigner
|
||||
[{:keys [region endpoint]}]
|
||||
(let [config (-> (S3Configuration/builder)
|
||||
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
|
||||
(.build))]
|
||||
|
||||
(-> (S3Presigner/builder)
|
||||
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint)))
|
||||
(.region (lookup-region region))
|
||||
(.serviceConfiguration ^S3Configuration config)
|
||||
(.build))))
|
||||
(if (string? endpoint)
|
||||
(let [uri (java.net.URI. endpoint)]
|
||||
(.. (S3Presigner/builder)
|
||||
(endpointOverride uri)
|
||||
(region (lookup-region region))
|
||||
(build)))
|
||||
(.. (S3Presigner/builder)
|
||||
(region (lookup-region region))
|
||||
(build))))
|
||||
|
||||
(defn- make-request-body
|
||||
[content]
|
||||
@@ -206,7 +198,7 @@
|
||||
buff-size (* 1024 64)
|
||||
sem (Semaphore. 0)
|
||||
|
||||
writer-fn (fn [^Subscriber s]
|
||||
writer-fn (fn [s]
|
||||
(try
|
||||
(loop []
|
||||
(.acquire sem 1)
|
||||
@@ -269,7 +261,7 @@
|
||||
;; not, read the contento into memory using bytearrays.
|
||||
(if (> size (* 1024 1024 2))
|
||||
(p/let [path (tmp/tempfile :prefix "penpot.storage.s3.")
|
||||
rxf (AsyncResponseTransformer/toFile ^Path path)
|
||||
rxf (AsyncResponseTransformer/toFile path)
|
||||
_ (.getObject ^S3AsyncClient client
|
||||
^GetObjectRequest gor
|
||||
^AsyncResponseTransformer rxf)]
|
||||
@@ -291,9 +283,9 @@
|
||||
(key (str prefix (impl/id->path id)))
|
||||
(build))
|
||||
rxf (AsyncResponseTransformer/toBytes)
|
||||
obj (.getObject ^S3AsyncClient client
|
||||
^GetObjectRequest gor
|
||||
^AsyncResponseTransformer rxf)]
|
||||
obj (.getObjectAsBytes ^S3AsyncClient client
|
||||
^GetObjectRequest gor
|
||||
^AsyncResponseTransformer rxf)]
|
||||
(.asByteArray ^ResponseBytes obj)))
|
||||
|
||||
(def default-max-age
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
(def default-flags
|
||||
[:enable-secure-session-cookies
|
||||
:enable-email-verification
|
||||
:enable-smtp
|
||||
:enable-quotes])
|
||||
:enable-smtp])
|
||||
|
||||
(defn state-init
|
||||
[next]
|
||||
@@ -323,9 +322,7 @@
|
||||
[{:keys [::type] :as data}]
|
||||
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])]
|
||||
;; (app.common.pprint/pprint (:app.rpc/methods *system*))
|
||||
(try-on! (method-fn (-> data
|
||||
(dissoc ::type)
|
||||
(assoc :app.rpc/request-at (dt/now)))))))
|
||||
(try-on! (method-fn (dissoc data ::type)))))
|
||||
|
||||
(defn mutation!
|
||||
[{:keys [::type profile-id] :as data}]
|
||||
|
||||
@@ -1,290 +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 backend-tests.rpc-comment-test
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.comments :as comments]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.quotes :as-alias quotes]
|
||||
[app.util.time :as dt]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest comment-and-threads-crud
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-teams-per-profile 200})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1 {:is-active true})
|
||||
profile-2 (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id profile-1)})
|
||||
;; role (th/create-team-role* {:team-id (:id team)
|
||||
;; :profile-id (:id profile-2)
|
||||
;; :role :admin})
|
||||
|
||||
project (th/create-project* 1 {:team-id (:id team)
|
||||
:profile-id (:id profile-1)})
|
||||
file-1 (th/create-file* 1 {:profile-id (:id profile-1)
|
||||
:project-id (:id project)})
|
||||
file-2 (th/create-file* 2 {:profile-id (:id profile-1)
|
||||
:project-id (:id project)})
|
||||
page-id (get-in file-1 [:data :pages 0])]
|
||||
|
||||
(t/testing "comment thread creation"
|
||||
(let [data {::th/type :create-comment-thread
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:file-id (:id file-1)
|
||||
:page-id page-id
|
||||
:position (gpt/point 0)
|
||||
:content "hello world"
|
||||
:frame-id uuid/zero}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:file-id result)))
|
||||
(t/is (uuid? (:page-id result)))
|
||||
(t/is (uuid? (:comment-id result)))
|
||||
(t/is (= (:file-id result) (:id file-1)))
|
||||
(t/is (= (:page-id result) page-id)))))
|
||||
|
||||
(t/testing "comment thread status update"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
;; comment (-> (th/db-query :comment {:thread-id (:id thread)}) first)
|
||||
data {::th/type :update-comment-thread-status
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:id (:id thread)}
|
||||
status (th/db-get :comment-thread-status
|
||||
{:thread-id (:id thread)
|
||||
:profile-id (:id profile-1)})]
|
||||
|
||||
|
||||
(t/is (= (:modified-at status) (:modified-at thread)))
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (dt/instant? (:modified-at result))))
|
||||
|
||||
(let [status' (th/db-get :comment-thread-status
|
||||
{:thread-id (:id thread)
|
||||
:profile-id (:id profile-1)})]
|
||||
(t/is (not= (:modified-at status') (:modified-at thread))))))
|
||||
|
||||
(t/testing "comment thread status update 2"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :update-comment-thread-status
|
||||
::rpc/profile-id (:id profile-2)
|
||||
:id (:id thread)}]
|
||||
|
||||
(let [{:keys [error] :as out} (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type error))))))
|
||||
|
||||
(t/testing "update comment thread"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :update-comment-thread
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:is-resolved true
|
||||
:id (:id thread)}]
|
||||
|
||||
(t/is (false? (:is-resolved thread)))
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (nil? result)))
|
||||
|
||||
(let [thread (th/db-get :comment-thread {:id (:id thread)})]
|
||||
(t/is (true? (:is-resolved thread))))))
|
||||
|
||||
(t/testing "create comment"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :create-comment
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:thread-id (:id thread)
|
||||
:content "comment 2"}]
|
||||
(let [{:keys [result] :as out} (th/command! data)
|
||||
{:keys [modified-at]} (th/db-get :comment-thread-status
|
||||
{:thread-id (:id thread)
|
||||
:profile-id (:id profile-1)})]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (= (:owner-id result) (:id profile-1)))
|
||||
(t/is (:modified-at result) modified-at))))
|
||||
|
||||
(t/testing "update comment"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2"}) first)
|
||||
data {::th/type :update-comment
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:id (:id comment)
|
||||
:content "comment 2 mod"}]
|
||||
(let [{:keys [result] :as out} (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (nil? result)))
|
||||
|
||||
(let [comment' (th/db-get :comment {:id (:id comment)})]
|
||||
(t/is (not= (:modified-at comment) (:modified-at comment')))
|
||||
(t/is (= (:content data) (:content comment'))))))
|
||||
|
||||
|
||||
(t/testing "retrieve threads"
|
||||
(let [data {::th/type :get-comment-threads
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:file-id (:id file-1)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [[thread :as result] (:result out)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= "Page 1" (:page-name thread)))
|
||||
(t/is (= "hello world" (:content thread)))
|
||||
(t/is (= 2 (:count-comments thread)))
|
||||
(t/is (true? (:is-resolved thread))))))
|
||||
|
||||
|
||||
(t/testing "unread comment threads"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :get-unread-comment-threads
|
||||
::rpc/profile-id (:id profile-1)}]
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:default-team-id profile-1)))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= [] result)))
|
||||
|
||||
(let [{:keys [error] :as out} (th/command! (assoc data :team-id (:default-team-id profile-2)))]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type error))))
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:id team)))]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [[thread :as result] (:result out)]
|
||||
(t/is (= 1 (count result)))))
|
||||
|
||||
(let [data {::th/type :update-comment-thread-status
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:id (:id thread)}
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out)))
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:id team)))]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 0 (count result)))))))
|
||||
|
||||
(t/testing "get comment thread"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :get-comment-thread
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:file-id (:id file-1)
|
||||
:id (:id thread)}]
|
||||
|
||||
(let [{:keys [result] :as out} (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (= (:id thread) (:id result))))))
|
||||
|
||||
(t/testing "get comments"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :get-comments
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:thread-id (:id thread)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [comments (:result out)]
|
||||
(t/is (= 2 (count comments))))))
|
||||
|
||||
(t/testing "get profiles"
|
||||
(let [data {::th/type :get-profiles-for-file-comments
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:file-id (:id file-1)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [[profile :as profiles] (:result out)]
|
||||
(t/is (= 1 (count profiles)))
|
||||
(t/is (= (:id profile-1) (:id profile))))))
|
||||
|
||||
(t/testing "get profiles 2"
|
||||
(let [data {::th/type :get-profiles-for-file-comments
|
||||
::rpc/profile-id (:id profile-2)
|
||||
:file-id (:id file-1)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))))
|
||||
|
||||
(t/testing "delete comment"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2 mod"}) first)
|
||||
data {::th/type :delete-comment
|
||||
::rpc/profile-id (:id profile-2)
|
||||
:id (:id comment)}
|
||||
out (th/command! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))
|
||||
(let [comments (th/db-query :comment {:thread-id (:id thread)})]
|
||||
(t/is (= 2 (count comments))))))
|
||||
|
||||
(t/testing "delete comment 2"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2 mod"}) first)
|
||||
data {::th/type :delete-comment
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:id (:id comment)}
|
||||
out (th/command! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [comments (th/db-query :comment {:thread-id (:id thread)})]
|
||||
(t/is (= 1 (count comments))))))
|
||||
|
||||
(t/testing "delete comment thread"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :delete-comment-thread
|
||||
::rpc/profile-id (:id profile-2)
|
||||
:id (:id thread)}
|
||||
out (th/command! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :not-found (th/ex-type (:error out))))
|
||||
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
|
||||
(t/is (= 1 (count threads))))))
|
||||
|
||||
(t/testing "delete comment thread 2"
|
||||
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
|
||||
data {::th/type :delete-comment-thread
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:id (:id thread)}
|
||||
out (th/command! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
|
||||
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
|
||||
(t/is (= 0 (count threads))))))
|
||||
|
||||
)))
|
||||
@@ -6,14 +6,13 @@
|
||||
|
||||
(ns backend-tests.rpc-file-test
|
||||
(:require
|
||||
[backend-tests.helpers :as th]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]))
|
||||
|
||||
@@ -29,13 +28,13 @@
|
||||
|
||||
(t/testing "create file"
|
||||
(let [data {::th/type :create-file
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:project-id proj-id
|
||||
:id file-id
|
||||
:name "foobar"
|
||||
:is-shared false
|
||||
:components-v2 true}
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
@@ -48,8 +47,8 @@
|
||||
(let [data {::th/type :rename-file
|
||||
:id file-id
|
||||
:name "new name"
|
||||
::rpc/profile-id (:id prof)}
|
||||
out (th/command! data)]
|
||||
:profile-id (:id prof)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(let [result (:result out)]
|
||||
@@ -57,10 +56,10 @@
|
||||
(t/is (= (:name data) (:name result))))))
|
||||
|
||||
(t/testing "query files"
|
||||
(let [data {::th/type :get-project-files
|
||||
::rpc/profile-id (:id prof)
|
||||
:project-id proj-id}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :project-files
|
||||
:project-id proj-id
|
||||
:profile-id (:id prof)}
|
||||
out (th/query! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
@@ -71,11 +70,11 @@
|
||||
(t/is (= "new name" (get-in result [0 :name]))))))
|
||||
|
||||
(t/testing "query single file without users"
|
||||
(let [data {::th/type :get-file
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :file
|
||||
:profile-id (:id prof)
|
||||
:id file-id
|
||||
:components-v2 true}
|
||||
out (th/command! data)]
|
||||
out (th/query! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
@@ -89,18 +88,18 @@
|
||||
(t/testing "delete file"
|
||||
(let [data {::th/type :delete-file
|
||||
:id file-id
|
||||
::rpc/profile-id (:id prof)}
|
||||
out (th/command! data)]
|
||||
:profile-id (:id prof)}
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))))
|
||||
|
||||
(t/testing "query single file after delete"
|
||||
(let [data {::th/type :get-file
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :file
|
||||
:profile-id (:id prof)
|
||||
:id file-id
|
||||
:components-v2 true}
|
||||
out (th/command! data)]
|
||||
out (th/query! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
|
||||
@@ -110,10 +109,10 @@
|
||||
(t/is (= (:type error-data) :not-found)))))
|
||||
|
||||
(t/testing "query list files after delete"
|
||||
(let [data {::th/type :get-project-files
|
||||
::rpc/profile-id (:id prof)
|
||||
:project-id proj-id}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :project-files
|
||||
:project-id proj-id
|
||||
:profile-id (:id prof)}
|
||||
out (th/query! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
@@ -137,18 +136,19 @@
|
||||
out (th/mutation! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
|
||||
(t/is (nil? (:error out)))
|
||||
(:result out)))
|
||||
|
||||
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
|
||||
(let [params {::th/type :update-file
|
||||
::rpc/profile-id profile-id
|
||||
:id file-id
|
||||
:session-id (uuid/random)
|
||||
:profile-id profile-id
|
||||
:revn revn
|
||||
:components-v2 true
|
||||
:changes changes}
|
||||
out (th/command! params)]
|
||||
out (th/mutation! params)]
|
||||
(t/is (nil? (:error out)))
|
||||
(:result out)))]
|
||||
|
||||
@@ -257,12 +257,12 @@
|
||||
profile2 (th/create-profile* 2)
|
||||
|
||||
data {::th/type :create-file
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:project-id (:default-project-id profile1)
|
||||
:name "foobar"
|
||||
:is-shared false
|
||||
:components-v2 true}
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -277,9 +277,9 @@
|
||||
:profile-id (:id profile1)})
|
||||
data {::th/type :rename-file
|
||||
:id (:id file)
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:name "foobar"}
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -293,9 +293,9 @@
|
||||
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)})
|
||||
data {::th/type :delete-file
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:id (:id file)}
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -308,10 +308,10 @@
|
||||
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)})
|
||||
data {::th/type :set-file-shared
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:id (:id file)
|
||||
:is-shared true}
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -328,11 +328,11 @@
|
||||
:profile-id (:id profile1)})
|
||||
|
||||
data {::th/type :link-file-to-library
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:file-id (:id file2)
|
||||
:library-id (:id file1)}
|
||||
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -350,11 +350,11 @@
|
||||
:profile-id (:id profile2)})
|
||||
|
||||
data {::th/type :link-file-to-library
|
||||
::rpc/profile-id (:id profile2)
|
||||
:profile-id (:id profile2)
|
||||
:file-id (:id file2)
|
||||
:library-id (:id file1)}
|
||||
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
@@ -372,10 +372,10 @@
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; query the list of files
|
||||
(let [data {::th/type :get-project-files
|
||||
::rpc/profile-id (:id profile1)
|
||||
:project-id (:default-project-id profile1)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :project-files
|
||||
:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
@@ -384,15 +384,15 @@
|
||||
;; Request file to be deleted
|
||||
(let [params {::th/type :delete-file
|
||||
:id (:id file)
|
||||
::rpc/profile-id (:id profile1)}
|
||||
out (th/command! params)]
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! params)]
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; query the list of files after soft deletion
|
||||
(let [data {::th/type :get-project-files
|
||||
::rpc/profile-id (:id profile1)
|
||||
:project-id (:default-project-id profile1)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :project-files
|
||||
:project-id (:default-project-id profile1)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
@@ -403,10 +403,10 @@
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; query the list of file libraries of a after hard deletion
|
||||
(let [data {::th/type :get-file-libraries
|
||||
::rpc/profile-id (:id profile1)
|
||||
:file-id (:id file)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :file-libraries
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
@@ -417,10 +417,10 @@
|
||||
(t/is (= 1 (:processed result))))
|
||||
|
||||
;; query the list of file libraries of a after hard deletion
|
||||
(let [data {::th/type :get-file-libraries
|
||||
::rpc/profile-id (:id profile1)
|
||||
:file-id (:id file)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :file-libraries
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(let [error (:error out)
|
||||
error-data (ex-data error)]
|
||||
@@ -483,11 +483,11 @@
|
||||
(t/testing "RPC page query (rendering purposes)"
|
||||
|
||||
;; Query :page RPC method without passing page-id
|
||||
(let [data {::th/type :get-page
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
@@ -500,12 +500,12 @@
|
||||
)
|
||||
|
||||
;; Query :page RPC method with page-id
|
||||
(let [data {::th/type :get-page
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :objects))
|
||||
@@ -516,13 +516,13 @@
|
||||
(t/is (contains? (:objects result) uuid/zero)))
|
||||
|
||||
;; Query :page RPC method with page-id and object-id
|
||||
(let [data {::th/type :get-page
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:page-id page-id
|
||||
:object-id frame1-id
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (map? result))
|
||||
@@ -534,12 +534,12 @@
|
||||
(t/is (not (contains? (:objects result) shape2-id))))
|
||||
|
||||
;; Query :page RPC method with wrong params
|
||||
(let [data {::th/type :get-page
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :page
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id frame1-id
|
||||
:components-v2 true}
|
||||
out (th/command! data)]
|
||||
out (th/query! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [{:keys [type code]} (-> out :error ex-data)]
|
||||
@@ -551,21 +551,21 @@
|
||||
(t/testing "RPC :file-data-for-thumbnail"
|
||||
;; Insert a thumbnail data for the frame-id
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id (str page-id frame1-id)
|
||||
:data "random-data-1"}
|
||||
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Check the result
|
||||
(let [data {::th/type :get-file-data-for-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :file-data-for-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :page))
|
||||
@@ -578,21 +578,21 @@
|
||||
|
||||
;; Delete thumbnail data
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id (str page-id frame1-id)
|
||||
:data nil}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
;; Check the result
|
||||
(let [data {::th/type :get-file-data-for-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
(let [data {::th/type :file-data-for-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (map? result))
|
||||
(t/is (contains? result :page))
|
||||
@@ -606,11 +606,11 @@
|
||||
|
||||
;; insert object snapshot for known frame
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id (str page-id frame1-id)
|
||||
:data "new-data"}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
@@ -629,11 +629,11 @@
|
||||
|
||||
;; insert object snapshot for for unknown frame
|
||||
(let [data {::th/type :upsert-file-object-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:object-id (str page-id (uuid/next))
|
||||
:data "new-data-2"}
|
||||
{:keys [error result] :as out} (th/command! data)]
|
||||
{:keys [error result] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result)))
|
||||
|
||||
@@ -661,8 +661,8 @@
|
||||
:project-id (:default-project-id prof)
|
||||
:revn 2
|
||||
:is-shared false})
|
||||
data {::th/type :get-file-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
data {::th/type :file-thumbnail
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)}]
|
||||
|
||||
(t/testing "query a thumbnail with single revn"
|
||||
@@ -673,7 +673,7 @@
|
||||
:revn 1
|
||||
:data "testvalue1"})
|
||||
|
||||
(let [{:keys [result error] :as out} (th/command! data)]
|
||||
(let [{:keys [result error] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= 4 (count result)))
|
||||
@@ -687,7 +687,7 @@
|
||||
:revn 2
|
||||
:data "testvalue2"})
|
||||
|
||||
(let [{:keys [result error] :as out} (th/command! data)]
|
||||
(let [{:keys [result error] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= 4 (count result)))
|
||||
@@ -695,7 +695,7 @@
|
||||
(t/is (= 2 (:revn result))))
|
||||
|
||||
;; Then query the specific revn
|
||||
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
|
||||
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= 4 (count result)))
|
||||
@@ -704,18 +704,18 @@
|
||||
|
||||
(t/testing "upsert file-thumbnail"
|
||||
(let [data {::th/type :upsert-file-thumbnail
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:data "foobar"
|
||||
:props {:baz 1}
|
||||
:revn 2}
|
||||
{:keys [result error] :as out} (th/command! data)]
|
||||
{:keys [result error] :as out} (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (nil? result))))
|
||||
|
||||
(t/testing "query last result"
|
||||
(let [{:keys [result error] :as out} (th/command! data)]
|
||||
(let [{:keys [result error] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= 4 (count result)))
|
||||
@@ -734,7 +734,7 @@
|
||||
(t/is (= 1 (:processed res))))
|
||||
|
||||
;; Then query the specific revn
|
||||
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
|
||||
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
|
||||
(t/is (th/ex-of-type? error :not-found))
|
||||
(t/is (th/ex-of-code? error :file-thumbnail-not-found))))
|
||||
))
|
||||
|
||||
@@ -6,56 +6,52 @@
|
||||
|
||||
(ns backend-tests.rpc-font-test
|
||||
(:require
|
||||
[backend-tests.helpers :as th]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
[datoteka.io :as io]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest ttf-font-upload-1
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
font-id (uuid/custom 10 1)
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
font-id (uuid/custom 10 1)
|
||||
|
||||
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
io/input-stream
|
||||
io/read-as-bytes)
|
||||
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
io/input-stream
|
||||
io/read-as-bytes)
|
||||
|
||||
params {::th/type :create-font-variant
|
||||
:profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "somefont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:data {"font/ttf" ttfdata}}
|
||||
out (th/mutation! params)]
|
||||
params {::th/type :create-font-variant
|
||||
:profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "somefont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:data {"font/ttf" ttfdata}}
|
||||
out (th/mutation! params)]
|
||||
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:ttf-file-id result)))
|
||||
(t/is (uuid? (:otf-file-id result)))
|
||||
(t/is (uuid? (:woff1-file-id result)))
|
||||
(t/are [k] (= (get params k)
|
||||
(get result k))
|
||||
:team-id
|
||||
:font-id
|
||||
:font-family
|
||||
:font-weight
|
||||
:font-style)))))
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:ttf-file-id result)))
|
||||
(t/is (uuid? (:otf-file-id result)))
|
||||
(t/is (uuid? (:woff1-file-id result)))
|
||||
(t/are [k] (= (get params k)
|
||||
(get result k))
|
||||
:team-id
|
||||
:font-id
|
||||
:font-family
|
||||
:font-weight
|
||||
:font-style))))
|
||||
|
||||
(t/deftest ttf-font-upload-2
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
(ns backend-tests.rpc-media-test
|
||||
(:require
|
||||
[backend-tests.helpers :as th]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]))
|
||||
|
||||
@@ -135,123 +134,3 @@
|
||||
(t/is (= "image/jpeg" (:mtype result)))
|
||||
(t/is (uuid? (:media-id result)))
|
||||
(t/is (uuid? (:thumbnail-id result))))))
|
||||
|
||||
|
||||
(t/deftest media-object-from-url-command
|
||||
(let [prof (th/create-profile* 1)
|
||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||
:team-id (:default-team-id prof)})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg"
|
||||
params {::th/type :create-file-media-object-from-url
|
||||
::rpc/profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:is-local true
|
||||
:url url}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||
(t/is (= (:id file) (:file-id result)))
|
||||
(t/is (= 1024 (:width result)))
|
||||
(t/is (= 683 (:height result)))
|
||||
(t/is (= "image/jpeg" (:mtype result)))
|
||||
(t/is (uuid? media-id))
|
||||
(t/is (uuid? thumbnail-id))
|
||||
(let [storage (:app.storage/storage th/*system*)
|
||||
mobj1 @(sto/get-object storage media-id)
|
||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||
(t/is (sto/storage-object? mobj1))
|
||||
(t/is (sto/storage-object? mobj2))
|
||||
(t/is (= 122785 (:size mobj1)))
|
||||
;; This is because in ubuntu 21.04 generates different
|
||||
;; thumbnail that in ubuntu 22.04. This hack should be removed
|
||||
;; when we all use the ubuntu 22.04 devenv image.
|
||||
(t/is (or (= 3302 (:size mobj2))
|
||||
(= 3303 (:size mobj2))))))))
|
||||
|
||||
(t/deftest media-object-upload-command
|
||||
(let [prof (th/create-profile* 1)
|
||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||
:team-id (:default-team-id prof)})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
mfile {:filename "sample.jpg"
|
||||
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||
:mtype "image/jpeg"
|
||||
:size 312043}
|
||||
|
||||
params {::th/type :upload-file-media-object
|
||||
::rpc/profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:is-local true
|
||||
:name "testfile"
|
||||
:content mfile}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||
(t/is (= (:id file) (:file-id result)))
|
||||
(t/is (= 800 (:width result)))
|
||||
(t/is (= 800 (:height result)))
|
||||
(t/is (= "image/jpeg" (:mtype result)))
|
||||
(t/is (uuid? media-id))
|
||||
(t/is (uuid? thumbnail-id))
|
||||
(let [storage (:app.storage/storage th/*system*)
|
||||
mobj1 @(sto/get-object storage media-id)
|
||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||
(t/is (sto/storage-object? mobj1))
|
||||
(t/is (sto/storage-object? mobj2))
|
||||
(t/is (= 312043 (:size mobj1)))
|
||||
(t/is (= 3887 (:size mobj2)))))
|
||||
))
|
||||
|
||||
|
||||
(t/deftest media-object-upload-idempotency-command
|
||||
(let [prof (th/create-profile* 1)
|
||||
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||
:team-id (:default-team-id prof)})
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})
|
||||
mfile {:filename "sample.jpg"
|
||||
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||
:mtype "image/jpeg"
|
||||
:size 312043}
|
||||
|
||||
params {::th/type :upload-file-media-object
|
||||
::rpc/profile-id (:id prof)
|
||||
:file-id (:id file)
|
||||
:is-local true
|
||||
:name "testfile"
|
||||
:content mfile
|
||||
:id (uuid/next)}]
|
||||
|
||||
;; First try
|
||||
(let [{:keys [result error] :as out} (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= (:id params) (:id result)))
|
||||
(t/is (= (:file-id params) (:file-id result)))
|
||||
(t/is (= 800 (:width result)))
|
||||
(t/is (= 800 (:height result)))
|
||||
(t/is (= "image/jpeg" (:mtype result)))
|
||||
(t/is (uuid? (:media-id result)))
|
||||
(t/is (uuid? (:thumbnail-id result))))
|
||||
|
||||
;; Second try
|
||||
(let [{:keys [result error] :as out} (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (= (:id params) (:id result)))
|
||||
(t/is (= (:file-id params) (:file-id result)))
|
||||
(t/is (= 800 (:width result)))
|
||||
(t/is (= 800 (:height result)))
|
||||
(t/is (= "image/jpeg" (:mtype result)))
|
||||
(t/is (uuid? (:media-id result)))
|
||||
(t/is (uuid? (:thumbnail-id result))))))
|
||||
|
||||
@@ -1,344 +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 backend-tests.rpc-quotes-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.quotes :as-alias quotes]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest teams-per-profile-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-teams-per-profile 2})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team
|
||||
::rpc/profile-id (:id profile-1)}
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :name (str "team" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :name (str "team" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "teams-per-profile" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "teams-per-profile"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "teams-per-profile"
|
||||
:quote 3})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3))))
|
||||
|
||||
(t/deftest projects-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-projects-per-team 2})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
team-id (:default-team-id profile-1)
|
||||
data {::th/type :create-project
|
||||
:profile-id (:id profile-1)
|
||||
:team-id team-id}
|
||||
|
||||
check-ok! (fn [name]
|
||||
(let [data (assoc data :name (str "project" name))
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
|
||||
check-ko! (fn [name]
|
||||
;; create second project
|
||||
(let [data (assoc data :name (str "project" name))
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "projects-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:target "projects-per-team"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:profile-id (:id profile-2)
|
||||
:target "projects-per-team"
|
||||
:quote 10})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id team-id
|
||||
:profile-id (:id profile-1)
|
||||
:target "projects-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "projects-per-team"
|
||||
:quote 5})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
)))
|
||||
|
||||
(t/deftest invitations-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-invitations-per-team 2})}]
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)
|
||||
:role :editor}
|
||||
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "invitations-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "invitations-per-team"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "invitations-per-team"
|
||||
:quote 5})
|
||||
|
||||
(check-ok! 5)
|
||||
(check-ko! 6))))
|
||||
|
||||
|
||||
(t/deftest profiles-per-team-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-profiles-per-team 3})}]
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)
|
||||
:role :editor}
|
||||
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :emails [(str "foo" n "@example.net")])
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "profiles-per-team" (:target (ex-data error)))))))]
|
||||
|
||||
(th/create-team-role* {:team-id (:default-team-id profile-1)
|
||||
:profile-id (:id profile-2)
|
||||
:role :admin})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "profiles-per-team"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-2)
|
||||
:target "profiles-per-team"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:default-team-id profile-1)
|
||||
:target "profiles-per-team"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3))))
|
||||
|
||||
|
||||
|
||||
(t/deftest files-per-project-quote
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/config-get-mock
|
||||
{:quotes-files-per-project 1})}]
|
||||
|
||||
(let [profile-1 (th/create-profile* 1)
|
||||
profile-2 (th/create-profile* 2)
|
||||
project-1 (th/create-project* 1 {:profile-id (:id profile-1)
|
||||
:team-id (:default-team-id profile-1)})
|
||||
project-2 (th/create-project* 2 {:profile-id (:id profile-2)
|
||||
:team-id (:default-team-id profile-2)})
|
||||
data {::th/type :create-file
|
||||
::rpc/profile-id (:id profile-1)
|
||||
:project-id (:id project-1)}
|
||||
check-ok! (fn [n]
|
||||
(let [data (assoc data :name (str "file" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some? (:result out)))))
|
||||
check-ko! (fn [n]
|
||||
(let [data (assoc data :name (str "file" n))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
(let [error (:error out)]
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :max-quote-reached (th/ex-code error)))
|
||||
(t/is (= "files-per-project" (:target (ex-data error)))))))]
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 1)
|
||||
(check-ko! 2)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-1)
|
||||
:target "files-per-project"
|
||||
:quote 2})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:project-id (:id project-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
(check-ok! 2)
|
||||
(check-ko! 3)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-1)
|
||||
:target "files-per-project"
|
||||
:quote 3})
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:team-id (:team-id project-1)
|
||||
:profile-id (:id profile-2)
|
||||
:target "files-per-project"
|
||||
:quote 100})
|
||||
|
||||
|
||||
(check-ok! 3)
|
||||
(check-ko! 4)
|
||||
|
||||
(th/db-insert! :usage-quote
|
||||
{:profile-id (:id profile-1)
|
||||
:target "files-per-project"
|
||||
:quote 4})
|
||||
|
||||
(check-ok! 4)
|
||||
(check-ko! 5)
|
||||
|
||||
)))
|
||||
@@ -21,7 +21,7 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest create-team-invitations
|
||||
(t/deftest invite-team-member
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
@@ -30,14 +30,14 @@
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
data {::th/type :invite-team-member
|
||||
:team-id (:id team)
|
||||
:role :editor}]
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}]
|
||||
|
||||
;; invite external user without complaints
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/command! data)
|
||||
out (th/mutation! data)
|
||||
;; retrieve the value from the database and check its content
|
||||
invitation (db/exec-one!
|
||||
th/*pool*
|
||||
@@ -52,7 +52,7 @@
|
||||
;; invite internal user without complaints
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
@@ -92,7 +92,7 @@
|
||||
(th/reset-mock! mock)
|
||||
|
||||
(let [data (assoc data :email (:email profile3))
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
@@ -115,12 +115,12 @@
|
||||
pool (:app.db/pool th/*system*)]
|
||||
|
||||
;; Try to invite a not existing user
|
||||
(let [data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
(let [data {::th/type :invite-team-member
|
||||
:email "notexisting@example.com"
|
||||
:team-id (:id team)
|
||||
:role :editor}
|
||||
out (th/command! data)]
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
@@ -139,12 +139,12 @@
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; Try to invite existing user
|
||||
(let [data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
(let [data {::th/type :invite-team-member
|
||||
:email (:email profile2)
|
||||
:team-id (:id team)
|
||||
:role :editor}
|
||||
out (th/command! data)]
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
@@ -215,9 +215,7 @@
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile2)
|
||||
:token token}
|
||||
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
@@ -238,9 +236,7 @@
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [data {::th/type :verify-token
|
||||
::rpc/profile-id (:id profile1)
|
||||
:token token}
|
||||
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
@@ -250,7 +246,7 @@
|
||||
|
||||
)))
|
||||
|
||||
(t/deftest create-team-invitations-with-email-verification-disabled
|
||||
(t/deftest invite-team-member-with-email-verification-disabled
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
@@ -259,16 +255,16 @@
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
data {::th/type :invite-team-member
|
||||
:team-id (:id team)
|
||||
:role :editor}]
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}]
|
||||
|
||||
;; invite internal user without complaints
|
||||
(with-redefs [app.config/flags #{}]
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
out (th/mutation! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
|
||||
@@ -283,8 +279,8 @@
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :delete-team
|
||||
::rpc/profile-id (:id profile1)
|
||||
:team-id (:id team)}]
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile1)}]
|
||||
|
||||
;; team is not deleted because it does not meet all
|
||||
;; conditions to be deleted.
|
||||
@@ -292,9 +288,9 @@
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; query the list of teams
|
||||
(let [data {::th/type :get-teams
|
||||
::rpc/profile-id (:id profile1)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :teams
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
@@ -304,15 +300,15 @@
|
||||
|
||||
;; Request team to be deleted
|
||||
(let [params {::th/type :delete-team
|
||||
::rpc/profile-id (:id profile1)
|
||||
:id (:id team)}
|
||||
out (th/command! params)]
|
||||
:id (:id team)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! params)]
|
||||
(t/is (th/success? out)))
|
||||
|
||||
;; query the list of teams after soft deletion
|
||||
(let [data {::th/type :get-teams
|
||||
::rpc/profile-id (:id profile1)}
|
||||
out (th/command! data)]
|
||||
(let [data {::th/type :teams
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
@@ -325,8 +321,8 @@
|
||||
|
||||
;; query the list of projects after hard deletion
|
||||
(let [data {::th/type :projects
|
||||
:profile-id (:id profile1)
|
||||
:team-id (:id team)}
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (not (th/success? out)))
|
||||
@@ -339,8 +335,8 @@
|
||||
|
||||
;; query the list of projects of a after hard deletion
|
||||
(let [data {::th/type :projects
|
||||
:profile-id (:id profile1)
|
||||
:team-id (:id team)}
|
||||
:team-id (:id team)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
|
||||
@@ -352,8 +348,8 @@
|
||||
(t/deftest query-team-invitations
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :get-team-invitations
|
||||
::rpc/profile-id (:id prof)
|
||||
data {::th/type :team-invitations
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)}]
|
||||
|
||||
;; insert an entry on the database with an enabled invitation
|
||||
@@ -370,7 +366,7 @@
|
||||
:role "editor"
|
||||
:valid-until (dt/in-past "48h")})
|
||||
|
||||
(let [out (th/command! data)]
|
||||
(let [out (th/query! data)]
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)
|
||||
one (first result)
|
||||
@@ -385,7 +381,7 @@
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :update-team-invitation-role
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)
|
||||
:email "TEST1@mail.com"
|
||||
:role :admin}]
|
||||
@@ -397,7 +393,7 @@
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [out (th/command! data)
|
||||
(let [out (th/mutation! data)
|
||||
;; retrieve the value from the database and check its content
|
||||
res (db/get* th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||
@@ -409,7 +405,7 @@
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :delete-team-invitation
|
||||
::rpc/profile-id (:id prof)
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)
|
||||
:email "TEST1@mail.com"}]
|
||||
|
||||
@@ -420,7 +416,7 @@
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [out (th/command! data)
|
||||
(let [out (th/mutation! data)
|
||||
;; retrieve the value from the database and check its content
|
||||
res (db/get* th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
|
||||
(ns backend-tests.rpc-webhooks-test
|
||||
(:require
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.storage :as sto]
|
||||
[app.rpc :as-alias rpc]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
@@ -32,7 +31,7 @@
|
||||
(let [params {::th/type :create-webhook
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:uri (u/uri "http://example.com")
|
||||
:uri "http://example.com"
|
||||
:mtype "application/json"}
|
||||
out (th/command! params)]
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{:deps
|
||||
{org.clojure/clojure {:mvn/version "1.11.1"}
|
||||
org.clojure/data.json {:mvn/version "2.4.0"}
|
||||
org.clojure/tools.cli {:mvn/version "1.0.214"}
|
||||
metosin/jsonista {:mvn/version "0.3.7"}
|
||||
org.clojure/tools.cli {:mvn/version "1.0.206"}
|
||||
metosin/jsonista {:mvn/version "0.3.6"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.60"}
|
||||
org.clojure/test.check {:mvn/version "1.1.1"}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/promesa {:mvn/version "10.0.594"}
|
||||
funcool/promesa {:mvn/version "10.0.571"}
|
||||
funcool/cuerdas {:mvn/version "2022.06.16-403"}
|
||||
|
||||
lambdaisland/uri {:mvn/version "1.13.95"
|
||||
@@ -37,22 +37,21 @@
|
||||
|
||||
;; exception printing
|
||||
fipp/fipp {:mvn/version "0.6.26"}
|
||||
io.aviso/pretty {:mvn/version "1.3"}
|
||||
io.aviso/pretty {:mvn/version "1.1.1"}
|
||||
environ/environ {:mvn/version "1.2.0"}}
|
||||
:paths ["src" "target/classes"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
thheller/shadow-cljs {:mvn/version "2.20.16"}
|
||||
thheller/shadow-cljs {:mvn/version "2.20.2"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
|
||||
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"main": "index.js",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"luxon": "^3.1.1"
|
||||
"luxon": "^1.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test",
|
||||
@@ -13,8 +13,8 @@
|
||||
"test": "yarn run compile-test && yarn run run-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "2.20.16",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.11.0"
|
||||
"shadow-cljs": "2.19.8",
|
||||
"source-map-support": "^0.5.19",
|
||||
"ws": "^7.4.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
(defmacro error
|
||||
[& {:keys [type hint] :as params}]
|
||||
`(ex-info ~(or hint (name type))
|
||||
`(ex-info ~(or hint (pr-str type))
|
||||
(merge
|
||||
~(dissoc params :cause ::data)
|
||||
~(::data params))
|
||||
@@ -89,9 +89,9 @@
|
||||
(contains? data :explain))
|
||||
(explain (:explain data) opts)
|
||||
|
||||
(and (contains? data ::s/problems)
|
||||
(contains? data ::s/value)
|
||||
(contains? data ::s/spec))
|
||||
(and (::s/problems data)
|
||||
(::s/value data)
|
||||
(::s/spec data))
|
||||
(binding [s/*explain-out* expound/printer]
|
||||
(with-out-str
|
||||
(s/explain-out (update data ::s/problems #(take max-problems %))))))))
|
||||
|
||||
@@ -44,12 +44,7 @@
|
||||
(let [component-id (:current-component-id file)
|
||||
change (cond-> change
|
||||
(and add-container? (some? component-id))
|
||||
(cond->
|
||||
:always
|
||||
(assoc :component-id component-id)
|
||||
|
||||
(some? (:current-frame-id file))
|
||||
(assoc :frame-id (:current-frame-id file)))
|
||||
(assoc :component-id component-id)
|
||||
|
||||
(and add-container? (nil? component-id))
|
||||
(assoc :page-id (:current-page-id file)
|
||||
@@ -200,7 +195,7 @@
|
||||
|
||||
(assert (nil? (:current-component-id file)))
|
||||
(let [page-id (or (:id data) (uuid/next))
|
||||
page (-> (ctp/make-empty-page page-id "Page 1")
|
||||
page (-> (ctp/make-empty-page page-id "Page-1")
|
||||
(d/deep-merge data))]
|
||||
(-> file
|
||||
(commit-change
|
||||
@@ -228,6 +223,7 @@
|
||||
(clear-names)))
|
||||
|
||||
(defn add-artboard [file data]
|
||||
(assert (nil? (:current-component-id file)))
|
||||
(let [obj (-> (cts/make-minimal-shape :frame)
|
||||
(merge data)
|
||||
(check-name file :frame)
|
||||
@@ -241,11 +237,11 @@
|
||||
(update :parent-stack conjv (:id obj)))))
|
||||
|
||||
(defn close-artboard [file]
|
||||
(assert (nil? (:current-component-id file)))
|
||||
|
||||
(let [parent-id (-> file :parent-id peek)
|
||||
parent (lookup-shape file parent-id)
|
||||
current-frame-id (or (:frame-id parent)
|
||||
(when (nil? (:current-component-id file))
|
||||
root-frame))]
|
||||
current-frame-id (or (:frame-id parent) root-frame)]
|
||||
(-> file
|
||||
(assoc :current-frame-id current-frame-id)
|
||||
(update :parent-stack pop))))
|
||||
@@ -565,37 +561,35 @@
|
||||
:id id}))))
|
||||
|
||||
(defn start-component
|
||||
([file data] (start-component file data :group))
|
||||
([file data root-type]
|
||||
(let [selrect (or (gsh/make-selrect (:x data) (:y data) (:width data) (:height data))
|
||||
cts/empty-selrect)
|
||||
name (:name data)
|
||||
path (:path data)
|
||||
main-instance-id (:main-instance-id data)
|
||||
main-instance-page (:main-instance-page data)
|
||||
obj (-> (cts/make-shape root-type selrect data)
|
||||
(dissoc :path
|
||||
:main-instance-id
|
||||
:main-instance-page
|
||||
:main-instance-x
|
||||
:main-instance-y)
|
||||
(check-name file root-type)
|
||||
(d/without-nils))]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :add-component
|
||||
:id (:id obj)
|
||||
:name name
|
||||
:path path
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:shapes [obj]})
|
||||
[file data]
|
||||
|
||||
(assoc :last-id (:id obj))
|
||||
(update :parent-stack conjv (:id obj))
|
||||
(assoc :current-component-id (:id obj))
|
||||
(assoc :current-frame-id (when (= (:type obj) :frame)
|
||||
(:id obj)))))))
|
||||
(let [selrect cts/empty-selrect
|
||||
name (:name data)
|
||||
path (:path data)
|
||||
main-instance-id (:main-instance-id data)
|
||||
main-instance-page (:main-instance-page data)
|
||||
obj (-> (cts/make-minimal-group nil selrect name)
|
||||
(merge data)
|
||||
(dissoc :path
|
||||
:main-instance-id
|
||||
:main-instance-page
|
||||
:main-instance-x
|
||||
:main-instance-y)
|
||||
(check-name file :group)
|
||||
(d/without-nils))]
|
||||
(-> file
|
||||
(commit-change
|
||||
{:type :add-component
|
||||
:id (:id obj)
|
||||
:name name
|
||||
:path path
|
||||
:main-instance-id main-instance-id
|
||||
:main-instance-page main-instance-page
|
||||
:shapes [obj]})
|
||||
|
||||
(assoc :last-id (:id obj))
|
||||
(update :parent-stack conjv (:id obj))
|
||||
(assoc :current-component-id (:id obj)))))
|
||||
|
||||
(defn finish-component
|
||||
[file]
|
||||
@@ -630,7 +624,7 @@
|
||||
|
||||
{:add-container? true}))
|
||||
|
||||
(= (:type component) :group)
|
||||
:else
|
||||
(let [component' (gsh/update-group-selrect component children)]
|
||||
(commit-change
|
||||
file
|
||||
@@ -643,13 +637,11 @@
|
||||
{:type :set :attr :y :val (-> component' :selrect :y) :ignore-touched true}
|
||||
{:type :set :attr :width :val (-> component' :selrect :width) :ignore-touched true}
|
||||
{:type :set :attr :height :val (-> component' :selrect :height) :ignore-touched true}]}
|
||||
{:add-container? true}))
|
||||
|
||||
:else file)]
|
||||
{:add-container? true})))]
|
||||
|
||||
(-> file
|
||||
(dissoc :current-component-id)
|
||||
(dissoc :current-frame-id)
|
||||
(update :parent-stack pop))))
|
||||
|
||||
(defn finish-deleted-component
|
||||
@@ -708,7 +700,7 @@
|
||||
(gpt/point x
|
||||
y)
|
||||
#_{:main-instance? true
|
||||
:force-id main-instance-id})]
|
||||
:force-id main-instance-id})]
|
||||
|
||||
(as-> file $
|
||||
(reduce #(commit-change %1
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
(def precision 6)
|
||||
|
||||
;; --- Matrix Impl
|
||||
|
||||
(defrecord Matrix [^double a
|
||||
^double b
|
||||
^double c
|
||||
@@ -27,13 +28,13 @@
|
||||
^double f]
|
||||
Object
|
||||
(toString [_]
|
||||
(dm/fmt "matrix(%, %, %, %, %, %)"
|
||||
(mth/to-fixed a precision)
|
||||
(mth/to-fixed b precision)
|
||||
(mth/to-fixed c precision)
|
||||
(mth/to-fixed d precision)
|
||||
(mth/to-fixed e precision)
|
||||
(mth/to-fixed f precision))))
|
||||
(str "matrix("
|
||||
(mth/precision a precision) ","
|
||||
(mth/precision b precision) ","
|
||||
(mth/precision c precision) ","
|
||||
(mth/precision d precision) ","
|
||||
(mth/precision e precision) ","
|
||||
(mth/precision f precision) ")")))
|
||||
|
||||
(defn matrix?
|
||||
"Return true if `v` is Matrix instance."
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
:clj [clojure.core :as c])
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.math :as mth]
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -60,10 +59,10 @@
|
||||
(point v v)
|
||||
|
||||
(point-like? v)
|
||||
(Point. (:x v) (:y v))
|
||||
(map->Point v)
|
||||
|
||||
:else
|
||||
(ex/raise :hint "invalid arguments (on pointer constructor)" :value v)))
|
||||
(throw (ex-info "Invalid arguments" {:v v}))))
|
||||
([x y]
|
||||
(Point. x y)))
|
||||
|
||||
@@ -170,7 +169,8 @@
|
||||
(dm/get-prop p2 :x))
|
||||
dy (- (dm/get-prop p1 :y)
|
||||
(dm/get-prop p2 :y))]
|
||||
(mth/hypot dx dy)))
|
||||
(mth/sqrt (+ (mth/pow dx 2)
|
||||
(mth/pow dy 2)))))
|
||||
|
||||
(defn distance-vector
|
||||
"Calculate the distance, separated x and y."
|
||||
@@ -190,7 +190,8 @@
|
||||
(assert (point? pt) "point instance expected")
|
||||
(let [x (dm/get-prop pt :x)
|
||||
y (dm/get-prop pt :y)]
|
||||
(mth/hypot x y)))
|
||||
(mth/sqrt (+ (mth/pow x 2)
|
||||
(mth/pow y 2)))))
|
||||
|
||||
(defn angle
|
||||
"Returns the smaller angle between two vectors.
|
||||
@@ -274,12 +275,12 @@
|
||||
(Point. (mth/precision (dm/get-prop pt :x) decimals)
|
||||
(mth/precision (dm/get-prop pt :y) decimals))))
|
||||
|
||||
(defn round-step
|
||||
(defn half-round
|
||||
"Round the coordinates to the closest half-point"
|
||||
[pt step]
|
||||
[pt]
|
||||
(assert (point? pt) "expected point instance")
|
||||
(Point. (mth/round (dm/get-prop pt :x) step)
|
||||
(mth/round (dm/get-prop pt :y) step)))
|
||||
(Point. (mth/half-round (dm/get-prop pt :x))
|
||||
(mth/half-round (dm/get-prop pt :y))))
|
||||
|
||||
(defn transform
|
||||
"Transform a point applying a matrix transformation."
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
(defn bounding-box
|
||||
"Returns a rect that wraps the shape after all transformations applied."
|
||||
[shape]
|
||||
;; TODO: perhaps we need to store this calculation in a shape attribute
|
||||
; TODO: perhaps we need to store this calculation in a shape attribute
|
||||
(gpr/points->rect (:points shape)))
|
||||
|
||||
(defn left-bound
|
||||
"Returns the lowest x coord of the shape BEFORE applying transformations."
|
||||
;; TODO: perhaps some day we want after transformations, but for the
|
||||
;; moment it's enough as is now.
|
||||
; TODO: perhaps some day we want after transformations, but for the
|
||||
; moment it's enough as is now.
|
||||
[shape]
|
||||
(or (:x shape) (:x (:selrect shape)))) ; Paths don't have :x attribute
|
||||
|
||||
@@ -82,12 +82,24 @@
|
||||
(update :height (comp inc inc))))))
|
||||
|
||||
(defn selrect->areas [bounds selrect]
|
||||
(let [{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds
|
||||
(let [make-selrect
|
||||
(fn [x1 y1 x2 y2]
|
||||
(let [x1 (min x1 x2)
|
||||
x2 (max x1 x2)
|
||||
y1 (min y1 y2)
|
||||
y2 (max y1 y2)]
|
||||
{:x1 x1 :y1 y1
|
||||
:x2 x2 :y2 y2
|
||||
:x x1 :y y1
|
||||
:width (- x2 x1)
|
||||
:height (- y2 y1)
|
||||
:type :rect}))
|
||||
{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds
|
||||
{sr-x1 :x1 sr-x2 :x2 sr-y1 :y1 sr-y2 :y2} selrect]
|
||||
{:left (gpr/corners->selrect bound-x1 sr-y1 sr-x1 sr-y2)
|
||||
:top (gpr/corners->selrect sr-x1 bound-y1 sr-x2 sr-y1)
|
||||
:right (gpr/corners->selrect sr-x2 sr-y1 bound-x2 sr-y2)
|
||||
:bottom (gpr/corners->selrect sr-x1 sr-y2 sr-x2 bound-y2)}))
|
||||
{:left (make-selrect bound-x1 sr-y1 sr-x1 sr-y2)
|
||||
:top (make-selrect sr-x1 bound-y1 sr-x2 sr-y1)
|
||||
:right (make-selrect sr-x2 sr-y1 bound-x2 sr-y2)
|
||||
:bottom (make-selrect sr-x1 sr-y2 sr-x2 bound-y2)}))
|
||||
|
||||
(defn distance-selrect [selrect other]
|
||||
(let [{:keys [x1 y1]} other
|
||||
@@ -106,8 +118,8 @@
|
||||
|
||||
([attr val1 val2 precision]
|
||||
(let [close-val? (fn [num1 num2]
|
||||
(when (and (number? num1) (number? num2))
|
||||
(< (mth/abs (- num1 num2)) precision)))]
|
||||
(when (and (number? num1) (number? num2))
|
||||
(< (mth/abs (- num1 num2)) precision)))]
|
||||
(cond
|
||||
(and (number? val1) (number? val2))
|
||||
(close-val? val1 val2)
|
||||
@@ -149,7 +161,6 @@
|
||||
(dm/export gpr/contains-selrect?)
|
||||
(dm/export gpr/contains-point?)
|
||||
(dm/export gpr/close-selrect?)
|
||||
(dm/export gpr/clip-selrect)
|
||||
|
||||
(dm/export gtr/move)
|
||||
(dm/export gtr/absolute-move)
|
||||
@@ -183,6 +194,7 @@
|
||||
(dm/export gsi/rect-contains-shape?)
|
||||
|
||||
;; Bool
|
||||
|
||||
(dm/export gsb/calc-bool-content)
|
||||
|
||||
;; Constraints
|
||||
@@ -195,3 +207,4 @@
|
||||
|
||||
;; Modifiers
|
||||
(dm/export gsm/set-objects-modifiers)
|
||||
|
||||
|
||||
@@ -146,13 +146,10 @@
|
||||
((if (= :x axis) center-horizontal-vector center-vertical-vector) child-points parent-points))
|
||||
|
||||
(defn displacement
|
||||
[before-v after-v before-parent-side-v after-parent-side-v]
|
||||
|
||||
(let [before-angl (gpt/angle-with-other before-v before-parent-side-v)
|
||||
after-angl (gpt/angle-with-other after-v after-parent-side-v)
|
||||
sign (if (mth/close? before-angl after-angl) 1 -1)
|
||||
[before-v after-v]
|
||||
(let [angl (gpt/angle-with-other before-v after-v)
|
||||
sign (if (mth/close? angl 180) -1 1)
|
||||
length (* sign (gpt/length before-v))]
|
||||
|
||||
(if (mth/almost-zero? length)
|
||||
after-v
|
||||
(gpt/subtract after-v (gpt/scale (gpt/unit after-v) length)))))
|
||||
@@ -176,18 +173,14 @@
|
||||
(defmethod constraint-modifier :start
|
||||
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
|
||||
(let [start-before (start-vector axis child-points-before parent-points-before)
|
||||
start-after (start-vector axis child-points-after parent-points-after)
|
||||
before-side-vector (side-vector axis parent-points-before)
|
||||
after-side-vector (side-vector axis parent-points-after)]
|
||||
(ctm/move-modifiers (displacement start-before start-after before-side-vector after-side-vector))))
|
||||
start-after (start-vector axis child-points-after parent-points-after)]
|
||||
(ctm/move-modifiers (displacement start-before start-after))))
|
||||
|
||||
(defmethod constraint-modifier :end
|
||||
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
|
||||
(let [end-before (end-vector axis child-points-before parent-points-before)
|
||||
end-after (end-vector axis child-points-after parent-points-after)
|
||||
before-side-vector (side-vector axis parent-points-before)
|
||||
after-side-vector (side-vector axis parent-points-after)]
|
||||
(ctm/move-modifiers (displacement end-before end-after before-side-vector after-side-vector))))
|
||||
end-after (end-vector axis child-points-after parent-points-after)]
|
||||
(ctm/move-modifiers (displacement end-before end-after))))
|
||||
|
||||
(defmethod constraint-modifier :fixed
|
||||
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
|
||||
@@ -197,11 +190,8 @@
|
||||
start-before (start-vector axis child-points-before parent-points-before)
|
||||
start-after (start-vector axis child-points-after parent-points-after)
|
||||
|
||||
before-side-vector (side-vector axis parent-points-before)
|
||||
after-side-vector (side-vector axis parent-points-after)
|
||||
|
||||
disp-end (displacement end-before end-after before-side-vector after-side-vector)
|
||||
disp-start (displacement start-before start-after before-side-vector after-side-vector)
|
||||
disp-end (displacement end-before end-after)
|
||||
disp-start (displacement start-before start-after)
|
||||
|
||||
;; We get the current axis side and grow it on both side by the end+start displacements
|
||||
before-vec (side-vector axis child-points-after)
|
||||
@@ -210,7 +200,7 @@
|
||||
;; after-vec will contain the side length of the grown side
|
||||
;; we scale the shape by the diference and translate it by the start
|
||||
;; displacement (so its left+top position is constant)
|
||||
scale (/ (gpt/length after-vec) (max 0.01 (gpt/length before-vec)))
|
||||
scale (/ (gpt/length after-vec) (gpt/length before-vec))
|
||||
|
||||
resize-origin (gpo/origin child-points-after)
|
||||
|
||||
@@ -224,10 +214,8 @@
|
||||
(defmethod constraint-modifier :center
|
||||
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
|
||||
(let [center-before (center-vector axis child-points-before parent-points-before)
|
||||
center-after (center-vector axis child-points-after parent-points-after)
|
||||
before-side-vector (side-vector axis parent-points-before)
|
||||
after-side-vector (side-vector axis parent-points-after)]
|
||||
(ctm/move-modifiers (displacement center-before center-after before-side-vector after-side-vector))))
|
||||
center-after (center-vector axis child-points-after parent-points-after)]
|
||||
(ctm/move-modifiers (displacement center-before center-after))))
|
||||
|
||||
(defmethod constraint-modifier :default [_ _ _ _ _]
|
||||
[])
|
||||
@@ -268,11 +256,11 @@
|
||||
|
||||
scale-x (if (= :scale constraints-h)
|
||||
1
|
||||
(/ (gpo/width-points child-bb-before) (max 0.01 (gpo/width-points child-bb-after))))
|
||||
(/ (gpo/width-points child-bb-before) (gpo/width-points child-bb-after)))
|
||||
|
||||
scale-y (if (= :scale constraints-v)
|
||||
1
|
||||
(/ (gpo/height-points child-bb-before) (max 0.01 (gpo/height-points child-bb-after))))
|
||||
(/ (gpo/height-points child-bb-before) (gpo/height-points child-bb-after)))
|
||||
|
||||
resize-vector (gpt/point scale-x scale-y)
|
||||
resize-origin (gpo/origin transformed-child-bounds)
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
[app.common.geom.shapes.flex-layout.modifiers :as fmo]))
|
||||
|
||||
(dm/export fbo/layout-content-bounds)
|
||||
(dm/export fbo/layout-content-points)
|
||||
(dm/export fbo/child-layout-bound-points)
|
||||
(dm/export fdr/get-drop-index)
|
||||
(dm/export fdr/get-drop-areas)
|
||||
(dm/export fdr/layout-drop-areas)
|
||||
(dm/export fli/calc-layout-data)
|
||||
(dm/export fmo/layout-child-modifiers)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.common.geom.shapes.flex-layout.bounds
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.points :as gpo]
|
||||
[app.common.types.shape.layout :as ctl]))
|
||||
@@ -28,19 +27,16 @@
|
||||
h-center? (ctl/h-center? parent)
|
||||
h-end? (ctl/h-end? parent)
|
||||
|
||||
fill-w? (ctl/fill-width? child)
|
||||
fill-h? (ctl/fill-height? child)
|
||||
|
||||
base-p (gpo/origin child-bounds)
|
||||
|
||||
width (gpo/width-points child-bounds)
|
||||
height (gpo/height-points child-bounds)
|
||||
|
||||
min-width (if fill-w?
|
||||
min-width (if (ctl/fill-width? child)
|
||||
(ctl/child-min-width child)
|
||||
width)
|
||||
|
||||
min-height (if fill-h?
|
||||
min-height (if (ctl/fill-height? child)
|
||||
(ctl/child-min-height child)
|
||||
height)
|
||||
|
||||
@@ -64,52 +60,26 @@
|
||||
min-width (max min-width 0.01)
|
||||
min-height (max min-height 0.01)]
|
||||
|
||||
(cond-> [base-p
|
||||
(gpt/add base-p (hv 0.01))
|
||||
(gpt/add base-p (vv 0.01))]
|
||||
(-> [base-p]
|
||||
(conj (cond-> base-p
|
||||
(or row? h-start?)
|
||||
(gpt/add (hv min-width))
|
||||
|
||||
(and col? h-start?)
|
||||
(conj (gpt/add base-p (hv min-width)))
|
||||
(and col? h-center?)
|
||||
(gpt/add (hv (/ min-width 2)))
|
||||
|
||||
(and col? h-center?)
|
||||
(conj (gpt/add base-p (hv (/ min-width 2))))
|
||||
(and col? h-center?)
|
||||
(gpt/subtract (hv min-width))))
|
||||
|
||||
(and col? h-center?)
|
||||
(conj (gpt/subtract base-p (hv min-width)))
|
||||
(conj (cond-> base-p
|
||||
(or col? v-start?)
|
||||
(gpt/add (vv min-height))
|
||||
|
||||
(and row? v-start?)
|
||||
(conj (gpt/add base-p (vv min-height)))
|
||||
(and row? v-center?)
|
||||
(gpt/add (vv (/ min-height 2)))
|
||||
|
||||
(and row? v-center?)
|
||||
(conj (gpt/add base-p (vv (/ min-height 2))))
|
||||
|
||||
(and row? v-end?)
|
||||
(conj (gpt/subtract base-p (vv min-height))))))
|
||||
|
||||
(defn layout-content-points
|
||||
[bounds parent children]
|
||||
|
||||
(let [parent-id (:id parent)
|
||||
parent-bounds @(get bounds parent-id)
|
||||
get-child-bounds
|
||||
(fn [child]
|
||||
(let [child-id (:id child)
|
||||
child-bounds @(get bounds child-id)
|
||||
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)
|
||||
|
||||
child-bounds
|
||||
(if (or (ctl/fill-width? child) (ctl/fill-height? child))
|
||||
(child-layout-bound-points parent child parent-bounds child-bounds)
|
||||
child-bounds)
|
||||
|
||||
child-bounds
|
||||
(when (d/not-empty? child-bounds)
|
||||
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
|
||||
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left))))]
|
||||
|
||||
child-bounds))]
|
||||
|
||||
(->> children (map get-child-bounds))))
|
||||
(and row? v-end?)
|
||||
(gpt/subtract (vv min-height)))))))
|
||||
|
||||
(defn layout-content-bounds
|
||||
[bounds {:keys [layout-padding] :as parent} children]
|
||||
@@ -117,34 +87,27 @@
|
||||
(let [parent-id (:id parent)
|
||||
parent-bounds @(get bounds parent-id)
|
||||
|
||||
row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
space-around? (ctl/space-around? parent)
|
||||
content-around? (ctl/content-around? parent)
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
row-pad (if (or (and col? space-around?)
|
||||
(and row? content-around?))
|
||||
layout-gap-row
|
||||
0)
|
||||
|
||||
col-pad (if (or(and row? space-around?)
|
||||
(and col? content-around?))
|
||||
layout-gap-col
|
||||
0)
|
||||
|
||||
{pad-top :p1 pad-right :p2 pad-bottom :p3 pad-left :p4} layout-padding
|
||||
pad-top (+ (or pad-top 0) row-pad)
|
||||
pad-right (+ (or pad-right 0) col-pad)
|
||||
pad-bottom (+ (or pad-bottom 0) row-pad)
|
||||
pad-left (+ (or pad-left 0) col-pad)
|
||||
pad-top (or pad-top 0)
|
||||
pad-right (or pad-right 0)
|
||||
pad-bottom (or pad-bottom 0)
|
||||
pad-left (or pad-left 0)
|
||||
|
||||
layout-points
|
||||
(layout-content-points bounds parent children)]
|
||||
child-bounds
|
||||
(fn [child]
|
||||
(let [child-id (:id child)
|
||||
child-bounds @(get bounds child-id)
|
||||
child-bounds
|
||||
(if (or (ctl/fill-height? child) (ctl/fill-height? child))
|
||||
(child-layout-bound-points parent child parent-bounds child-bounds)
|
||||
child-bounds)
|
||||
|
||||
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)]
|
||||
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
|
||||
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left)))))]
|
||||
|
||||
(as-> children $
|
||||
(map child-bounds $)
|
||||
(gpo/merge-parent-coords-bounds $ parent-bounds)
|
||||
(gpo/pad-points $ (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))))
|
||||
|
||||
(if (d/not-empty? layout-points)
|
||||
(-> layout-points
|
||||
(gpo/merge-parent-coords-bounds parent-bounds)
|
||||
(gpo/pad-points (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))
|
||||
;; Cannot create some bounds from the children so we return the parent's
|
||||
parent-bounds)))
|
||||
|
||||
@@ -8,14 +8,11 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.flex-layout.lines :as fli]
|
||||
[app.common.geom.shapes.points :as gpo]
|
||||
[app.common.geom.shapes.rect :as gsr]
|
||||
[app.common.geom.shapes.transforms :as gtr]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape.layout :as ctl]))
|
||||
|
||||
(defn drop-child-areas
|
||||
@@ -44,7 +41,7 @@
|
||||
(:width parent-rect)
|
||||
|
||||
:else
|
||||
(+ box-width (- box-x prev-x) (/ layout-gap-col 2)))
|
||||
(+ box-width (- box-x prev-x) (/ layout-gap-row 2)))
|
||||
|
||||
height
|
||||
(cond
|
||||
@@ -55,7 +52,7 @@
|
||||
(:height parent-rect)
|
||||
|
||||
:else
|
||||
(+ box-height (- box-y prev-y) (/ layout-gap-row 2)))]
|
||||
(+ box-height (- box-y prev-y) (/ layout-gap-col 2)))]
|
||||
|
||||
(if row?
|
||||
(let [half-point-width (+ (- box-x x) (/ box-width 2))]
|
||||
@@ -90,14 +87,14 @@
|
||||
(if row?
|
||||
(:width frame)
|
||||
(+ line-width margin-x
|
||||
(if row? (* layout-gap-col (dec num-children)) 0)))
|
||||
(if row? (* layout-gap-row (dec num-children)) 0)))
|
||||
|
||||
line-height
|
||||
(if col?
|
||||
(:height frame)
|
||||
(+ line-height margin-y
|
||||
(if col?
|
||||
(* layout-gap-row (dec num-children))
|
||||
(* layout-gap-col (dec num-children))
|
||||
0)))
|
||||
|
||||
box-x
|
||||
@@ -125,7 +122,7 @@
|
||||
(:width frame)
|
||||
|
||||
:else
|
||||
(+ line-width (- box-x prev-x) (/ layout-gap-col 2)))
|
||||
(+ line-width (- box-x prev-x) (/ layout-gap-row 2)))
|
||||
|
||||
height (cond
|
||||
(and row? last?)
|
||||
@@ -135,7 +132,7 @@
|
||||
(:height frame)
|
||||
|
||||
:else
|
||||
(+ line-height (- box-y prev-y) (/ layout-gap-row 2)))]
|
||||
(+ line-height (- box-y prev-y) (/ layout-gap-col 2)))]
|
||||
(gsr/make-rect x y width height)))
|
||||
|
||||
(defn layout-drop-areas
|
||||
@@ -182,36 +179,13 @@
|
||||
(+ (:y line-area) (:height line-area))
|
||||
(rest lines)))))))
|
||||
|
||||
(defn get-flip-modifiers
|
||||
[{:keys [flip-x flip-y transform transform-inverse] :as shape}]
|
||||
|
||||
(if (or flip-x flip-y)
|
||||
(let [modifiers
|
||||
(-> (ctm/empty)
|
||||
(ctm/resize (gpt/point (if flip-x -1.0 1.0)
|
||||
(if flip-y -1.0 1.0))
|
||||
(gco/center-shape shape)
|
||||
transform
|
||||
transform-inverse))]
|
||||
[(gtr/transform-shape shape modifiers) modifiers])
|
||||
[shape nil]))
|
||||
|
||||
(defn get-drop-areas
|
||||
[frame objects]
|
||||
(let [[frame modifiers] (get-flip-modifiers frame)
|
||||
children (->> (cph/get-immediate-children objects (:id frame))
|
||||
(remove :hidden)
|
||||
(map #(cond-> % (some? modifiers)
|
||||
(gtr/transform-shape modifiers)))
|
||||
(map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %)))
|
||||
layout-data (fli/calc-layout-data frame children (:points frame))
|
||||
drop-areas (layout-drop-areas frame layout-data children)]
|
||||
drop-areas))
|
||||
|
||||
(defn get-drop-index
|
||||
[frame-id objects position]
|
||||
(let [frame (get objects frame-id)
|
||||
drop-areas (get-drop-areas frame objects)
|
||||
position (gmt/transform-point-center position (gco/center-shape frame) (:transform-inverse frame))
|
||||
children (->> (cph/get-immediate-children objects frame-id)
|
||||
(map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %)))
|
||||
layout-data (fli/calc-layout-data frame children (:points frame))
|
||||
drop-areas (layout-drop-areas frame layout-data children)
|
||||
area (d/seek #(gsr/contains-point? % position) drop-areas)]
|
||||
(:index area)))
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
(let [col? (ctl/col? shape)
|
||||
row? (ctl/row? shape)
|
||||
space-around? (ctl/space-around? shape)
|
||||
|
||||
wrap? (and (ctl/wrap? shape)
|
||||
(or col? (not (ctl/auto-width? shape)))
|
||||
@@ -78,18 +77,8 @@
|
||||
next-max-width (+ child-margin-width (if fill-width? child-max-width child-width))
|
||||
next-max-height (+ child-margin-height (if fill-height? child-max-height child-height))
|
||||
|
||||
total-gap-col (if space-around?
|
||||
(* layout-gap-col (+ num-children 2))
|
||||
(* layout-gap-col num-children))
|
||||
|
||||
total-gap-row (if space-around?
|
||||
(* layout-gap-row (+ num-children 2))
|
||||
(* layout-gap-row num-children))
|
||||
|
||||
next-line-min-width (+ line-min-width next-min-width total-gap-col)
|
||||
next-line-min-height (+ line-min-height next-min-height total-gap-row)
|
||||
|
||||
]
|
||||
next-line-min-width (+ line-min-width next-min-width (* layout-gap-row num-children))
|
||||
next-line-min-height (+ line-min-height next-min-height (* layout-gap-col num-children))]
|
||||
|
||||
(if (and (some? line-data)
|
||||
(or (not wrap?)
|
||||
@@ -152,9 +141,6 @@
|
||||
|
||||
(let [row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
space-around? (ctl/space-around? parent)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
@@ -182,85 +168,48 @@
|
||||
(let [[total-min-width total-min-height total-max-width total-max-height]
|
||||
(->> layout-lines (reduce add-ranges [0 0 0 0]))
|
||||
|
||||
get-layout-width (fn [{:keys [num-children]}]
|
||||
(let [num-gap (if space-around? (inc num-children) (dec num-children))]
|
||||
(- layout-width (* layout-gap-col num-gap))))
|
||||
get-layout-height (fn [{:keys [num-children]}]
|
||||
(let [num-gap (if space-around? (inc num-children) (dec num-children))]
|
||||
(- layout-height (* layout-gap-row num-gap))))
|
||||
get-layout-width (fn [{:keys [num-children]}] (- layout-width (* layout-gap-row (dec num-children))))
|
||||
get-layout-height (fn [{:keys [num-children]}] (- layout-height (* layout-gap-col (dec num-children))))
|
||||
|
||||
num-lines (count layout-lines)
|
||||
|
||||
;; When align-items is stretch we need to adjust the main axis size to grow for the full content
|
||||
stretch-width-fix
|
||||
(if (and col? (ctl/content-stretch? parent) (not auto-width?))
|
||||
(/ (- layout-width (* layout-gap-col (dec num-lines)) total-max-width) num-lines)
|
||||
(if (and col? (ctl/content-stretch? parent))
|
||||
(/ (- layout-width (* layout-gap-row (dec num-lines)) total-max-width) num-lines)
|
||||
0)
|
||||
|
||||
stretch-height-fix
|
||||
(if (and row? (ctl/content-stretch? parent) (not auto-height?))
|
||||
(/ (- layout-height (* layout-gap-row (dec num-lines)) total-max-height) num-lines)
|
||||
(if (and row? (ctl/content-stretch? parent))
|
||||
(/ (- layout-height (* layout-gap-col (dec num-lines)) total-max-height) num-lines)
|
||||
0)
|
||||
|
||||
rest-layout-height (- layout-height (* (dec num-lines) layout-gap-row))
|
||||
rest-layout-width (- layout-width (* (dec num-lines) layout-gap-col))
|
||||
|
||||
;; Distributes the space between the layout lines based on its max/min constraints
|
||||
layout-lines
|
||||
(cond->> layout-lines
|
||||
row?
|
||||
(map #(assoc % :line-width
|
||||
(if (ctl/auto-width? parent)
|
||||
(:line-min-width %)
|
||||
(max (:line-min-width %) (min (get-layout-width %) (:line-max-width %))))))
|
||||
(map #(assoc % :line-width (max (:line-min-width %) (min (get-layout-width %) (:line-max-width %)))))
|
||||
|
||||
col?
|
||||
(map #(assoc % :line-height
|
||||
(if (ctl/auto-height? parent)
|
||||
(:line-min-height %)
|
||||
(max (:line-min-height %) (min (get-layout-height %) (:line-max-height %))))))
|
||||
(map #(assoc % :line-height (max (:line-min-height %) (min (get-layout-height %) (:line-max-height %)))))
|
||||
|
||||
(and row? (or (>= total-min-height rest-layout-height) (ctl/auto-height? parent)))
|
||||
(and row? (>= total-min-height layout-height))
|
||||
(map #(assoc % :line-height (:line-min-height %)))
|
||||
|
||||
(and row? (<= total-max-height rest-layout-height) (not (ctl/auto-height? parent)))
|
||||
(and row? (<= total-max-height layout-height))
|
||||
(map #(assoc % :line-height (+ (:line-max-height %) stretch-height-fix)))
|
||||
|
||||
(and row? (< total-min-height rest-layout-height total-max-height) (not (ctl/auto-height? parent)))
|
||||
(distribute-space :line-height :line-min-height :line-max-height total-min-height rest-layout-height)
|
||||
(and row? (< total-min-height layout-height total-max-height))
|
||||
(distribute-space :line-height :line-min-height :line-max-height total-min-height (- layout-height (* (dec num-lines) layout-gap-col)))
|
||||
|
||||
(and col? (or (>= total-min-width rest-layout-width) (ctl/auto-width? parent)))
|
||||
(and col? (>= total-min-width layout-width))
|
||||
(map #(assoc % :line-width (:line-min-width %)))
|
||||
|
||||
(and col? (<= total-max-width rest-layout-width) (not (ctl/auto-width? parent)))
|
||||
(and col? (<= total-max-width layout-width))
|
||||
(map #(assoc % :line-width (+ (:line-max-width %) stretch-width-fix)))
|
||||
|
||||
(and col? (< total-min-width rest-layout-width total-max-width) (not (ctl/auto-width? parent)))
|
||||
(distribute-space :line-width :line-min-width :line-max-width total-min-width rest-layout-width))
|
||||
|
||||
;; Add information to limit the growth of width: 100% shapes to the bounds of the layout
|
||||
layout-lines
|
||||
(cond
|
||||
row?
|
||||
(->> layout-lines
|
||||
(reduce
|
||||
(fn [[result rest-layout-height] {:keys [line-height] :as line}]
|
||||
[(conj result (assoc line :to-bound-height rest-layout-height))
|
||||
(- rest-layout-height line-height layout-gap-row)])
|
||||
[[] layout-height])
|
||||
(first))
|
||||
|
||||
col?
|
||||
(->> layout-lines
|
||||
(reduce
|
||||
(fn [[result rest-layout-width] {:keys [line-width] :as line}]
|
||||
[(conj result (assoc line :to-bound-width rest-layout-width))
|
||||
(- rest-layout-width line-width layout-gap-col)])
|
||||
[[] layout-width])
|
||||
(first))
|
||||
|
||||
:else
|
||||
layout-lines)
|
||||
(and col? (< total-min-width layout-width total-max-width))
|
||||
(distribute-space :line-width :line-min-width :line-max-width total-min-width (- layout-width (* (dec num-lines) layout-gap-row))))
|
||||
|
||||
[total-width total-height] (->> layout-lines (reduce add-lines [0 0]))
|
||||
|
||||
@@ -277,52 +226,40 @@
|
||||
|
||||
row? (ctl/row? shape)
|
||||
col? (ctl/col? shape)
|
||||
auto-height? (ctl/auto-height? shape)
|
||||
auto-width? (ctl/auto-width? shape)
|
||||
space-between? (ctl/space-between? shape)
|
||||
space-around? (ctl/space-around? shape)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps shape)
|
||||
|
||||
margin-x
|
||||
(cond (and row? space-around? (not auto-width?))
|
||||
(max layout-gap-col (/ (- width line-width) (inc num-children)))
|
||||
|
||||
(and row? space-around? auto-width?)
|
||||
layout-gap-col
|
||||
|
||||
:else
|
||||
0)
|
||||
|
||||
margin-y
|
||||
(cond (and col? space-around? (not auto-height?))
|
||||
(max layout-gap-row (/ (- height line-height) (inc num-children)))
|
||||
|
||||
(and col? space-around? auto-height?)
|
||||
layout-gap-row
|
||||
|
||||
:else
|
||||
0)
|
||||
|
||||
layout-gap-col
|
||||
layout-gap-row
|
||||
(cond (and row? space-around?)
|
||||
0
|
||||
|
||||
(and row? space-between? (not auto-width?))
|
||||
(max layout-gap-col (/ (- width line-width) (dec num-children)))
|
||||
(and row? space-between?)
|
||||
(/ (- width line-width) (dec num-children))
|
||||
|
||||
:else
|
||||
layout-gap-row)
|
||||
|
||||
layout-gap-col
|
||||
(cond (and col? space-around?)
|
||||
0
|
||||
|
||||
(and col? space-between?)
|
||||
(/ (- height line-height) (dec num-children))
|
||||
|
||||
:else
|
||||
layout-gap-col)
|
||||
|
||||
layout-gap-row
|
||||
(cond (and col? space-around?)
|
||||
0
|
||||
margin-x
|
||||
(if (and row? space-around?)
|
||||
(/ (- width line-width) (inc num-children))
|
||||
0)
|
||||
|
||||
(and col? space-between? (not auto-height?))
|
||||
(max layout-gap-row (/ (- height line-height) (dec num-children)))
|
||||
|
||||
:else
|
||||
layout-gap-row)]
|
||||
margin-y
|
||||
(if (and col? space-around?)
|
||||
(/ (- height line-height) (inc num-children))
|
||||
0)]
|
||||
(assoc line-data
|
||||
:layout-bounds layout-bounds
|
||||
:layout-gap-row layout-gap-row
|
||||
@@ -371,3 +308,4 @@
|
||||
{:layout-lines layout-lines
|
||||
:layout-bounds layout-bounds
|
||||
:reverse? reverse?}))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
transform-inverse
|
||||
child
|
||||
child-origin child-width
|
||||
{:keys [children-data line-width to-bound-width] :as layout-data}]
|
||||
{:keys [children-data line-width] :as layout-data}]
|
||||
|
||||
(cond
|
||||
(ctl/row? parent)
|
||||
@@ -30,9 +30,8 @@
|
||||
:modifiers (ctm/resize-modifiers (gpt/point fill-scale 1) child-origin transform transform-inverse)})
|
||||
|
||||
(ctl/col? parent)
|
||||
(let [line-width (min line-width (or to-bound-width line-width))
|
||||
target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
|
||||
max-width (max (ctl/child-max-width child) 0.01)
|
||||
(let [target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
|
||||
max-width (ctl/child-max-width child)
|
||||
target-width (min max-width target-width)
|
||||
fill-scale (/ target-width child-width)]
|
||||
{:width target-width
|
||||
@@ -44,7 +43,7 @@
|
||||
transform transform-inverse
|
||||
child
|
||||
child-origin child-height
|
||||
{:keys [children-data line-height to-bound-height] :as layout-data}]
|
||||
{:keys [children-data line-height] :as layout-data}]
|
||||
|
||||
(cond
|
||||
(ctl/col? parent)
|
||||
@@ -54,9 +53,8 @@
|
||||
:modifiers (ctm/resize-modifiers (gpt/point 1 fill-scale) child-origin transform transform-inverse)})
|
||||
|
||||
(ctl/row? parent)
|
||||
(let [line-height (min line-height (or to-bound-height line-height))
|
||||
target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
|
||||
max-height (max (ctl/child-max-height child) 0.01)
|
||||
(let [target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
|
||||
max-height (ctl/child-max-height child)
|
||||
target-height (min max-height target-height)
|
||||
fill-scale (/ target-height child-height)]
|
||||
{:height target-height
|
||||
@@ -70,16 +68,11 @@
|
||||
child-height (gpo/height-points child-bounds)
|
||||
|
||||
[_ transform transform-inverse]
|
||||
(when (or (ctl/fill-width? child) (ctl/fill-height? child))
|
||||
(when (or (ctl/fill-width? child) (ctl/fill-width? child))
|
||||
(gtr/calculate-geometry @parent-bounds))
|
||||
|
||||
fill-width
|
||||
(when (ctl/fill-width? child)
|
||||
(calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
|
||||
|
||||
fill-height
|
||||
(when (ctl/fill-height? child)
|
||||
(calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
|
||||
fill-width (when (ctl/fill-width? child) (calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
|
||||
fill-height (when (ctl/fill-height? child) (calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
|
||||
|
||||
child-width (or (:width fill-width) child-width)
|
||||
child-height (or (:height fill-height) child-height)
|
||||
|
||||
@@ -20,21 +20,17 @@
|
||||
hv (partial gpo/start-hv layout-bounds)
|
||||
vv (partial gpo/start-vv layout-bounds)
|
||||
|
||||
wrap? (ctl/wrap? parent)
|
||||
|
||||
end? (or (and wrap? (ctl/content-end? parent))
|
||||
(and (not wrap?) (ctl/align-items-end? parent)))
|
||||
center? (or (and wrap? (ctl/content-center? parent))
|
||||
(and (not wrap?) (ctl/align-items-center? parent)))
|
||||
around? (and wrap? (ctl/content-around? parent))
|
||||
end? (ctl/content-end? parent)
|
||||
center? (ctl/content-center? parent)
|
||||
around? (ctl/content-around? parent)
|
||||
|
||||
;; Adjust the totals so it takes into account the gaps
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
lines-gap-row (* (dec num-lines) layout-gap-row)
|
||||
lines-gap-col (* (dec num-lines) layout-gap-col)
|
||||
|
||||
free-width-gap (- layout-width total-width lines-gap-col)
|
||||
free-height-gap (- layout-height total-height lines-gap-row)
|
||||
free-width-gap (- layout-width total-width lines-gap-row)
|
||||
free-height-gap (- layout-height total-height lines-gap-col)
|
||||
free-width (- layout-width total-width)
|
||||
free-height (- layout-height total-height)]
|
||||
|
||||
@@ -47,7 +43,7 @@
|
||||
(gpt/add (vv free-height-gap))
|
||||
|
||||
around?
|
||||
(gpt/add (vv (max lines-gap-row (/ free-height (inc num-lines))))))
|
||||
(gpt/add (vv (/ free-height (inc num-lines)))))
|
||||
|
||||
col?
|
||||
(cond-> center?
|
||||
@@ -57,7 +53,7 @@
|
||||
(gpt/add (hv free-width-gap))
|
||||
|
||||
around?
|
||||
(gpt/add (hv (max lines-gap-col (/ free-width (inc num-lines)))))))))
|
||||
(gpt/add (hv (/ free-width (inc num-lines))))))))
|
||||
|
||||
(defn get-next-line
|
||||
[parent layout-bounds {:keys [line-width line-height]} base-p total-width total-height num-lines]
|
||||
@@ -67,9 +63,6 @@
|
||||
row? (ctl/row? parent)
|
||||
col? (ctl/col? parent)
|
||||
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
|
||||
[layout-gap-row layout-gap-col] (ctl/gaps parent)
|
||||
|
||||
hv #(gpo/start-hv layout-bounds %)
|
||||
@@ -82,11 +75,8 @@
|
||||
free-width (- layout-width total-width)
|
||||
free-height (- layout-height total-height)
|
||||
|
||||
line-gap-col
|
||||
line-gap-row
|
||||
(cond
|
||||
auto-width?
|
||||
layout-gap-col
|
||||
|
||||
stretch?
|
||||
(/ free-width num-lines)
|
||||
|
||||
@@ -97,13 +87,10 @@
|
||||
(/ free-width (inc num-lines))
|
||||
|
||||
:else
|
||||
layout-gap-col)
|
||||
layout-gap-row)
|
||||
|
||||
line-gap-row
|
||||
line-gap-col
|
||||
(cond
|
||||
auto-height?
|
||||
layout-gap-row
|
||||
|
||||
stretch?
|
||||
(/ free-height num-lines)
|
||||
|
||||
@@ -114,14 +101,14 @@
|
||||
(/ free-height (inc num-lines))
|
||||
|
||||
:else
|
||||
layout-gap-row)]
|
||||
layout-gap-col)]
|
||||
|
||||
(cond-> base-p
|
||||
row?
|
||||
(gpt/add (vv (+ line-height (max layout-gap-row line-gap-row))))
|
||||
(gpt/add (vv (+ line-height (max layout-gap-col line-gap-col))))
|
||||
|
||||
col?
|
||||
(gpt/add (hv (+ line-width (max layout-gap-col line-gap-col)))))))
|
||||
(gpt/add (hv (+ line-width (max layout-gap-row line-gap-row)))))))
|
||||
|
||||
(defn get-start-line
|
||||
"Cross axis line. It's position is fixed along the different lines"
|
||||
@@ -139,20 +126,18 @@
|
||||
v-center? (ctl/v-center? parent)
|
||||
v-end? (ctl/v-end? parent)
|
||||
content-stretch? (ctl/content-stretch? parent)
|
||||
auto-width? (ctl/auto-width? parent)
|
||||
auto-height? (ctl/auto-height? parent)
|
||||
hv (partial gpo/start-hv layout-bounds)
|
||||
vv (partial gpo/start-vv layout-bounds)
|
||||
children-gap-width (* layout-gap-col (dec num-children))
|
||||
children-gap-height (* layout-gap-row (dec num-children))
|
||||
children-gap-width (* layout-gap-row (dec num-children))
|
||||
children-gap-height (* layout-gap-col (dec num-children))
|
||||
|
||||
line-height
|
||||
(if (and row? content-stretch? (not auto-height?))
|
||||
(if (and row? content-stretch?)
|
||||
(+ line-height (/ (- layout-height total-height) num-lines))
|
||||
line-height)
|
||||
|
||||
line-width
|
||||
(if (and col? content-stretch? (not auto-width?))
|
||||
(if (and col? content-stretch?)
|
||||
(+ line-width (/ (- layout-width total-width) num-lines))
|
||||
line-width)
|
||||
|
||||
@@ -272,13 +257,13 @@
|
||||
next-p
|
||||
(cond-> start-p
|
||||
row?
|
||||
(-> (gpt/add (hv (+ child-width layout-gap-col)))
|
||||
(-> (gpt/add (hv (+ child-width layout-gap-row)))
|
||||
(gpt/add (hv (+ margin-left margin-right))))
|
||||
|
||||
col?
|
||||
(-> (gpt/add (vv (+ margin-top margin-bottom)))
|
||||
(gpt/add (vv (+ child-height layout-gap-row))))
|
||||
|
||||
(gpt/add (vv (+ child-height layout-gap-col))))
|
||||
|
||||
(some? margin-x)
|
||||
(gpt/add (hv margin-x))
|
||||
|
||||
|
||||
@@ -363,10 +363,10 @@
|
||||
c2 (+ (* a2 (:x c)) (* b2 (:y c)))
|
||||
|
||||
;; Cramer's rule
|
||||
det (- (* a1 b2) (* a2 b1))
|
||||
det (if (mth/almost-zero? det) 0.001 det)
|
||||
det (- (* a1 b2) (* a2 b1))]
|
||||
|
||||
x (/ (- (* b2 c1) (* b1 c2)) det)
|
||||
y (/ (- (* c2 a1) (* c1 a2)) det)]
|
||||
|
||||
(gpt/point x y)))
|
||||
;; If almost zero the lines are parallel
|
||||
(when (not (mth/almost-zero? det))
|
||||
(let [x (/ (- (* b2 c1) (* b1 c2)) det)
|
||||
y (/ (- (* c2 a1) (* c1 a2)) det)]
|
||||
(gpt/point x y)))))
|
||||
|
||||
@@ -28,16 +28,6 @@
|
||||
;; [(get-in objects [k :name]) v]))
|
||||
;; modif-tree))))
|
||||
|
||||
(defn children-sequence
|
||||
"Given an id returns a sequence of its children"
|
||||
[id objects]
|
||||
|
||||
(->> (tree-seq
|
||||
#(d/not-empty? (dm/get-in objects [% :shapes]))
|
||||
#(dm/get-in objects [% :shapes])
|
||||
id)
|
||||
(map #(get objects %))))
|
||||
|
||||
(defn resolve-tree-sequence
|
||||
"Given the ids that have changed search for layout roots to recalculate"
|
||||
[ids objects]
|
||||
@@ -85,12 +75,20 @@
|
||||
|
||||
(cond-> result
|
||||
(not contains-parent?)
|
||||
(conj root)))))]
|
||||
(conj root)))))
|
||||
|
||||
(generate-tree ;; Generate a tree sequence from a given root id
|
||||
[id]
|
||||
(->> (tree-seq
|
||||
#(d/not-empty? (dm/get-in objects [% :shapes]))
|
||||
#(dm/get-in objects [% :shapes])
|
||||
id)
|
||||
(map #(get objects %))))]
|
||||
|
||||
(let [roots (->> ids (reduce calculate-common-roots #{}))]
|
||||
(concat
|
||||
(when (contains? ids uuid/zero) [(get objects uuid/zero)])
|
||||
(mapcat #(children-sequence % objects) roots)))))
|
||||
(mapcat generate-tree roots)))))
|
||||
|
||||
(defn- set-children-modifiers
|
||||
"Propagates the modifiers from a parent too its children applying constraints if necesary"
|
||||
@@ -132,7 +130,7 @@
|
||||
children (cph/get-immediate-children objects shape-id)]
|
||||
|
||||
(cond
|
||||
(and (cph/mask-shape? shape) (seq children))
|
||||
(cph/mask-shape? shape)
|
||||
(get-group-bounds objects bounds modif-tree (-> children first))
|
||||
|
||||
(cph/group-shape? shape)
|
||||
@@ -171,9 +169,8 @@
|
||||
|
||||
[layout-line modif-tree]))]
|
||||
|
||||
(let [children (->> (cph/get-immediate-children objects (:id parent))
|
||||
(remove :hidden)
|
||||
(map apply-modifiers))
|
||||
(let [children (->> (:shapes parent)
|
||||
(map (comp apply-modifiers (d/getf objects))))
|
||||
layout-data (gcl/calc-layout-data parent children @transformed-parent-bounds)
|
||||
children (into [] (cond-> children (not (:reverse? layout-data)) reverse))
|
||||
max-idx (dec (count children))
|
||||
@@ -212,9 +209,7 @@
|
||||
(-> modifiers
|
||||
(ctm/resize-parent (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent)))))
|
||||
|
||||
children (->> (cph/get-immediate-children objects parent-id)
|
||||
(remove :hidden))
|
||||
|
||||
children (->> parent :shapes (map (d/getf objects)))
|
||||
content-bounds
|
||||
(when (and (d/not-empty? children) (or (ctl/auto-height? parent) (ctl/auto-width? parent)))
|
||||
(gcl/layout-content-bounds bounds parent children))
|
||||
@@ -298,136 +293,57 @@
|
||||
result (assoc result (:id shape) new-bounds)]
|
||||
(recur result (rest shapes)))))))
|
||||
|
||||
(defn reflow-layout
|
||||
[objects old-modif-tree bounds ignore-constraints id]
|
||||
|
||||
(let [tree-seq (children-sequence id objects)
|
||||
|
||||
[modif-tree _]
|
||||
(reduce
|
||||
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}]
|
||||
tree-seq)
|
||||
|
||||
bounds (transform-bounds bounds objects modif-tree tree-seq)
|
||||
|
||||
modif-tree (merge-modif-tree old-modif-tree modif-tree)]
|
||||
[modif-tree bounds]))
|
||||
|
||||
(defn sizing-auto-modifiers
|
||||
"Recalculates the layouts to adjust the sizing: auto new sizes"
|
||||
[modif-tree sizing-auto-layouts objects bounds ignore-constraints]
|
||||
(let [;; Step-1 resize the auto-width/height. Reflow the parents if they are also auto-width/height
|
||||
[modif-tree bounds to-reflow]
|
||||
(loop [modif-tree modif-tree
|
||||
bounds bounds
|
||||
sizing-auto-layouts (reverse sizing-auto-layouts)
|
||||
to-reflow #{}]
|
||||
(if-let [current (first sizing-auto-layouts)]
|
||||
(let [parent-base (get objects current)
|
||||
(loop [modif-tree modif-tree
|
||||
bounds bounds
|
||||
sizing-auto-layouts (reverse sizing-auto-layouts)]
|
||||
(if-let [current (first sizing-auto-layouts)]
|
||||
(let [parent-base (get objects current)
|
||||
|
||||
[modif-tree bounds]
|
||||
(if (contains? to-reflow current)
|
||||
(reflow-layout objects modif-tree bounds ignore-constraints current)
|
||||
[modif-tree bounds])
|
||||
resize-modif-tree
|
||||
{current {:modifiers (calc-auto-modifiers objects bounds parent-base)}}
|
||||
|
||||
auto-resize-modifiers
|
||||
(calc-auto-modifiers objects bounds parent-base)
|
||||
tree-seq (resolve-tree-sequence #{current} objects)
|
||||
|
||||
to-reflow
|
||||
(cond-> to-reflow
|
||||
(contains? to-reflow current)
|
||||
(disj current))]
|
||||
[resize-modif-tree _]
|
||||
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}] tree-seq)
|
||||
|
||||
(if (ctm/empty? auto-resize-modifiers)
|
||||
(recur modif-tree
|
||||
bounds
|
||||
(rest sizing-auto-layouts)
|
||||
to-reflow)
|
||||
bounds (transform-bounds bounds objects resize-modif-tree tree-seq)
|
||||
|
||||
(let [resize-modif-tree {current {:modifiers auto-resize-modifiers}}
|
||||
|
||||
tree-seq (children-sequence current objects)
|
||||
|
||||
[resize-modif-tree _]
|
||||
(reduce
|
||||
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}]
|
||||
tree-seq)
|
||||
|
||||
bounds (transform-bounds bounds objects resize-modif-tree tree-seq)
|
||||
|
||||
modif-tree (merge-modif-tree modif-tree resize-modif-tree)
|
||||
|
||||
to-reflow
|
||||
(cond-> to-reflow
|
||||
(and (ctl/layout-descent? objects parent-base)
|
||||
(not= uuid/zero (:frame-id parent-base)))
|
||||
(conj (:frame-id parent-base)))]
|
||||
(recur modif-tree
|
||||
bounds
|
||||
(rest sizing-auto-layouts)
|
||||
to-reflow))))
|
||||
[modif-tree bounds to-reflow]))
|
||||
|
||||
;; Step-2: After resizing we still need to reflow the layout parents that are not auto-width/height
|
||||
|
||||
tree-seq (resolve-tree-sequence to-reflow objects)
|
||||
|
||||
[reflow-modif-tree _]
|
||||
(reduce
|
||||
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}]
|
||||
tree-seq)
|
||||
|
||||
result (merge-modif-tree modif-tree reflow-modif-tree)]
|
||||
|
||||
result))
|
||||
modif-tree (merge-modif-tree modif-tree resize-modif-tree)]
|
||||
(recur modif-tree bounds (rest sizing-auto-layouts)))
|
||||
modif-tree)))
|
||||
|
||||
(defn set-objects-modifiers
|
||||
([modif-tree objects]
|
||||
(set-objects-modifiers modif-tree objects nil))
|
||||
[modif-tree objects ignore-constraints snap-pixel?]
|
||||
|
||||
([modif-tree objects params]
|
||||
(set-objects-modifiers nil modif-tree objects params))
|
||||
(let [objects (apply-structure-modifiers objects modif-tree)
|
||||
|
||||
([old-modif-tree modif-tree objects
|
||||
{:keys [ignore-constraints snap-pixel? snap-precision snap-ignore-axis]
|
||||
:or {ignore-constraints false snap-pixel? false snap-precision 1 snap-ignore-axis nil}}]
|
||||
(let [objects (-> objects
|
||||
(cond-> (some? old-modif-tree)
|
||||
(apply-structure-modifiers old-modif-tree))
|
||||
(apply-structure-modifiers modif-tree))
|
||||
bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))
|
||||
shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects)
|
||||
|
||||
modif-tree
|
||||
(cond-> modif-tree
|
||||
snap-pixel? (gpp/adjust-pixel-precision objects snap-precision snap-ignore-axis))
|
||||
;; Calculate the input transformation and constraints
|
||||
modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree)
|
||||
bounds (transform-bounds bounds objects modif-tree shapes-tree)
|
||||
|
||||
bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))
|
||||
bounds (cond-> bounds
|
||||
(some? old-modif-tree)
|
||||
(transform-bounds objects old-modif-tree))
|
||||
[modif-tree-layout sizing-auto-layouts]
|
||||
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] shapes-tree)
|
||||
|
||||
shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects)
|
||||
modif-tree (merge-modif-tree modif-tree modif-tree-layout)
|
||||
|
||||
;; Calculate the input transformation and constraints
|
||||
modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree)
|
||||
bounds (transform-bounds bounds objects modif-tree shapes-tree)
|
||||
;; Calculate hug layouts positions
|
||||
bounds (transform-bounds bounds objects modif-tree-layout shapes-tree)
|
||||
|
||||
[modif-tree-layout sizing-auto-layouts]
|
||||
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] shapes-tree)
|
||||
modif-tree
|
||||
(-> modif-tree
|
||||
(sizing-auto-modifiers sizing-auto-layouts objects bounds ignore-constraints))
|
||||
|
||||
modif-tree (merge-modif-tree modif-tree modif-tree-layout)
|
||||
modif-tree
|
||||
(cond-> modif-tree
|
||||
snap-pixel? (gpp/adjust-pixel-precision objects))]
|
||||
|
||||
;; Calculate hug layouts positions
|
||||
bounds (transform-bounds bounds objects modif-tree-layout shapes-tree)
|
||||
|
||||
modif-tree
|
||||
(-> modif-tree
|
||||
(sizing-auto-modifiers sizing-auto-layouts objects bounds ignore-constraints))
|
||||
|
||||
modif-tree
|
||||
(if old-modif-tree
|
||||
(merge-modif-tree old-modif-tree modif-tree)
|
||||
modif-tree)]
|
||||
|
||||
;;#?(:cljs
|
||||
;; (.log js/console ">result" (modif->js modif-tree objects)))
|
||||
modif-tree)))
|
||||
;;#?(:cljs
|
||||
;; (.log js/console ">result" (modif->js modif-tree objects)))
|
||||
modif-tree))
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
[x y] (->> coords (mapv solve-derivative))
|
||||
|
||||
;; normalize value
|
||||
d (mth/hypot x y)]
|
||||
d (mth/sqrt (+ (* x x) (* y y)))]
|
||||
|
||||
(if (mth/almost-zero? d)
|
||||
(gpt/point 0 0)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
[app.common.types.modifiers :as ctm]))
|
||||
|
||||
(defn size-pixel-precision
|
||||
[modifiers shape points precision]
|
||||
[modifiers shape points]
|
||||
(let [origin (gpo/origin points)
|
||||
curr-width (gpo/width-points points)
|
||||
curr-height (gpo/height-points points)
|
||||
@@ -29,42 +29,34 @@
|
||||
vertical-line? (and path? (<= curr-width 0.01))
|
||||
horizontal-line? (and path? (<= curr-height 0.01))
|
||||
|
||||
target-width (if vertical-line? curr-width (max 1 (mth/round curr-width precision)))
|
||||
target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height precision)))
|
||||
target-width (if vertical-line? curr-width (max 1 (mth/round curr-width)))
|
||||
target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height)))
|
||||
|
||||
ratio-width (/ target-width curr-width)
|
||||
ratio-height (/ target-height curr-height)
|
||||
scalev (gpt/point ratio-width ratio-height)]
|
||||
|
||||
(-> modifiers
|
||||
(ctm/resize scalev origin transform transform-inverse {:precise? true}))))
|
||||
(ctm/resize scalev origin transform transform-inverse))))
|
||||
|
||||
(defn position-pixel-precision
|
||||
[modifiers _ points precision ignore-axis]
|
||||
[modifiers _ points]
|
||||
(let [bounds (gpr/bounds->rect points)
|
||||
corner (gpt/point bounds)
|
||||
target-corner
|
||||
(cond-> corner
|
||||
(= ignore-axis :x)
|
||||
(update :y mth/round precision)
|
||||
|
||||
(= ignore-axis :y)
|
||||
(update :x mth/round precision)
|
||||
|
||||
(nil? ignore-axis)
|
||||
(gpt/round-step precision))
|
||||
target-corner (gpt/round corner)
|
||||
deltav (gpt/to-vec corner target-corner)]
|
||||
(ctm/move modifiers deltav)))
|
||||
|
||||
(defn set-pixel-precision
|
||||
"Adjust modifiers so they adjust to the pixel grid"
|
||||
[modifiers shape precision ignore-axis]
|
||||
[modifiers shape]
|
||||
(let [points (-> shape :points (gco/transform-points (ctm/modifiers->transform modifiers)))
|
||||
has-resize? (not (ctm/only-move? modifiers))
|
||||
|
||||
[modifiers points]
|
||||
(let [modifiers
|
||||
(cond-> modifiers
|
||||
has-resize? (size-pixel-precision shape points precision))
|
||||
has-resize? (size-pixel-precision shape points))
|
||||
|
||||
points
|
||||
(if has-resize?
|
||||
@@ -72,16 +64,16 @@
|
||||
(gco/transform-points (ctm/modifiers->transform modifiers)) )
|
||||
points)]
|
||||
[modifiers points])]
|
||||
(position-pixel-precision modifiers shape points precision ignore-axis)))
|
||||
(position-pixel-precision modifiers shape points)))
|
||||
|
||||
(defn adjust-pixel-precision
|
||||
[modif-tree objects precision ignore-axis]
|
||||
[modif-tree objects]
|
||||
(let [update-modifiers
|
||||
(fn [modif-tree shape]
|
||||
(let [modifiers (dm/get-in modif-tree [(:id shape) :modifiers])]
|
||||
(cond-> modif-tree
|
||||
(ctm/has-geometry? modifiers)
|
||||
(update-in [(:id shape) :modifiers] set-pixel-precision shape precision ignore-axis))))]
|
||||
(update-in [(:id shape) :modifiers] set-pixel-precision shape))))]
|
||||
|
||||
(->> (keys modif-tree)
|
||||
(map (d/getf objects))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.common.geom.shapes.points
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.intersect :as gsi]
|
||||
@@ -55,11 +54,11 @@
|
||||
|
||||
(defn width-points
|
||||
[[p0 p1 _ _]]
|
||||
(max 0.01 (gpt/length (gpt/to-vec p0 p1))))
|
||||
(gpt/length (gpt/to-vec p0 p1)))
|
||||
|
||||
(defn height-points
|
||||
[[p0 _ _ p3]]
|
||||
(max 0.01 (gpt/length (gpt/to-vec p0 p3))))
|
||||
(gpt/length (gpt/to-vec p0 p3)))
|
||||
|
||||
(defn pad-points
|
||||
[[p0 p1 p2 p3 :as points] pad-top pad-right pad-bottom pad-left]
|
||||
@@ -116,9 +115,7 @@
|
||||
(max tv-max ctv)]))
|
||||
|
||||
[th-min th-max tv-min tv-max]
|
||||
(->> child-bounds
|
||||
(filter #(and (d/num? (:x %)) (d/num? (:y %))))
|
||||
(reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
|
||||
(->> child-bounds (reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
|
||||
|
||||
minv-start (pv tv-min)
|
||||
minv-end (gpt/add minv-start hv)
|
||||
@@ -134,7 +131,6 @@
|
||||
i2 (gsi/line-line-intersect minv-start minv-end maxh-start maxh-end)
|
||||
i3 (gsi/line-line-intersect maxv-start maxv-end maxh-start maxh-end)
|
||||
i4 (gsi/line-line-intersect maxv-start maxv-end minh-start minh-end)]
|
||||
|
||||
[i1 i2 i3 i4])))
|
||||
|
||||
(defn merge-parent-coords-bounds
|
||||
@@ -147,8 +143,3 @@
|
||||
height (height-points points)
|
||||
center (gco/center-points points)]
|
||||
(gre/center->selrect center width height)))
|
||||
|
||||
(defn move
|
||||
[bounds vector]
|
||||
(->> bounds
|
||||
(map #(gpt/add % vector))))
|
||||
|
||||
@@ -212,13 +212,9 @@
|
||||
(<= (:y2 sr2) (:y2 sr1))))
|
||||
|
||||
(defn corners->selrect
|
||||
([p1 p2]
|
||||
(corners->selrect (:x p1) (:y p1) (:x p2) (:y p2)))
|
||||
([xp1 yp1 xp2 yp2]
|
||||
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
|
||||
|
||||
(defn clip-selrect
|
||||
[{:keys [x1 y1 x2 y2] :as sr} bounds]
|
||||
(when (some? sr)
|
||||
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2} (rect->selrect bounds)]
|
||||
(corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2)))))
|
||||
[p1 p2]
|
||||
(let [xp1 (:x p1)
|
||||
xp2 (:x p2)
|
||||
yp1 (:y p1)
|
||||
yp2 (:y p2)]
|
||||
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.geom.shapes.path :as gpa]
|
||||
[app.common.geom.shapes.rect :as gpr]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.uuid :as uuid]))
|
||||
@@ -44,16 +43,14 @@
|
||||
(mapv #(gpt/add % move-vec))))
|
||||
|
||||
(defn move-position-data
|
||||
([position-data {:keys [x y]}]
|
||||
(move-position-data position-data x y))
|
||||
[position-data dx dy]
|
||||
|
||||
([position-data dx dy]
|
||||
(when (some? position-data)
|
||||
(cond->> position-data
|
||||
(d/num? dx dy)
|
||||
(mapv #(-> %
|
||||
(update :x + dx)
|
||||
(update :y + dy)))))))
|
||||
(when (some? position-data)
|
||||
(cond->> position-data
|
||||
(d/num? dx dy)
|
||||
(mapv #(-> %
|
||||
(update :x + dx)
|
||||
(update :y + dy))))))
|
||||
|
||||
(defn move
|
||||
"Move the shape relatively to its current
|
||||
@@ -114,10 +111,10 @@
|
||||
(cond-> (some? transform)
|
||||
(gmt/multiply transform))
|
||||
|
||||
(cond-> (and flip-x no-flip)
|
||||
(cond-> (and flip-x (not no-flip))
|
||||
(gmt/scale (gpt/point -1 1)))
|
||||
|
||||
(cond-> (and flip-y no-flip)
|
||||
(cond-> (and flip-y (not no-flip))
|
||||
(gmt/scale (gpt/point 1 -1)))
|
||||
|
||||
(gmt/translate (gpt/negate shape-center)))))
|
||||
@@ -129,8 +126,8 @@
|
||||
([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip] :as params}]
|
||||
(if (and (some? shape)
|
||||
(or (some? transform)
|
||||
(and no-flip flip-x)
|
||||
(and no-flip flip-y)))
|
||||
(and (not no-flip) flip-x)
|
||||
(and (not no-flip) flip-y)))
|
||||
(dm/str (transform-matrix shape params))
|
||||
"")))
|
||||
|
||||
@@ -160,79 +157,67 @@
|
||||
"Calculate the transform matrix to convert from the selrect to the points bounds
|
||||
TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
|
||||
[{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
|
||||
;; If the coordinates are very close to zero (but not zero) the rounding can mess with the
|
||||
;; transforms. So we round to zero the values
|
||||
(let [x1 (mth/round-to-zero x1)
|
||||
y1 (mth/round-to-zero y1)
|
||||
x2 (mth/round-to-zero x2)
|
||||
y2 (mth/round-to-zero y2)
|
||||
d1x (mth/round-to-zero (:x d1))
|
||||
d1y (mth/round-to-zero (:y d1))
|
||||
d2x (mth/round-to-zero (:x d2))
|
||||
d2y (mth/round-to-zero (:y d2))
|
||||
d4x (mth/round-to-zero (:x d4))
|
||||
d4y (mth/round-to-zero (:y d4))]
|
||||
#?(:clj
|
||||
;; NOTE: the source matrix may not be invertible we can't
|
||||
;; calculate the transform, so on exception we return `nil`
|
||||
(ex/ignoring
|
||||
(let [target-points-matrix
|
||||
(->> (list d1x d2x d4x
|
||||
d1y d2y d4y
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
#?(:clj
|
||||
;; NOTE: the source matrix may not be invertible we can't
|
||||
;; calculate the transform, so on exception we return `nil`
|
||||
(ex/ignoring
|
||||
(let [target-points-matrix
|
||||
(->> (list (:x d1) (:x d2) (:x d4)
|
||||
(:y d1) (:y d2) (:y d4)
|
||||
1 1 1 )
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
|
||||
source-points-matrix
|
||||
(->> (list x1 x2 x1
|
||||
y1 y1 y2
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
source-points-matrix
|
||||
(->> (list x1 x2 x1
|
||||
y1 y1 y2
|
||||
1 1 1)
|
||||
(into-array Double/TYPE)
|
||||
(Matrix/from1DArray 3 3))
|
||||
|
||||
;; May throw an exception if the matrix is not invertible
|
||||
source-points-matrix-inv
|
||||
(.. source-points-matrix
|
||||
(withInverter LinearAlgebra/GAUSS_JORDAN)
|
||||
(inverse))
|
||||
;; May throw an exception if the matrix is not invertible
|
||||
source-points-matrix-inv
|
||||
(.. source-points-matrix
|
||||
(withInverter LinearAlgebra/GAUSS_JORDAN)
|
||||
(inverse))
|
||||
|
||||
transform-jvm
|
||||
(.. target-points-matrix
|
||||
(multiply source-points-matrix-inv))]
|
||||
transform-jvm
|
||||
(.. target-points-matrix
|
||||
(multiply source-points-matrix-inv))]
|
||||
|
||||
(gmt/matrix (.get transform-jvm 0 0)
|
||||
(.get transform-jvm 1 0)
|
||||
(.get transform-jvm 0 1)
|
||||
(.get transform-jvm 1 1)
|
||||
(.get transform-jvm 0 2)
|
||||
(.get transform-jvm 1 2))))
|
||||
(gmt/matrix (.get transform-jvm 0 0)
|
||||
(.get transform-jvm 1 0)
|
||||
(.get transform-jvm 0 1)
|
||||
(.get transform-jvm 1 1)
|
||||
(.get transform-jvm 0 2)
|
||||
(.get transform-jvm 1 2))))
|
||||
|
||||
:cljs
|
||||
(let [target-points-matrix
|
||||
(Matrix. #js [#js [d1x d2x d4x]
|
||||
#js [d1y d2y d4y]
|
||||
#js [ 1 1 1]])
|
||||
:cljs
|
||||
(let [target-points-matrix
|
||||
(Matrix. #js [#js [(:x d1) (:x d2) (:x d4)]
|
||||
#js [(:y d1) (:y d2) (:y d4)]
|
||||
#js [ 1 1 1]])
|
||||
|
||||
source-points-matrix
|
||||
(Matrix. #js [#js [x1 x2 x1]
|
||||
#js [y1 y1 y2]
|
||||
#js [ 1 1 1]])
|
||||
source-points-matrix
|
||||
(Matrix. #js [#js [x1 x2 x1]
|
||||
#js [y1 y1 y2]
|
||||
#js [ 1 1 1]])
|
||||
|
||||
;; returns nil if not invertible
|
||||
source-points-matrix-inv (.getInverse source-points-matrix)
|
||||
;; returns nil if not invertible
|
||||
source-points-matrix-inv (.getInverse source-points-matrix)
|
||||
|
||||
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
|
||||
transform-js
|
||||
(when source-points-matrix-inv
|
||||
(.multiply target-points-matrix source-points-matrix-inv))]
|
||||
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
|
||||
transform-js
|
||||
(when source-points-matrix-inv
|
||||
(.multiply target-points-matrix source-points-matrix-inv))]
|
||||
|
||||
(when transform-js
|
||||
(gmt/matrix (.getValueAt transform-js 0 0)
|
||||
(.getValueAt transform-js 1 0)
|
||||
(.getValueAt transform-js 0 1)
|
||||
(.getValueAt transform-js 1 1)
|
||||
(.getValueAt transform-js 0 2)
|
||||
(.getValueAt transform-js 1 2)))))))
|
||||
(when transform-js
|
||||
(gmt/matrix (.getValueAt transform-js 0 0)
|
||||
(.getValueAt transform-js 1 0)
|
||||
(.getValueAt transform-js 0 1)
|
||||
(.getValueAt transform-js 1 1)
|
||||
(.getValueAt transform-js 0 2)
|
||||
(.getValueAt transform-js 1 2))))))
|
||||
|
||||
(defn calculate-geometry
|
||||
[points]
|
||||
@@ -251,15 +236,7 @@
|
||||
points-transform-mtx
|
||||
(gmt/translate-matrix center)))
|
||||
|
||||
transform-inverse (when transform (gmt/inverse transform))
|
||||
|
||||
;; There is a rounding error when the matrix returned have float point values
|
||||
;; when the matrix is unit we return a "pure" matrix so we don't accumulate
|
||||
;; rounding problems
|
||||
[transform transform-inverse]
|
||||
(if (gmt/unit? transform)
|
||||
[(gmt/matrix) (gmt/matrix)]
|
||||
[transform transform-inverse])]
|
||||
transform-inverse (when transform (gmt/inverse transform))]
|
||||
|
||||
[sr transform transform-inverse]))
|
||||
|
||||
|
||||
@@ -175,15 +175,14 @@
|
||||
#?(:clj
|
||||
(defn get-error-context
|
||||
[error]
|
||||
(merge
|
||||
{:hint (ex-message error)}
|
||||
(when-let [data (ex-data error)]
|
||||
(merge
|
||||
{:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))}
|
||||
(when-let [explain (ex/explain data)]
|
||||
{:spec-explain explain}))))))
|
||||
(when-let [data (ex-data error)]
|
||||
(merge
|
||||
{:hint (ex-message error)
|
||||
:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))}
|
||||
(when-let [explain (ex/explain data)]
|
||||
{:spec-explain explain})))))
|
||||
|
||||
(defmacro log
|
||||
[& props]
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
(defn abs
|
||||
[v]
|
||||
#?(:cljs (js/Math.abs v)
|
||||
:clj (Math/abs (double v))))
|
||||
:clj (Math/abs v)))
|
||||
|
||||
(defn sin
|
||||
"Returns the sine of a number"
|
||||
@@ -104,16 +104,15 @@
|
||||
|
||||
(defn round
|
||||
"Returns the value of a number rounded to
|
||||
the nearest integer.
|
||||
If given step rounds to the next closest step, for example:
|
||||
(round 13.4 0.5) => 13.5
|
||||
(round 13.4 0.3) => 13.3"
|
||||
([v step]
|
||||
(* (round (/ v step)) step))
|
||||
the nearest integer."
|
||||
[v]
|
||||
#?(:cljs (js/Math.round v)
|
||||
:clj (Math/round (float v))))
|
||||
|
||||
([v]
|
||||
#?(:cljs (js/Math.round v)
|
||||
:clj (Math/round (float v)))))
|
||||
(defn half-round
|
||||
"Returns a value rounded to the next point or half point"
|
||||
[v]
|
||||
(/ (round (* v 2)) 2))
|
||||
|
||||
(defn ceil
|
||||
"Returns the smallest integer greater than
|
||||
@@ -128,12 +127,6 @@
|
||||
(let [d (pow 10 n)]
|
||||
(/ (round (* v d)) d))))
|
||||
|
||||
(defn to-fixed
|
||||
"Returns a string representing the given number, using fixed precision."
|
||||
[v n]
|
||||
#?(:cljs (.toFixed ^js v n)
|
||||
:clj (str (precision v n))))
|
||||
|
||||
(defn radians
|
||||
"Converts degrees to radians."
|
||||
[degrees]
|
||||
@@ -146,18 +139,12 @@
|
||||
#?(:cljs (math/toDegrees radians)
|
||||
:clj (Math/toDegrees radians)))
|
||||
|
||||
(defn hypot
|
||||
"Square root of the squares addition"
|
||||
[a b]
|
||||
#?(:cljs (js/Math.hypot a b)
|
||||
:clj (Math/hypot a b)))
|
||||
|
||||
(defn distance
|
||||
"Calculate the distance between two points."
|
||||
[[x1 y1] [x2 y2]]
|
||||
(let [dx (- x1 x2)
|
||||
dy (- y1 y2)]
|
||||
(-> (hypot dx dy)
|
||||
(-> (sqrt (+ (pow dx 2) (pow dy 2)))
|
||||
(precision 2))))
|
||||
|
||||
(defn log10
|
||||
@@ -174,13 +161,6 @@
|
||||
(defn almost-zero? [num]
|
||||
(< (abs (double num)) 1e-4))
|
||||
|
||||
(defn round-to-zero
|
||||
"Given a number if it's close enough to zero round to the zero to avoid precision problems"
|
||||
[num]
|
||||
(if (almost-zero? num)
|
||||
0
|
||||
num))
|
||||
|
||||
(defonce float-equal-precision 0.001)
|
||||
|
||||
(defn close?
|
||||
@@ -202,4 +182,3 @@
|
||||
"Get the sign (+1 / -1) for the number"
|
||||
[n]
|
||||
(if (neg? n) -1 1))
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; We have added ".ttf" as string to solve a problem with chrome input selector
|
||||
(def valid-font-types #{"font/ttf", ".ttf", "font/woff", "application/font-woff", "font/otf"})
|
||||
(def valid-font-types #{"font/ttf" "font/woff", "application/font-woff", "font/otf"})
|
||||
(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
|
||||
(def str-image-types (str/join "," valid-image-types))
|
||||
(def str-font-types (str/join "," valid-font-types))
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
(dm/export common/file-version)
|
||||
(dm/export common/default-color)
|
||||
(dm/export common/component-sync-attrs)
|
||||
(dm/export common/retrieve-used-names)
|
||||
(dm/export common/generate-unique-name)
|
||||
|
||||
;; Focus
|
||||
(dm/export focus/focus-objects)
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
(d/update-in-when data [:components component-id :objects] reg-objects))))
|
||||
|
||||
(defmethod process-change :mov-objects
|
||||
[data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape]}]
|
||||
[data {:keys [parent-id shapes index page-id component-id ignore-touched]}]
|
||||
(letfn [(calculate-invalid-targets [objects shape-id]
|
||||
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
|
||||
(->> (get-in objects [shape-id :shapes])
|
||||
@@ -200,9 +200,21 @@
|
||||
(cph/insert-at-index prev-shapes index shapes)
|
||||
(cph/append-at-the-end prev-shapes shapes))))
|
||||
|
||||
(check-insert-items [prev-shapes parent index shapes]
|
||||
(if-not (:masked-group? parent)
|
||||
(insert-items prev-shapes index shapes)
|
||||
;; For masked groups, the first shape is the mask
|
||||
;; and it cannot be moved.
|
||||
(let [mask-id (first prev-shapes)
|
||||
other-ids (rest prev-shapes)
|
||||
not-mask-shapes (without-obj shapes mask-id)
|
||||
new-index (if (nil? index) nil (max (dec index) 0))
|
||||
new-shapes (insert-items other-ids new-index not-mask-shapes)]
|
||||
(into [mask-id] new-shapes))))
|
||||
|
||||
(add-to-parent [parent index shapes]
|
||||
(let [parent (-> parent
|
||||
(update :shapes insert-items index shapes)
|
||||
(update :shapes check-insert-items parent index shapes)
|
||||
;; We need to ensure that no `nil` in the
|
||||
;; shapes list after adding all the
|
||||
;; incoming shapes to the parent.
|
||||
@@ -248,13 +260,9 @@
|
||||
(not= :frame (:type obj))
|
||||
(as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj))))))
|
||||
|
||||
|
||||
|
||||
(move-objects [objects]
|
||||
(let [valid? (every? (partial is-valid-move? objects) shapes)
|
||||
parent (get objects parent-id)
|
||||
after-shape-index (d/index-of (:shapes parent) after-shape)
|
||||
index (if (nil? after-shape-index) index (inc after-shape-index))
|
||||
frame-id (if (= :frame (:type parent))
|
||||
(:id parent)
|
||||
(:frame-id parent))]
|
||||
@@ -275,7 +283,7 @@
|
||||
;; Ensure that all shapes of the new parent has a
|
||||
;; correct link to the topside frame.
|
||||
(reduce (partial assign-frame-id frame-id) $ shapes))
|
||||
objects)))]
|
||||
objects)))]
|
||||
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id :objects] move-objects)
|
||||
@@ -387,10 +395,6 @@
|
||||
is-geometry? (and (or (= group :geometry-group)
|
||||
(and (= group :content-group) (= (:type shape) :path)))
|
||||
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
|
||||
;; TODO: the check of :width and :height probably may be removed
|
||||
;; after the check added in data/workspace/modifiers/check-delta
|
||||
;; function. Better check it and test toroughly when activating
|
||||
;; components-v2 mode.
|
||||
shape-ref (:shape-ref shape)
|
||||
root-name? (and (= group :name-group)
|
||||
(:component-root? shape))
|
||||
|
||||
@@ -244,32 +244,39 @@
|
||||
(assert-page-id changes)
|
||||
(assert-objects changes)
|
||||
(let [objects (lookup-objects changes)
|
||||
|
||||
set-parent-change
|
||||
(cond-> {:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:page-id (::page-id (meta changes))
|
||||
:shapes (->> shapes reverse (mapv :id))}
|
||||
:shapes (->> shapes (mapv :id))}
|
||||
|
||||
(some? index)
|
||||
(assoc :index index))
|
||||
|
||||
mk-undo-change
|
||||
(fn [change-set shape]
|
||||
(let [prev-sibling (cph/get-prev-sibling objects (:id shape))]
|
||||
(let [idx (or (cph/get-position-on-parent objects (:id shape)) 0)
|
||||
;; Different index if the movement was from top to bottom or the other way
|
||||
;; Similar that on frontend/src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
;; with the 'side' property of the on-drop
|
||||
idx (if (< index idx)
|
||||
(inc idx)
|
||||
idx)]
|
||||
(d/preconj
|
||||
change-set
|
||||
{:type :mov-objects
|
||||
:page-id (::page-id (meta changes))
|
||||
:parent-id (:parent-id shape)
|
||||
:shapes [(:id shape)]
|
||||
:after-shape prev-sibling
|
||||
:index 0})))] ; index is used in case there is no after-shape (moving bottom shapes)
|
||||
:index idx})))]
|
||||
|
||||
(-> changes
|
||||
(update :redo-changes conj set-parent-change)
|
||||
(update :undo-changes #(reduce mk-undo-change % shapes))
|
||||
(apply-changes-local)))))
|
||||
|
||||
|
||||
(defn changed-attrs
|
||||
"Returns the list of attributes that will change when `update-fn` is applied"
|
||||
[object update-fn {:keys [attrs]}]
|
||||
@@ -377,16 +384,14 @@
|
||||
|
||||
add-undo-change-parent
|
||||
(fn [change-set id]
|
||||
(let [shape (get objects id)
|
||||
prev-sibling (cph/get-prev-sibling objects (:id shape))]
|
||||
(let [shape (get objects id)]
|
||||
(d/preconj
|
||||
change-set
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id (:parent-id shape)
|
||||
:shapes [id]
|
||||
:after-shape prev-sibling
|
||||
:index 0
|
||||
:index (cph/get-position-on-parent objects id)
|
||||
:ignore-touched true})))]
|
||||
|
||||
(-> changes
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::index integer?)
|
||||
@@ -62,7 +61,7 @@
|
||||
(some? (:frame-id o)))
|
||||
(and (contains? o :component-id)
|
||||
(not (contains? o :page-id))
|
||||
(not= (:frame-id o) uuid/zero))))
|
||||
(nil? (:frame-id o)))))
|
||||
|
||||
(defn- valid-container-id?
|
||||
[o]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user