mirror of
https://github.com/penpot/penpot.git
synced 2025-04-16 08:51:32 -05:00
Merge branch 'release-1.2.0' into main
This commit is contained in:
commit
136d00797c
90 changed files with 2369 additions and 875 deletions
|
@ -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.
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
:database-username "penpot"
|
||||
:database-password "penpot"
|
||||
|
||||
:default-blob-version 1
|
||||
|
||||
:asserts-enabled false
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
|
@ -38,6 +40,9 @@
|
|||
: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
|
||||
|
@ -79,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)
|
||||
|
@ -89,7 +95,11 @@
|
|||
(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/string)
|
||||
(s/def ::smtp-default-from ::us/string)
|
||||
|
@ -142,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
|
||||
|
@ -230,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}))
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -472,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]
|
||||
|
|
|
@ -38,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
|
||||
|
@ -72,3 +72,17 @@
|
|||
(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])))
|
||||
|
|
|
@ -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]
|
||||
|
@ -196,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
|
||||
|
|
|
@ -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])))
|
||||
|
||||
))
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))))
|
||||
|
|
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 "$@";
|
||||
|
|
|
@ -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
|
||||
already explained in the comments. All other configuration options are
|
||||
explained in [configuration guide](./05-Configuration-Guide.md).
|
||||
|
|
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;
|
||||
|
|
|
@ -102,6 +102,14 @@ textarea {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: $color-gray-20;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
font-size: $fs14;
|
||||
|
@ -131,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;
|
||||
|
@ -143,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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -417,3 +417,12 @@
|
|||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :hover] (when hover? id)))))
|
||||
|
||||
|
||||
(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})))))))
|
||||
|
|
|
@ -808,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)
|
||||
|
@ -826,13 +988,37 @@
|
|||
;; If we try to move a parent into a child we remove it
|
||||
ids (filter #(not (cp/is-parent? objects parent-id %)) 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))))
|
||||
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]
|
||||
|
@ -849,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)
|
||||
|
@ -876,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
|
||||
|
@ -1404,11 +1481,16 @@
|
|||
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))]
|
||||
(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))
|
||||
|
@ -1418,7 +1500,9 @@
|
|||
(rx/filter #(= :copied-shapes (:type %)))
|
||||
(rx/map #(paste-shape % in-viewport?)))
|
||||
|
||||
(string? text-data)
|
||||
;; 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
|
||||
|
@ -1722,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -319,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
|
||||
|
|
|
@ -417,13 +417,22 @@
|
|||
(defn- handle-upload-error [on-error stream]
|
||||
(->> stream
|
||||
(rx/catch
|
||||
(fn on-error [error]
|
||||
(fn on-error* [error]
|
||||
(if (ex/ex-info? error)
|
||||
(on-error (ex-data 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-not-allowed)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (:code error) :ubable-to-access-to-url)
|
||||
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
|
||||
|
||||
(= (: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")))
|
||||
|
||||
|
|
|
@ -164,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
|
||||
|
|
|
@ -108,6 +108,14 @@
|
|||
: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)}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -82,8 +82,6 @@
|
|||
{:keys [rotation]} shape
|
||||
shapev (-> (gpt/point width height))
|
||||
|
||||
rotation (if (= :path (:type shape)) 0 rotation)
|
||||
|
||||
;; Vector modifiers depending on the handler
|
||||
handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y))
|
||||
|
||||
|
@ -125,15 +123,7 @@
|
|||
;; lock flag that can be activated on element options.
|
||||
(normalize-proportion-lock [[point shift?]]
|
||||
(let [proportion-lock? (:proportion-lock shape)]
|
||||
[point (or proportion-lock? shift?)]))
|
||||
|
||||
;; Applies alginment to point if it is currently
|
||||
;; activated on the current workspace
|
||||
;; (apply-grid-alignment [point]
|
||||
;; (if @refs/selected-alignment
|
||||
;; (uwrk/align-point point)
|
||||
;; (rx/of point)))
|
||||
]
|
||||
[point (or proportion-lock? shift?)]))]
|
||||
(reify
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
|
@ -142,8 +132,7 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [current-pointer @ms/mouse-position
|
||||
initial-position (merge current-pointer initial)
|
||||
(let [initial-position @ms/mouse-position
|
||||
stoper (rx/filter ms/mouse-up? stream)
|
||||
layout (:workspace-layout state)
|
||||
page-id (:current-page-id state)
|
||||
|
@ -541,3 +530,37 @@
|
|||
objects (dwc/lookup-page-objects state page-id)
|
||||
ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))]
|
||||
(rx/of (apply-modifiers ids))))))
|
||||
|
||||
(defn flip-horizontal-selected []
|
||||
(ptk/reify ::flip-horizontal-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [objects (dwc/lookup-page-objects state)
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
shapes (map #(get objects %) selected)
|
||||
selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape)))
|
||||
origin (gpt/point (:x selrect) (+ (:y selrect) (/ (:height selrect) 2)))]
|
||||
|
||||
(rx/of (set-modifiers selected
|
||||
{:resize-vector (gpt/point -1.0 1.0)
|
||||
:resize-origin origin
|
||||
:displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))}
|
||||
false)
|
||||
(apply-modifiers selected))))))
|
||||
|
||||
(defn flip-vertical-selected []
|
||||
(ptk/reify ::flip-vertical-selected
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [objects (dwc/lookup-page-objects state)
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
shapes (map #(get objects %) selected)
|
||||
selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape)))
|
||||
origin (gpt/point (+ (:x selrect) (/ (:width selrect) 2)) (:y selrect))]
|
||||
|
||||
(rx/of (set-modifiers selected
|
||||
{:resize-vector (gpt/point 1.0 -1.0)
|
||||
:resize-origin origin
|
||||
:displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}
|
||||
false)
|
||||
(apply-modifiers selected))))))
|
||||
|
|
|
@ -96,6 +96,9 @@
|
|||
(def current-hover
|
||||
(l/derived :hover workspace-local))
|
||||
|
||||
(def editors
|
||||
(l/derived :editors workspace-local))
|
||||
|
||||
(def workspace-layout
|
||||
(l/derived :workspace-layout st/state))
|
||||
|
||||
|
|
|
@ -59,17 +59,18 @@
|
|||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
["/register" :auth-register]
|
||||
["/login" :auth-login]
|
||||
["/register" :auth-register]
|
||||
["/register/success" :auth-register-success]
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings-profile]
|
||||
["/profile" :settings-profile]
|
||||
["/password" :settings-password]
|
||||
["/options" :settings-options]]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id/:page-id"
|
||||
{:name :viewer
|
||||
|
@ -89,11 +90,11 @@
|
|||
["/render-object/:file-id/:page-id/:object-id" :render-object]
|
||||
|
||||
["/dashboard/team/:team-id"
|
||||
["/members" :dashboard-team-members]
|
||||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
["/libraries" :dashboard-libraries]
|
||||
["/members" :dashboard-team-members]
|
||||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
["/libraries" :dashboard-libraries]
|
||||
["/projects/:project-id" :dashboard-files]]
|
||||
|
||||
["/workspace/:project-id/:file-id" :workspace]])
|
||||
|
@ -121,7 +122,8 @@
|
|||
|
||||
(:settings-profile
|
||||
:settings-password
|
||||
:settings-options)
|
||||
:settings-options
|
||||
:settings-feedback)
|
||||
[:& settings/settings {:route route}]
|
||||
|
||||
:debug-icons-preview
|
||||
|
|
|
@ -37,7 +37,9 @@
|
|||
(dom/prevent-default event)
|
||||
(->> (rp/mutation! :login-with-google {})
|
||||
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
||||
(.replace js/location redirect-uri)))))
|
||||
(.replace js/location redirect-uri))
|
||||
(fn [{:keys [type] :as error}]
|
||||
(st/emit! (dm/error (tr "errors.google-auth-not-enabled")))))))
|
||||
|
||||
(defn- login-with-gitlab
|
||||
[event]
|
||||
|
@ -111,7 +113,7 @@
|
|||
(when cfg/login-with-ldap
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-with-ldap-submit")
|
||||
:on-click on-submit}])]]))
|
||||
:on-click on-submit-ldap}])]]))
|
||||
|
||||
(mf/defc login-page
|
||||
[]
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.object :as obj]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
["react" :as react]
|
||||
[app.util.dom :as dom]))
|
||||
|
||||
|
@ -28,7 +28,6 @@
|
|||
|
||||
type' (mf/use-state type)
|
||||
focus? (mf/use-state false)
|
||||
locale (mf/deref i18n/locale)
|
||||
|
||||
touched? (get-in @form [:touched name])
|
||||
error (get-in @form [:errors name])
|
||||
|
@ -94,7 +93,59 @@
|
|||
help-icon'])
|
||||
(cond
|
||||
(and touched? (:message error))
|
||||
[:span.error (t locale (:message error))]
|
||||
[:span.error (tr (:message error))]
|
||||
|
||||
(string? hint)
|
||||
[:span.hint hint])]]))
|
||||
|
||||
|
||||
(mf/defc textarea
|
||||
[{:keys [label disabled name form hint trim] :as props}]
|
||||
(let [form (or form (mf/use-ctx form-ctx))
|
||||
|
||||
type' (mf/use-state type)
|
||||
focus? (mf/use-state false)
|
||||
|
||||
touched? (get-in @form [:touched name])
|
||||
error (get-in @form [:errors name])
|
||||
|
||||
value (get-in @form [:data name] "")
|
||||
|
||||
klass (dom/classnames
|
||||
:focus @focus?
|
||||
:valid (and touched? (not error))
|
||||
:invalid (and touched? error)
|
||||
:disabled disabled
|
||||
;; :empty (str/empty? value)
|
||||
)
|
||||
|
||||
on-focus #(reset! focus? true)
|
||||
on-change (fm/on-input-change form name trim)
|
||||
|
||||
on-blur
|
||||
(fn [event]
|
||||
(reset! focus? false)
|
||||
(when-not (get-in @form [:touched name])
|
||||
(swap! form assoc-in [:touched name] true)))
|
||||
|
||||
props (-> props
|
||||
(dissoc :help-icon :form :trim)
|
||||
(assoc :value value
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
;; :placeholder label
|
||||
:on-change on-change
|
||||
:type @type')
|
||||
(obj/clj->props))]
|
||||
|
||||
[:div.custom-input
|
||||
{:class klass}
|
||||
[:*
|
||||
[:label label]
|
||||
[:> :textarea props]
|
||||
(cond
|
||||
(and touched? (:message error))
|
||||
[:span.error (tr (:message error))]
|
||||
|
||||
(string? hint)
|
||||
[:span.hint hint])]]))
|
||||
|
|
|
@ -466,10 +466,12 @@
|
|||
[:li {:on-click (partial on-click (da/logout))}
|
||||
[:span.icon i/exit]
|
||||
[:span.text (t locale "labels.logout")]]
|
||||
[:li.feedback {:on-click #(.open js/window "https://github.com/penpot/penpot/discussions" "_blank")}
|
||||
[:span.icon i/msg-info]
|
||||
[:span.text (t locale "labels.feedback")]
|
||||
[:span.primary-badge "ALPHA"]]]]]
|
||||
|
||||
(when cfg/feedback-enabled
|
||||
[:li.feedback {:on-click (partial on-click :settings-feedback)}
|
||||
[:span.icon i/msg-info]
|
||||
[:span.text (t locale "labels.give-feedback")]
|
||||
[:span.primary-badge "ALPHA"]])]]]
|
||||
|
||||
(when (and team profile)
|
||||
[:& comments-section {:profile profile
|
||||
|
|
|
@ -5,30 +5,31 @@
|
|||
;; 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
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.settings
|
||||
(:require
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.settings.options :refer [options-page]]
|
||||
[app.main.ui.settings.feedback :refer [feedback-page]]
|
||||
[app.main.ui.settings.password :refer [password-page]]
|
||||
[app.main.ui.settings.profile :refer [profile-page]]
|
||||
[app.main.ui.settings.sidebar :refer [sidebar]]
|
||||
[app.main.ui.settings.change-email]
|
||||
[app.main.ui.settings.delete-account]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [locale] :as props}]
|
||||
[]
|
||||
(let [logout (constantly nil)]
|
||||
[:header.dashboard-header
|
||||
[:div.dashboard-title
|
||||
[:h1 (t locale "dashboard.your-account-title")]]
|
||||
[:h1 (tr "dashboard.your-account-title")]]
|
||||
[:a.btn-secondary.btn-small {:on-click logout}
|
||||
(t locale "labels.logout")]]))
|
||||
(tr "labels.logout")]]))
|
||||
|
||||
(mf/defc settings
|
||||
[{:keys [route] :as props}]
|
||||
|
@ -41,12 +42,15 @@
|
|||
:section section}]
|
||||
|
||||
[:div.dashboard-content
|
||||
[:& header {:locale locale}]
|
||||
[:& header]
|
||||
[:section.dashboard-container
|
||||
(case section
|
||||
:settings-profile
|
||||
[:& profile-page {:locale locale}]
|
||||
|
||||
:settings-feedback
|
||||
[:& feedback-page]
|
||||
|
||||
:settings-password
|
||||
[:& password-page {:locale locale}]
|
||||
|
||||
|
|
120
frontend/src/app/main/ui/settings/feedback.cljs
Normal file
120
frontend/src/app/main/ui/settings/feedback.cljs
Normal file
|
@ -0,0 +1,120 @@
|
|||
;; 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.ui.settings.feedback
|
||||
"Feedback form."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[app.main.repo :as rp]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(s/def ::content ::us/not-empty-string)
|
||||
(s/def ::subject ::us/not-empty-string)
|
||||
|
||||
(s/def ::feedback-form
|
||||
(s/keys :req-un [::subject ::content]))
|
||||
|
||||
(defn- on-error
|
||||
[form error]
|
||||
(st/emit! (dm/error (tr "errors.generic"))))
|
||||
|
||||
(defn- on-success
|
||||
[form]
|
||||
(st/emit! (dm/success (tr "notifications.profile-saved"))))
|
||||
|
||||
|
||||
(mf/defc options-form
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
form (fm/use-form :spec ::feedback-form)
|
||||
|
||||
loading (mf/use-state false)
|
||||
|
||||
on-succes
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
(fn [event]
|
||||
(st/emit! (dm/success (tr "labels.feedback-sent")))
|
||||
(swap! form assoc :data {} :touched {} :errors {})))
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
(fn [{:keys [code] :as error}]
|
||||
(reset! loading false)
|
||||
(if (= code :feedbck-disabled)
|
||||
(st/emit! (dm/error (tr "labels.feedback-disabled")))
|
||||
(st/emit! (dm/error (tr "errors.generic"))))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
(fn [form event]
|
||||
(reset! loading true)
|
||||
(let [data (:clean-data @form)]
|
||||
(prn "on-submit" data)
|
||||
(->> (rp/mutation! :send-profile-feedback data)
|
||||
(rx/subs on-succes on-error #(reset! loading false))))))]
|
||||
|
||||
[:& fm/form {:class "feedback-form"
|
||||
:on-submit on-submit
|
||||
:form form}
|
||||
|
||||
;; --- Feedback section
|
||||
[:h2 (tr "feedback.title")]
|
||||
[:p (tr "feedback.subtitle")]
|
||||
|
||||
[:div.fields-row
|
||||
[:& fm/input {:label (tr "feedback.subject")
|
||||
:name :subject}]]
|
||||
[:div.fields-row
|
||||
[:& fm/textarea
|
||||
{:label (tr "feedback.description")
|
||||
:name :content
|
||||
:rows 5}]]
|
||||
|
||||
[:& fm/submit-button
|
||||
{:label (if @loading (tr "labels.sending") (tr "labels.send"))
|
||||
:disabled @loading}]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:h2 (tr "feedback.discussions-title")]
|
||||
[:p (tr "feedback.discussions-subtitle1")]
|
||||
[:p (tr "feedback.discussions-subtitle2")]
|
||||
|
||||
[:a.btn-secondary.btn-large
|
||||
{:href "https://github.com/penpot/penpot/discussions" :target "_blank"}
|
||||
(tr "feedback.discussions-go-to")]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:h2 "Gitter"]
|
||||
[:p (tr "feedback.chat-subtitle")]
|
||||
[:a.btn-secondary.btn-large
|
||||
{:href "https://gitter.im/penpot/community" :target "_blank"}
|
||||
(tr "feedback.chat-start")]
|
||||
|
||||
]))
|
||||
|
||||
(mf/defc feedback-page
|
||||
[]
|
||||
[:div.dashboard-settings
|
||||
[:div.form-container
|
||||
[:& options-form]]])
|
|
@ -9,31 +9,20 @@
|
|||
|
||||
(ns app.main.ui.settings.sidebar
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.refs :as refs]
|
||||
[app.config :as cfg]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.dashboard.sidebar :refer [profile-section]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[goog.functions :as f]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc sidebar-content
|
||||
[{:keys [locale profile section] :as props}]
|
||||
[{:keys [profile section] :as props}]
|
||||
(let [profile? (= section :settings-profile)
|
||||
password? (= section :settings-password)
|
||||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
|
||||
go-dashboard
|
||||
(mf/use-callback
|
||||
|
@ -45,6 +34,11 @@
|
|||
(mf/deps profile)
|
||||
(st/emitf (rt/nav :settings-profile)))
|
||||
|
||||
go-settings-feedback
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
(st/emitf (rt/nav :settings-feedback)))
|
||||
|
||||
go-settings-password
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
|
@ -59,7 +53,7 @@
|
|||
[:div.sidebar-content-section
|
||||
[:div.back-to-dashboard {:on-click go-dashboard}
|
||||
[:span.icon i/arrow-down]
|
||||
[:span.text (t locale "labels.dashboard")]]]
|
||||
[:span.text (tr "labels.dashboard")]]]
|
||||
[:hr]
|
||||
|
||||
[:div.sidebar-content-section
|
||||
|
@ -67,25 +61,30 @@
|
|||
[:li {:class (when profile? "current")
|
||||
:on-click go-settings-profile}
|
||||
i/user
|
||||
[:span.element-title (t locale "labels.profile")]]
|
||||
[:span.element-title (tr "labels.profile")]]
|
||||
|
||||
[:li {:class (when password? "current")
|
||||
:on-click go-settings-password}
|
||||
i/lock
|
||||
[:span.element-title (t locale "labels.password")]]
|
||||
[:span.element-title (tr "labels.password")]]
|
||||
|
||||
[:li {:class (when options? "current")
|
||||
:on-click go-settings-options}
|
||||
i/tree
|
||||
[:span.element-title (t locale "labels.settings")]]]]]))
|
||||
[:span.element-title (tr "labels.settings")]]
|
||||
|
||||
(when cfg/feedback-enabled
|
||||
[:li {:class (when feedback? "current")
|
||||
:on-click go-settings-feedback}
|
||||
i/msg-info
|
||||
[:span.element-title (tr "labels.give-feedback")]])]]]))
|
||||
|
||||
(mf/defc sidebar
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [profile locale section]}]
|
||||
[:div.dashboard-sidebar.settings
|
||||
[:div.sidebar-inside
|
||||
[:& sidebar-content {:locale locale
|
||||
:profile profile
|
||||
[:& sidebar-content {:profile profile
|
||||
:section section}]
|
||||
[:& profile-section {:profile profile
|
||||
:locale locale}]]])
|
||||
|
|
|
@ -20,15 +20,13 @@
|
|||
|
||||
(mf/defc linear-gradient [{:keys [id gradient shape]}]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
transform (case (:type shape)
|
||||
:path (gmt/matrix)
|
||||
(gsh/inverse-transform-matrix shape (gpt/point 0.5 0.5)))]
|
||||
transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))]
|
||||
[:linearGradient {:id id
|
||||
:x1 (:start-x gradient)
|
||||
:y1 (:start-y gradient)
|
||||
:x2 (:end-x gradient)
|
||||
:y2 (:end-y gradient)
|
||||
:gradient-transform transform}
|
||||
:gradientTransform transform}
|
||||
(for [{:keys [offset color opacity]} (:stops gradient)]
|
||||
[:stop {:key (str id "-stop-" offset)
|
||||
:offset (or offset 0)
|
||||
|
@ -37,9 +35,8 @@
|
|||
|
||||
(mf/defc radial-gradient [{:keys [id gradient shape]}]
|
||||
(let [{:keys [x y width height]} (:selrect shape)
|
||||
transform (case (:type shape)
|
||||
:path (gmt/matrix)
|
||||
(gsh/inverse-transform-matrix shape))]
|
||||
center (gsh/center-shape shape)
|
||||
transform (when (= :path (:type shape)) (gsh/transform-matrix shape))]
|
||||
(let [[x y] (if (= (:type shape) :frame) [0 0] [x y])
|
||||
translate-vec (gpt/point (+ x (* width (:start-x gradient)))
|
||||
(+ y (* height (:start-y gradient))))
|
||||
|
|
|
@ -20,28 +20,36 @@
|
|||
([props] (generate-root-styles (clj->js (obj/get props "node")) props))
|
||||
([data props]
|
||||
(let [valign (obj/get data "vertical-align" "top")
|
||||
talign (obj/get data "text-align" "flex-start")
|
||||
shape (obj/get props "shape")
|
||||
base #js {:height (or (:height shape) "100%")
|
||||
:width (or (:width shape) "100%")
|
||||
:display "flex"}]
|
||||
:width (or (:width shape) "100%")}]
|
||||
(cond-> base
|
||||
(= valign "top") (obj/set! "alignItems" "flex-start")
|
||||
(= valign "center") (obj/set! "alignItems" "center")
|
||||
(= valign "bottom") (obj/set! "alignItems" "flex-end")
|
||||
|
||||
(= talign "left") (obj/set! "justifyContent" "flex-start")
|
||||
(= talign "center") (obj/set! "justifyContent" "center")
|
||||
(= talign "right") (obj/set! "justifyContent" "flex-end")
|
||||
(= talign "justify") (obj/set! "justifyContent" "stretch")))))
|
||||
(= valign "top") (obj/set! "justifyContent" "flex-start")
|
||||
(= valign "center") (obj/set! "justifyContent" "center")
|
||||
(= valign "bottom") (obj/set! "justifyContent" "flex-end")
|
||||
))))
|
||||
|
||||
(defn generate-paragraph-set-styles
|
||||
([props] (generate-paragraph-set-styles nil props))
|
||||
([props] (generate-paragraph-set-styles (clj->js (obj/get props "node")) props))
|
||||
([data props]
|
||||
;; The position absolute is used so the paragraph is "outside"
|
||||
;; the normal layout and can grow outside its parent
|
||||
;; We use this element to measure the size of the text
|
||||
(let [base #js {:display "inline-block"}]
|
||||
;; This element will control the auto-width/auto-height size for the
|
||||
;; shape. The properties try to adjust to the shape and "overflow" if
|
||||
;; the shape is not big enough.
|
||||
;; We `inherit` the property `justify-content` so it's set by the root where
|
||||
;; the property it's known.
|
||||
;; `inline-flex` is similar to flex but `overflows` outside the bounds of the
|
||||
;; parent
|
||||
(let [shape (obj/get props "shape")
|
||||
grow-type (:grow-type shape)
|
||||
auto-width? (= grow-type :auto-width)
|
||||
auto-height? (= grow-type :auto-height)
|
||||
|
||||
base #js {:display "inline-flex"
|
||||
:flexDirection "column"
|
||||
:justifyContent "inherit"
|
||||
:minHeight (when-not (or auto-width? auto-height?) "100%")
|
||||
:minWidth (when-not auto-width? "100%")
|
||||
:verticalAlign "top"}]
|
||||
base)))
|
||||
|
||||
(defn generate-paragraph-styles
|
||||
|
|
|
@ -196,14 +196,9 @@
|
|||
|
||||
on-goback
|
||||
(mf/use-callback
|
||||
(mf/deps project-id file-id page-id anonymous?)
|
||||
(fn []
|
||||
(if anonymous?
|
||||
(st/emit! (rt/nav :login))
|
||||
(st/emit! (rt/nav :workspace
|
||||
{:project-id project-id
|
||||
:file-id file-id}
|
||||
{:page-id page-id})))))
|
||||
(mf/deps project)
|
||||
(st/emitf (dv/go-to-dashboard project)))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/deps project-id file-id page-id)
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
do-remove-group (st/emitf dw/ungroup-selected)
|
||||
do-mask-group (st/emitf dw/mask-group)
|
||||
do-unmask-group (st/emitf dw/unmask-group)
|
||||
do-flip-vertical (st/emitf (dw/flip-vertical-selected))
|
||||
do-flip-horizontal (st/emitf (dw/flip-horizontal-selected))
|
||||
do-add-component (st/emitf dwl/add-component)
|
||||
do-detach-component (st/emitf (dwl/detach-component id))
|
||||
do-reset-component (st/emitf (dwl/reset-component id))
|
||||
|
@ -133,7 +135,18 @@
|
|||
:on-click do-create-group}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.mask")
|
||||
:shortcut (sc/get-tooltip :mask)
|
||||
:on-click do-mask-group}]])
|
||||
:on-click do-mask-group}]
|
||||
[:& menu-separator]])
|
||||
|
||||
(when (>= (count selected) 1)
|
||||
[:*
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.flip-vertical")
|
||||
:shortcut (sc/get-tooltip :flip-vertical)
|
||||
:on-click do-flip-vertical}]
|
||||
[:& menu-entry {:title (t locale "workspace.shape.menu.flip-horizontal")
|
||||
:shortcut (sc/get-tooltip :flip-horizontal)
|
||||
:on-click do-flip-horizontal}]
|
||||
[:& menu-separator]])
|
||||
|
||||
(when (and (= (count selected) 1) (= (:type shape) :group))
|
||||
[:*
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[beicon.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[app.common.math :as mth]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -238,16 +239,22 @@
|
|||
gradient (mf/deref current-gradient-ref)
|
||||
editing-spot (mf/deref editing-spot-ref)
|
||||
|
||||
transform (gsh/transform-matrix shape)
|
||||
transform-inverse (gsh/inverse-transform-matrix shape)
|
||||
|
||||
{:keys [x y width height] :as sr} (:selrect shape)
|
||||
|
||||
[{start-color :color start-opacity :opacity}
|
||||
{end-color :color end-opacity :opacity}] (:stops gradient)
|
||||
|
||||
from-p (gpt/point (+ x (* width (:start-x gradient)))
|
||||
(+ y (* height (:start-y gradient))))
|
||||
from-p (-> (gpt/point (+ x (* width (:start-x gradient)))
|
||||
(+ y (* height (:start-y gradient))))
|
||||
|
||||
to-p (gpt/point (+ x (* width (:end-x gradient)))
|
||||
(+ y (* height (:end-y gradient))))
|
||||
(gpt/transform transform))
|
||||
|
||||
to-p (-> (gpt/point (+ x (* width (:end-x gradient)))
|
||||
(+ y (* height (:end-y gradient))))
|
||||
(gpt/transform transform))
|
||||
|
||||
gradient-vec (gpt/to-vec from-p to-p)
|
||||
gradient-length (gpt/length gradient-vec)
|
||||
|
@ -263,14 +270,16 @@
|
|||
(st/emit! (dc/update-gradient changes)))
|
||||
|
||||
on-change-start (fn [point]
|
||||
(let [start-x (/ (- (:x point) x) width)
|
||||
(let [point (gpt/transform point transform-inverse)
|
||||
start-x (/ (- (:x point) x) width)
|
||||
start-y (/ (- (:y point) y) height)
|
||||
start-x (mth/precision start-x 2)
|
||||
start-y (mth/precision start-y 2)]
|
||||
(change! {:start-x start-x :start-y start-y})))
|
||||
|
||||
on-change-finish (fn [point]
|
||||
(let [end-x (/ (- (:x point) x) width)
|
||||
(let [point (gpt/transform point transform-inverse)
|
||||
end-x (/ (- (:x point) x) width)
|
||||
end-y (/ (- (:y point) y) height)
|
||||
|
||||
end-x (mth/precision end-x 2)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
;; 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
|
||||
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.header
|
||||
(:require
|
||||
|
@ -227,9 +227,10 @@
|
|||
[:li {:on-click on-add-shared}
|
||||
[:span (tr "dashboard.add-shared")]])
|
||||
|
||||
[:li.feedback {:on-click #(.open js/window "https://github.com/penpot/penpot/discussions" "_blank")}
|
||||
[:span (tr "labels.feedback")]
|
||||
[:span.primary-badge "ALPHA"]]
|
||||
(when cfg/feedback-enabled
|
||||
[:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))}
|
||||
[:span (tr "labels.give-feedback")]
|
||||
[:span.primary-badge "ALPHA"]])
|
||||
]]]))
|
||||
|
||||
;; --- Header Component
|
||||
|
|
|
@ -173,13 +173,15 @@
|
|||
options (dom/get-element-by-class "element-options")
|
||||
assets (dom/get-element-by-class "assets-bar")
|
||||
cpicker (dom/get-element-by-class "colorpicker-tooltip")
|
||||
palette (dom/get-element-by-class "color-palette")
|
||||
self (mf/ref-val self-ref)
|
||||
selecting? (mf/ref-val selecting-ref)]
|
||||
|
||||
(when-not (or (and options (.contains options target))
|
||||
(and assets (.contains assets target))
|
||||
(and self (.contains self target))
|
||||
(and cpicker (.contains cpicker target)))
|
||||
(and cpicker (.contains cpicker target))
|
||||
(and palette (.contains palette target)))
|
||||
(do
|
||||
|
||||
(if selecting?
|
||||
|
|
|
@ -152,8 +152,9 @@
|
|||
(-> values
|
||||
(merge-attrs (select-keys shape attrs))
|
||||
(merge-attrs (ut/get-text-attrs-multi content attrs)))]
|
||||
:children (let [children (->> (:shapes shape []) (map #(get objects %)))]
|
||||
(get-attrs children objects attr-type))
|
||||
:children (let [children (->> (:shapes shape []) (map #(get objects %)))
|
||||
[new-ids new-values] (get-attrs children objects attr-type)]
|
||||
[(d/concat ids new-ids) (merge-attrs values new-values)])
|
||||
[])]
|
||||
result))]
|
||||
(reduce extract-attrs [[] []] shapes)))
|
||||
|
|
|
@ -285,8 +285,8 @@
|
|||
(let [ids [(:id shape)]
|
||||
type (:type shape)
|
||||
|
||||
local (deref refs/workspace-local)
|
||||
editor (get-in local [:editors (:id shape)])
|
||||
editors (mf/deref refs/editors)
|
||||
editor (get editors (:id shape))
|
||||
|
||||
measure-values (select-keys shape measure-attrs)
|
||||
|
||||
|
|
|
@ -231,6 +231,27 @@
|
|||
:shape (gsh/transform-shape shape)
|
||||
:color color}])])))
|
||||
|
||||
(mf/defc pixel-grid
|
||||
[{:keys [vbox zoom]}]
|
||||
[:g.pixel-grid
|
||||
[:defs
|
||||
[:pattern {:id "pixel-grid"
|
||||
:viewBox "0 0 1 1"
|
||||
:width 1
|
||||
:height 1
|
||||
:pattern-units "userSpaceOnUse"}
|
||||
[:path {:d "M 1 0 L 0 0 0 1"
|
||||
:style {:fill "none"
|
||||
:stroke "#59B9E2"
|
||||
:stroke-opacity "0.2"
|
||||
:stroke-width (str (/ 1 zoom))}}]]]
|
||||
[:rect {:x (:x vbox)
|
||||
:y (:y vbox)
|
||||
:width (:width vbox)
|
||||
:height (:height vbox)
|
||||
:fill (str "url(#pixel-grid)")
|
||||
:style {:pointer-events "none"}}]])
|
||||
|
||||
(mf/defc frames
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
|
@ -779,6 +800,10 @@
|
|||
(when show-grids?
|
||||
[:& frame-grid {:zoom zoom}])
|
||||
|
||||
(when (>= zoom 8)
|
||||
[:& pixel-grid {:vbox vbox
|
||||
:zoom zoom}])
|
||||
|
||||
(when show-snap-points?
|
||||
[:& snap-points {:layout layout
|
||||
:transform transform
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
|
||||
form))
|
||||
|
||||
|
||||
(defn- wrap-update-fn
|
||||
[f {:keys [spec validators]}]
|
||||
(fn [& args]
|
||||
|
|
372
frontend/tests/app/test_components_basic.cljs
Normal file
372
frontend/tests/app/test_components_basic.cljs
Normal file
|
@ -0,0 +1,372 @@
|
|||
(ns app.test-components-basic
|
||||
(:require [cljs.test :as t :include-macros true]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[clojure.stacktrace :as stk]
|
||||
[beicon.core :as rx]
|
||||
[linked.core :as lks]
|
||||
[app.test-helpers.events :as the]
|
||||
[app.test-helpers.pages :as thp]
|
||||
[app.test-helpers.libraries :as thl]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.libraries-helpers :as dwlh]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
|
||||
(t/deftest test-add-component-from-single-shape
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"}))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shape (thp/id :shape1)))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1] [c-group c-shape1] component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:parent-id shape1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name group) "Component-1"))
|
||||
(t/is (= (:name component) "Component-1"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-group) "Component-1"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-add-component-from-several-shapes
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/sample-shape :shape2 :rect
|
||||
{:name "Rect 2"}))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shapes (lks/set
|
||||
(thp/id :shape1)
|
||||
(thp/id :shape2))))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1 shape2]
|
||||
[c-group c-shape1 c-shape2]
|
||||
component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:parent-id shape1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
;; NOTE: the group name depends on having executed
|
||||
;; the previous test.
|
||||
(t/is (= (:name group) "Component-2"))
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name shape2) "Rect 2"))
|
||||
(t/is (= (:name component) "Component-2"))
|
||||
(t/is (= (:name c-group) "Component-2"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-shape2) "Rect 2"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
|
||||
(t/deftest test-add-component-from-group
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/sample-shape :shape2 :rect
|
||||
{:name "Rect 2"})
|
||||
(thp/group-shapes :group1
|
||||
[(thp/id :shape1)
|
||||
(thp/id :shape2)]))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shape (thp/id :group1)))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [[[group shape1 shape2]
|
||||
[c-group c-shape1 c-shape2]
|
||||
component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(thp/id :group1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name shape2) "Rect 2"))
|
||||
(t/is (= (:name group) "Group-3"))
|
||||
(t/is (= (:name component) "Group-3"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-shape2) "Rect 2"))
|
||||
(t/is (= (:name c-group) "Group-3"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-rename-component
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
instance1 (thp/get-shape state :instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/rename-component
|
||||
(:component-id instance1)
|
||||
"Renamed component"))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [file (dwlh/get-local-file new-state)
|
||||
component (cph/get-component
|
||||
(:component-id instance1)
|
||||
(:component-file instance1)
|
||||
file
|
||||
{})]
|
||||
|
||||
(t/is (= (:name component)
|
||||
"Renamed component")))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-duplicate-component
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
component-id (:component-id instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/duplicate-component
|
||||
{:id component-id}))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [new-component-id (->> (get-in new-state
|
||||
[:workspace-data
|
||||
:components])
|
||||
(keys)
|
||||
(filter #(not= % component-id))
|
||||
(first))
|
||||
|
||||
[[instance1 shape1]
|
||||
[c-instance1 c-shape1]
|
||||
component1]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:id instance1))
|
||||
|
||||
[[c-component2 c-shape2]
|
||||
component2]
|
||||
(thl/resolve-component
|
||||
new-state
|
||||
new-component-id)]
|
||||
|
||||
(t/is (= (:name component2)
|
||||
"Component-6")))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-delete-component
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
component-id (:component-id instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/delete-component
|
||||
{:id component-id}))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [[instance1 shape1]
|
||||
(thl/resolve-instance
|
||||
new-state
|
||||
(:id instance1))
|
||||
|
||||
file (dwlh/get-local-file new-state)
|
||||
component (cph/get-component
|
||||
(:component-id instance1)
|
||||
(:component-file instance1)
|
||||
file
|
||||
{})]
|
||||
|
||||
(t/is (nil? component)))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-instantiate-component
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
file (dwlh/get-local-file state)
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
component-id (:component-id instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/instantiate-component
|
||||
(:id file)
|
||||
(:component-id instance1)
|
||||
(gpt/point 100 100)))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [new-instance-id (-> (get-in new-state
|
||||
[:workspace-local :selected])
|
||||
first)
|
||||
|
||||
[[instance2 shape2]
|
||||
[c-instance2 c-shape2]
|
||||
component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
new-instance-id)]
|
||||
|
||||
(t/is (not= (:id instance1) (:id instance2)))
|
||||
(t/is (= (:id component) component-id))
|
||||
(t/is (= (:name instance2) "Component-8"))
|
||||
(t/is (= (:name shape2) "Rect 1"))
|
||||
(t/is (= (:name c-instance2) "Component-7"))
|
||||
(t/is (= (:name c-shape2) "Rect 1")))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-detach-component
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
component-id (:component-id instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/detach-component
|
||||
(:id instance1)))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [[instance1 shape1]
|
||||
(thl/resolve-noninstance
|
||||
new-state
|
||||
(:id instance1))]
|
||||
|
||||
(t/is (= (:name "Rect 1"))))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
123
frontend/tests/app/test_components_sync.cljs
Normal file
123
frontend/tests/app/test_components_sync.cljs
Normal file
|
@ -0,0 +1,123 @@
|
|||
(ns app.test-components-sync
|
||||
(:require [cljs.test :as t :include-macros true]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[clojure.stacktrace :as stk]
|
||||
[beicon.core :as rx]
|
||||
[linked.core :as lks]
|
||||
[app.test-helpers.events :as the]
|
||||
[app.test-helpers.pages :as thp]
|
||||
[app.test-helpers.libraries :as thl]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.libraries-helpers :as dwlh]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
|
||||
(t/deftest test-touched
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"
|
||||
:fill-color "#ffffff"
|
||||
:fill-opacity 1})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
shape1 (thp/get-shape state :shape1)
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
|
||||
update-shape (fn [shape]
|
||||
(merge shape {:fill-color "#fabada"
|
||||
:fill-opacity 0.5}))]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwc/update-shapes [(:id shape1)]
|
||||
update-shape))
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1] [c-group c-shape1] component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:id instance1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:fill-color shape1) "#fabada"))
|
||||
(t/is (= (:fill-opacity shape1) 0.5))
|
||||
(t/is (= (:touched shape1) #{:fill-group}))
|
||||
(t/is (= (:fill-color c-shape1) "#ffffff"))
|
||||
(t/is (= (:fill-opacity c-shape1) 1))
|
||||
(t/is (= (:touched c-shape1) nil)))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
||||
(t/deftest test-reset-changes
|
||||
(t/async done
|
||||
(try
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"
|
||||
:fill-color "#ffffff"
|
||||
:fill-opacity 1})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
shape1 (thp/get-shape state :shape1)
|
||||
instance1 (thp/get-shape state :instance1)
|
||||
|
||||
update-shape (fn [shape]
|
||||
(merge shape {:fill-color "#fabada"
|
||||
:fill-opacity 0.5}))]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwc/update-shapes [(:id shape1)]
|
||||
update-shape))
|
||||
|
||||
(rx/mapcat #(the/do-watch-update
|
||||
(dwl/reset-component (:id instance1)) %))
|
||||
|
||||
(rx/do
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1] [c-group c-shape1] component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:id instance1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:fill-color shape1) "#ffffff"))
|
||||
(t/is (= (:fill-opacity shape1) 1))
|
||||
(t/is (= (:touched shape1) nil))
|
||||
(t/is (= (:fill-color c-shape1) "#ffffff"))
|
||||
(t/is (= (:fill-opacity c-shape1) 1))
|
||||
(t/is (= (:touched c-shape1) nil)))))
|
||||
|
||||
(rx/subs
|
||||
done
|
||||
#(do
|
||||
(println (.-stack %))
|
||||
(done)))))
|
||||
|
||||
(catch :default e
|
||||
(println (.-stack e))
|
||||
(done)))))
|
||||
|
|
@ -52,12 +52,39 @@
|
|||
(t/is (= (:component-file shape)
|
||||
(:id file))))
|
||||
|
||||
(defn resolve-instance
|
||||
[state root-inst-id]
|
||||
(let [page (thp/current-page state)
|
||||
root-inst (cph/get-shape page root-inst-id)
|
||||
shapes-inst (cph/get-object-with-children
|
||||
root-inst-id
|
||||
(:objects page))]
|
||||
|
||||
;; Validate that the instance tree is well constructed
|
||||
(t/is (is-instance-root (first shapes-inst)))
|
||||
(run! is-instance-child (rest shapes-inst))
|
||||
|
||||
shapes-inst))
|
||||
|
||||
(defn resolve-noninstance
|
||||
[state root-inst-id]
|
||||
(let [page (thp/current-page state)
|
||||
root-inst (cph/get-shape page root-inst-id)
|
||||
shapes-inst (cph/get-object-with-children
|
||||
root-inst-id
|
||||
(:objects page))]
|
||||
|
||||
;; Validate that the tree is not an instance
|
||||
(run! is-noninstance shapes-inst)
|
||||
|
||||
shapes-inst))
|
||||
|
||||
(defn resolve-instance-and-master
|
||||
[state root-inst-id]
|
||||
(let [page (thp/current-page state)
|
||||
root-inst (cph/get-shape page root-inst-id)
|
||||
|
||||
file (dwlh/get-local-file state)
|
||||
file (dwlh/get-local-file state)
|
||||
component (cph/get-component
|
||||
(:component-id root-inst)
|
||||
(:id file)
|
||||
|
@ -88,3 +115,25 @@
|
|||
|
||||
[shapes-inst shapes-master component]))
|
||||
|
||||
(defn resolve-component
|
||||
[state component-id]
|
||||
(let [page (thp/current-page state)
|
||||
|
||||
file (dwlh/get-local-file state)
|
||||
component (cph/get-component
|
||||
component-id
|
||||
(:id file)
|
||||
file
|
||||
nil)
|
||||
|
||||
root-master (cph/get-component-root
|
||||
component)
|
||||
shapes-master (cph/get-object-with-children
|
||||
(:id root-master)
|
||||
(:objects component))]
|
||||
|
||||
;; Validate that the component tree is well constructed
|
||||
(run! is-noninstance shapes-master)
|
||||
|
||||
[shapes-master component]))
|
||||
|
||||
|
|
|
@ -1,206 +0,0 @@
|
|||
(ns app.test-library-sync
|
||||
(:require [cljs.test :as t :include-macros true]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[beicon.core :as rx]
|
||||
[linked.core :as lks]
|
||||
[app.test-helpers.events :as the]
|
||||
[app.test-helpers.pages :as thp]
|
||||
[app.test-helpers.libraries :as thl]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.libraries-helpers :as dwlh]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
|
||||
(t/deftest test-create-page
|
||||
(t/testing "create page"
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page))
|
||||
page (thp/current-page state)]
|
||||
(t/is (= (:name page) "page1")))))
|
||||
|
||||
(t/deftest test-create-shape
|
||||
(t/testing "create shape"
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"}))
|
||||
shape (thp/get-shape state :shape1)]
|
||||
(t/is (= (:name shape) "Rect 1")))))
|
||||
|
||||
(t/deftest synctest
|
||||
(t/testing "synctest"
|
||||
(let [state {:workspace-local {:color-for-rename "something"}}
|
||||
new-state (->> state
|
||||
(the/do-update
|
||||
dwl/clear-color-for-rename))]
|
||||
(t/is (= (get-in new-state [:workspace-local :color-for-rename])
|
||||
nil)))))
|
||||
|
||||
(t/deftest asynctest
|
||||
(t/testing "asynctest"
|
||||
(t/async done
|
||||
(let [state {}
|
||||
color {:color "#ffffff"}]
|
||||
(->> state
|
||||
(the/do-watch-update
|
||||
(dwl/add-recent-color color))
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(t/is (= (get-in new-state [:workspace-file
|
||||
:data
|
||||
:recent-colors])
|
||||
[color]))
|
||||
(t/is (= (get-in new-state [:workspace-data
|
||||
:recent-colors])
|
||||
[color]))))
|
||||
(rx/subs done))))))
|
||||
|
||||
(t/deftest test-add-component-from-single-shape
|
||||
(t/testing "Add a component from a single shape"
|
||||
(t/async done
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"}))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shape (thp/id :shape1)))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1] [c-group c-shape1] component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:parent-id shape1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name group) "Component-1"))
|
||||
(t/is (= (:name component) "Component-1"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-group) "Component-1"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs done))))))
|
||||
|
||||
(t/deftest test-add-component-from-several-shapes
|
||||
(t/testing "Add a component from several shapes"
|
||||
(t/async done
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/sample-shape :shape2 :rect
|
||||
{:name "Rect 2"}))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shapes (lks/set
|
||||
(thp/id :shape1)
|
||||
(thp/id :shape2))))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(let [shape1 (thp/get-shape new-state :shape1)
|
||||
|
||||
[[group shape1 shape2]
|
||||
[c-group c-shape1 c-shape2]
|
||||
component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(:parent-id shape1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
;; NOTE: the group name depends on having executed
|
||||
;; the previous test.
|
||||
(t/is (= (:name group) "Component-2"))
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name shape2) "Rect 2"))
|
||||
(t/is (= (:name component) "Component-2"))
|
||||
(t/is (= (:name c-group) "Component-2"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-shape2) "Rect 2"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs done))))))
|
||||
|
||||
(t/deftest test-add-component-from-group
|
||||
(t/testing "Add a component from a group"
|
||||
(t/async done
|
||||
(let [
|
||||
state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/sample-shape :shape2 :rect
|
||||
{:name "Rect 2"})
|
||||
(thp/group-shapes :group1
|
||||
[(thp/id :shape1)
|
||||
(thp/id :shape2)]))]
|
||||
|
||||
(->> state
|
||||
(the/do-update (dw/select-shape (thp/id :group1)))
|
||||
(the/do-watch-update dwl/add-component)
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(let [[[group shape1 shape2]
|
||||
[c-group c-shape1 c-shape2]
|
||||
component]
|
||||
(thl/resolve-instance-and-master
|
||||
new-state
|
||||
(thp/id :group1))
|
||||
|
||||
file (dwlh/get-local-file new-state)]
|
||||
|
||||
(t/is (= (:name shape1) "Rect 1"))
|
||||
(t/is (= (:name shape2) "Rect 2"))
|
||||
(t/is (= (:name group) "Group-3"))
|
||||
(t/is (= (:name component) "Group-3"))
|
||||
(t/is (= (:name c-shape1) "Rect 1"))
|
||||
(t/is (= (:name c-shape2) "Rect 2"))
|
||||
(t/is (= (:name c-group) "Group-3"))
|
||||
|
||||
(thl/is-from-file group file))))
|
||||
|
||||
(rx/subs done))))))
|
||||
|
||||
(t/deftest test-rename-component
|
||||
(t/testing "Rename a component"
|
||||
(t/async done
|
||||
(let [
|
||||
state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"})
|
||||
(thp/make-component :instance1
|
||||
[(thp/id :shape1)]))
|
||||
|
||||
instance1 (thp/get-shape state :instance1)]
|
||||
|
||||
(->> state
|
||||
(the/do-watch-update (dwl/rename-component
|
||||
(:component-id instance1)
|
||||
"Renamed component"))
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(let [file (dwlh/get-local-file new-state)
|
||||
component (cph/get-component
|
||||
(:component-id instance1)
|
||||
(:component-file instance1)
|
||||
file
|
||||
{})]
|
||||
|
||||
(t/is (= (:name component)
|
||||
"Renamed component")))))
|
||||
|
||||
(rx/subs done))))))
|
||||
|
61
frontend/tests/app/test_shapes.cljs
Normal file
61
frontend/tests/app/test_shapes.cljs
Normal file
|
@ -0,0 +1,61 @@
|
|||
(ns app.test-shapes
|
||||
(:require [cljs.test :as t :include-macros true]
|
||||
[cljs.pprint :refer [pprint]]
|
||||
[clojure.stacktrace :as stk]
|
||||
[beicon.core :as rx]
|
||||
[linked.core :as lks]
|
||||
[app.test-helpers.events :as the]
|
||||
[app.test-helpers.pages :as thp]
|
||||
[app.test-helpers.libraries :as thl]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.main.data.workspace.libraries :as dwl]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
|
||||
(t/deftest test-create-page
|
||||
(t/testing "create page"
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page))
|
||||
page (thp/current-page state)]
|
||||
(t/is (= (:name page) "page1")))))
|
||||
|
||||
(t/deftest test-create-shape
|
||||
(t/testing "create shape"
|
||||
(let [state (-> thp/initial-state
|
||||
(thp/sample-page)
|
||||
(thp/sample-shape :shape1 :rect
|
||||
{:name "Rect 1"}))
|
||||
shape (thp/get-shape state :shape1)]
|
||||
(t/is (= (:name shape) "Rect 1")))))
|
||||
|
||||
(t/deftest synctest
|
||||
(t/testing "synctest"
|
||||
(let [state {:workspace-local {:color-for-rename "something"}}
|
||||
new-state (->> state
|
||||
(the/do-update
|
||||
dwl/clear-color-for-rename))]
|
||||
(t/is (= (get-in new-state [:workspace-local :color-for-rename])
|
||||
nil)))))
|
||||
|
||||
(t/deftest asynctest
|
||||
(t/testing "asynctest"
|
||||
(t/async done
|
||||
(let [state {}
|
||||
color {:color "#ffffff"}]
|
||||
(->> state
|
||||
(the/do-watch-update
|
||||
(dwl/add-recent-color color))
|
||||
(rx/map
|
||||
(fn [new-state]
|
||||
(t/is (= (get-in new-state [:workspace-file
|
||||
:data
|
||||
:recent-colors])
|
||||
[color]))
|
||||
(t/is (= (get-in new-state [:workspace-data
|
||||
:recent-colors])
|
||||
[color]))))
|
||||
(rx/subs done done))))))
|
||||
|
Loading…
Add table
Reference in a new issue