mirror of
https://github.com/penpot/penpot.git
synced 2026-01-06 05:18:52 -05:00
Compare commits
138 Commits
1.0.0-alph
...
1.2.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136d00797c | ||
|
|
964dad0d5b | ||
|
|
30819a08f4 | ||
|
|
22b8eb856e | ||
|
|
f8ccd0b120 | ||
|
|
9d49d781cc | ||
|
|
a81d20a2d1 | ||
|
|
d5ff5ea91e | ||
|
|
cf465d93f9 | ||
|
|
521ccc25cf | ||
|
|
dc0765f6b0 | ||
|
|
8cfc2ec21a | ||
|
|
10cad69fac | ||
|
|
b7d3158514 | ||
|
|
4b8334fe1c | ||
|
|
608b5cc9f9 | ||
|
|
42a55015fa | ||
|
|
0a6e0d0f2c | ||
|
|
7846682223 | ||
|
|
5336bbbe65 | ||
|
|
8e5fd5892e | ||
|
|
eaff888486 | ||
|
|
f1383f4dca | ||
|
|
d9c10cea5d | ||
|
|
d48a1ca0b0 | ||
|
|
bfcfe2fd31 | ||
|
|
648c088d02 | ||
|
|
70258e0eee | ||
|
|
5b1e9ec7da | ||
|
|
7a250a170e | ||
|
|
2e438385f3 | ||
|
|
d6f3efb358 | ||
|
|
884410c0d8 | ||
|
|
cdab9ff69c | ||
|
|
1da43bb5b5 | ||
|
|
6f3a08be0c | ||
|
|
e5cb6ebec7 | ||
|
|
f60ad9e559 | ||
|
|
69b23e4000 | ||
|
|
bedfb9a1ee | ||
|
|
e4fb802d7a | ||
|
|
068a099f37 | ||
|
|
fa573f8a24 | ||
|
|
ebb745cc11 | ||
|
|
2b33300d79 | ||
|
|
946d40e6cd | ||
|
|
36285a65d2 | ||
|
|
fc49674997 | ||
|
|
d0a8647186 | ||
|
|
9b875aba21 | ||
|
|
76e43f339a | ||
|
|
32e832eb39 | ||
|
|
60704bca17 | ||
|
|
43e4712b86 | ||
|
|
5359c3a7ed | ||
|
|
81bf68c67c | ||
|
|
4d5231598f | ||
|
|
c1a139fc51 | ||
|
|
1cb18ad7cb | ||
|
|
6f0258c8d4 | ||
|
|
124efc0d88 | ||
|
|
924ecd998f | ||
|
|
07a94de607 | ||
|
|
7bd05d63ac | ||
|
|
bb15924c95 | ||
|
|
1ebce37e17 | ||
|
|
b93dc752fe | ||
|
|
dbbe1f7df2 | ||
|
|
a709c47f6f | ||
|
|
68ed30ff35 | ||
|
|
a65a31810c | ||
|
|
8c50dc0c72 | ||
|
|
a8a036206b | ||
|
|
8313f1d96e | ||
|
|
1898ed215e | ||
|
|
83aceba913 | ||
|
|
c56fb0ea47 | ||
|
|
83a2df3ef3 | ||
|
|
4703f6d5c7 | ||
|
|
8d2797f8a1 | ||
|
|
6cdde84445 | ||
|
|
afa35379b2 | ||
|
|
1099e08b7d | ||
|
|
89cb20ada7 | ||
|
|
32b0fd7b36 | ||
|
|
04670bb5f2 | ||
|
|
8566fe4ac1 | ||
|
|
e607e8315c | ||
|
|
a9b7cf61a5 | ||
|
|
7c7bda669c | ||
|
|
0c82c6f2f5 | ||
|
|
b7cbe49cb2 | ||
|
|
7378089f4a | ||
|
|
62b6b12066 | ||
|
|
39fdff9052 | ||
|
|
32c0913f00 | ||
|
|
7eb90d62b0 | ||
|
|
ec2683417f | ||
|
|
cb23c8b093 | ||
|
|
687f7ddf64 | ||
|
|
992a8e9aef | ||
|
|
6e08c6bc35 | ||
|
|
b71d05935a | ||
|
|
c14dbc19f8 | ||
|
|
1eff1c94c4 | ||
|
|
53be7feee1 | ||
|
|
e182cc4028 | ||
|
|
80309cbff3 | ||
|
|
816db29f9c | ||
|
|
526e0afc70 | ||
|
|
77973af49f | ||
|
|
dc5cff645a | ||
|
|
0ea8e9e750 | ||
|
|
69b4968578 | ||
|
|
b7e266e350 | ||
|
|
b056cc35e4 | ||
|
|
d66452423f | ||
|
|
d85537fa7b | ||
|
|
fc11fb6e3d | ||
|
|
cbdfb4349b | ||
|
|
19ed0b70c2 | ||
|
|
3092747b5f | ||
|
|
0adfc2ddab | ||
|
|
8fd8bc4537 | ||
|
|
e7d6a54907 | ||
|
|
e3c273c84b | ||
|
|
8aedbd1418 | ||
|
|
e713c30785 | ||
|
|
74a168d87e | ||
|
|
ca63ff621a | ||
|
|
d120af2c81 | ||
|
|
95ab5b57b7 | ||
|
|
2e7f90f3cc | ||
|
|
8403352af8 | ||
|
|
526b6e1f03 | ||
|
|
f2fd976934 | ||
|
|
8b9371d7e1 | ||
|
|
948a4038c6 |
@@ -1,5 +1,6 @@
|
||||
{:lint-as {potok.core/reify clojure.core/reify
|
||||
promesa.core/let clojure.core/let
|
||||
rumext.alpha/defc clojure.core/defn
|
||||
app.db/with-atomic clojure.core/with-open}
|
||||
:output
|
||||
{:exclude-files ["data_readers.clj"]}
|
||||
|
||||
63
CHANGES.md
Normal file
63
CHANGES.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# CHANGELOG #
|
||||
|
||||
## Next
|
||||
|
||||
### New features
|
||||
|
||||
### Bugs fixed
|
||||
|
||||
|
||||
## 1.2.0-alpha
|
||||
|
||||
### New features
|
||||
|
||||
- Add horizontal/vertical flip
|
||||
- Add images lock proportions by default [#541](https://github.com/penpot/penpot/discussions/541), [#609](https://github.com/penpot/penpot/issues/609)
|
||||
- Add new blob storage format (Zstd+nippy)
|
||||
- Add user feedback form
|
||||
- Improve French translations
|
||||
- Improve component testing
|
||||
- Increase default deletion delay to 7 days
|
||||
- Show a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519)
|
||||
- Fix behavior of select all command when there are objects outside frames [Taiga #1209](https://tree.taiga.io/project/penpot/issue/1209)
|
||||
|
||||
|
||||
### Bugs fixed
|
||||
|
||||
- Fix 404 when access shared link [#615](https://github.com/penpot/penpot/issues/615)
|
||||
- Fix 500 when requestion password reset
|
||||
- Fix Problems when transforming path shapes [Taiga #1170](https://tree.taiga.io/project/penpot/issue/1170)
|
||||
- Fix apply a color to a text selection from color palette was not working [Taiga #1189](https://tree.taiga.io/project/penpot/issue/1189)
|
||||
- Fix issues when moving shapes outside groups [Taiga #1138](https://tree.taiga.io/project/penpot/issue/1138)
|
||||
- Fix ldap function called on login click
|
||||
- Fix logo icon in viewer should go to dashboard [Taiga #1149](https://tree.taiga.io/project/penpot/issue/1149)
|
||||
- Fix ordering when restoring deleted shapes in sync [Taiga #1163](https://tree.taiga.io/project/penpot/issue/1163)
|
||||
- Fix problem when editing text immediately after creating [Taiga #1207](https://tree.taiga.io/project/penpot/issue/1207)
|
||||
- Fix problem when pasting URL's copied from the browser url bar [Taiga #1187](https://tree.taiga.io/project/penpot/issue/1187)
|
||||
- Fix problem with multiple selection and groups [Taiga #1128](https://tree.taiga.io/project/penpot/issue/1128)
|
||||
- Fix problem with red handler indicator on resize [Taiga #1188](https://tree.taiga.io/project/penpot/issue/1188)
|
||||
- Fix show correct error when google auth is disabled [Taiga #1119](https://tree.taiga.io/project/penpot/issue/1119)
|
||||
- Fix text alignment in preview [#594](https://github.com/penpot/penpot/issues/594)
|
||||
- Fix unexpected exception when uploading image [Taiga #1120](https://tree.taiga.io/project/penpot/issue/1120)
|
||||
- Fix updates on collaborative editing not updating selection rectangles [Taiga #1127](https://tree.taiga.io/project/penpot/issue/1127)
|
||||
- Make the team deletion deferred (in the same way other objects)
|
||||
|
||||
### Community contributions by (Thank you! :heart:)
|
||||
|
||||
- abtinmo [#538](https://github.com/penpot/penpot/pull/538)
|
||||
- kdrag0n [#585](https://github.com/penpot/penpot/pull/585)
|
||||
- nisrulz [#586](https://github.com/penpot/penpot/pull/586)
|
||||
- tomer [#575](https://github.com/penpot/penpot/pull/575)
|
||||
- violoncelloCH [#554](https://github.com/penpot/penpot/pull/554)
|
||||
|
||||
## 1.1.0-alpha
|
||||
|
||||
- Bugfixing and stabilization post-launch
|
||||
- Some changes to the register flow
|
||||
- Improved MacOS shortcuts and helpers
|
||||
- Small changes to shape creation
|
||||
|
||||
|
||||
## 1.0.0-alpha
|
||||
|
||||
Initial release
|
||||
@@ -1,8 +1,8 @@
|
||||
# Contributing Guide #
|
||||
|
||||
Thank you for your interest in contributing to Penpot. This is a
|
||||
generic guide that details how to contribute to Penpot in a way that is
|
||||
efficient for everyone. If you want a specific documentation for
|
||||
generic guide that details how to contribute to Penpot in a way that
|
||||
is efficient for everyone. If you want a specific documentation for
|
||||
different parts of the platform, please refer to `docs/` directory.
|
||||
|
||||
|
||||
@@ -19,12 +19,20 @@ If you found a bug, please report it, as far as possible with:
|
||||
- a browser and the browser version used
|
||||
- a dev tools console exception stack trace (if it is available)
|
||||
|
||||
If you found a bug that you consider better discuse in private (for
|
||||
example: security bugs), consider first send an email to
|
||||
`info@penpot.app`.
|
||||
|
||||
**We don't have formal bug bounty program for security reports; this
|
||||
is an open source application and your contribution will be recognized
|
||||
in the changelog.**
|
||||
|
||||
|
||||
## Pull requests ##
|
||||
|
||||
If you want propose a change or bug fix with the Pull-Request system
|
||||
firstly you should carefully read the **Contributor License Aggreement**
|
||||
section and format your commits accordingly.
|
||||
firstly you should carefully read the **DCO** section and format your
|
||||
commits accordingly.
|
||||
|
||||
If you intend to fix a bug it's fine to submit a pull request right
|
||||
away but we still recommend to file an issue detailing what you're
|
||||
@@ -127,7 +135,7 @@ This Code of Conduct is adapted from the Contributor Covenant, version
|
||||
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
|
||||
|
||||
|
||||
## Contributor License Agreement ##
|
||||
## Developer's Certificate of Origin (DCO) ##
|
||||
|
||||
By submitting code you are agree and can certify the below:
|
||||
|
||||
@@ -157,9 +165,9 @@ By submitting code you are agree and can certify the below:
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
Then, all your patches should contain a sign-off at the end of the
|
||||
patch/commit description body. It can be automatically added on adding
|
||||
`-s` parameter to `git commit`.
|
||||
Then, all your code patches (**documentation are excluded**) should
|
||||
contain a sign-off at the end of the patch/commit description body. It
|
||||
can be automatically added on adding `-s` parameter to `git commit`.
|
||||
|
||||
This is an example of the aspect of the line:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[![License: MPL-2.0][uri_license_image]][uri_license]
|
||||
[](https://gitter.im/penpot/community)
|
||||
[](https://tree.taiga.io/project/uxboxproject/ "Managed with Taiga.io")
|
||||
[](https://tree.taiga.io/project/penpot/ "Managed with Taiga.io")
|
||||
|
||||
|
||||
# PENPOT #
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
org.slf4j/slf4j-api {:mvn/version "1.7.30"}
|
||||
|
||||
org.graalvm.js/js {:mvn/version "20.3.0"}
|
||||
com.taoensso/nippy {:mvn/version "3.1.1"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.4.8-3"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.9.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.common.exceptions :as ex]
|
||||
[taoensso.nippy :as nippy]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.test :as test]
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
for security reasons.
|
||||
</mj-text>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -57,7 +57,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
Accept invite
|
||||
</mj-button>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -50,7 +50,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
it. Your password won't be changed.
|
||||
</mj-text>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -59,7 +59,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<mj-column>
|
||||
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
|
||||
<mj-text>
|
||||
Thanks for signing up for your UXBOX account! Please verify your
|
||||
Thanks for signing up for your Penpot account! Please verify your
|
||||
email using the link below adn get started building mockups and
|
||||
prototypes today!
|
||||
</mj-text>
|
||||
@@ -29,14 +29,14 @@
|
||||
Verify email
|
||||
</mj-button>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -56,7 +56,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -10,4 +10,4 @@ If you received this email by mistake, please consider changing your password
|
||||
for security reasons.
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
1
backend/resources/emails/feedback/en.subj
Normal file
1
backend/resources/emails/feedback/en.subj
Normal file
@@ -0,0 +1 @@
|
||||
[FEEDBACK]: From {{ profile.email }}
|
||||
7
backend/resources/emails/feedback/en.txt
Normal file
7
backend/resources/emails/feedback/en.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Feedback from: {{profile.fullname}} <{{profile.email}}>
|
||||
|
||||
Profile ID: {{profile.id}}
|
||||
|
||||
Subject: {{subject}}
|
||||
|
||||
{{content}}
|
||||
@@ -7,4 +7,4 @@ Accept invitation using this link:
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -9,4 +9,4 @@ If you received this email by mistake, you can safely ignore it. Your password
|
||||
won't be changed.
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Hello {{name}}!
|
||||
|
||||
Thanks for signing up for your UXBOX account! Please verify your email using the
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the
|
||||
link below adn get started building mockups and prototypes today!
|
||||
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -67,39 +67,61 @@
|
||||
<div class="table-val">{{profile-id}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user-agent %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">VERS: </div>
|
||||
<div class="table-key">UAGENT: </div>
|
||||
<div class="table-val">{{user-agent}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frontend-version %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">FVERS: </div>
|
||||
<div class="table-val">{{frontend-version}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">BVERS: </div>
|
||||
<div class="table-val">{{version}}</div>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">HOST: </div>
|
||||
<div class="table-val">{{host}}</div>
|
||||
</div>
|
||||
|
||||
{% if type %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">TYPE: </div>
|
||||
<div class="table-val">{{type}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if code %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">CODE: </div>
|
||||
<div class="table-val">{{code}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">CLASS: </div>
|
||||
<div class="table-val">{{class}}</div>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<div class="table-key">HINT: </div>
|
||||
<div class="table-val">{{hint}}</div>
|
||||
</div>
|
||||
|
||||
{% if method %}
|
||||
<div class="table-row">
|
||||
<div class="table-key">PATH: </div>
|
||||
<div class="table-val">{{method|upper}} {{path}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if params %}
|
||||
<div class="table-row multiline">
|
||||
@@ -128,7 +150,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="table-row multiline">
|
||||
<div class="table-key">TRACE:</div>
|
||||
<div class="table-val">
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
(let [rng (java.util.Random. 1)]
|
||||
(letfn [(create-profile [conn index]
|
||||
(let [id (mk-uuid "profile" index)
|
||||
_ (log/info "create profile" id)
|
||||
_ (log/info "create profile" index id)
|
||||
|
||||
prof (register-profile conn
|
||||
{:id id
|
||||
@@ -98,10 +98,9 @@
|
||||
(create-team [conn index]
|
||||
(let [id (mk-uuid "team" index)
|
||||
name (str "Team" index)]
|
||||
(log/info "create team" id)
|
||||
(log/info "create team" index id)
|
||||
(db/insert! conn :team {:id id
|
||||
:name name
|
||||
:photo ""})
|
||||
:name name})
|
||||
id))
|
||||
|
||||
(create-teams [conn]
|
||||
@@ -113,7 +112,7 @@
|
||||
(let [id (mk-uuid "file" project-id index)
|
||||
name (str "file" index)
|
||||
data (cp/make-file-data id)]
|
||||
(log/info "create file" id)
|
||||
(log/info "create file" index id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
@@ -135,7 +134,7 @@
|
||||
(create-project [conn team-id owner-id index]
|
||||
(let [id (mk-uuid "project" team-id index)
|
||||
name (str "project " index)]
|
||||
(log/info "create project" id)
|
||||
(log/info "create project" index id)
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
@@ -188,7 +187,7 @@
|
||||
project-id (:default-project-id owner)
|
||||
data (cp/make-file-data id)]
|
||||
|
||||
(log/info "create draft file" id)
|
||||
(log/info "create draft file" index id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
:database-username "penpot"
|
||||
:database-password "penpot"
|
||||
|
||||
:default-blob-version 1
|
||||
|
||||
:asserts-enabled false
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
@@ -38,21 +40,23 @@
|
||||
:storage-s3-region :eu-central-1
|
||||
:storage-s3-bucket "penpot-devenv-assets-pre"
|
||||
|
||||
:feedback-destination "info@example.com"
|
||||
:feedback-enabled false
|
||||
|
||||
:assets-path "/internal/assets/"
|
||||
|
||||
:rlimits-password 10
|
||||
:rlimits-image 2
|
||||
|
||||
:smtp-enabled false
|
||||
:smtp-default-reply-to "no-reply@example.com"
|
||||
:smtp-default-from "no-reply@example.com"
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
|
||||
:allow-demo-users true
|
||||
:registration-enabled true
|
||||
:registration-domain-whitelist ""
|
||||
|
||||
:telemetry-enabled false
|
||||
:telemetry-with-taiga true
|
||||
:telemetry-uri "https://telemetry.penpot.app/"
|
||||
|
||||
;; LDAP auth disabled by default. Set ldap-auth-host to enable
|
||||
@@ -80,6 +84,7 @@
|
||||
(s/def ::database-uri ::us/string)
|
||||
(s/def ::redis-uri ::us/string)
|
||||
|
||||
|
||||
(s/def ::storage-backend ::us/keyword)
|
||||
(s/def ::storage-fs-directory ::us/string)
|
||||
(s/def ::assets-path ::us/string)
|
||||
@@ -90,10 +95,14 @@
|
||||
(s/def ::media-directory ::us/string)
|
||||
(s/def ::asserts-enabled ::us/boolean)
|
||||
|
||||
(s/def ::feedback-enabled ::us/boolean)
|
||||
(s/def ::feedback-destination ::us/string)
|
||||
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::smtp-default-reply-to ::us/email)
|
||||
(s/def ::smtp-default-from ::us/email)
|
||||
(s/def ::smtp-default-reply-to ::us/string)
|
||||
(s/def ::smtp-default-from ::us/string)
|
||||
(s/def ::smtp-host ::us/string)
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
@@ -143,13 +152,18 @@
|
||||
(s/def ::initial-data-file ::us/string)
|
||||
(s/def ::initial-data-project-name ::us/string)
|
||||
|
||||
(s/def ::default-blob-version ::us/integer)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::allow-demo-users
|
||||
::asserts-enabled
|
||||
::database-password
|
||||
::database-uri
|
||||
::database-username
|
||||
::default-blob-version
|
||||
::error-report-webhook
|
||||
::feedback-enabled
|
||||
::feedback-destination
|
||||
::github-client-id
|
||||
::github-client-secret
|
||||
::gitlab-base-uri
|
||||
@@ -231,5 +245,5 @@
|
||||
(def config (read-config env))
|
||||
(def test-config (read-test-config env))
|
||||
|
||||
(def default-deletion-delay
|
||||
(dt/duration {:hours 48}))
|
||||
(def deletion-delay
|
||||
(dt/duration {:days 7}))
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def initsql
|
||||
(str "SET statement_timeout = 10000;\n"
|
||||
(str "SET statement_timeout = 60000;\n"
|
||||
"SET idle_in_transaction_session_timeout = 120000;"))
|
||||
|
||||
(defn- create-datasource-config
|
||||
|
||||
@@ -76,38 +76,42 @@
|
||||
(tr/decode-stream input))))
|
||||
|
||||
(defn create-profile-initial-data
|
||||
[conn profile]
|
||||
(when-let [initial-data-path (:initial-data-file cfg/config)]
|
||||
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data initial-data-path)]
|
||||
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
|
||||
proj (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:default-team-id profile)
|
||||
:name sample-project-name})
|
||||
([conn profile]
|
||||
(when-let [initial-data-path (:initial-data-file cfg/config)]
|
||||
(create-profile-initial-data conn initial-data-path profile)))
|
||||
|
||||
map-ids {}
|
||||
([conn file profile]
|
||||
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data file)]
|
||||
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
|
||||
|
||||
;; Create new ID's and change the references
|
||||
[map-ids file] (change-ids map-ids file #{:id})
|
||||
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
|
||||
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
|
||||
proj (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:default-team-id profile)
|
||||
:name sample-project-name})
|
||||
|
||||
file (map #(assoc % :project-id (:id proj)) file)
|
||||
file-profile-rel (map #(array-map :file-id (:id %)
|
||||
:profile-id (:id profile)
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
file)]
|
||||
map-ids {}
|
||||
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
;; Create new ID's and change the references
|
||||
[map-ids file] (change-ids map-ids file #{:id})
|
||||
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
|
||||
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
|
||||
|
||||
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
|
||||
:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
file (map #(assoc % :project-id (:id proj)) file)
|
||||
file-profile-rel (map #(array-map :file-id (:id %)
|
||||
:profile-id (:id profile)
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true)
|
||||
file)]
|
||||
|
||||
;; Re-insert into the database
|
||||
(db/insert-multi! conn :file file)
|
||||
(db/insert-multi! conn :file-profile-rel file-profile-rel)
|
||||
(db/insert-multi! conn :file-library-rel file-library-rel)
|
||||
(db/insert-multi! conn :file-media-object file-media-object)))))
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
|
||||
:project-id (:id proj)
|
||||
:profile-id (:id profile)})
|
||||
|
||||
;; Re-insert into the database
|
||||
(db/insert-multi! conn :file file)
|
||||
(db/insert-multi! conn :file-profile-rel file-profile-rel)
|
||||
(db/insert-multi! conn :file-library-rel file-library-rel)
|
||||
(db/insert-multi! conn :file-media-object file-media-object)))))
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
|
||||
;; --- Emails
|
||||
|
||||
(s/def ::subject ::us/string)
|
||||
(s/def ::content ::us/string)
|
||||
|
||||
(s/def ::feedback
|
||||
(s/keys :req-un [::subject ::content]))
|
||||
|
||||
(def feedback
|
||||
"A profile feedback email."
|
||||
(emails/template-factory ::feedback default-context))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::register
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(defonce enabled-mattermost (atom true))
|
||||
(defonce queue (a/chan (a/sliding-buffer 64)))
|
||||
(defonce queue-fn (fn [event] (a/>!! queue event)))
|
||||
|
||||
@@ -117,7 +118,7 @@
|
||||
[cfg event]
|
||||
(try
|
||||
(let [cdata (get-context-data event)]
|
||||
(when (:uri cfg)
|
||||
(when (and (:uri cfg) @enabled-mattermost)
|
||||
(send-mattermost-notification! cfg cdata))
|
||||
(persist-on-database! cfg cdata))
|
||||
(catch Exception e
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.http.errors :as errors]
|
||||
[app.http.middleware :as middleware]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[integrant.core :as ig]
|
||||
@@ -103,7 +104,7 @@
|
||||
(catch Throwable e
|
||||
(try
|
||||
(let [cdata (errors/get-error-context request e)]
|
||||
(errors/update-thread-context! cdata)
|
||||
(update-thread-context! cdata)
|
||||
(log/errorf e "Unhandled exception: %s (id: %s)" (ex-message e) (str (:id cdata)))
|
||||
{:status 500
|
||||
:body "internal server error"})
|
||||
|
||||
@@ -35,51 +35,40 @@
|
||||
|
||||
(defn- get-access-token
|
||||
[cfg code]
|
||||
(let [params {:code code
|
||||
:client_id (:client-id cfg)
|
||||
:client_secret (:client-secret cfg)
|
||||
:redirect_uri (build-redirect-url cfg)
|
||||
:grant_type "authorization_code"}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:uri "https://oauth2.googleapis.com/token"
|
||||
:body (uri/map->query-string params)}
|
||||
res (http/send! req)]
|
||||
(try
|
||||
(let [params {:code code
|
||||
:client_id (:client-id cfg)
|
||||
:client_secret (:client-secret cfg)
|
||||
:redirect_uri (build-redirect-url cfg)
|
||||
:grant_type "authorization_code"}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:uri "https://oauth2.googleapis.com/token"
|
||||
:body (uri/map->query-string params)}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-google
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
(when (= 200 (:status res))
|
||||
(-> (json/read-str (:body res))
|
||||
(get "access_token"))))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
(get data "access_token"))
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from google access token request" e)
|
||||
nil))))
|
||||
(catch Exception e
|
||||
(log/error e "unexpected error on get-access-token")
|
||||
nil)))
|
||||
|
||||
(defn- get-user-info
|
||||
[token]
|
||||
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
:headers {"Authorization" (str "Bearer " token)}
|
||||
:method :get}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-google
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
;; (clojure.pprint/pprint data)
|
||||
{:email (get data "email")
|
||||
:fullname (get data "name")})
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from google access token request" e)
|
||||
nil))))
|
||||
(try
|
||||
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
:headers {"Authorization" (str "Bearer " token)}
|
||||
:method :get}
|
||||
res (http/send! req)]
|
||||
(when (= 200 (:status res))
|
||||
(let [data (json/read-str (:body res))]
|
||||
{:email (get data "email")
|
||||
:fullname (get data "name")})))
|
||||
(catch Exception e
|
||||
(log/error e "unexpected exception on get-user-info")
|
||||
nil)))
|
||||
|
||||
(defn- auth
|
||||
[{:keys [tokens] :as cfg} _req]
|
||||
@@ -99,33 +88,39 @@
|
||||
|
||||
(defn- callback
|
||||
[{:keys [tokens rpc session] :as cfg} request]
|
||||
(let [token (get-in request [:params :state])
|
||||
_ (tokens :verify {:token token :iss :google-oauth})
|
||||
info (some->> (get-in request [:params :code])
|
||||
(get-access-token cfg)
|
||||
(get-user-info))]
|
||||
|
||||
(when-not info
|
||||
(ex/raise :type :authentication
|
||||
:code :unable-to-authenticate-with-google))
|
||||
|
||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||
(try
|
||||
(let [token (get-in request [:params :state])
|
||||
_ (tokens :verify {:token token :iss :google-oauth})
|
||||
info (some->> (get-in request [:params :code])
|
||||
(get-access-token cfg)
|
||||
(get-user-info))
|
||||
_ (when-not info
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth))
|
||||
method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||
profile (method-fn {:email (:email info)
|
||||
:fullname (:fullname info)})
|
||||
uagent (get-in request [:headers "user-agent"])
|
||||
token (tokens :generate {:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)})
|
||||
|
||||
uri (-> (uri/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (uri/map->query-string {:token token})))
|
||||
|
||||
sid (session/create! session {:profile-id (:id profile)
|
||||
:user-agent uagent})]
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:cookies (session/cookies session {:value sid})
|
||||
:body ""})))
|
||||
:body ""})
|
||||
(catch Exception _e
|
||||
(let [uri (-> (uri/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (uri/map->query-string {:error "unable-to-auth"})))]
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:body ""}))))
|
||||
|
||||
(s/def ::client-id ::us/not-empty-string)
|
||||
(s/def ::client-secret ::us/not-empty-string)
|
||||
|
||||
@@ -12,25 +12,10 @@
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[clojure.tools.logging :as log]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound])
|
||||
(:import
|
||||
org.apache.logging.log4j.ThreadContext))
|
||||
|
||||
(defn update-thread-context!
|
||||
[data]
|
||||
(run! (fn [[key val]]
|
||||
(ThreadContext/put
|
||||
(name key)
|
||||
(cond
|
||||
(coll? val)
|
||||
(binding [clojure.pprint/*print-right-margin* 120]
|
||||
(with-out-str (pprint val)))
|
||||
(instance? clojure.lang.Named val) (name val)
|
||||
:else (str val))))
|
||||
data))
|
||||
[expound.alpha :as expound]))
|
||||
|
||||
(defn- explain-error
|
||||
[error]
|
||||
@@ -48,10 +33,12 @@
|
||||
:version (:full cfg/version)
|
||||
:host (:public-uri cfg/config)
|
||||
:class (.getCanonicalName ^java.lang.Class (class error))
|
||||
:hint (ex-message error)}
|
||||
:hint (ex-message error)
|
||||
:data edata}
|
||||
|
||||
(when (map? edata)
|
||||
edata)
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})
|
||||
|
||||
(when (and (map? edata) (:data edata))
|
||||
{:explain (explain-error edata)}))))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
(ns app.http.session
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.http.errors :refer [update-thread-context!]]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
:app.http.assets/handlers
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:assets-path (:assets-path cfg/config)
|
||||
:assets-path (:assets-path config)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:cache-max-age (dt/duration {:hours 24})
|
||||
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
|
||||
@@ -148,11 +148,11 @@
|
||||
|
||||
;; RLimit definition for password hashing
|
||||
:app.rlimits/password
|
||||
(:rlimits-password cfg/config)
|
||||
(:rlimits-password config)
|
||||
|
||||
;; RLimit definition for image processing
|
||||
:app.rlimits/image
|
||||
(:rlimits-image cfg/config)
|
||||
(:rlimits-image config)
|
||||
|
||||
;; A collection of rlimits as hash-map.
|
||||
:app.rlimits/all
|
||||
@@ -192,29 +192,29 @@
|
||||
:fn (ig/ref :app.tasks.file-media-gc/handler)}
|
||||
|
||||
{:id "file-xlog-gc"
|
||||
:cron #app/cron "0 0 */6 * * ?" ;; every 2 hours
|
||||
:cron #app/cron "0 0 */1 * * ?" ;; hourly
|
||||
:fn (ig/ref :app.tasks.file-xlog-gc/handler)}
|
||||
|
||||
{:id "storage-deleted-gc"
|
||||
:cron #app/cron "0 0 */6 * * ?" ;; every 6 hours
|
||||
:cron #app/cron "0 0 1 */1 * ?" ;; daily (1 hour shift)
|
||||
:fn (ig/ref :app.storage/gc-deleted-task)}
|
||||
|
||||
{:id "storage-touched-gc"
|
||||
:cron #app/cron "0 30 */6 * * ?" ;; every 6 hours
|
||||
:cron #app/cron "0 0 2 */1 * ?" ;; daily (2 hour shift)
|
||||
:fn (ig/ref :app.storage/gc-touched-task)}
|
||||
|
||||
{:id "storage-recheck"
|
||||
:cron #app/cron "0 0 */6 * * ?" ;; every 6 hours
|
||||
:cron #app/cron "0 0 */1 * * ?" ;; hourly
|
||||
:fn (ig/ref :app.storage/recheck-task)}
|
||||
|
||||
{:id "tasks-gc"
|
||||
:cron #app/cron "0 0 0 */1 * ?" ;; daily
|
||||
:fn (ig/ref :app.tasks.tasks-gc/handler)}
|
||||
|
||||
(when (:telemetry-enabled cfg/config)
|
||||
(when (:telemetry-enabled config)
|
||||
{:id "telemetry"
|
||||
:cron #app/cron "0 0 */6 * * ?" ;; every 6h
|
||||
:uri (:telemetry-uri cfg/config)
|
||||
:uri (:telemetry-uri config)
|
||||
:fn (ig/ref :app.tasks.telemetry/handler)})]}
|
||||
|
||||
:app.tasks/all
|
||||
@@ -260,23 +260,23 @@
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:max-age (dt/duration {:hours 24})}
|
||||
:max-age (dt/duration {:hours 48})}
|
||||
|
||||
:app.tasks.telemetry/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:version (:full cfg/version)
|
||||
:uri (:telemetry-uri cfg/config)
|
||||
:uri (:telemetry-uri config)
|
||||
:sprops (ig/ref :app.sprops/props)}
|
||||
|
||||
:app.srepl/server
|
||||
{:port (:srepl-port cfg/config)
|
||||
:host (:srepl-host cfg/config)}
|
||||
{:port (:srepl-port config)
|
||||
:host (:srepl-host config)}
|
||||
|
||||
:app.sprops/props
|
||||
{:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.error-reporter/reporter
|
||||
{:uri (:error-report-webhook cfg/config)
|
||||
{:uri (:error-report-webhook config)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
@@ -286,18 +286,18 @@
|
||||
:app.storage/storage
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)
|
||||
:backend (:storage-backend cfg/config :fs)
|
||||
:backend (:storage-backend config :fs)
|
||||
:backends {:s3 (ig/ref [::main :app.storage.s3/backend])
|
||||
:db (ig/ref [::main :app.storage.db/backend])
|
||||
:fs (ig/ref [::main :app.storage.fs/backend])
|
||||
:tmp (ig/ref [::tmp :app.storage.fs/backend])}}
|
||||
|
||||
[::main :app.storage.s3/backend]
|
||||
{:region (:storage-s3-region cfg/config)
|
||||
:bucket (:storage-s3-bucket cfg/config)}
|
||||
{:region (:storage-s3-region config)
|
||||
:bucket (:storage-s3-bucket config)}
|
||||
|
||||
[::main :app.storage.fs/backend]
|
||||
{:directory (:storage-fs-directory cfg/config)}
|
||||
{:directory (:storage-fs-directory config)}
|
||||
|
||||
[::tmp :app.storage.fs/backend]
|
||||
{:directory "/tmp/penpot"}
|
||||
@@ -305,7 +305,7 @@
|
||||
[::main :app.storage.db/backend]
|
||||
{:pool (ig/ref :app.db/pool)}}
|
||||
|
||||
(when (:telemetry-server-enabled cfg/config)
|
||||
(when (:telemetry-server-enabled config)
|
||||
{:app.telemetry/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
@@ -175,7 +175,12 @@
|
||||
(ex/raise :type :internal
|
||||
:code :rlimit-not-configured
|
||||
:hint ":image rlimit not configured"))
|
||||
(rlm/execute rlimit (process params))))
|
||||
(try
|
||||
(rlm/execute rlimit (process params))
|
||||
(catch org.im4java.core.InfoException e
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:cause e)))))
|
||||
|
||||
;; --- Utility functions
|
||||
|
||||
|
||||
@@ -155,11 +155,12 @@
|
||||
:dec (.. ^Gauge instance (labels labels) (dec)))))))
|
||||
|
||||
(defn make-summary
|
||||
[{:keys [name help registry reg labels] :as props}]
|
||||
[{:keys [name help registry reg labels max-age] :or {max-age 3600} :as props}]
|
||||
(let [registry (or registry reg)
|
||||
instance (doto (Summary/build)
|
||||
(.name name)
|
||||
(.help help)
|
||||
(.maxAgeSeconds max-age)
|
||||
(.quantile 0.75 0.02)
|
||||
(.quantile 0.99 0.001))
|
||||
_ (when (seq labels)
|
||||
|
||||
@@ -145,6 +145,9 @@
|
||||
|
||||
{:name "0044-add-storage-refcount"
|
||||
:fn (mg/resource "app/migrations/sql/0044-add-storage-refcount.sql")}
|
||||
|
||||
{:name "0045-add-index-to-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX file_change__created_at_idx
|
||||
ON file_change (created_at);
|
||||
@@ -126,6 +126,7 @@
|
||||
'app.rpc.mutations.projects
|
||||
'app.rpc.mutations.viewer
|
||||
'app.rpc.mutations.teams
|
||||
'app.rpc.mutations.feedback
|
||||
'app.rpc.mutations.verify-token)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
;; Schedule deletion of the demo profile
|
||||
(tasks/submit! conn {:name "delete-profile"
|
||||
:delay cfg/default-deletion-delay
|
||||
:delay cfg/deletion-delay
|
||||
:props {:profile-id id}})
|
||||
|
||||
{:email email
|
||||
|
||||
41
backend/src/app/rpc/mutations/feedback.clj
Normal file
41
backend/src/app/rpc/mutations/feedback.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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.rpc.mutations.feedback
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.emails :as emails]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::subject ::us/string)
|
||||
(s/def ::content ::us/string)
|
||||
|
||||
(s/def ::send-profile-feedback
|
||||
(s/keys :req-un [::profile-id ::subject ::content]))
|
||||
|
||||
(sv/defmethod ::send-profile-feedback
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id subject content] :as params}]
|
||||
(when-not (:feedback-enabled cfg/config)
|
||||
(ex/raise :type :validation
|
||||
:code :feedback-disabled
|
||||
:hint "feedback module is disabled"))
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (profile/retrieve-profile-data conn profile-id)]
|
||||
(emails/send! conn emails/feedback
|
||||
{:to (:feedback-destination cfg/config)
|
||||
:profile profile
|
||||
:subject subject
|
||||
:content content})
|
||||
nil)))
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
;; Schedule object deletion
|
||||
(tasks/submit! conn {:name "delete-object"
|
||||
:delay cfg/default-deletion-delay
|
||||
:delay cfg/deletion-delay
|
||||
:props {:id id :type :file}})
|
||||
|
||||
(mark-file-deleted conn params)))
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
(declare select-file-for-update)
|
||||
(declare select-file)
|
||||
|
||||
(s/def ::content ::media/upload)
|
||||
(s/def ::is-local ::us/boolean)
|
||||
@@ -50,7 +50,7 @@
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (select-file-for-update conn file-id)]
|
||||
(let [file (select-file conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
(-> (assoc cfg :conn conn)
|
||||
(create-file-media-object params)))))
|
||||
@@ -66,9 +66,18 @@
|
||||
[info]
|
||||
(= (:mtype info) "image/svg+xml"))
|
||||
|
||||
(defn- fetch-url
|
||||
[url]
|
||||
(try
|
||||
(http/get! url {:as :byte-array})
|
||||
(catch Exception e
|
||||
(ex/raise :type :validation
|
||||
:code :unable-to-access-to-url
|
||||
:cause e))))
|
||||
|
||||
(defn- download-media
|
||||
[{:keys [storage] :as cfg} url]
|
||||
(let [result (http/get! url {:as :byte-array})
|
||||
(let [result (fetch-url url)
|
||||
data (:body result)
|
||||
mtype (get (:headers result) "content-type")
|
||||
format (cm/mtype->format mtype)]
|
||||
@@ -129,7 +138,7 @@
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
[{:keys [pool storage] :as cfg} {:keys [profile-id file-id url name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (select-file-for-update conn file-id)]
|
||||
(let [file (select-file conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
(let [mobj (download-media cfg url)
|
||||
content {:filename "tempfile"
|
||||
@@ -152,7 +161,7 @@
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (select-file-for-update conn file-id)]
|
||||
(let [file (select-file conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
|
||||
(-> (assoc cfg :conn conn)
|
||||
@@ -175,17 +184,17 @@
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def ^:private sql:select-file-for-update
|
||||
(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 = ?
|
||||
for update of file")
|
||||
where file.id = ?")
|
||||
|
||||
(defn- select-file-for-update
|
||||
(defn- select-file
|
||||
[conn id]
|
||||
(let [row (db/exec-one! conn [sql:select-file-for-update id])]
|
||||
(let [row (db/exec-one! conn [sql:select-file id])]
|
||||
(when-not row
|
||||
(ex/raise :type :not-found))
|
||||
row))
|
||||
|
||||
@@ -78,8 +78,10 @@
|
||||
;; profile data.
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims :member-id (:id profile))
|
||||
params (assoc params :profile-id (:id profile))]
|
||||
(process-token conn params claims)
|
||||
params (assoc params :profile-id (:id profile))
|
||||
cfg (assoc cfg :conn conn)]
|
||||
|
||||
(process-token cfg params claims)
|
||||
|
||||
;; Automatically mark the created profile as active because
|
||||
;; we already have the verification of email with the
|
||||
@@ -168,15 +170,24 @@
|
||||
active? (if demo? true is-active)
|
||||
props (db/tjson (or props {}))
|
||||
password (derive-password password)]
|
||||
(-> (db/insert! conn :profile
|
||||
{:id id
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:password password
|
||||
:props props
|
||||
:is-active active?
|
||||
:is-demo demo?})
|
||||
(update :props db/decode-transit-pgobject))))
|
||||
(try
|
||||
(-> (db/insert! conn :profile
|
||||
{:id id
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:password password
|
||||
:props props
|
||||
:is-active active?
|
||||
:is-demo demo?})
|
||||
(update :props db/decode-transit-pgobject))
|
||||
(catch org.postgresql.util.PSQLException e
|
||||
(let [state (.getSQLState e)]
|
||||
(if (not= state "23505")
|
||||
(throw e)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists
|
||||
:cause e)))))))
|
||||
|
||||
|
||||
(defn- create-profile-relations
|
||||
[conn profile]
|
||||
@@ -389,14 +400,18 @@
|
||||
(emails/send! conn emails/password-recovery
|
||||
{:to (:email profile)
|
||||
:token (:token profile)
|
||||
:name (:fullname profile)}))]
|
||||
:name (:fullname profile)})
|
||||
nil)]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(some->> email
|
||||
(profile/retrieve-profile-data-by-email conn)
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))
|
||||
nil)))
|
||||
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
||||
(when-not (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :profile-not-verified
|
||||
:hint "the user need to validate profile before recover password"))
|
||||
(->> profile
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))))))
|
||||
|
||||
|
||||
;; --- Mutation: Recover Profile
|
||||
@@ -457,7 +472,7 @@
|
||||
|
||||
;; Schedule a complete deletion of profile
|
||||
(tasks/submit! conn {:name "delete-profile"
|
||||
:delay (dt/duration {:hours 48})
|
||||
:delay cfg/deletion-delay
|
||||
:props {:profile-id profile-id}})
|
||||
|
||||
(db/update! conn :profile
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
@@ -113,8 +114,6 @@
|
||||
|
||||
;; --- Mutation: Delete Project
|
||||
|
||||
(declare mark-project-deleted)
|
||||
|
||||
(s/def ::delete-project
|
||||
(s/keys :req-un [::id ::profile-id]))
|
||||
|
||||
@@ -125,18 +124,10 @@
|
||||
|
||||
;; Schedule object deletion
|
||||
(tasks/submit! conn {:name "delete-object"
|
||||
:delay cfg/default-deletion-delay
|
||||
:delay cfg/deletion-delay
|
||||
:props {:id id :type :project}})
|
||||
|
||||
(mark-project-deleted conn params)))
|
||||
|
||||
(def ^:private sql:mark-project-deleted
|
||||
"update project
|
||||
set deleted_at = clock_timestamp()
|
||||
where id = ?
|
||||
returning id")
|
||||
|
||||
(defn mark-project-deleted
|
||||
[conn {:keys [id] :as params}]
|
||||
(db/exec! conn [sql:mark-project-deleted id])
|
||||
nil)
|
||||
(db/update! conn :project
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.emails :as emails]
|
||||
[app.media :as media]
|
||||
@@ -20,6 +21,7 @@
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -133,7 +135,14 @@
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
(db/delete! conn :team {:id id})
|
||||
;; Schedule object deletion
|
||||
(tasks/submit! conn {:name "delete-object"
|
||||
:delay cfg/deletion-delay
|
||||
:props {:id id :type :team}})
|
||||
|
||||
(db/update! conn :team
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id})
|
||||
nil)))
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
;; TODO: session
|
||||
|
||||
(ns app.rpc.mutations.verify-token
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -44,20 +42,29 @@
|
||||
claims)
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
|
||||
(when (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-validated))
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
[{:keys [conn session] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
(when-not (:is-active profile)
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)}))
|
||||
|
||||
(with-meta claims
|
||||
{:transform-response
|
||||
(fn [request response]
|
||||
(let [uagent (get-in request [:headers "user-agent"])
|
||||
id (session/create! session {:profile-id profile-id
|
||||
:user-agent uagent})]
|
||||
(assoc response
|
||||
:cookies (session/cookies session {:value id}))))})))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.db.profile-initial-data :as pid]
|
||||
[app.main :refer [system]]
|
||||
[app.rpc.queries.profile :as prof]
|
||||
[app.srepl.dev :as dev]
|
||||
[app.util.blob :as blob]
|
||||
[clojure.pprint :refer [pprint]]))
|
||||
@@ -36,7 +38,7 @@
|
||||
{:id id})))
|
||||
|
||||
(defn get-file
|
||||
[id]
|
||||
[system id]
|
||||
(with-open [conn (db/open (:app.db/pool system))]
|
||||
(let [file (db/get-by-id conn :file id)]
|
||||
(-> file
|
||||
@@ -58,3 +60,29 @@
|
||||
([system project-id path]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(pid/create-initial-data-dump conn project-id path))))
|
||||
|
||||
(defn load-data-into-user
|
||||
([system user-email]
|
||||
(if-let [file (:initial-data-file cfg/config)]
|
||||
(load-data-into-user system file user-email)
|
||||
(prn "Data file not found in configuration")))
|
||||
|
||||
([system file user-email]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [profile (prof/retrieve-profile-data-by-email conn user-email)
|
||||
profile (merge profile (prof/retrieve-additional-data conn (:id profile)))]
|
||||
(pid/create-profile-initial-data conn file profile)))))
|
||||
|
||||
|
||||
;; Migrate
|
||||
|
||||
(defn update-file-data-blob-format
|
||||
[system]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(doseq [id (->> (db/exec! conn ["select id from file;"]) (map :id))]
|
||||
(let [{:keys [data]} (db/get-by-id conn :file id {:columns [:id :data]})]
|
||||
(prn "Updating file:" id)
|
||||
(db/update! conn :file
|
||||
{:data (-> (blob/decode data)
|
||||
(blob/encode {:version 2}))}
|
||||
{:id id})))))
|
||||
|
||||
@@ -121,11 +121,16 @@
|
||||
|
||||
(defn parse
|
||||
[data]
|
||||
(with-open [istream (IOUtils/toInputStream data "UTF-8")]
|
||||
(xml/parse istream)))
|
||||
(try
|
||||
(with-open [istream (IOUtils/toInputStream data "UTF-8")]
|
||||
(xml/parse istream))
|
||||
(catch org.xml.sax.SAXParseException _e
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file))))
|
||||
|
||||
(defn process-request
|
||||
[{:keys [svgc] :as cfg} body]
|
||||
(let [data (slurp body)
|
||||
data (svgc data)]
|
||||
(parse data)))
|
||||
|
||||
|
||||
@@ -42,11 +42,12 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(handle-deletion conn props)))
|
||||
|
||||
(defmulti handle-deletion (fn [_ props] (:type props)))
|
||||
(defmulti handle-deletion
|
||||
(fn [_ props] (:type props)))
|
||||
|
||||
(defmethod handle-deletion :default
|
||||
[_conn {:keys [type]}]
|
||||
(log/warn "no handler found for" type))
|
||||
(log/warnf "no handler found for %s" type))
|
||||
|
||||
(defmethod handle-deletion :file
|
||||
[conn {:keys [id] :as props}]
|
||||
@@ -57,3 +58,8 @@
|
||||
[conn {:keys [id] :as props}]
|
||||
(let [sql "delete from project where id=? and deleted_at is not null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(defmethod handle-deletion :team
|
||||
[conn {:keys [id] :as props}]
|
||||
(let [sql "delete from team where id=? and deleted_at is not null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
:uri (:uri cfg)
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/encode-str data)})]
|
||||
|
||||
(when (not= 200 (:status response))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-google
|
||||
@@ -129,7 +130,7 @@
|
||||
[{:keys [conn version]}]
|
||||
(merge
|
||||
{:version version
|
||||
:with-taiga (:telemetry-with-taiga cfg/config)
|
||||
:with-taiga (:telemetry-with-taiga cfg/config false)
|
||||
:total-teams (retrieve-num-teams conn)
|
||||
:total-projects (retrieve-num-projects conn)
|
||||
:total-files (retrieve-num-files conn)}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.http.middleware :refer [wrap-parse-request-body]]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[integrant.core :as ig]
|
||||
@@ -87,7 +88,12 @@
|
||||
(catch Exception e
|
||||
;; We don't want notify user of a error, just log it for posible
|
||||
;; future investigation.
|
||||
(log/warnf e "Unexpected error on telemetry.")))
|
||||
(log/warn e (str "Unexpected error on telemetry:\n"
|
||||
(when-let [edata (ex-data e)]
|
||||
(str "ex-data: \n"
|
||||
(with-out-str (pprint edata))))
|
||||
(str "params: \n"
|
||||
(with-out-str (pprint params)))))))
|
||||
{:status 200
|
||||
:body "OK\n"})
|
||||
|
||||
|
||||
@@ -10,61 +10,93 @@
|
||||
(ns app.util.blob
|
||||
"A generic blob storage encoding. Mainly used for
|
||||
page data, page options and txlog payload storage."
|
||||
(:require [app.util.transit :as t])
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.util.transit :as t]
|
||||
[taoensso.nippy :as n])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
java.io.ByteArrayOutputStream
|
||||
java.io.DataInputStream
|
||||
java.io.DataOutputStream
|
||||
com.github.luben.zstd.Zstd
|
||||
net.jpountz.lz4.LZ4Factory
|
||||
net.jpountz.lz4.LZ4FastDecompressor
|
||||
net.jpountz.lz4.LZ4Compressor))
|
||||
|
||||
(defprotocol IDataToBytes
|
||||
(->bytes [data] "convert data to bytes"))
|
||||
|
||||
(extend-protocol IDataToBytes
|
||||
(Class/forName "[B")
|
||||
(->bytes [data] data)
|
||||
|
||||
String
|
||||
(->bytes [data] (.getBytes ^String data "UTF-8")))
|
||||
|
||||
(def lz4-factory (LZ4Factory/fastestInstance))
|
||||
|
||||
(defn encode
|
||||
[data]
|
||||
(let [data (t/encode data {:type :json})
|
||||
data-len (alength ^bytes data)
|
||||
cp (.fastCompressor ^LZ4Factory lz4-factory)
|
||||
max-len (.maxCompressedLength cp data-len)
|
||||
cdata (byte-array max-len)
|
||||
clen (.compress ^LZ4Compressor cp ^bytes data 0 data-len cdata 0 max-len)]
|
||||
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
||||
^DataOutputStream dos (DataOutputStream. baos)]
|
||||
(.writeShort dos (short 1)) ;; version number
|
||||
(.writeInt dos (int data-len))
|
||||
(.write dos ^bytes cdata (int 0) clen)
|
||||
(.toByteArray baos))))
|
||||
|
||||
(declare decode-v1)
|
||||
(declare decode-v2)
|
||||
(declare encode-v1)
|
||||
(declare encode-v2)
|
||||
|
||||
(def default-version
|
||||
(:default-blob-version cfg/config 1))
|
||||
|
||||
(defn encode
|
||||
([data] (encode data nil))
|
||||
([data {:keys [version] :or {version default-version}}]
|
||||
(case version
|
||||
1 (encode-v1 data)
|
||||
2 (encode-v2 data)
|
||||
(throw (ex-info "unsupported version" {:version version})))))
|
||||
|
||||
(defn decode
|
||||
"A function used for decode persisted blobs in the database."
|
||||
[^bytes data]
|
||||
(with-open [bais (ByteArrayInputStream. data)
|
||||
dis (DataInputStream. bais)]
|
||||
(let [version (.readShort dis)
|
||||
ulen (.readInt dis)]
|
||||
(case version
|
||||
1 (decode-v1 data ulen)
|
||||
2 (decode-v2 data ulen)
|
||||
(throw (ex-info "unsupported version" {:version version}))))))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn- encode-v1
|
||||
[data]
|
||||
(let [data (->bytes data)]
|
||||
(with-open [bais (ByteArrayInputStream. data)
|
||||
dis (DataInputStream. bais)]
|
||||
(let [version (.readShort dis)
|
||||
udata-len (.readInt dis)]
|
||||
(case version
|
||||
1 (decode-v1 data udata-len)
|
||||
(throw (ex-info "unsupported version" {:version version})))))))
|
||||
(let [data (t/encode data {:type :json})
|
||||
dlen (alength ^bytes data)
|
||||
cp (.fastCompressor ^LZ4Factory lz4-factory)
|
||||
mlen (.maxCompressedLength cp dlen)
|
||||
cdata (byte-array mlen)
|
||||
clen (.compress ^LZ4Compressor cp ^bytes data 0 dlen cdata 0 mlen)]
|
||||
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
||||
^DataOutputStream dos (DataOutputStream. baos)]
|
||||
(.writeShort dos (short 1)) ;; version number
|
||||
(.writeInt dos (int dlen))
|
||||
(.write dos ^bytes cdata (int 0) clen)
|
||||
(.toByteArray baos))))
|
||||
|
||||
(defn- decode-v1
|
||||
[^bytes cdata ^long udata-len]
|
||||
(let [^LZ4FastDecompressor dcp (.fastDecompressor ^LZ4Factory lz4-factory)
|
||||
^bytes udata (byte-array udata-len)]
|
||||
(.decompress dcp cdata 6 udata 0 udata-len)
|
||||
[^bytes cdata ^long ulen]
|
||||
(let [dcp (.fastDecompressor ^LZ4Factory lz4-factory)
|
||||
udata (byte-array ulen)]
|
||||
(.decompress ^LZ4FastDecompressor dcp cdata 6 ^bytes udata 0 ulen)
|
||||
(t/decode udata {:type :json})))
|
||||
|
||||
(defn- encode-v2
|
||||
[data]
|
||||
(let [data (n/fast-freeze data)
|
||||
dlen (alength data)
|
||||
mlen (Zstd/compressBound dlen)
|
||||
cdata (byte-array mlen)
|
||||
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
|
||||
^bytes data 0 dlen
|
||||
8)]
|
||||
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
||||
^DataOutputStream dos (DataOutputStream. baos)]
|
||||
(.writeShort dos (short 2)) ;; version number
|
||||
(.writeInt dos (int dlen))
|
||||
(.write dos ^bytes cdata (int 0) clen)
|
||||
(.toByteArray baos))))
|
||||
|
||||
(defn- decode-v2
|
||||
[^bytes cdata ^long ulen]
|
||||
(let [udata (byte-array ulen)]
|
||||
(Zstd/decompressByteArray ^bytes udata 0 ulen
|
||||
^bytes cdata 6 (- (alength cdata) 6))
|
||||
(n/fast-thaw udata)))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
(ns app.util.emails
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.util.template :as tmpl]
|
||||
@@ -29,24 +30,11 @@
|
||||
;; Email Building
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn build-address
|
||||
[v charset]
|
||||
(try
|
||||
(cond
|
||||
(string? v)
|
||||
(InternetAddress. v nil charset)
|
||||
(defn- parse-address
|
||||
[v]
|
||||
(InternetAddress/parse ^String v))
|
||||
|
||||
(map? v)
|
||||
(InternetAddress. (:addr v)
|
||||
(:name v)
|
||||
(:charset v charset))
|
||||
|
||||
:else
|
||||
(throw (ex-info "Invalid address" {:data v})))
|
||||
(catch Exception e
|
||||
(throw (ex-info "Invalid address" {:data v} e)))))
|
||||
|
||||
(defn- resolve-recipient-type
|
||||
(defn- ^Message$RecipientType resolve-recipient-type
|
||||
[type]
|
||||
(case type
|
||||
:to Message$RecipientType/TO
|
||||
@@ -54,33 +42,33 @@
|
||||
:bcc Message$RecipientType/BCC))
|
||||
|
||||
(defn- assign-recipient
|
||||
[^MimeMessage mmsg type address charset]
|
||||
[^MimeMessage mmsg type address]
|
||||
(if (sequential? address)
|
||||
(reduce #(assign-recipient %1 type %2 charset) mmsg address)
|
||||
(let [address (build-address address charset)
|
||||
(reduce #(assign-recipient %1 type %2) mmsg address)
|
||||
(let [address (parse-address address)
|
||||
type (resolve-recipient-type type)]
|
||||
(.addRecipient mmsg type address)
|
||||
(.addRecipients mmsg type address)
|
||||
mmsg)))
|
||||
|
||||
(defn- assign-recipients
|
||||
[mmsg {:keys [to cc bcc charset] :or {charset "utf-8"} :as params}]
|
||||
[mmsg {:keys [to cc bcc] :as params}]
|
||||
(cond-> mmsg
|
||||
(some? to) (assign-recipient :to to charset)
|
||||
(some? cc) (assign-recipient :cc cc charset)
|
||||
(some? bcc) (assign-recipient :bcc bcc charset)))
|
||||
(some? to) (assign-recipient :to to)
|
||||
(some? cc) (assign-recipient :cc cc)
|
||||
(some? bcc) (assign-recipient :bcc bcc)))
|
||||
|
||||
(defn- assign-from
|
||||
[mmsg {:keys [from charset] :or {charset "utf-8"}}]
|
||||
(when from
|
||||
(let [from (build-address from charset)]
|
||||
(.setFrom ^MimeMessage mmsg ^InternetAddress from))))
|
||||
[mmsg {:keys [default-from]} {:keys [from] :as props}]
|
||||
(let [from (or from default-from)]
|
||||
(when from
|
||||
(let [from (parse-address from)]
|
||||
(.addFrom ^MimeMessage mmsg from)))))
|
||||
|
||||
(defn- assign-reply-to
|
||||
[mmsg {:keys [defaut-reply-to]} {:keys [reply-to charset] :or {charset "utf-8"}}]
|
||||
(let [reply-to (or reply-to defaut-reply-to)]
|
||||
[mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}]
|
||||
(let [reply-to (or reply-to default-reply-to)]
|
||||
(when reply-to
|
||||
(let [reply-to (build-address reply-to charset)
|
||||
reply-to (into-array InternetAddress [reply-to])]
|
||||
(let [reply-to (parse-address reply-to)]
|
||||
(.setReplyTo ^MimeMessage mmsg reply-to)))))
|
||||
|
||||
(defn- assign-subject
|
||||
@@ -136,7 +124,7 @@
|
||||
[cfg session params]
|
||||
(let [mmsg (MimeMessage. ^Session session)]
|
||||
(assign-recipients mmsg params)
|
||||
(assign-from mmsg params)
|
||||
(assign-from mmsg cfg params)
|
||||
(assign-reply-to mmsg cfg params)
|
||||
(assign-subject mmsg params)
|
||||
(assign-extra-headers mmsg params)
|
||||
@@ -156,12 +144,12 @@
|
||||
(Properties.)
|
||||
{"mail.user" username
|
||||
"mail.host" host
|
||||
"mail.from" default-from
|
||||
"mail.smtp.auth" (boolean username)
|
||||
"mail.smtp.starttls.enable" tls
|
||||
"mail.smtp.starttls.required" tls
|
||||
"mail.smtp.host" host
|
||||
"mail.smtp.port" port
|
||||
"mail.smtp.from" default-from
|
||||
"mail.smtp.user" username
|
||||
"mail.smtp.timeout" timeout
|
||||
"mail.smtp.connectiontimeout" timeout}))
|
||||
@@ -183,7 +171,9 @@
|
||||
(defn send!
|
||||
[cfg message]
|
||||
(let [^MimeMessage message (smtp-message cfg message)]
|
||||
(Transport/send message (:username cfg) (:password cfg))
|
||||
(Transport/send message
|
||||
(:username cfg)
|
||||
(:password cfg))
|
||||
nil))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -207,15 +197,17 @@
|
||||
text (render-email-template-part :txt id context)
|
||||
html (render-email-template-part :html id context)]
|
||||
(when (or (not subj)
|
||||
(not text)
|
||||
(not html))
|
||||
(and (not text)
|
||||
(not html)))
|
||||
(ex/raise :type :internal
|
||||
:code :missing-email-templates))
|
||||
{:subject subj
|
||||
:body [{:type "text/plain"
|
||||
:content text}
|
||||
{:type "text/html"
|
||||
:content html}]}))
|
||||
:body (d/concat
|
||||
[{:type "text/plain"
|
||||
:content text}]
|
||||
(when html
|
||||
[{:type "text/html"
|
||||
:content html}]))}))
|
||||
|
||||
(s/def ::priority #{:high :low})
|
||||
(s/def ::to (s/or :sigle ::us/email
|
||||
|
||||
27
backend/src/app/util/log4j.clj
Normal file
27
backend/src/app/util/log4j.clj
Normal file
@@ -0,0 +1,27 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2021 UXBOX Labs SL
|
||||
|
||||
(ns app.util.log4j
|
||||
(:require
|
||||
[clojure.pprint :refer [pprint]])
|
||||
(:import
|
||||
org.apache.logging.log4j.ThreadContext))
|
||||
|
||||
(defn update-thread-context!
|
||||
[data]
|
||||
(run! (fn [[key val]]
|
||||
(ThreadContext/put
|
||||
(name key)
|
||||
(cond
|
||||
(coll? val)
|
||||
(binding [clojure.pprint/*print-right-margin* 120]
|
||||
(with-out-str (pprint val)))
|
||||
(instance? clojure.lang.Named val) (name val)
|
||||
:else (str val))))
|
||||
data))
|
||||
@@ -12,11 +12,12 @@
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.util.log4j :refer [update-thread-context!]]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[integrant.core :as ig]
|
||||
@@ -205,6 +206,17 @@
|
||||
(log/warn "no task handler found for" (pr-str name)))
|
||||
{:status :completed :task item}))
|
||||
|
||||
(defn get-error-context
|
||||
[error item]
|
||||
(let [edata (ex-data error)]
|
||||
{:id (uuid/next)
|
||||
:version (:full cfg/version)
|
||||
:host (:public-uri cfg/config)
|
||||
:class (.getCanonicalName ^java.lang.Class (class error))
|
||||
:hint (ex-message error)
|
||||
:data edata
|
||||
:params item}))
|
||||
|
||||
(defn- handle-exception
|
||||
[error item]
|
||||
(let [edata (ex-data error)]
|
||||
@@ -218,14 +230,9 @@
|
||||
(= ::noop (:strategy edata))
|
||||
(assoc :inc-by 0))
|
||||
|
||||
(do
|
||||
(log/errorf error
|
||||
(str "Unhandled exception.\n"
|
||||
"=> task: " (:name item) "\n"
|
||||
"=> retry: " (:retry-num item) "\n"
|
||||
"=> props: \n"
|
||||
(with-out-str
|
||||
(pprint (:props item)))))
|
||||
(let [cdata (get-error-context error item)]
|
||||
(update-thread-context! cdata)
|
||||
(log/errorf error "Unhandled exception on task (id: %s)" (:id cdata))
|
||||
(if (>= (:retry-num item) (:max-retries item))
|
||||
{:status :failed :task item :error error}
|
||||
{:status :retry :task item :error error})))))
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :refer [close?]]
|
||||
[app.common.pages :refer [make-minimal-shape]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
@@ -32,7 +33,9 @@
|
||||
:points points)))
|
||||
|
||||
(defn add-rect-data [shape]
|
||||
(let [selrect (gsh/rect->selrect shape)
|
||||
(let [shape (-> shape
|
||||
(assoc :width 20 :height 20))
|
||||
selrect (gsh/rect->selrect shape)
|
||||
points (gsh/rect->points selrect)]
|
||||
(assoc shape
|
||||
:selrect selrect
|
||||
@@ -64,17 +67,17 @@
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
(t/is (not= shape-before shape-after))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :x])
|
||||
(- 10 (get-in shape-after [:selrect :x]))))
|
||||
(t/is (close? (get-in shape-before [:selrect :x])
|
||||
(- 10 (get-in shape-after [:selrect :x]))))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :y])
|
||||
(+ 10 (get-in shape-after [:selrect :y]))))
|
||||
(t/is (close? (get-in shape-before [:selrect :y])
|
||||
(+ 10 (get-in shape-after [:selrect :y]))))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :width])
|
||||
(get-in shape-after [:selrect :width])))
|
||||
(t/is (close? (get-in shape-before [:selrect :width])
|
||||
(get-in shape-after [:selrect :width])))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :height])
|
||||
(get-in shape-after [:selrect :height])))))
|
||||
(t/is (close? (get-in shape-before [:selrect :height])
|
||||
(get-in shape-after [:selrect :height])))))
|
||||
|
||||
:rect :path))
|
||||
|
||||
@@ -84,8 +87,8 @@
|
||||
shape-before (create-test-shape type {:modifiers modifiers})
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
(t/are [prop]
|
||||
(t/is (== (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
(t/is (close? (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
:x :y :width :height :x1 :y1 :x2 :y2))
|
||||
:rect :path))
|
||||
|
||||
@@ -98,17 +101,17 @@
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
(t/is (not= shape-before shape-after))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :x])
|
||||
(get-in shape-after [:selrect :x])))
|
||||
(t/is (close? (get-in shape-before [:selrect :x])
|
||||
(get-in shape-after [:selrect :x])))
|
||||
|
||||
(t/is (== (get-in shape-before [:selrect :y])
|
||||
(get-in shape-after [:selrect :y])))
|
||||
(t/is (close? (get-in shape-before [:selrect :y])
|
||||
(get-in shape-after [:selrect :y])))
|
||||
|
||||
(t/is (== (* 2 (get-in shape-before [:selrect :width]))
|
||||
(get-in shape-after [:selrect :width])))
|
||||
(t/is (close? (* 2 (get-in shape-before [:selrect :width]))
|
||||
(get-in shape-after [:selrect :width])))
|
||||
|
||||
(t/is (== (* 2 (get-in shape-before [:selrect :height]))
|
||||
(get-in shape-after [:selrect :height]))))
|
||||
(t/is (close? (* 2 (get-in shape-before [:selrect :height]))
|
||||
(get-in shape-after [:selrect :height]))))
|
||||
:rect :path))
|
||||
|
||||
(t/testing "Transform with empty resize"
|
||||
@@ -119,8 +122,8 @@
|
||||
shape-before (create-test-shape type {:modifiers modifiers})
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
(t/are [prop]
|
||||
(t/is (== (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
(t/is (close? (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
:x :y :width :height :x1 :y1 :x2 :y2))
|
||||
:rect :path))
|
||||
|
||||
@@ -145,13 +148,23 @@
|
||||
(let [modifiers {:rotation 30}
|
||||
shape-before (create-test-shape type {:modifiers modifiers})
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
|
||||
(t/is (not= shape-before shape-after))
|
||||
|
||||
(t/is (not (== (get-in shape-before [:selrect :x])
|
||||
(get-in shape-after [:selrect :x]))))
|
||||
;; Selrect won't change with a rotation, but points will
|
||||
(t/is (close? (get-in shape-before [:selrect :x])
|
||||
(get-in shape-after [:selrect :x])))
|
||||
|
||||
(t/is (not (== (get-in shape-before [:selrect :y])
|
||||
(get-in shape-after [:selrect :y])))))
|
||||
(t/is (close? (get-in shape-before [:selrect :y])
|
||||
(get-in shape-after [:selrect :y])))
|
||||
|
||||
(t/is (= (count (:points shape-before)) (count (:points shape-after))))
|
||||
|
||||
(for [idx (range 0 (count (:point shape-before)))]
|
||||
(do (t/is (not (close? (get-in shape-before [:points idx :x])
|
||||
(get-in shape-after [:points idx :x]))))
|
||||
(t/is (not (close? (get-in shape-before [:points idx :y])
|
||||
(get-in shape-after [:points idx :y])))))))
|
||||
:rect :path))
|
||||
|
||||
(t/testing "Transform shape with rotation = 0 should leave equal selrect"
|
||||
@@ -160,8 +173,8 @@
|
||||
shape-before (create-test-shape type {:modifiers modifiers})
|
||||
shape-after (gsh/transform-shape shape-before)]
|
||||
(t/are [prop]
|
||||
(t/is (== (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
(t/is (close? (get-in shape-before [:selrect prop])
|
||||
(get-in shape-after [:selrect prop])))
|
||||
:x :y :width :height :x1 :y1 :x2 :y2))
|
||||
:rect :path))
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
(t/is (= [rect-a-id rect-e-id rect-d-id]
|
||||
(get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements and delete the empty group"
|
||||
(t/testing "Move all elements from a group"
|
||||
(let [changes [{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id group-a-id
|
||||
@@ -368,9 +368,9 @@
|
||||
res (cp/process-changes data changes)]
|
||||
|
||||
(let [objects (get-in res [:pages-index page-id :objects])]
|
||||
(t/is (= [group-a-id rect-e-id]
|
||||
(t/is (= [group-a-id group-b-id rect-e-id]
|
||||
(get-in objects [frame-a-id :shapes])))
|
||||
(t/is (nil? (get-in objects [group-b-id]))))))
|
||||
(t/is (empty? (get-in objects [group-b-id :shapes]))))))
|
||||
|
||||
(t/testing "Move elements to a group with different frame"
|
||||
(let [changes [{:type :mov-objects
|
||||
@@ -727,11 +727,11 @@
|
||||
|
||||
;; After
|
||||
|
||||
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id]
|
||||
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
|
||||
(get-in res [:pages-index page-id :objects cp/root :shapes])))
|
||||
|
||||
(t/is (= nil
|
||||
(get-in res [:pages-index page-id :objects group-1-id])))
|
||||
(t/is (not= nil
|
||||
(get-in res [:pages-index page-id :objects group-1-id])))
|
||||
|
||||
))
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
(let [new-val (get curr attr ::undefined)
|
||||
value (cond
|
||||
(= new-val ::undefined) value
|
||||
(= new-val :multiple) :multiple
|
||||
(= value ::undefined) (sel new-val)
|
||||
(eqfn new-val value) value
|
||||
:else :multiple)]
|
||||
|
||||
@@ -328,6 +328,11 @@
|
||||
nil
|
||||
(apply f args))))
|
||||
|
||||
(defn nilv
|
||||
"Returns a default value if the given value is nil"
|
||||
[v default]
|
||||
(if (some? v) v default))
|
||||
|
||||
(defn check-num
|
||||
"Function that checks if a number is nil or nan. Will return 0 when not
|
||||
valid and the number otherwise."
|
||||
|
||||
@@ -134,3 +134,9 @@
|
||||
(th-eq m1f m2f))))
|
||||
|
||||
(defmethod pp/simple-dispatch Matrix [obj] (pr obj))
|
||||
|
||||
(defn transform-in [pt mtx]
|
||||
(-> (matrix)
|
||||
(translate pt)
|
||||
(multiply mtx)
|
||||
(translate (gpt/negate pt))))
|
||||
|
||||
@@ -11,42 +11,36 @@
|
||||
|
||||
;; --- Proportions
|
||||
|
||||
(declare assign-proportions-path)
|
||||
(declare assign-proportions-rect)
|
||||
|
||||
(defn assign-proportions
|
||||
[{:keys [type] :as shape}]
|
||||
(case type
|
||||
:path (assign-proportions-path shape)
|
||||
(assign-proportions-rect shape)))
|
||||
|
||||
(defn- assign-proportions-rect
|
||||
[{:keys [width height] :as shape}]
|
||||
(assoc shape :proportion (/ width height)))
|
||||
|
||||
[shape]
|
||||
(let [{:keys [width height]} (:selrect shape)]
|
||||
(assoc shape :proportion (/ width height))))
|
||||
|
||||
;; --- Setup Proportions
|
||||
|
||||
(declare setup-proportions-const)
|
||||
(declare setup-proportions-image)
|
||||
|
||||
(defn setup-proportions
|
||||
[shape]
|
||||
(case (:type shape)
|
||||
:icon (setup-proportions-image shape)
|
||||
:image (setup-proportions-image shape)
|
||||
:text shape
|
||||
(setup-proportions-const shape)))
|
||||
|
||||
(defn setup-proportions-image
|
||||
[{:keys [metadata] :as shape}]
|
||||
(let [{:keys [width height]} metadata]
|
||||
(assoc shape
|
||||
:proportion (/ width height)
|
||||
:proportion-lock false)))
|
||||
:proportion-lock true)))
|
||||
|
||||
(defn setup-proportions-svg
|
||||
[{:keys [width height] :as shape}]
|
||||
(assoc shape
|
||||
:proportion (/ width height)
|
||||
:proportion-lock true))
|
||||
|
||||
(defn setup-proportions-const
|
||||
[shape]
|
||||
(assoc shape
|
||||
:proportion 1
|
||||
:proportion-lock false))
|
||||
|
||||
(defn setup-proportions
|
||||
[shape]
|
||||
(case (:type shape)
|
||||
:svg-raw (setup-proportions-svg shape)
|
||||
:image (setup-proportions-image shape)
|
||||
:text shape
|
||||
(setup-proportions-const shape)))
|
||||
|
||||
@@ -43,10 +43,13 @@
|
||||
(let [shape-center (or (gco/center-shape shape)
|
||||
(gpt/point 0 0))]
|
||||
(inverse-transform-matrix shape shape-center)))
|
||||
([shape center]
|
||||
([{:keys [flip-x flip-y] :as shape} center]
|
||||
(let []
|
||||
(-> (gmt/matrix)
|
||||
(gmt/translate center)
|
||||
(cond->
|
||||
flip-x (gmt/scale (gpt/point -1 1))
|
||||
flip-y (gmt/scale (gpt/point 1 -1)))
|
||||
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
|
||||
(gmt/translate (gpt/negate center))))))
|
||||
|
||||
@@ -203,29 +206,7 @@
|
||||
(gmt/rotate (- rotation-angle)))]
|
||||
[stretch-matrix stretch-matrix-inverse]))
|
||||
|
||||
|
||||
(defn apply-transform-path
|
||||
[shape transform]
|
||||
(let [content (gpa/transform-content (:content shape) transform)
|
||||
|
||||
;; Calculate the new selrect by "unrotate" the shape
|
||||
rotation (modif-rotation shape)
|
||||
center (gpt/transform (gco/center-shape shape) transform)
|
||||
content-rotated (gpa/transform-content content (gmt/rotate-matrix (- rotation) center))
|
||||
selrect (gpa/content->selrect content-rotated)
|
||||
|
||||
;; Transform the points
|
||||
points (-> (:points shape)
|
||||
(transform-points transform))]
|
||||
(assoc shape
|
||||
:content content
|
||||
:points points
|
||||
:selrect selrect
|
||||
:transform (gmt/rotate-matrix rotation)
|
||||
:transform-inverse (gmt/rotate-matrix (- rotation))
|
||||
:rotation rotation)))
|
||||
|
||||
(defn apply-transform-rect
|
||||
(defn apply-transform
|
||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||
its properties. We adjust de x,y,width,height and create a custom transform"
|
||||
[shape transform]
|
||||
@@ -246,13 +227,21 @@
|
||||
(:height points-temp-dim))
|
||||
rect-points (gpr/rect->points rect-shape)
|
||||
|
||||
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))]
|
||||
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))
|
||||
|
||||
shape (cond
|
||||
(= :path (:type shape))
|
||||
(-> shape
|
||||
(update :content #(gpa/transform-content % transform)))
|
||||
|
||||
:else
|
||||
(-> shape
|
||||
(merge rect-shape)
|
||||
(update :x #(mth/precision % 0))
|
||||
(update :y #(mth/precision % 0))
|
||||
(update :width #(mth/precision % 0))
|
||||
(update :height #(mth/precision % 0))))]
|
||||
(as-> shape $
|
||||
(merge $ rect-shape)
|
||||
(update $ :x #(mth/precision % 0))
|
||||
(update $ :y #(mth/precision % 0))
|
||||
(update $ :width #(mth/precision % 0))
|
||||
(update $ :height #(mth/precision % 0))
|
||||
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
|
||||
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
|
||||
(assoc $ :points (into [] points))
|
||||
@@ -260,37 +249,6 @@
|
||||
(update $ :rotation #(mod (+ (or % 0)
|
||||
(or (get-in $ [:modifiers :rotation]) 0)) 360)))))
|
||||
|
||||
(defn apply-transform [shape transform]
|
||||
(let [apply-transform-fn
|
||||
(case (:type shape)
|
||||
:path apply-transform-path
|
||||
apply-transform-rect)]
|
||||
(apply-transform-fn shape transform)))
|
||||
|
||||
(defn transform-gradients [shape modifiers]
|
||||
(let [angle (d/check-num (get modifiers :rotation))
|
||||
;; Gradients are represented with unit vectors so its center is 0.5, 0.5
|
||||
center (gpt/point 0.5 0.5)
|
||||
transform (gmt/rotate-matrix angle center)
|
||||
transform-gradient
|
||||
(fn [{:keys [start-x start-y end-x end-y] :as gradient}]
|
||||
(let [start-point (gpt/point start-x start-y)
|
||||
end-point (gpt/point end-x end-y)
|
||||
{start-x :x start-y :y} (gpt/transform start-point transform)
|
||||
{end-x :x end-y :y} (gpt/transform end-point transform)]
|
||||
|
||||
(assoc gradient
|
||||
:start-x start-x
|
||||
:start-y start-y
|
||||
:end-x end-x
|
||||
:end-y end-y)))]
|
||||
(cond-> shape
|
||||
(:fill-color-gradient shape)
|
||||
(update :fill-color-gradient transform-gradient)
|
||||
|
||||
(:stroke-color-gradient shape)
|
||||
(update :stroke-color-gradient transform-gradient))))
|
||||
|
||||
(defn set-flip [shape modifiers]
|
||||
(let [rx (get-in modifiers [:resize-vector :x])
|
||||
ry (get-in modifiers [:resize-vector :y])]
|
||||
@@ -305,12 +263,13 @@
|
||||
(-> shape
|
||||
(set-flip (:modifiers shape))
|
||||
(apply-transform transform)
|
||||
(transform-gradients (:modifiers shape))
|
||||
(dissoc :modifiers)))
|
||||
shape)))
|
||||
|
||||
(defn update-group-selrect [group children]
|
||||
(let [shape-center (gco/center-shape group)
|
||||
transform (:transform group (gmt/matrix))
|
||||
transform-inverse (:transform-inverse group (gmt/matrix))
|
||||
|
||||
;; Points for every shape inside the group
|
||||
points (->> children (mapcat :points))
|
||||
@@ -330,5 +289,10 @@
|
||||
(-> group
|
||||
(assoc :selrect new-selrect)
|
||||
(assoc :points new-points)
|
||||
(apply-transform-rect (gmt/matrix)))))
|
||||
|
||||
;; We're regenerating the selrect from its children so we
|
||||
;; need to remove the flip flags
|
||||
(assoc :flip-x false)
|
||||
(assoc :flip-y false)
|
||||
(apply-transform (gmt/matrix)))))
|
||||
|
||||
|
||||
@@ -142,3 +142,10 @@
|
||||
|
||||
(defn almost-zero? [num]
|
||||
(< (abs num) 1e-8))
|
||||
|
||||
(defonce float-equal-precision 0.001)
|
||||
|
||||
(defn close?
|
||||
"Equality for float numbers. Check if the difference is within a range"
|
||||
[num1 num2]
|
||||
(<= (abs (- num1 num2)) float-equal-precision))
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
(d/export helpers/set-touched-group)
|
||||
(d/export helpers/touched-group?)
|
||||
(d/export helpers/get-base-shape)
|
||||
(d/export helpers/is-parent?)
|
||||
|
||||
;; Process changes
|
||||
(d/export changes/process-changes)
|
||||
|
||||
@@ -36,8 +36,20 @@
|
||||
(when verify?
|
||||
(us/verify ::spec/changes items))
|
||||
|
||||
(->> items
|
||||
(reduce #(or (process-change %1 %2) %1) data))))
|
||||
(let [pages (into #{} (map :page-id) items)
|
||||
result (->> items
|
||||
(reduce #(or (process-change %1 %2) %1) data))]
|
||||
|
||||
;; Validate result shapes (only on the backend)
|
||||
#?(:clj
|
||||
(doseq [page-id pages]
|
||||
(let [page (get-in result [:pages-index page-id])]
|
||||
(doseq [[id shape] (:objects page)]
|
||||
(if-not (= shape (get-in data [:pages-index page-id :objects id]))
|
||||
;; If object has change verify is correct
|
||||
(us/verify ::spec/shape shape))))))
|
||||
|
||||
result)))
|
||||
|
||||
(defmethod process-change :set-option
|
||||
[data {:keys [page-id option value]}]
|
||||
@@ -94,7 +106,6 @@
|
||||
(let [update-fn (fn [objects]
|
||||
(if-let [obj (get objects id)]
|
||||
(let [result (reduce process-operation obj operations)]
|
||||
#?(:clj (us/verify ::spec/shape result))
|
||||
(assoc objects id result))
|
||||
objects))]
|
||||
(if page-id
|
||||
@@ -142,16 +153,25 @@
|
||||
(map :id)
|
||||
(distinct))
|
||||
shapes)))
|
||||
(set-mask-selrect [group children]
|
||||
(let [mask (first children)]
|
||||
(-> group
|
||||
(merge (select-keys mask [:selrect :points]))
|
||||
(assoc :x (-> mask :selrect :x)
|
||||
:y (-> mask :selrect :y)
|
||||
:width (-> mask :selrect :width)
|
||||
:height (-> mask :selrect :height)))))
|
||||
(update-group [group objects]
|
||||
(let [children (->> group :shapes (map #(get objects %)))]
|
||||
(if (:masked-group? group)
|
||||
(let [mask (first children)]
|
||||
(-> group
|
||||
(merge (select-keys mask [:selrect :points]))
|
||||
(assoc :x (-> mask :selrect :x)
|
||||
:y (-> mask :selrect :y)
|
||||
:width (-> mask :selrect :width)
|
||||
:height (-> mask :selrect :height))))
|
||||
(cond
|
||||
;; If the group is empty we don't make any changes. Should be removed by a later process
|
||||
(empty? children)
|
||||
group
|
||||
|
||||
(:masked-group? group)
|
||||
(set-mask-selrect group children)
|
||||
|
||||
:else
|
||||
(gsh/update-group-selrect group children))))]
|
||||
|
||||
(if page-id
|
||||
@@ -206,23 +226,17 @@
|
||||
pid prev-parent-id
|
||||
objects objects]
|
||||
(let [obj (get objects pid)]
|
||||
(if (and (= 1 (count (:shapes obj)))
|
||||
(= sid (first (:shapes obj)))
|
||||
(= :group (:type obj)))
|
||||
(recur pid
|
||||
(:parent-id obj)
|
||||
(dissoc objects pid))
|
||||
(cond-> objects
|
||||
true
|
||||
(update-in [pid :shapes] strip-id sid)
|
||||
(cond-> objects
|
||||
true
|
||||
(update-in [pid :shapes] strip-id sid)
|
||||
|
||||
(and (:shape-ref obj)
|
||||
(= (:type obj) :group)
|
||||
(not ignore-touched))
|
||||
(->
|
||||
(update-in [pid :touched]
|
||||
cph/set-touched-group :shapes-group)
|
||||
(d/dissoc-in [pid :remote-synced?])))))))))
|
||||
(and (:shape-ref obj)
|
||||
(= (:type obj) :group)
|
||||
(not ignore-touched))
|
||||
(->
|
||||
(update-in [pid :touched]
|
||||
cph/set-touched-group :shapes-group)
|
||||
(d/dissoc-in [pid :remote-synced?]))))))))
|
||||
|
||||
(update-parent-id [objects id]
|
||||
(assoc-in objects [id :parent-id] parent-id))
|
||||
|
||||
@@ -224,7 +224,9 @@
|
||||
|
||||
(defn select-toplevel-shapes
|
||||
([objects] (select-toplevel-shapes objects nil))
|
||||
([objects {:keys [include-frames?] :or {include-frames? false}}]
|
||||
([objects {:keys [include-frames? include-frame-children?]
|
||||
:or {include-frames? false
|
||||
include-frame-children? true}}]
|
||||
(let [lookup #(get objects %)
|
||||
root (lookup uuid/zero)
|
||||
root-children (:shapes root)
|
||||
@@ -241,7 +243,7 @@
|
||||
(or (not= :frame typ) include-frames?)
|
||||
(d/concat [obj])
|
||||
|
||||
(= :frame typ)
|
||||
(and (= :frame typ) include-frame-children?)
|
||||
(d/concat (map lookup children))))))]
|
||||
|
||||
(reduce lookup-shapes [] root-children))))
|
||||
@@ -376,3 +378,25 @@
|
||||
|
||||
;; The first id will be the top-most
|
||||
(get objects (first sorted-ids))))
|
||||
|
||||
(defn is-parent?
|
||||
"Check if `parent-candidate` is parent of `shape-id`"
|
||||
[objects shape-id parent-candidate]
|
||||
|
||||
(loop [current (get objects parent-candidate)
|
||||
done #{}
|
||||
pending (:shapes current)]
|
||||
|
||||
(cond
|
||||
(contains? done (:id current))
|
||||
(recur (get objects (first pending))
|
||||
done
|
||||
(rest pending))
|
||||
|
||||
(empty? pending) false
|
||||
(and current (contains? (set (:shapes current)) shape-id)) true
|
||||
|
||||
:else
|
||||
(recur (get objects (first pending))
|
||||
(conj done (:id current))
|
||||
(concat (rest pending) (:shapes current))))))
|
||||
|
||||
@@ -16,6 +16,6 @@ RUN set -ex; \
|
||||
apt-get -qqy install adoptopenjdk-15-hotspot; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
ADD ./bundle/backend/ /opt/bundle/
|
||||
ADD ./bundle-app/backend/ /opt/bundle/
|
||||
WORKDIR /opt/bundle
|
||||
CMD ["/bin/bash", "run.sh"]
|
||||
|
||||
@@ -83,7 +83,7 @@ RUN set -ex; \
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
ADD ./bundle/exporter/ /opt/app/
|
||||
ADD ./bundle-exporter/ /opt/app/
|
||||
|
||||
RUN set -ex; \
|
||||
export PATH="$PATH:/usr/local/nodejs/bin"; \
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM nginx:latest
|
||||
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||
|
||||
ADD ./bundle/frontend /var/www/app/
|
||||
ADD ./bundle-app/frontend /var/www/app/
|
||||
ADD ./files/config.js /var/www/app/js/config.js
|
||||
ADD ./files/nginx.conf /etc/nginx/nginx.conf
|
||||
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ volumes:
|
||||
|
||||
services:
|
||||
penpot-frontend:
|
||||
image: "penpotapp/frontend:develop"
|
||||
image: "penpotapp/frontend:latest"
|
||||
ports:
|
||||
- 9001:80
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- penpot
|
||||
|
||||
penpot-backend:
|
||||
image: "penpotapp/backend:develop"
|
||||
image: "penpotapp/backend:latest"
|
||||
volumes:
|
||||
- penpot_assets_data:/opt/data
|
||||
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
- penpot
|
||||
|
||||
penpot-exporter:
|
||||
image: "penpotapp/exporter:develop"
|
||||
image: "penpotapp/exporter:latest"
|
||||
environment:
|
||||
# Don't touch it; this uses internal docker network to
|
||||
# communicate with the frontend.
|
||||
|
||||
9
docker/images/files/config.js
Normal file
9
docker/images/files/config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Frontend configuration
|
||||
|
||||
//var penpotPublicURI = "https://penpot.example.com";
|
||||
//var penpotDemoWarning = <true|false>;
|
||||
//var penpotAllowDemoUsers = <true|false>;
|
||||
//var penpotGoogleClientID = "<google-client-id-here>";
|
||||
//var penpotGitlabClientID = "<gitlab-client-id-here>";
|
||||
//var penpotGithubClientID = "<github-client-id-here>";
|
||||
//var penpotLoginWithLDAP = <true|false>;
|
||||
@@ -1,3 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
log() {
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*"
|
||||
}
|
||||
|
||||
|
||||
#########################################
|
||||
## App Frontend config
|
||||
#########################################
|
||||
|
||||
|
||||
update_public_uri() {
|
||||
if [ -n "$PENPOT_PUBLIC_URI" ]; then
|
||||
log "Updating Public URI: $PENPOT_PUBLIC_URI"
|
||||
sed -i \
|
||||
-e "s|^//var penpotPublicURI = \".*\";|var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_demo_warning() {
|
||||
if [ -n "$PENPOT_DEMO_WARNING" ]; then
|
||||
log "Updating Demo Warning: $PENPOT_DEMO_WARNING"
|
||||
sed -i \
|
||||
-e "s|^//var penpotDemoWarning = .*;|var penpotDemoWarning = $PENPOT_DEMO_WARNING;|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_allow_demo_users() {
|
||||
if [ -n "$PENPOT_ALLOW_DEMO_USERS" ]; then
|
||||
log "Updating Allow Demo Users: $PENPOT_ALLOW_DEMO_USERS"
|
||||
sed -i \
|
||||
-e "s|^//var penpotAllowDemoUsers = .*;|var penpotAllowDemoUsers = $PENPOT_ALLOW_DEMO_USERS;|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_google_client_id() {
|
||||
if [ -n "$PENPOT_GOOGLE_CLIENT_ID" ]; then
|
||||
log "Updating Google Client Id: $PENPOT_GOOGLE_CLIENT_ID"
|
||||
sed -i \
|
||||
-e "s|^//var penpotGoogleClientID = \".*\";|var penpotGoogleClientID = \"$PENPOT_GOOGLE_CLIENT_ID\";|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_gitlab_client_id() {
|
||||
if [ -n "$PENPOT_GITLAB_CLIENT_ID" ]; then
|
||||
log "Updating GitLab Client Id: $PENPOT_GITLAB_CLIENT_ID"
|
||||
sed -i \
|
||||
-e "s|^//var penpotGitlabClientID = \".*\";|var penpotGitlabClientID = \"$PENPOT_GITLAB_CLIENT_ID\";|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_github_client_id() {
|
||||
if [ -n "$PENPOT_GITHUB_CLIENT_ID" ]; then
|
||||
log "Updating GitHub Client Id: $PENPOT_GITHUB_CLIENT_ID"
|
||||
sed -i \
|
||||
-e "s|^//var penpotGithubClientID = \".*\";|var penpotGithubClientID = \"$PENPOT_GITHUB_CLIENT_ID\";|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
update_login_with_ldap() {
|
||||
if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then
|
||||
log "Updating Login with LDAP: $PENPOT_LOGIN_WITH_LDAP"
|
||||
sed -i \
|
||||
-e "s|^//var penpotLoginWithLDAP = .*;|var penpotLoginWithLDAP = $PENPOT_LOGIN_WITH_LDAP;|g" \
|
||||
"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
update_public_uri /var/www/app/js/config.js
|
||||
update_demo_warning /var/www/app/js/config.js
|
||||
update_allow_demo_users /var/www/app/js/config.js
|
||||
update_google_client_id /var/www/app/js/config.js
|
||||
update_gitlab_client_id /var/www/app/js/config.js
|
||||
update_github_client_id /var/www/app/js/config.js
|
||||
update_login_with_ldap /var/www/app/js/config.js
|
||||
|
||||
exec "$@";
|
||||
|
||||
@@ -107,7 +107,7 @@ http {
|
||||
}
|
||||
|
||||
location /assets {
|
||||
proxy_pass http://127.0.0.1:6060/assets;
|
||||
proxy_pass http://penpot-backend:6060/assets;
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
@@ -115,7 +115,7 @@ http {
|
||||
|
||||
location /internal/assets {
|
||||
internal;
|
||||
alias /var/www/assets;
|
||||
alias /opt/data/assets;
|
||||
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ The simplest approach is using docker and docker-compose.
|
||||
|
||||
## Install Docker ##
|
||||
|
||||
Skip this section if you alreasdy have docker installed, up and running.
|
||||
Skip this section if you already have docker installed, up and running.
|
||||
|
||||
You can install docker and its dependencies from your distribution
|
||||
repositores with:
|
||||
repository with:
|
||||
|
||||
```bash
|
||||
sudo apt-get install docker docker-compose
|
||||
```
|
||||
|
||||
Or follow installation instructions from docker.com; (for debian
|
||||
Or follow installation instructions from docker.com; (for Debian
|
||||
https://docs.docker.com/engine/install/debian/).
|
||||
|
||||
Ensure that the docker is started and optionally enable it to start
|
||||
@@ -33,7 +33,7 @@ And finally, add your user to the docker group:
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
This will make use the docker without `sudo` command all the time.
|
||||
This will make use of the docker without `sudo` command all the time.
|
||||
|
||||
NOTE: probably you will need to re-login again to make this change
|
||||
take effect.
|
||||
@@ -58,5 +58,5 @@ docker-compose -p penpot -f docker-compose.yaml up
|
||||
|
||||
The docker compose file contains the essential configuration for
|
||||
getting the application running, and many essential configurations
|
||||
already explained in comments. All other configuration options are
|
||||
explained in [management guide](./05-Management-Guide.md).
|
||||
already explained in the comments. All other configuration options are
|
||||
explained in [configuration guide](./05-Configuration-Guide.md).
|
||||
|
||||
@@ -200,10 +200,10 @@ If any of the following variables are defined, they will enable the
|
||||
corresponding auth button in the login page
|
||||
|
||||
```js
|
||||
var appGoogleClientID = "<google-client-id-here>";
|
||||
var appGitlabClientID = "<gitlab-client-id-here>";
|
||||
var appGithubClientID = "<github-client-id-here>";
|
||||
var appLoginWithLDAP = <true|false>;
|
||||
var penpotGoogleClientID = "<google-client-id-here>";
|
||||
var penpotGitlabClientID = "<gitlab-client-id-here>";
|
||||
var penpotGithubClientID = "<github-client-id-here>";
|
||||
var penpotLoginWithLDAP = <true|false>;
|
||||
```
|
||||
|
||||
**NOTE:** The configuration should match the backend configuration for
|
||||
@@ -216,8 +216,8 @@ It is possible to display a warning message on a demo environment and
|
||||
disable/enable demo users:
|
||||
|
||||
```js
|
||||
var appDemoWarning = <true|false>;
|
||||
var appAllowDemoUsers = <true|false>;
|
||||
var penpotDemoWarning = <true|false>;
|
||||
var penpotAllowDemoUsers = <true|false>;
|
||||
```
|
||||
|
||||
**NOTE:** The configuration for demo users should match the backend
|
||||
|
||||
3
frontend/resources/images/icons/icon-verify.svg
Normal file
3
frontend/resources/images/icons/icon-verify.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="279.383" height="369.004" viewBox="0 0 261.922 345.941">
|
||||
<path d="M68.319 0L31.94 51.236v28.962L.286 95.255 0 95.12v188.953l123.419 58.308 7.542 3.56 7.542-3.56 123.419-58.308V95.12l-.233.11-31.666-15.062V51.236l-1.106-1.558L193.645 0l-36.38 51.236v.052l-26.47-37.283L104.527 51l-.94-1.322L68.318 0zm6.436 29.762l14.07 19.815H47.81l13.908-19.583 13.036-.232zm125.325 0l14.071 19.815H173.14l13.904-19.583 13.037-.232zm-62.85 14.007l14.07 19.814h-41.008L124.195 44l13.035-.23zM43.923 59.564h19.452v65.497l-19.452-9.19V59.564zm29.438 0h19.355l-.002 79.356-19.355-9.142.002-70.214zm95.887 0h19.453l-.001 70.146-19.452 9.188V59.564zm29.438 0h19.353v56.285l-19.353 9.142V59.564zM106.4 73.57h19.451v81.004l-19.45-9.19V73.57zm29.44 0h19.35l-.001 71.971-19.353 9.145.004-81.116zm94.18 21.526l17.126 7.002-17.126 8.09V95.095zm-198.08.025v15.09l-17.12-8.09 17.12-7zm-16.857 23.81l108.337 51.178v155.588L15.082 274.52V118.93zm231.756 0v155.588l-108.335 51.178V170.11l108.335-51.179zm-19.521 44.302l-45.187 82.05-26.366-21.373-7.364 12.228 37.954 30.627 51.45-94.185-10.487-9.347z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
||||
min-width: 25px;
|
||||
padding: 0 1rem;
|
||||
transition: all .4s;
|
||||
text-decoration: none !important;
|
||||
svg {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
width: 18%;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.grid-item-th {
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
min-width: 115px;
|
||||
max-width: 115px;
|
||||
|
||||
@@ -61,6 +61,35 @@ textarea {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-60;
|
||||
height: 40%;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
font-size: $fs18;
|
||||
color: $color-gray-60;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
background: $color-gray-10;
|
||||
border-radius: $br-small;
|
||||
color: $color-gray-60;
|
||||
font-size: $fs18;
|
||||
font-weight: 500;
|
||||
margin: 1.5rem 0 2.5rem 0;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $fs24;
|
||||
color: $color-gray-60;
|
||||
@@ -73,6 +102,14 @@ textarea {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: $color-gray-20;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
font-size: $fs14;
|
||||
@@ -102,7 +139,8 @@ textarea {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
background-color: $color-white;
|
||||
border-radius: 2px;
|
||||
border: 1px solid $color-gray-20;
|
||||
@@ -114,6 +152,13 @@ textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
font-size: $fs14;
|
||||
font-family: "worksans", sans-serif;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
// Makes the background for autocomplete white
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
|
||||
@@ -211,8 +211,10 @@
|
||||
|
||||
width: calc(100% - 1rem);
|
||||
min-height: 5rem;
|
||||
|
||||
img {
|
||||
max-height: 8rem;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,14 @@
|
||||
position: fixed;
|
||||
right: calc(#{$width-settings-bar} + 10px);
|
||||
text-align: center;
|
||||
width: 110px;
|
||||
width: 125px;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 2px;
|
||||
transition: bottom 0.5s;
|
||||
|
||||
&.color-palette-open {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $color-white;
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>PENPOT - The Open-Source prototyping tool</title>
|
||||
<title>Penpot - Design Freedom for Teams</title>
|
||||
<meta name="description" content="The open-source solution for design and prototyping.">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:title" content="Penpot | Design Freedom for Teams">
|
||||
<meta property="og:description" content="The open-source solution for design and prototyping">
|
||||
<meta property="og:image" content="https://penpot.app/images/workspace-ui.jpg">
|
||||
<meta name="twitter:title" content="Penpot | Design Freedom for Teams">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:description" content="The open-source solution for design and prototyping">
|
||||
<meta name="twitter:image" content="https://penpot.app/images/workspace-ui.jpg">
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<link id="theme" href="/css/main-{{& th}}.css?ts={{& ts}}"
|
||||
rel="stylesheet" type="text/css" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{:deps {:aliases [:dev]}
|
||||
:http {:port 3448}
|
||||
:nrepl {:port 3447}
|
||||
:jvm-opts ["-Xmx1g" "-Xms512m"]
|
||||
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC"]
|
||||
|
||||
:builds
|
||||
{:main
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
(def default-language "en")
|
||||
|
||||
(def demo-warning (obj/get global "penpotDemoWarning" false))
|
||||
(def feedback-enabled (obj/get global "penpotFeedbackEnabled" false))
|
||||
(def allow-demo-users (obj/get global "penpotAllowDemoUsers" true))
|
||||
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
|
||||
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[app.main.repo :as rp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.auth :refer [logout]]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as udu]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui :as ui]
|
||||
[app.main.ui.confirm]
|
||||
@@ -22,12 +23,12 @@
|
||||
[app.main.worker]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.theme :as theme]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
@@ -71,7 +72,7 @@
|
||||
(st/emit! (rt/nav :auth-login))
|
||||
|
||||
(nil? match)
|
||||
(st/emit! (rt/nav :not-found))
|
||||
(st/emit! (dm/assign-exception {:type :not-found}))
|
||||
|
||||
:else
|
||||
(st/emit! #(assoc % :route match)))))
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
(let [team-id (:default-team-id profile)]
|
||||
(rx/merge
|
||||
(rx/of (du/profile-fetched profile)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))
|
||||
(rt/nav' :dashboard-projects {:team-id team-id}))
|
||||
(when-not (get-in profile [:props :onboarding-viewed])
|
||||
(->> (rx/of (modal/show {:type :onboarding}))
|
||||
(rx/delay 1000))))))))
|
||||
@@ -77,9 +77,7 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [this state s]
|
||||
(let [team-id (:default-team-id profile)]
|
||||
(rx/of (du/profile-fetched profile)
|
||||
(rt/nav' :dashboard-projects {:team-id team-id}))))))
|
||||
(rx/of (logged-in profile)))))
|
||||
|
||||
(defn login-with-ldap
|
||||
[{:keys [email password] :as data}]
|
||||
@@ -184,10 +182,7 @@
|
||||
|
||||
(->> (rp/mutation :request-profile-recovery data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch (fn [err]
|
||||
(on-error err)
|
||||
(rx/empty))))))))
|
||||
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- Recovery (Password)
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
(-> state
|
||||
(update-in [:workspace-file :colors] #(d/replace-by-id % color))))))
|
||||
|
||||
(defn change-palette-size [size]
|
||||
(defn change-palette-size
|
||||
[size]
|
||||
(s/assert #{:big :small} size)
|
||||
(ptk/reify ::change-palette-size
|
||||
ptk/UpdateEvent
|
||||
@@ -58,14 +59,27 @@
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette-size] size)))))
|
||||
|
||||
(defn change-palette-selected [selected]
|
||||
(defn change-palette-selected
|
||||
"Change the library used by the general palette tool"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette] selected)))))
|
||||
|
||||
(defn show-palette [selected]
|
||||
(defn change-palette-selected-colorpicker
|
||||
"Change the library used by the color picker"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected-colorpicker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected-palette-colorpicker] selected)))))
|
||||
|
||||
(defn show-palette
|
||||
"Show the palette tool and change the library it uses"
|
||||
[selected]
|
||||
(ptk/reify ::change-palette-selected
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -73,14 +87,16 @@
|
||||
(update :workspace-layout conj :colorpalette)
|
||||
(assoc-in [:workspace-local :selected-palette] selected)))))
|
||||
|
||||
(defn start-picker []
|
||||
(defn start-picker
|
||||
[]
|
||||
(ptk/reify ::start-picker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :picking-color?] true)))))
|
||||
|
||||
(defn stop-picker []
|
||||
(defn stop-picker
|
||||
[]
|
||||
(ptk/reify ::stop-picker
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -89,14 +105,16 @@
|
||||
(update :workspace-local dissoc :picked-shift?)
|
||||
(assoc-in [:workspace-local :picking-color?] false)))))
|
||||
|
||||
(defn pick-color [rgba]
|
||||
(defn pick-color
|
||||
[rgba]
|
||||
(ptk/reify ::pick-color
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :picked-color] rgba)))))
|
||||
|
||||
(defn pick-color-select [value shift?]
|
||||
(defn pick-color-select
|
||||
[value shift?]
|
||||
(ptk/reify ::pick-color-select
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -116,11 +134,21 @@
|
||||
text-ids (filter is-text? ids)
|
||||
shape-ids (filter (comp not is-text?) ids)
|
||||
|
||||
attrs {:fill-color (:color color)
|
||||
:fill-color-ref-id (:id color)
|
||||
:fill-color-ref-file (:file-id color)
|
||||
:fill-color-gradient (:gradient color)
|
||||
:fill-opacity (:opacity color)}
|
||||
attrs (cond-> {}
|
||||
(contains? color :color)
|
||||
(assoc :fill-color (:color color))
|
||||
|
||||
(contains? color :id)
|
||||
(assoc :fill-color-ref-id (:id color))
|
||||
|
||||
(contains? color :file-id)
|
||||
(assoc :fill-color-ref-file (:file-id color))
|
||||
|
||||
(contains? color :gradient)
|
||||
(assoc :fill-color-gradient (:gradient color))
|
||||
|
||||
(contains? color :opacity)
|
||||
(assoc :fill-opacity (:opacity color)))
|
||||
|
||||
update-fn (fn [shape] (merge shape attrs))
|
||||
editors (get-in state [:workspace-local :editors])
|
||||
@@ -131,29 +159,42 @@
|
||||
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
|
||||
(dwc/update-shapes shape-ids update-fn))))))))
|
||||
|
||||
(defn change-stroke [ids color]
|
||||
(defn change-stroke
|
||||
[ids color]
|
||||
(ptk/reify ::change-stroke
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(let [pid (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index pid :objects])
|
||||
not-frame (fn [shape-id] (not= (get-in objects [shape-id :type]) :frame))
|
||||
update-fn (fn [s]
|
||||
(cond-> s
|
||||
true
|
||||
(assoc :stroke-color (:color color)
|
||||
:stroke-opacity (:opacity color)
|
||||
:stroke-color-gradient (:gradient color)
|
||||
:stroke-color-ref-id (:id color)
|
||||
:stroke-color-ref-file (:file-id color))
|
||||
|
||||
(= (:stroke-style s) :none)
|
||||
(assoc :stroke-style :solid
|
||||
:stroke-width 1
|
||||
:stroke-opacity 1)))]
|
||||
color-attrs (cond-> {}
|
||||
(contains? color :color)
|
||||
(assoc :stroke-color (:color color))
|
||||
|
||||
(contains? color :id)
|
||||
(assoc :stroke-color-ref-id (:id color))
|
||||
|
||||
(contains? color :file-id)
|
||||
(assoc :stroke-color-ref-file (:file-id color))
|
||||
|
||||
(contains? color :gradient)
|
||||
(assoc :stroke-color-gradient (:gradient color))
|
||||
|
||||
(contains? color :opacity)
|
||||
(assoc :stroke-opacity (:opacity color)))
|
||||
|
||||
update-fn (fn [shape]
|
||||
(-> shape
|
||||
(merge color-attrs)
|
||||
(cond-> (= (:stroke-style s) :none)
|
||||
(assoc :stroke-style :solid
|
||||
:stroke-width 1
|
||||
:stroke-opacity 1))))]
|
||||
(rx/of (dwc/update-shapes ids update-fn))))))
|
||||
|
||||
(defn picker-for-selected-shape []
|
||||
(defn picker-for-selected-shape
|
||||
[]
|
||||
(let [sub (rx/subject)]
|
||||
(ptk/reify ::picker-for-selected-shape
|
||||
ptk/WatchEvent
|
||||
@@ -189,7 +230,8 @@
|
||||
:props {:on-change handle-change-color}
|
||||
:allow-click-outside true})))))))
|
||||
|
||||
(defn start-gradient [gradient]
|
||||
(defn start-gradient
|
||||
[gradient]
|
||||
(ptk/reify ::start-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -198,21 +240,24 @@
|
||||
(assoc-in [:workspace-local :current-gradient] gradient)
|
||||
(assoc-in [:workspace-local :current-gradient :shape-id] id))))))
|
||||
|
||||
(defn stop-gradient []
|
||||
(defn stop-gradient
|
||||
[]
|
||||
(ptk/reify ::stop-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :current-gradient)))))
|
||||
|
||||
(defn update-gradient [changes]
|
||||
(defn update-gradient
|
||||
[changes]
|
||||
(ptk/reify ::update-gradient
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:workspace-local :current-gradient] merge changes)))))
|
||||
|
||||
(defn select-gradient-stop [spot]
|
||||
(defn select-gradient-stop
|
||||
[spot]
|
||||
(ptk/reify ::select-gradient-stop
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
@@ -50,9 +51,13 @@
|
||||
;; Check that a file obtained with the file javascript API is valid.
|
||||
[file]
|
||||
(when (> (.-size file) cm/max-file-size)
|
||||
(throw (ex-info (tr "errors.media-too-large") {})))
|
||||
(ex/raise :type :validation
|
||||
:code :media-too-large
|
||||
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
|
||||
(when-not (contains? cm/valid-media-types (.-type file))
|
||||
(throw (ex-info (tr "errors.media-format-unsupported") {})))
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint (str/fmt "media type %s is not supported" (.-type file))))
|
||||
file)
|
||||
|
||||
(defn notify-start-loading
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
(declare show)
|
||||
|
||||
(def default-animation-timeout 600)
|
||||
(def default-timeout 2000)
|
||||
(def default-timeout 5000)
|
||||
|
||||
(s/def ::type #{:success :error :info :warning})
|
||||
(s/def ::position #{:fixed :floating :inline})
|
||||
|
||||
75
frontend/src/app/main/data/shortcuts.cljs
Normal file
75
frontend/src/app/main/data/shortcuts.cljs
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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.shortcuts
|
||||
(:require
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[potok.core :as ptk]
|
||||
[beicon.core :as rx]
|
||||
[app.config :as cfg])
|
||||
(:refer-clojure :exclude [meta]))
|
||||
|
||||
(def mac-command "\u2318")
|
||||
(def mac-option "\u2325")
|
||||
(def mac-delete "\u232B")
|
||||
(def mac-shift "\u21E7")
|
||||
(def mac-control "\u2303")
|
||||
(def mac-esc "\u238B")
|
||||
|
||||
(def left-arrow "\u2190")
|
||||
(def up-arrow "\u2191")
|
||||
(def right-arrow "\u2192")
|
||||
(def down-arrow "\u2193")
|
||||
|
||||
(defn c-mod
|
||||
"Adds the control/command modifier to a shortcuts depending on the
|
||||
operating system for the user"
|
||||
[shortcut]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(str "command+" shortcut)
|
||||
(str "ctrl+" shortcut)))
|
||||
|
||||
(defn bind-shortcuts [shortcuts bind-fn cb-fn]
|
||||
(doseq [[key {:keys [command disabled fn]}] shortcuts]
|
||||
(when-not disabled
|
||||
(if (vector? command)
|
||||
(doseq [cmd (seq command)]
|
||||
(bind-fn cmd (cb-fn key fn)))
|
||||
(bind-fn command (cb-fn key fn))))))
|
||||
|
||||
(defn meta [key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-command
|
||||
"Ctrl+")
|
||||
key))
|
||||
|
||||
(defn shift [key]
|
||||
(str
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-shift
|
||||
"Shift+")
|
||||
key))
|
||||
|
||||
(defn meta-shift [key]
|
||||
(-> key meta shift))
|
||||
|
||||
(defn supr []
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-delete
|
||||
"Supr"))
|
||||
|
||||
(defn esc []
|
||||
(if (cfg/check-platform? :macos)
|
||||
mac-esc
|
||||
"Escape"))
|
||||
|
||||
@@ -112,21 +112,22 @@
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file page share-token token libraries users] :as bundle}]
|
||||
(us/verify ::bundle bundle)
|
||||
(ptk/reify ::file-fetched
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (:objects page)
|
||||
frames (extract-frames objects)]
|
||||
(assoc state
|
||||
:viewer-libraries (d/index-by :id libraries)
|
||||
:viewer-data {:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token})))))
|
||||
(-> state
|
||||
(assoc :viewer-libraries (d/index-by :id libraries))
|
||||
(update :viewer-data assoc
|
||||
:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
:token token
|
||||
:share-token share-token))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
@@ -136,7 +137,8 @@
|
||||
(d/index-by :id)
|
||||
(assoc state :comment-threads)))
|
||||
(on-error [{:keys [type] :as err}]
|
||||
(if (= :authentication type)
|
||||
(if (or (= :authentication type)
|
||||
(= :not-found type))
|
||||
(rx/empty)
|
||||
(rx/throw err)))]
|
||||
|
||||
@@ -346,7 +348,7 @@
|
||||
|
||||
|
||||
(defn set-current-frame [frame-id]
|
||||
(ptk/reify ::current-frame
|
||||
(ptk/reify ::set-current-frame
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
|
||||
@@ -415,15 +417,12 @@
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
;; --- Shortcuts
|
||||
|
||||
(def shortcuts
|
||||
{"+" (st/emitf increase-zoom)
|
||||
"-" (st/emitf decrease-zoom)
|
||||
"ctrl+a" (st/emitf (select-all))
|
||||
"shift+0" (st/emitf zoom-to-50)
|
||||
"shift+1" (st/emitf reset-zoom)
|
||||
"shift+2" (st/emitf zoom-to-200)
|
||||
"left" (st/emitf select-prev-frame)
|
||||
"right" (st/emitf select-next-frame)})
|
||||
|
||||
(defn go-to-dashboard
|
||||
([] (go-to-dashboard nil))
|
||||
([{:keys [team-id]}]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))))
|
||||
|
||||
57
frontend/src/app/main/data/viewer/shortcuts.cljs
Normal file
57
frontend/src/app/main/data/viewer/shortcuts.cljs
Normal file
@@ -0,0 +1,57 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.viewer.shortcuts
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.shortcuts :refer [c-mod]]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(def shortcuts
|
||||
{:increase-zoom {:tooltip "+"
|
||||
:command "+"
|
||||
:fn (st/emitf dv/increase-zoom)}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command "-"
|
||||
:fn (st/emitf dv/decrease-zoom)}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:fn (st/emitf (dv/select-all))}
|
||||
|
||||
:zoom-50 {:tooltip (ds/shift "0")
|
||||
:command "shift+0"
|
||||
:fn (st/emitf dv/zoom-to-50)}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "1")
|
||||
:command "shift+1"
|
||||
:fn (st/emitf dv/reset-zoom)}
|
||||
|
||||
:zoom-200 {:tooltip (ds/shift "2")
|
||||
:command "shift+2"
|
||||
:fn (st/emitf dv/zoom-to-200)}
|
||||
|
||||
:next-frame {:tooltip ds/left-arrow
|
||||
:command "left"
|
||||
:fn (st/emitf dv/select-prev-frame)}
|
||||
|
||||
:prev-frame {:tooltip ds/right-arrow
|
||||
:command "right"
|
||||
:fn (st/emitf dv/select-next-frame)}})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
@@ -9,14 +9,13 @@
|
||||
|
||||
(ns app.main.data.workspace
|
||||
(:require
|
||||
[goog.string.path :as path]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.align :as gal]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.proportions :as gpr]
|
||||
[app.common.geom.align :as gal]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
@@ -27,33 +26,33 @@
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.drawing.path :as dwdp]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.notifications :as dwn]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.texts :as dwtxt]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.drawing.path :as dwdp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.logging :as log]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.transit :as t]
|
||||
[app.util.webapi :as wapi]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.object :as obj]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
;; [cljs.pprint :refer [pprint]]
|
||||
[goog.string.path :as path]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; (log/set-level! :trace)
|
||||
@@ -121,8 +120,10 @@
|
||||
:left-sidebar? true
|
||||
:right-sidebar? true
|
||||
:color-for-rename nil
|
||||
:selected-palette-colorpicker :recent
|
||||
:selected-palette :recent
|
||||
:selected-palette-size :big
|
||||
:assets-files-open {}
|
||||
:picking-color? false
|
||||
:picked-color nil
|
||||
:picked-color-select false})
|
||||
@@ -807,6 +808,168 @@
|
||||
|
||||
;; --- Change Shape Order (D&D Ordering)
|
||||
|
||||
(defn relocate-shapes-changes [objects parents parent-id page-id to-index ids groups-to-delete groups-to-unmask shapes-to-detach shapes-to-reroot shapes-to-deroot]
|
||||
(let [;; Changes to the shapes that are being move
|
||||
r-mov-change
|
||||
[{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:page-id page-id
|
||||
:index to-index
|
||||
:shapes (vec (reverse ids))}]
|
||||
|
||||
u-mov-change
|
||||
(map (fn [id]
|
||||
(let [obj (get objects id)]
|
||||
{:type :mov-objects
|
||||
:parent-id (:parent-id obj)
|
||||
:page-id page-id
|
||||
:index (cp/position-on-parent id objects)
|
||||
:shapes [id]}))
|
||||
(reverse ids))
|
||||
|
||||
;; Changes deleting empty groups
|
||||
r-del-change
|
||||
(map (fn [group-id]
|
||||
{:type :del-obj
|
||||
:page-id page-id
|
||||
:id group-id})
|
||||
groups-to-delete)
|
||||
|
||||
u-del-change
|
||||
(d/concat
|
||||
[]
|
||||
;; Create the groups
|
||||
(map (fn [group-id]
|
||||
(let [group (get objects group-id)]
|
||||
{:type :add-obj
|
||||
:page-id page-id
|
||||
:parent-id parent-id
|
||||
:frame-id (:frame-id group)
|
||||
:id group-id
|
||||
:obj (-> group
|
||||
(assoc :shapes []))}))
|
||||
groups-to-delete)
|
||||
;; Creates the hierarchy
|
||||
(map (fn [group-id]
|
||||
(let [group (get objects group-id)]
|
||||
{:type :mov-objects
|
||||
:page-id page-id
|
||||
:parent-id (:id group)
|
||||
:shapes (:shapes group)}))
|
||||
groups-to-delete))
|
||||
|
||||
;; Changes removing the masks from the groups without mask shape
|
||||
r-mask-change
|
||||
(map (fn [group-id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id group-id
|
||||
:operations [{:type :set
|
||||
:attr :masked-group?
|
||||
:val false}]})
|
||||
groups-to-unmask)
|
||||
|
||||
u-mask-change
|
||||
(map (fn [group-id]
|
||||
(let [group (get objects group-id)]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id group-id
|
||||
:operations [{:type :set
|
||||
:attr :masked-group?
|
||||
:val (:masked-group? group)}]}))
|
||||
groups-to-unmask)
|
||||
|
||||
;; Changes to the components metadata
|
||||
|
||||
detach-keys [:component-id :component-file :component-root? :remote-synced? :shape-ref :touched]
|
||||
|
||||
r-detach-change
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations (mapv #(hash-map :type :set :attr % :val nil) detach-keys)})
|
||||
shapes-to-detach)
|
||||
|
||||
u-detach-change
|
||||
(map (fn [id]
|
||||
(let [obj (get objects id)]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations (mapv #(hash-map :type :set :attr % :val (get obj %)) detach-keys)}))
|
||||
shapes-to-detach)
|
||||
|
||||
r-deroot-change
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val nil}]})
|
||||
shapes-to-deroot)
|
||||
|
||||
u-deroot-change
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val true}]})
|
||||
shapes-to-deroot)
|
||||
|
||||
r-reroot-change
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val true}]})
|
||||
shapes-to-reroot)
|
||||
|
||||
u-reroot-change
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val nil}]})
|
||||
shapes-to-reroot)
|
||||
|
||||
r-reg-change
|
||||
[{:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes (vec parents)}]
|
||||
|
||||
u-reg-change
|
||||
[{:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes (vec parents)}]
|
||||
|
||||
rchanges (d/concat []
|
||||
r-mov-change
|
||||
r-del-change
|
||||
r-mask-change
|
||||
r-detach-change
|
||||
r-deroot-change
|
||||
r-reroot-change
|
||||
r-reg-change)
|
||||
|
||||
uchanges (d/concat []
|
||||
u-del-change
|
||||
u-reroot-change
|
||||
u-deroot-change
|
||||
u-detach-change
|
||||
u-mask-change
|
||||
u-mov-change
|
||||
u-reg-change)]
|
||||
[rchanges uchanges]))
|
||||
|
||||
(defn relocate-shapes
|
||||
[ids parent-id to-index]
|
||||
(us/verify (s/coll-of ::us/uuid) ids)
|
||||
@@ -822,13 +985,40 @@
|
||||
;; Ignore any shape whose parent is also intented to be moved
|
||||
ids (cp/clean-loops objects ids)
|
||||
|
||||
parents (loop [res #{parent-id}
|
||||
ids (seq ids)]
|
||||
(if (nil? ids)
|
||||
(vec res)
|
||||
(recur
|
||||
(conj res (cp/get-parent (first ids) objects))
|
||||
(next ids))))
|
||||
;; If we try to move a parent into a child we remove it
|
||||
ids (filter #(not (cp/is-parent? objects parent-id %)) ids)
|
||||
|
||||
parents (reduce (fn [result id]
|
||||
(conj result (cp/get-parent id objects)))
|
||||
#{parent-id} ids)
|
||||
|
||||
groups-to-delete
|
||||
(loop [current-id (first parents)
|
||||
to-check (rest parents)
|
||||
removed-id? (set ids)
|
||||
result #{}]
|
||||
|
||||
(if-not current-id
|
||||
;; Base case, no next element
|
||||
result
|
||||
|
||||
(let [group (get objects current-id)]
|
||||
(if (and (not= uuid/zero current-id)
|
||||
(not= current-id parent-id)
|
||||
(empty? (remove removed-id? (:shapes group))))
|
||||
|
||||
;; Adds group to the remove and check its parent
|
||||
(let [to-check (d/concat [] to-check [(cp/get-parent current-id objects)]) ]
|
||||
(recur (first to-check)
|
||||
(rest to-check)
|
||||
(conj removed-id? current-id)
|
||||
(conj result current-id)))
|
||||
|
||||
;; otherwise recur
|
||||
(recur (first to-check)
|
||||
(rest to-check)
|
||||
removed-id?
|
||||
result)))))
|
||||
|
||||
groups-to-unmask
|
||||
(reduce (fn [group-ids id]
|
||||
@@ -845,6 +1035,10 @@
|
||||
#{}
|
||||
ids)
|
||||
|
||||
;; Sets the correct components metadata for the moved shapes
|
||||
;; `shapes-to-detach` Detach from a component instance a shape that was inside a component and is moved outside
|
||||
;; `shapes-to-deroot` Removes the root flag from a component instance moved inside another component
|
||||
;; `shapes-to-reroot` Adds a root flag when a nested component instance is moved outside
|
||||
[shapes-to-detach shapes-to-deroot shapes-to-reroot]
|
||||
(reduce (fn [[shapes-to-detach shapes-to-deroot shapes-to-reroot] id]
|
||||
(let [shape (get objects id)
|
||||
@@ -872,131 +1066,18 @@
|
||||
[[] [] []]
|
||||
ids)
|
||||
|
||||
rchanges (d/concat
|
||||
[{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:page-id page-id
|
||||
:index to-index
|
||||
:shapes (vec (reverse ids))}
|
||||
{:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes parents}]
|
||||
(map (fn [group-id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id group-id
|
||||
:operations [{:type :set
|
||||
:attr :masked-group?
|
||||
:val false}]})
|
||||
groups-to-unmask)
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-id
|
||||
:val nil}
|
||||
{:type :set
|
||||
:attr :component-file
|
||||
:val nil}
|
||||
{:type :set
|
||||
:attr :component-root?
|
||||
:val nil}
|
||||
{:type :set
|
||||
:attr :remote-synced?
|
||||
:val nil}
|
||||
{:type :set
|
||||
:attr :shape-ref
|
||||
:val nil}
|
||||
{:type :set
|
||||
:attr :touched
|
||||
:val nil}]})
|
||||
shapes-to-detach)
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val nil}]})
|
||||
shapes-to-deroot)
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val true}]})
|
||||
shapes-to-reroot))
|
||||
|
||||
uchanges (d/concat
|
||||
(reduce (fn [res id]
|
||||
(let [obj (get objects id)]
|
||||
(conj res
|
||||
{:type :mov-objects
|
||||
:parent-id (:parent-id obj)
|
||||
:page-id page-id
|
||||
:index (cp/position-on-parent id objects)
|
||||
:shapes [id]})))
|
||||
[] (reverse ids))
|
||||
[{:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes parents}]
|
||||
(map (fn [group-id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id group-id
|
||||
:operations [{:type :set
|
||||
:attr :masked-group?
|
||||
:val true}]})
|
||||
groups-to-unmask)
|
||||
(map (fn [id]
|
||||
(let [obj (get objects id)]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-id
|
||||
:val (:component-id obj)}
|
||||
{:type :set
|
||||
:attr :component-file
|
||||
:val (:component-file obj)}
|
||||
{:type :set
|
||||
:attr :component-root?
|
||||
:val (:component-root? obj)}
|
||||
{:type :set
|
||||
:attr :remote-synced?
|
||||
:val (:remote-synced? obj)}
|
||||
{:type :set
|
||||
:attr :shape-ref
|
||||
:val (:shape-ref obj)}
|
||||
{:type :set
|
||||
:attr :touched
|
||||
:val (:touched obj)}]}))
|
||||
shapes-to-detach)
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val true}]})
|
||||
shapes-to-deroot)
|
||||
(map (fn [id]
|
||||
{:type :mod-obj
|
||||
:page-id page-id
|
||||
:id id
|
||||
:operations [{:type :set
|
||||
:attr :component-root?
|
||||
:val nil}]})
|
||||
shapes-to-reroot))]
|
||||
|
||||
;; (println "================ rchanges")
|
||||
;; (cljs.pprint/pprint rchanges)
|
||||
;; (println "================ uchanges")
|
||||
;; (cljs.pprint/pprint uchanges)
|
||||
(rx/of (dwc/commit-changes rchanges uchanges
|
||||
{:commit-local? true})
|
||||
[rchanges uchanges] (relocate-shapes-changes objects
|
||||
parents
|
||||
parent-id
|
||||
page-id
|
||||
to-index
|
||||
ids
|
||||
groups-to-delete
|
||||
groups-to-unmask
|
||||
shapes-to-detach
|
||||
shapes-to-reroot
|
||||
shapes-to-deroot)]
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(dwc/expand-collapse parent-id))))))
|
||||
|
||||
(defn relocate-selected-shapes
|
||||
@@ -1220,20 +1301,26 @@
|
||||
|
||||
|
||||
(defn go-to-viewer
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :viewer params {:index 0})))))
|
||||
([] (go-to-viewer {}))
|
||||
([{:keys [file-id page-id]}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [{:keys [current-file-id current-page-id]} state
|
||||
params {:file-id (or file-id current-file-id)
|
||||
:page-id (or page-id current-page-id)}]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :viewer params {:index 0})))))))
|
||||
|
||||
(defn go-to-dashboard
|
||||
[{:keys [team-id] :as project}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))
|
||||
([] (go-to-dashboard nil))
|
||||
([{:keys [team-id]}]
|
||||
(ptk/reify ::go-to-dashboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [team-id (or team-id (get-in state [:workspace-project :team-id]))]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav :dashboard-projects {:team-id team-id})))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Context Menu
|
||||
@@ -1279,7 +1366,6 @@
|
||||
;; Clipboard
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
|
||||
(defn copy-selected
|
||||
[]
|
||||
(letfn [;; Retrieve all ids of selected shapes with corresponding
|
||||
@@ -1395,15 +1481,32 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(try
|
||||
(let [paste-data (wapi/read-from-paste-event event)
|
||||
(let [objects (dwc/lookup-page-objects state)
|
||||
paste-data (wapi/read-from-paste-event event)
|
||||
image-data (wapi/extract-images paste-data)
|
||||
text-data (wapi/extract-text paste-data)
|
||||
decoded-data (and (t/transit? text-data) (t/decode text-data))]
|
||||
decoded-data (and (t/transit? text-data)
|
||||
(t/decode text-data))
|
||||
|
||||
edit-id (get-in state [:workspace-local :edition])
|
||||
is-editing-text? (and edit-id (= :text (get-in objects [edit-id :type])))]
|
||||
|
||||
(cond
|
||||
(seq image-data) (rx/from (map paste-image image-data))
|
||||
decoded-data (rx/of (paste-shape decoded-data in-viewport?))
|
||||
(string? text-data) (rx/of (paste-text text-data))
|
||||
:else (rx/empty)))
|
||||
(seq image-data)
|
||||
(rx/from (map paste-image image-data))
|
||||
|
||||
(coll? decoded-data)
|
||||
(->> (rx/of decoded-data)
|
||||
(rx/filter #(= :copied-shapes (:type %)))
|
||||
(rx/map #(paste-shape % in-viewport?)))
|
||||
|
||||
;; Some paste events can be fired while we're editing a text
|
||||
;; we forbid that scenario so the default behaviour is executed
|
||||
(and (string? text-data) (not is-editing-text?))
|
||||
(rx/of (paste-text text-data))
|
||||
|
||||
:else
|
||||
(rx/empty)))
|
||||
(catch :default err
|
||||
(js/console.error "Clipboard error:" err))))))
|
||||
|
||||
@@ -1564,7 +1667,7 @@
|
||||
(watch [_ state stream]
|
||||
(let [id (uuid/next)
|
||||
{:keys [x y]} @ms/mouse-position
|
||||
width (min (* 7 (count text)) 700)
|
||||
width (max 8 (min (* 7 (count text)) 700))
|
||||
height 16
|
||||
page-id (:current-page-id state)
|
||||
frame-id (-> (dwc/lookup-page-objects state page-id)
|
||||
@@ -1703,6 +1806,8 @@
|
||||
(d/export dwt/set-modifiers)
|
||||
(d/export dwt/apply-modifiers)
|
||||
(d/export dwt/update-dimensions)
|
||||
(d/export dwt/flip-horizontal-selected)
|
||||
(d/export dwt/flip-vertical-selected)
|
||||
|
||||
;; Persistence
|
||||
|
||||
@@ -1738,80 +1843,3 @@
|
||||
(d/export dwg/unmask-group)
|
||||
(d/export dwg/group-selected)
|
||||
(d/export dwg/ungroup-selected)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shortcuts
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Shortcuts impl https://github.com/ccampbell/mousetrap
|
||||
|
||||
(defn esc-pressed []
|
||||
(ptk/reify :esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
;; Not interrupt when we're editing a path
|
||||
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
|
||||
(get-in state [:workspace-local :edition]))
|
||||
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
|
||||
(if-not (= :draw path-edit-mode)
|
||||
(rx/of :interrupt (deselect-all true))
|
||||
(rx/empty))))))
|
||||
|
||||
(defn c-mod
|
||||
"Adds the control/command modifier to a shortcuts depending on the
|
||||
operating system for the user"
|
||||
[shortcut]
|
||||
(if (cfg/check-platform? :macos)
|
||||
(str "command+" shortcut)
|
||||
(str "ctrl+" shortcut)))
|
||||
|
||||
(def shortcuts
|
||||
{(c-mod "i") #(st/emit! (toggle-layout-flags :assets))
|
||||
(c-mod "l") #(st/emit! (toggle-layout-flags :sitemap :layers))
|
||||
(c-mod "shift+r") #(st/emit! (toggle-layout-flags :rules))
|
||||
(c-mod "a") #(st/emit! (select-all))
|
||||
(c-mod "p") #(st/emit! (toggle-layout-flags :colorpalette))
|
||||
(c-mod "'") #(st/emit! (toggle-layout-flags :display-grid))
|
||||
(c-mod "shift+'") #(st/emit! (toggle-layout-flags :snap-grid))
|
||||
"+" #(st/emit! (increase-zoom nil))
|
||||
"-" #(st/emit! (decrease-zoom nil))
|
||||
(c-mod "g") #(st/emit! group-selected)
|
||||
"shift+g" #(st/emit! ungroup-selected)
|
||||
(c-mod "m") #(st/emit! mask-group)
|
||||
"shift+m" #(st/emit! unmask-group)
|
||||
(c-mod "k") #(st/emit! dwl/add-component)
|
||||
"shift+0" #(st/emit! reset-zoom)
|
||||
"shift+1" #(st/emit! zoom-to-fit-all)
|
||||
"shift+2" #(st/emit! zoom-to-selected-shape)
|
||||
(c-mod "d") #(st/emit! duplicate-selected)
|
||||
(c-mod "z") #(st/emit! dwc/undo)
|
||||
(c-mod "shift+z") #(st/emit! dwc/redo)
|
||||
(c-mod "y") #(st/emit! dwc/redo)
|
||||
(c-mod "q") #(st/emit! dwc/reinitialize-undo)
|
||||
"a" #(st/emit! (dwd/select-for-drawing :frame))
|
||||
"r" #(st/emit! (dwd/select-for-drawing :rect))
|
||||
"e" #(st/emit! (dwd/select-for-drawing :circle))
|
||||
"t" #(st/emit! dwtxt/start-edit-if-selected
|
||||
(dwd/select-for-drawing :text))
|
||||
"p" #(st/emit! (dwd/select-for-drawing :path))
|
||||
"k" (fn [event]
|
||||
(let [image-upload (dom/get-element "image-upload")]
|
||||
(dom/click image-upload)))
|
||||
(c-mod "c") #(st/emit! (copy-selected))
|
||||
(c-mod "x") #(st/emit! (copy-selected) delete-selected)
|
||||
"escape" #(st/emit! (esc-pressed))
|
||||
"del" #(st/emit! delete-selected)
|
||||
"backspace" #(st/emit! delete-selected)
|
||||
(c-mod "up") #(st/emit! (vertical-order-selected :up))
|
||||
(c-mod "down") #(st/emit! (vertical-order-selected :down))
|
||||
(c-mod "shift+up") #(st/emit! (vertical-order-selected :top))
|
||||
(c-mod "shift+down") #(st/emit! (vertical-order-selected :bottom))
|
||||
"shift+up" #(st/emit! (dwt/move-selected :up true))
|
||||
"shift+down" #(st/emit! (dwt/move-selected :down true))
|
||||
"shift+right" #(st/emit! (dwt/move-selected :right true))
|
||||
"shift+left" #(st/emit! (dwt/move-selected :left true))
|
||||
"up" #(st/emit! (dwt/move-selected :up false))
|
||||
"down" #(st/emit! (dwt/move-selected :down false))
|
||||
"right" #(st/emit! (dwt/move-selected :right false))
|
||||
"left" #(st/emit! (dwt/move-selected :left false))
|
||||
"i" #(st/emit! (mdc/picker-for-selected-shape ))})
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
(defn retrieve-used-names
|
||||
[objects]
|
||||
(into #{} (map :name) (vals objects)))
|
||||
(into #{} (comp (map :name) (remove nil?)) (vals objects)))
|
||||
|
||||
|
||||
(defn generate-unique-name
|
||||
@@ -536,20 +536,26 @@
|
||||
|
||||
(defn get-shape-layer-position
|
||||
[objects selected attrs]
|
||||
(cond
|
||||
(= :frame (:type attrs))
|
||||
|
||||
(if (= :frame (:type attrs))
|
||||
;; Frames are alwasy positioned on the root frame
|
||||
[uuid/zero uuid/zero nil]
|
||||
|
||||
(empty? selected)
|
||||
;; Calculate the frame over which we're drawing
|
||||
(let [position @ms/mouse-position
|
||||
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))]
|
||||
[frame-id frame-id nil])
|
||||
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))
|
||||
shape (when-not (empty? selected)
|
||||
(cp/get-base-shape objects selected))]
|
||||
|
||||
:else
|
||||
(let [shape (cp/get-base-shape objects selected)
|
||||
index (cp/position-on-parent (:id shape) objects)
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)])))
|
||||
;; When no shapes has been selected or we're over a different frame
|
||||
;; we add it as the latest shape of that frame
|
||||
(if (or (not shape) (not= (:frame-id shape) frame-id))
|
||||
[frame-id frame-id nil]
|
||||
|
||||
;; Otherwise, we add it to next to the selected shape
|
||||
(let [index (cp/position-on-parent (:id shape) objects)
|
||||
{:keys [frame-id parent-id]} shape]
|
||||
[frame-id parent-id (inc index)])))))
|
||||
|
||||
(defn add-shape-changes
|
||||
[page-id objects selected attrs]
|
||||
@@ -562,6 +568,9 @@
|
||||
|
||||
shape (merge default-attrs shape)
|
||||
|
||||
not-frame? #(not (= :frame (get-in objects [% :type])))
|
||||
selected (into #{} (filter not-frame?) selected)
|
||||
|
||||
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
|
||||
|
||||
redo-changes [{:type :add-obj
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
[app.main.data.workspace.drawing.common :as common]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn truncate-zero [num default]
|
||||
(if (mth/almost-zero? num) default num))
|
||||
|
||||
(defn resize-shape [{:keys [x y width height transform transform-inverse] :as shape} point lock?]
|
||||
(let [;; The new shape behaves like a resize on the bottom-right corner
|
||||
initial (gpt/point (+ x width) (+ y height))
|
||||
shapev (gpt/point width height)
|
||||
deltav (gpt/to-vec initial point)
|
||||
scalev (gpt/divide (gpt/add shapev deltav) shapev)
|
||||
scalev (-> (gpt/divide (gpt/add shapev deltav) shapev)
|
||||
(update :x truncate-zero 1)
|
||||
(update :y truncate-zero 1))
|
||||
scalev (if lock?
|
||||
(let [v (max (:x scalev) (:y scalev))]
|
||||
(gpt/point v v))
|
||||
|
||||
@@ -90,12 +90,15 @@
|
||||
path)))
|
||||
|
||||
(defn- points->components [shape content]
|
||||
(let [rotation (:rotation shape 0)
|
||||
(let [transform (:transform shape)
|
||||
transform-inverse (:transform-inverse shape)
|
||||
center (gsh/center-shape shape)
|
||||
content-rotated (gsh/transform-content content (gmt/rotate-matrix (- rotation) center))
|
||||
base-content (gsh/transform-content
|
||||
content
|
||||
(gmt/transform-in center transform-inverse))
|
||||
|
||||
;; Calculates the new selrect with points given the old center
|
||||
points (-> (gsh/content->selrect content-rotated)
|
||||
points (-> (gsh/content->selrect base-content)
|
||||
(gsh/rect->points)
|
||||
(gsh/transform-points center (:transform shape (gmt/matrix))))
|
||||
|
||||
@@ -692,8 +695,8 @@
|
||||
point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point))
|
||||
handler (-> content (get index) (ugp/get-handler prefix))
|
||||
|
||||
current-distance (gpt/distance (ugp/opposite-handler point handler) opposite-handler)
|
||||
match-opposite? (mth/almost-zero? current-distance)]
|
||||
current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler))
|
||||
match-opposite? (and opposite-handler (mth/almost-zero? current-distance))]
|
||||
|
||||
(drag-stream
|
||||
(rx/concat
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
(defonce ^:private default-square-params
|
||||
{:size 16
|
||||
:color {:color "#59B9E2"
|
||||
:opacity 0.2}})
|
||||
:opacity 0.4}})
|
||||
|
||||
(defonce ^:private default-layout-params
|
||||
{:size 12
|
||||
|
||||
@@ -65,6 +65,13 @@
|
||||
|
||||
(declare sync-file)
|
||||
|
||||
(defn set-assets-box-open
|
||||
[file-id box open?]
|
||||
(ptk/reify ::set-assets-box-open
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :assets-files-open file-id box] open?))))
|
||||
|
||||
(defn default-color-name [color]
|
||||
(or (:color color)
|
||||
(case (get-in color [:gradient :type])
|
||||
@@ -230,7 +237,8 @@
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)
|
||||
selected (get-in state [:workspace-local :selected])]
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
selected (cp/clean-loops objects selected)]
|
||||
(let [[group rchanges uchanges]
|
||||
(dwlh/generate-add-component selected objects page-id file-id)]
|
||||
(when-not (empty? rchanges)
|
||||
@@ -311,7 +319,7 @@
|
||||
|
||||
(defn instantiate-component
|
||||
"Create a new shape in the current page, from the component with the given id
|
||||
in the given file library / current file library."
|
||||
in the given file library. Then selects the newly created instance."
|
||||
[file-id component-id position]
|
||||
(us/assert ::us/uuid file-id)
|
||||
(us/assert ::us/uuid component-id)
|
||||
|
||||
@@ -626,6 +626,8 @@
|
||||
(contains? (:touched shape-inst)
|
||||
:shapes-group))
|
||||
(add-shape-to-instance child-master
|
||||
(d/index-of children-master
|
||||
child-master)
|
||||
component
|
||||
container
|
||||
root-inst
|
||||
@@ -649,11 +651,11 @@
|
||||
reset?
|
||||
initial-root?)))
|
||||
|
||||
moved (fn [shape-inst shape-master]
|
||||
moved (fn [child-inst child-master]
|
||||
(move-shape
|
||||
shape-inst
|
||||
(d/index-of children-inst shape-inst)
|
||||
(d/index-of children-master shape-master)
|
||||
child-inst
|
||||
(d/index-of children-inst child-inst)
|
||||
(d/index-of children-master child-master)
|
||||
container
|
||||
omit-touched?))
|
||||
|
||||
@@ -742,6 +744,8 @@
|
||||
|
||||
only-inst (fn [child-inst]
|
||||
(add-shape-to-master child-inst
|
||||
(d/index-of children-inst
|
||||
child-inst)
|
||||
component
|
||||
container
|
||||
root-inst
|
||||
@@ -768,11 +772,11 @@
|
||||
root-master)
|
||||
initial-root?)))
|
||||
|
||||
moved (fn [shape-inst shape-master]
|
||||
moved (fn [child-inst child-master]
|
||||
(move-shape
|
||||
shape-master
|
||||
(d/index-of children-master shape-master)
|
||||
(d/index-of children-inst shape-inst)
|
||||
child-master
|
||||
(d/index-of children-master child-master)
|
||||
(d/index-of children-inst child-inst)
|
||||
component-container
|
||||
false))
|
||||
|
||||
@@ -863,7 +867,7 @@
|
||||
(concat-changes (moved-cb child-inst' child-master))))))))))))
|
||||
|
||||
(defn- add-shape-to-instance
|
||||
[component-shape component container root-instance root-master omit-touched? set-remote-synced?]
|
||||
[component-shape index component container root-instance root-master omit-touched? set-remote-synced?]
|
||||
(log/info :msg (str "ADD [P] " (:name component-shape)))
|
||||
(let [component-parent-shape (cp/get-shape component (:parent-id component-shape))
|
||||
parent-shape (d/seek #(cp/is-master-of component-parent-shape %)
|
||||
@@ -904,6 +908,7 @@
|
||||
(as-> {:type :add-obj
|
||||
:id (:id shape')
|
||||
:parent-id (:parent-id shape')
|
||||
:index index
|
||||
:ignore-touched true
|
||||
:obj shape'} $
|
||||
(cond-> $
|
||||
@@ -929,7 +934,7 @@
|
||||
[rchanges uchanges])))
|
||||
|
||||
(defn- add-shape-to-master
|
||||
[shape component page root-instance root-master]
|
||||
[shape index component page root-instance root-master]
|
||||
(log/info :msg (str "ADD [C] " (:name shape)))
|
||||
(let [parent-shape (cp/get-shape page (:parent-id shape))
|
||||
component-parent-shape (d/seek #(cp/is-master-of % parent-shape)
|
||||
@@ -963,6 +968,7 @@
|
||||
:id (:id shape')
|
||||
:component-id (:id component)
|
||||
:parent-id (:parent-id shape')
|
||||
:index index
|
||||
:ignore-touched true
|
||||
:obj shape'})
|
||||
new-shapes)
|
||||
|
||||
@@ -200,13 +200,16 @@
|
||||
(ptk/reify ::handle-file-change
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-ids (into #{} (comp (map :page-id)
|
||||
(filter identity))
|
||||
changes)]
|
||||
(let [changes-by-pages (group-by :page-id changes)
|
||||
process-page-changes
|
||||
(fn [[page-id changes]]
|
||||
(dwc/update-indices page-id changes))]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (dwp/shapes-changes-persisted file-id msg))
|
||||
(when (seq page-ids)
|
||||
(rx/from (map dwc/update-indices page-ids changes))))))))
|
||||
|
||||
(when-not (empty? changes-by-pages)
|
||||
(rx/from (map process-page-changes changes-by-pages))))))))
|
||||
|
||||
(s/def ::library-change-event
|
||||
(s/keys :req-un [::type
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
(ns app.main.data.workspace.persistence
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.util.http :as http]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.media :as cm]
|
||||
[app.common.pages :as cp]
|
||||
@@ -21,21 +20,22 @@
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.avatars :as avatars]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.util.avatars :as avatars]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[potok.core :as ptk]
|
||||
[app.main.store :as st]))
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(declare persist-changes)
|
||||
(declare persist-sychronous-changes)
|
||||
@@ -417,24 +417,36 @@
|
||||
(defn- handle-upload-error [on-error stream]
|
||||
(->> stream
|
||||
(rx/catch
|
||||
(fn [error]
|
||||
(cond
|
||||
(= (:code error) :media-type-not-allowed)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
(fn on-error* [error]
|
||||
(if (ex/ex-info? error)
|
||||
(on-error* (ex-data error))
|
||||
(cond
|
||||
(= (:code error) :invalid-svg-file)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (:code error) :media-type-mismatch)
|
||||
(rx/of (dm/error (tr "errors.media-type-mismatch")))
|
||||
(= (:code error) :media-type-not-allowed)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (:code error) :unable-to-optimize)
|
||||
(rx/of (dm/error (:hint error)))
|
||||
(= (:code error) :ubable-to-access-to-url)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(fn? on-error)
|
||||
(do
|
||||
(= (:code error) :invalid-image)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (:code error) :media-too-large)
|
||||
(rx/of (dm/error (tr "errors.media-too-large")))
|
||||
|
||||
(= (:code error) :media-type-mismatch)
|
||||
(rx/of (dm/error (tr "errors.media-type-mismatch")))
|
||||
|
||||
(= (:code error) :unable-to-optimize)
|
||||
(rx/of (dm/error (:hint error)))
|
||||
|
||||
(fn? on-error)
|
||||
(on-error error)
|
||||
(rx/empty))
|
||||
|
||||
:else
|
||||
(rx/throw error))))))
|
||||
:else
|
||||
(rx/throw error)))))))
|
||||
|
||||
(defn- upload-uris [file-id local? name uris mtype on-image on-svg]
|
||||
(letfn [(svg-url? [url]
|
||||
@@ -490,7 +502,7 @@
|
||||
(rx/map #(assoc (first %) :name (.-name (second %))))
|
||||
(rx/do on-svg)))))
|
||||
|
||||
(defn upload-media-objects
|
||||
(defn- upload-media-objects
|
||||
[{:keys [file-id local? data name uris mtype svg-as-images] :as params}]
|
||||
(us/assert ::upload-media-objects params)
|
||||
(ptk/reify ::upload-media-objects
|
||||
@@ -499,7 +511,6 @@
|
||||
(let [{:keys [on-image on-svg on-error]
|
||||
:or {on-image identity
|
||||
on-svg identity}} (meta params)]
|
||||
|
||||
(rx/concat
|
||||
(rx/of (dm/show {:content (tr "media.loading")
|
||||
:type :info
|
||||
@@ -515,7 +526,8 @@
|
||||
(handle-upload-error on-error)
|
||||
(rx/finalize (st/emitf (dm/hide-tag :media-loading)))))))))
|
||||
|
||||
(defn upload-media-asset [params]
|
||||
(defn upload-media-asset
|
||||
[params]
|
||||
(let [params (-> params
|
||||
(assoc :svg-as-images true)
|
||||
(assoc :local? false)
|
||||
@@ -525,13 +537,12 @@
|
||||
(defn upload-media-workspace
|
||||
[params position]
|
||||
(let [{:keys [x y]} position
|
||||
params (-> params
|
||||
(assoc :local? true)
|
||||
(with-meta
|
||||
{:on-image
|
||||
#(st/emit! (dwc/image-uploaded % x y))
|
||||
:on-svg
|
||||
#(st/emit! (svg/svg-uploaded % x y))}))]
|
||||
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
|
||||
:on-svg #(st/emit! (svg/svg-uploaded % x y))}
|
||||
|
||||
params (-> (assoc params :local? true)
|
||||
(with-meta mdata))]
|
||||
|
||||
(upload-media-objects params)))
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,14 @@
|
||||
objects (dwc/lookup-page-objects state page-id)]
|
||||
(rx/of (dwc/expand-all-parents [id] objects)))))))
|
||||
|
||||
(defn deselect-shape
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::select-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected] disj id))))
|
||||
|
||||
(defn shift-select-shapes
|
||||
([id]
|
||||
(ptk/reify ::shift-select-shapes
|
||||
@@ -156,11 +164,10 @@
|
||||
(not= (:id common-frame-id) uuid/zero))
|
||||
(-> (get objects common-frame-id)
|
||||
:shapes)
|
||||
(let [frames (cp/select-frames objects)]
|
||||
(->> (if (seq frames)
|
||||
frames
|
||||
(cp/select-toplevel-shapes objects))
|
||||
(map :id)))))
|
||||
(->> (cp/select-toplevel-shapes objects
|
||||
{:include-frames? true
|
||||
:include-frame-children? false})
|
||||
(map :id))))
|
||||
|
||||
is-not-blocked (fn [shape-id] (not (get-in state [:workspace-data
|
||||
:pages-index page-id
|
||||
|
||||
262
frontend/src/app/main/data/workspace/shortcuts.cljs
Normal file
262
frontend/src/app/main/data/workspace/shortcuts.cljs
Normal file
@@ -0,0 +1,262 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.shortcuts
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.main.data.colors :as mdc]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.texts :as dwtxt]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; \u2318P
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shortcuts
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Shortcuts impl https://github.com/ccampbell/mousetrap
|
||||
|
||||
(defn esc-pressed []
|
||||
(ptk/reify :esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
;; Not interrupt when we're editing a path
|
||||
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
|
||||
(get-in state [:workspace-local :edition]))
|
||||
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
|
||||
(if-not (= :draw path-edit-mode)
|
||||
(rx/of :interrupt (dw/deselect-all true))
|
||||
(rx/empty))))))
|
||||
|
||||
(def shortcuts
|
||||
{:toggle-layers {:tooltip (ds/meta "L")
|
||||
:command (ds/c-mod "l")
|
||||
:fn #(st/emit! (dw/go-to-layout :layers))}
|
||||
|
||||
:toggle-assets {:tooltip (ds/meta "I")
|
||||
:command (ds/c-mod "i")
|
||||
:fn #(st/emit! (dw/go-to-layout :assets))}
|
||||
|
||||
:toggle-history {:tooltip (ds/meta "H")
|
||||
:command (ds/c-mod "h")
|
||||
:fn #(st/emit! (dw/go-to-layout :document-history))}
|
||||
|
||||
:toggle-palette {:tooltip (ds/meta "P")
|
||||
:command (ds/c-mod "p")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :colorpalette))}
|
||||
|
||||
:toggle-rules {:tooltip (ds/meta-shift "R")
|
||||
:command (ds/c-mod "shift+r")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :rules))}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:fn #(st/emit! (dw/select-all))}
|
||||
|
||||
:toggle-grid {:tooltip (ds/meta "'")
|
||||
:command (ds/c-mod "'")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :display-grid))}
|
||||
|
||||
:toggle-snap-grid {:tooltip (ds/meta-shift "'")
|
||||
:command (ds/c-mod "shift+'")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :snap-grid))}
|
||||
|
||||
:toggle-alignment {:tooltip (ds/meta "\\")
|
||||
:command (ds/c-mod "\\")
|
||||
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
|
||||
|
||||
:increase-zoom {:tooltip "+"
|
||||
:command "+"
|
||||
:fn #(st/emit! (dw/increase-zoom nil))}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command "-"
|
||||
:fn #(st/emit! (dw/decrease-zoom nil))}
|
||||
|
||||
:group {:tooltip (ds/meta "G")
|
||||
:command (ds/c-mod "g")
|
||||
:fn #(st/emit! dw/group-selected)}
|
||||
|
||||
:ungroup {:tooltip (ds/shift "G")
|
||||
:command "shift+g"
|
||||
:fn #(st/emit! dw/ungroup-selected)}
|
||||
|
||||
:mask {:tooltip (ds/meta "M")
|
||||
:command (ds/c-mod "m")
|
||||
:fn #(st/emit! dw/mask-group)}
|
||||
|
||||
:unmask {:tooltip (ds/meta-shift "M")
|
||||
:command (ds/c-mod "shift+m")
|
||||
:fn #(st/emit! dw/unmask-group)}
|
||||
|
||||
:create-component {:tooltip (ds/meta "K")
|
||||
:command (ds/c-mod "k")
|
||||
:fn #(st/emit! dwl/add-component)}
|
||||
|
||||
:flip-vertical {:tooltip (ds/shift "V")
|
||||
:command "shift+v"
|
||||
:fn #(st/emit! (dw/flip-vertical-selected))}
|
||||
|
||||
:flip-horizontal {:tooltip (ds/shift "V")
|
||||
:command "shift+h"
|
||||
:fn #(st/emit! (dw/flip-horizontal-selected))}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "0")
|
||||
:command "shift+0"
|
||||
:fn #(st/emit! dw/reset-zoom)}
|
||||
|
||||
:fit-all {:tooltip (ds/shift "1")
|
||||
:command "shift+1"
|
||||
:fn #(st/emit! dw/zoom-to-fit-all)}
|
||||
|
||||
:zoom-selected {:tooltip (ds/shift "2")
|
||||
:command "shift+2"
|
||||
:fn #(st/emit! dw/zoom-to-selected-shape)}
|
||||
|
||||
:duplicate {:tooltip (ds/meta "D")
|
||||
:command (ds/c-mod "d")
|
||||
:fn #(st/emit! dw/duplicate-selected)}
|
||||
|
||||
:undo {:tooltip (ds/meta "Z")
|
||||
:command (ds/c-mod "z")
|
||||
:fn #(st/emit! dwc/undo)}
|
||||
|
||||
:redo {:tooltip (ds/meta "Y")
|
||||
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
|
||||
:fn #(st/emit! dwc/redo)}
|
||||
|
||||
:clear-undo {:tooltip (ds/meta "Q")
|
||||
:command (ds/c-mod "q")
|
||||
:fn #(st/emit! dwc/reinitialize-undo)}
|
||||
|
||||
:draw-frame {:tooltip "A"
|
||||
:command "a"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :frame))}
|
||||
|
||||
:draw-rect {:tooltip "R"
|
||||
:command "r"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :rect))}
|
||||
|
||||
:draw-ellipse {:tooltip "E"
|
||||
:command "e"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :circle))}
|
||||
|
||||
:draw-text {:tooltip "T"
|
||||
:command "t"
|
||||
:fn #(st/emit! dwtxt/start-edit-if-selected
|
||||
(dwd/select-for-drawing :text))}
|
||||
|
||||
:draw-path {:tooltip "P"
|
||||
:command "p"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :path))}
|
||||
|
||||
:draw-curve {:tooltip (ds/shift "C")
|
||||
:command "shift+c"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :curve))}
|
||||
|
||||
:add-comment {:tooltip "C"
|
||||
:command "c"
|
||||
:fn #(st/emit! (dwd/select-for-drawing :comments))}
|
||||
|
||||
:insert-image {:tooltip "K"
|
||||
:command "k"
|
||||
:fn #(-> "image-upload" dom/get-element dom/click)}
|
||||
|
||||
:copy {:tooltip (ds/meta "C")
|
||||
:command (ds/c-mod "c")
|
||||
:fn #(st/emit! (dw/copy-selected))}
|
||||
|
||||
:cut {:tooltip (ds/meta "X")
|
||||
:command (ds/c-mod "x")
|
||||
:fn #(st/emit! (dw/copy-selected) dw/delete-selected)}
|
||||
|
||||
:paste {:tooltip (ds/meta "V")
|
||||
:disabled true
|
||||
:command (ds/c-mod "v")}
|
||||
|
||||
:delete {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:fn #(st/emit! dw/delete-selected)}
|
||||
|
||||
:bring-forward {:tooltip (ds/meta ds/up-arrow)
|
||||
:command (ds/c-mod "up")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :up))}
|
||||
|
||||
:bring-backward {:tooltip (ds/meta ds/down-arrow)
|
||||
:command (ds/c-mod "down")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :down))}
|
||||
|
||||
:bring-front {:tooltip (ds/meta-shift ds/up-arrow)
|
||||
:command (ds/c-mod "shift+up")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :top))}
|
||||
|
||||
:bring-back {:tooltip (ds/meta-shift ds/down-arrow)
|
||||
:command (ds/c-mod "shift+down")
|
||||
:fn #(st/emit! (dw/vertical-order-selected :bottom))}
|
||||
|
||||
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
|
||||
:command "shift+up"
|
||||
:fn #(st/emit! (dwt/move-selected :up true))}
|
||||
|
||||
:move-fast-down {:tooltip (ds/shift ds/down-arrow)
|
||||
:command "shift+down"
|
||||
:fn #(st/emit! (dwt/move-selected :down true))}
|
||||
|
||||
:move-fast-right {:tooltip (ds/shift ds/right-arrow)
|
||||
:command "shift+right"
|
||||
:fn #(st/emit! (dwt/move-selected :right true))}
|
||||
|
||||
:move-fast-left {:tooltip (ds/shift ds/left-arrow)
|
||||
:command "shift+left"
|
||||
:fn #(st/emit! (dwt/move-selected :left true))}
|
||||
|
||||
:move-unit-up {:tooltip ds/up-arrow
|
||||
:command "up"
|
||||
:fn #(st/emit! (dwt/move-selected :up false))}
|
||||
|
||||
:move-unit-down {:tooltip ds/down-arrow
|
||||
:command "down"
|
||||
:fn #(st/emit! (dwt/move-selected :down false))}
|
||||
|
||||
:move-unit-left {:tooltip ds/right-arrow
|
||||
:command "right"
|
||||
:fn #(st/emit! (dwt/move-selected :right false))}
|
||||
|
||||
:move-unit-right {:tooltip ds/left-arrow
|
||||
:command "left"
|
||||
:fn #(st/emit! (dwt/move-selected :left false))}
|
||||
|
||||
:open-color-picker {:tooltip "I"
|
||||
:command "i"
|
||||
:fn #(st/emit! (mdc/picker-for-selected-shape ))}
|
||||
|
||||
:open-viewer {:tooltip "G V"
|
||||
:command "g v"
|
||||
:fn #(st/emit! (dw/go-to-viewer))}
|
||||
|
||||
:open-dashboard {:tooltip "G D"
|
||||
:command "g d"
|
||||
:fn #(st/emit! (dw/go-to-dashboard))}
|
||||
|
||||
:escape {:tooltip (ds/esc)
|
||||
:command "escape"
|
||||
:fn #(st/emit! (esc-pressed))}})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
@@ -249,7 +249,7 @@
|
||||
(assoc :overflow-text true)
|
||||
|
||||
(and (= :fixed grow-type) overflow-text (<= new-height shape-height))
|
||||
(assoc :overflow-text true)
|
||||
(assoc :overflow-text false)
|
||||
|
||||
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
|
||||
(-> (assoc :modifiers modifier-width)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user