diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index 94fce53eb..becc05752 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -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"]}
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 000000000..a9ef44e59
--- /dev/null
+++ b/CHANGES.md
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 55acb5a3c..fc3d2b7fd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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:
diff --git a/README.md b/README.md
index 4c124d278..98d688935 100644
--- a/README.md
+++ b/README.md
@@ -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 #
diff --git a/backend/deps.edn b/backend/deps.edn
index 4c67fbfa7..c178b10a8 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -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"}
diff --git a/backend/dev/user.clj b/backend/dev/user.clj
index 17a5d360c..6e66cd6c1 100644
--- a/backend/dev/user.clj
+++ b/backend/dev/user.clj
@@ -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]
diff --git a/backend/resources/emails-mjml/change-email/en.mjml b/backend/resources/emails-mjml/change-email/en.mjml
index ae388201a..e3f83ddce 100644
--- a/backend/resources/emails-mjml/change-email/en.mjml
+++ b/backend/resources/emails-mjml/change-email/en.mjml
@@ -30,14 +30,14 @@
for security reasons.
Enjoy!
- The UXBOX team.
+ The Penpot team.
- 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.
@@ -57,7 +57,7 @@
- UXBOX © 2020 | Made with <3 and Open Source
+ Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails-mjml/invite-to-team/en.mjml b/backend/resources/emails-mjml/invite-to-team/en.mjml
index 48af2706a..257cacdb9 100644
--- a/backend/resources/emails-mjml/invite-to-team/en.mjml
+++ b/backend/resources/emails-mjml/invite-to-team/en.mjml
@@ -23,14 +23,14 @@
Accept invite
Enjoy!
- The UXBOX team.
+ The Penpot team.
- 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.
@@ -50,7 +50,7 @@
- UXBOX © 2020 | Made with <3 and Open Source
+ Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails-mjml/password-recovery/en.mjml b/backend/resources/emails-mjml/password-recovery/en.mjml
index 36f323dc1..3957b319f 100644
--- a/backend/resources/emails-mjml/password-recovery/en.mjml
+++ b/backend/resources/emails-mjml/password-recovery/en.mjml
@@ -32,14 +32,14 @@
it. Your password won't be changed.
Enjoy!
- The UXBOX team.
+ The Penpot team.
- 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.
@@ -59,7 +59,7 @@
- UXBOX © 2020 | Made with <3 and Open Source
+ Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails-mjml/register/en.mjml b/backend/resources/emails-mjml/register/en.mjml
index d3a12f256..177e1e8ab 100644
--- a/backend/resources/emails-mjml/register/en.mjml
+++ b/backend/resources/emails-mjml/register/en.mjml
@@ -21,7 +21,7 @@
Hello {{name}}!
- 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!
@@ -29,14 +29,14 @@
Verify email
Enjoy!
- The UXBOX team.
+ The Penpot team.
- 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.
@@ -56,7 +56,7 @@
- UXBOX © 2020 | Made with <3 and Open Source
+ Penpot © 2020 | Made with <3 and Open Source
diff --git a/backend/resources/emails/change-email/en.txt b/backend/resources/emails/change-email/en.txt
index f4232aac4..0a688cb0d 100644
--- a/backend/resources/emails/change-email/en.txt
+++ b/backend/resources/emails/change-email/en.txt
@@ -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.
diff --git a/backend/resources/emails/feedback/en.subj b/backend/resources/emails/feedback/en.subj
new file mode 100644
index 000000000..2ecd8c0c4
--- /dev/null
+++ b/backend/resources/emails/feedback/en.subj
@@ -0,0 +1 @@
+[FEEDBACK]: From {{ profile.email }}
diff --git a/backend/resources/emails/feedback/en.txt b/backend/resources/emails/feedback/en.txt
new file mode 100644
index 000000000..f6e602a19
--- /dev/null
+++ b/backend/resources/emails/feedback/en.txt
@@ -0,0 +1,7 @@
+Feedback from: {{profile.fullname}} <{{profile.email}}>
+
+Profile ID: {{profile.id}}
+
+Subject: {{subject}}
+
+{{content}}
diff --git a/backend/resources/emails/invite-to-team/en.txt b/backend/resources/emails/invite-to-team/en.txt
index c999019cc..ea85c084f 100644
--- a/backend/resources/emails/invite-to-team/en.txt
+++ b/backend/resources/emails/invite-to-team/en.txt
@@ -7,4 +7,4 @@ Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
-The UXBOX team.
+The Penpot team.
diff --git a/backend/resources/emails/password-recovery/en.txt b/backend/resources/emails/password-recovery/en.txt
index cbd00e35b..ad314b41d 100644
--- a/backend/resources/emails/password-recovery/en.txt
+++ b/backend/resources/emails/password-recovery/en.txt
@@ -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.
diff --git a/backend/resources/emails/register/en.txt b/backend/resources/emails/register/en.txt
index 70efbfe17..84ca0d487 100644
--- a/backend/resources/emails/register/en.txt
+++ b/backend/resources/emails/register/en.txt
@@ -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.
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index 6910d5a45..12022af73 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -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}))
diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj
index d6408b8e4..68441d821 100644
--- a/backend/src/app/emails.clj
+++ b/backend/src/app/emails.clj
@@ -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]))
diff --git a/backend/src/app/http/auth/google.clj b/backend/src/app/http/auth/google.clj
index aaa4c1a2b..a615ddd80 100644
--- a/backend/src/app/http/auth/google.clj
+++ b/backend/src/app/http/auth/google.clj
@@ -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)
diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj
index b775c4852..14a4191e0 100644
--- a/backend/src/app/media.clj
+++ b/backend/src/app/media.clj
@@ -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
diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj
index 9c20d6db6..a7e866068 100644
--- a/backend/src/app/metrics.clj
+++ b/backend/src/app/metrics.clj
@@ -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)
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 74e191701..2d247e11c 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -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")}
])
diff --git a/backend/src/app/migrations/sql/0045-add-index-to-file-change-table.sql b/backend/src/app/migrations/sql/0045-add-index-to-file-change-table.sql
new file mode 100644
index 000000000..1537bcd4c
--- /dev/null
+++ b/backend/src/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);
diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj
index b99987a0b..28f9e2f9e 100644
--- a/backend/src/app/rpc.clj
+++ b/backend/src/app/rpc.clj
@@ -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 {}))))
diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj
index 0461ef13f..c5959f91d 100644
--- a/backend/src/app/rpc/mutations/demo.clj
+++ b/backend/src/app/rpc/mutations/demo.clj
@@ -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
diff --git a/backend/src/app/rpc/mutations/feedback.clj b/backend/src/app/rpc/mutations/feedback.clj
new file mode 100644
index 000000000..875a93985
--- /dev/null
+++ b/backend/src/app/rpc/mutations/feedback.clj
@@ -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)))
diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj
index 0ccc65c05..9d04afeb2 100644
--- a/backend/src/app/rpc/mutations/files.clj
+++ b/backend/src/app/rpc/mutations/files.clj
@@ -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)))
diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj
index 0cc6b967e..e9d4d9bab 100644
--- a/backend/src/app/rpc/mutations/media.clj
+++ b/backend/src/app/rpc/mutations/media.clj
@@ -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)]
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index d897b5ac6..8ee5d7e8f 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -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
diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj
index bb2c4772d..de92bb054 100644
--- a/backend/src/app/rpc/mutations/projects.clj
+++ b/backend/src/app/rpc/mutations/projects.clj
@@ -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))
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index 46d3785b9..5abbb4152 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -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)))
diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj
index e1af35591..8f2a7d0f4 100644
--- a/backend/src/app/rpc/mutations/verify_token.clj
+++ b/backend/src/app/rpc/mutations/verify_token.clj
@@ -7,8 +7,6 @@
;;
;; Copyright (c) 2020 UXBOX Labs SL
-;; TODO: session
-
(ns app.rpc.mutations.verify-token
(:require
[app.common.exceptions :as ex]
diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj
index e28968d94..c626a6def 100644
--- a/backend/src/app/srepl/main.clj
+++ b/backend/src/app/srepl/main.clj
@@ -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})))))
diff --git a/backend/src/app/svgparse.clj b/backend/src/app/svgparse.clj
index f7a7c0ce7..f9d658663 100644
--- a/backend/src/app/svgparse.clj
+++ b/backend/src/app/svgparse.clj
@@ -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)))
+
diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj
index 4d31ff4f9..a65a75b1e 100644
--- a/backend/src/app/tasks/delete_object.clj
+++ b/backend/src/app/tasks/delete_object.clj
@@ -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])))
diff --git a/backend/src/app/util/blob.clj b/backend/src/app/util/blob.clj
index 7761ff7e5..332aedaeb 100644
--- a/backend/src/app/util/blob.clj
+++ b/backend/src/app/util/blob.clj
@@ -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)))
diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj
index b5e744f50..813cf4367 100644
--- a/backend/src/app/util/emails.clj
+++ b/backend/src/app/util/emails.clj
@@ -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
diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/backend/tests/app/tests/test_common_geom_shapes.clj
index 3048a8903..860f05ea4 100644
--- a/backend/tests/app/tests/test_common_geom_shapes.clj
+++ b/backend/tests/app/tests/test_common_geom_shapes.clj
@@ -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))
diff --git a/backend/tests/app/tests/test_common_pages.clj b/backend/tests/app/tests/test_common_pages.clj
index b8a220d48..f6b84cc67 100644
--- a/backend/tests/app/tests/test_common_pages.clj
+++ b/backend/tests/app/tests/test_common_pages.clj
@@ -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])))
))
diff --git a/common/app/common/geom/matrix.cljc b/common/app/common/geom/matrix.cljc
index e071e32f3..69921998a 100644
--- a/common/app/common/geom/matrix.cljc
+++ b/common/app/common/geom/matrix.cljc
@@ -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))))
diff --git a/common/app/common/geom/proportions.cljc b/common/app/common/geom/proportions.cljc
index 8fe1bf763..15e6bfffd 100644
--- a/common/app/common/geom/proportions.cljc
+++ b/common/app/common/geom/proportions.cljc
@@ -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)))
diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc
index e688a16eb..09f021d1e 100644
--- a/common/app/common/geom/shapes/transforms.cljc
+++ b/common/app/common/geom/shapes/transforms.cljc
@@ -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)))))
diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc
index e9ed34a29..ecb1c2c6f 100644
--- a/common/app/common/math.cljc
+++ b/common/app/common/math.cljc
@@ -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))
diff --git a/common/app/common/pages/changes.cljc b/common/app/common/pages/changes.cljc
index 681c40a26..3ff82791e 100644
--- a/common/app/common/pages/changes.cljc
+++ b/common/app/common/pages/changes.cljc
@@ -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))
diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc
index 778968bf6..ca025949f 100644
--- a/common/app/common/pages/helpers.cljc
+++ b/common/app/common/pages/helpers.cljc
@@ -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))))
diff --git a/docker/images/files/config.js b/docker/images/files/config.js
new file mode 100644
index 000000000..621108073
--- /dev/null
+++ b/docker/images/files/config.js
@@ -0,0 +1,9 @@
+// Frontend configuration
+
+//var penpotPublicURI = "https://penpot.example.com";
+//var penpotDemoWarning = ;
+//var penpotAllowDemoUsers = ;
+//var penpotGoogleClientID = "";
+//var penpotGitlabClientID = "";
+//var penpotGithubClientID = "";
+//var penpotLoginWithLDAP = ;
diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh
index e2fce2c44..6c0a428f5 100644
--- a/docker/images/files/nginx-entrypoint.sh
+++ b/docker/images/files/nginx-entrypoint.sh
@@ -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 "$@";
diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md
index d79bb4a50..9245ca5c9 100644
--- a/docs/00-Getting-Started.md
+++ b/docs/00-Getting-Started.md
@@ -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).
diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json
index abb75a4a6..46f51b44e 100644
--- a/frontend/resources/locales.json
+++ b/frontend/resources/locales.json
@@ -37,16 +37,17 @@
},
"auth.verification-email-sent": {
"translations": {
- "en": "We've sent a verification email to"
+ "en": "We've sent a verification email to",
+ "fr": "Nous avons envoyé un e-mail de vérification à"
}
},
"auth.check-your-email": {
"translations": {
- "en": "Check your email and click on the link to verify and start using Penpot."
+ "en": "Check your email and click on the link to verify and start using Penpot.",
+ "fr": "Vérifiez votre email et cliquez sur le lien pour vérifier et commencer à utiliser Penpot."
}
},
-
"auth.demo-warning" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:33" ],
"translations" : {
@@ -176,6 +177,7 @@
"auth.notifications.profile-not-verified": {
"translations": {
"en": "Profile is not verified, please verify profile before continue.",
+ "fr": "Le profil n'est pas vérifié, veuillez vérifier le profil avant de continuer.",
"es": "El perfil aun no ha sido verificado, por favor valida el perfil antes de continuar."
}
},
@@ -210,6 +212,7 @@
"used-in" : [ "src/app/main/ui/auth/verify_token.cljs:55", "src/app/main/ui/auth/register.cljs:50" ],
"translations" : {
"en" : "Joined the team succesfully",
+ "fr" : "Équipe rejoint avec succès",
"es" : "Te uniste al equipo"
}
},
@@ -316,7 +319,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:228", "src/app/main/ui/dashboard/grid.cljs:182" ],
"translations" : {
"en" : "Add as Shared Library",
- "fr" : "",
+ "fr" : "Ajouter une Bibliothèque Partagée",
"ru" : "",
"es" : "Añadir como Biblioteca Compartida"
}
@@ -334,6 +337,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:171" ],
"translations" : {
"en" : "+ Create new team",
+ "fr" : "+ Créer nouvelle équipe",
"es" : "+ Crear nuevo equipo"
}
},
@@ -341,6 +345,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:341" ],
"translations" : {
"en" : "Your Penpot",
+ "fr" : "Votre Penpot",
"es" : "Tu Penpot"
}
},
@@ -348,6 +353,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:325" ],
"translations" : {
"en" : "Delete team",
+ "fr" : "Supprimer l'équipe",
"es" : "Eliminar equipo"
}
},
@@ -373,6 +379,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:72" ],
"translations" : {
"en" : "Invite to team",
+ "fr" : "Inviter à l'équipe",
"es" : "Invitar al equipo"
}
},
@@ -380,6 +387,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:318", "src/app/main/ui/dashboard/sidebar.cljs:321" ],
"translations" : {
"en" : "Leave team",
+ "fr" : "Quitter l'équipe",
"es" : "Abandonar equipo"
}
},
@@ -387,7 +395,7 @@
"used-in" : [ "src/app/main/ui/dashboard/libraries.cljs:40" ],
"translations" : {
"en" : "Shared Libraries",
- "fr" : "",
+ "fr" : "Bibliothèques Partagées",
"ru" : "",
"es" : "Bibliotecas Compartidas"
}
@@ -422,7 +430,7 @@
"dashboard.library.add-library.icons" : {
"translations" : {
"en" : "+ New icon library",
- "fr" : "+ Nouvelle librairie d'icônes",
+ "fr" : "+ Nouvelle bibliothèque d'icônes",
"ru" : "+ Новая библиотека иконок",
"es" : "+ Nueva biblioteca de iconos"
},
@@ -431,7 +439,7 @@
"dashboard.library.add-library.images" : {
"translations" : {
"en" : "+ New image library",
- "fr" : "+ Nouvelle librairie d'image",
+ "fr" : "+ Nouvelle bibliothèque d'image",
"ru" : "+ Новая библиотека изображений",
"es" : "+ Nueva biblioteca de imágenes"
},
@@ -477,6 +485,7 @@
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:194" ],
"translations" : {
"en" : "loading your files ...",
+ "fr" : "chargement de vos fichiers ...",
"es" : "cargando tus ficheros ..."
}
},
@@ -511,6 +520,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:436" ],
"translations" : {
"en" : "Pinned projects will appear here",
+ "fr" : "Les projets épinglés apparaîtront ici",
"es" : "Los proyectos fijados aparecerán aquí"
}
},
@@ -545,6 +555,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:305" ],
"translations" : {
"en" : "%s members",
+ "fr" : "%s membres",
"es" : "%s integrantes"
}
},
@@ -570,6 +581,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:202" ],
"translations" : {
"en" : "Promote to owner",
+ "fr" : "Promouvoir en propriétaire",
"es" : "Promover a dueño"
}
},
@@ -586,7 +598,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:226", "src/app/main/ui/dashboard/grid.cljs:181" ],
"translations" : {
"en" : "Remove as Shared Library",
- "fr" : "",
+ "fr" : "Retirer en tant que Bibliothèque Partagée",
"ru" : "",
"es" : "Eliminar como Biblioteca Compartida"
}
@@ -631,6 +643,7 @@
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:262" ],
"translations" : {
"en" : "Show all files",
+ "fr" : "Voir tous les fichiers",
"es" : "Ver todos los ficheros"
}
},
@@ -647,6 +660,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:156" ],
"translations" : {
"en" : "Switch team",
+ "fr" : "Changer d'équipe",
"es" : "Cambiar equipo"
}
},
@@ -654,6 +668,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:288" ],
"translations" : {
"en" : "Team info",
+ "fr" : "Information de l'équipe",
"es" : "Información del equipo"
}
},
@@ -661,6 +676,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:299" ],
"translations" : {
"en" : "Team members",
+ "fr" : "Membres de l'équipe",
"es" : "Integrantes del equipo"
}
},
@@ -668,6 +684,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:308" ],
"translations" : {
"en" : "Team projects",
+ "fr" : "Projets de l'équipe",
"es" : "Proyectos del equipo"
}
},
@@ -711,6 +728,7 @@
"used-in" : [ "src/app/main/ui/settings.cljs:29" ],
"translations" : {
"en" : "Your account",
+ "fr" : "Votre compte",
"es" : "Su cuenta"
}
},
@@ -736,6 +754,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:160" ],
"translations" : {
"en" : "Your Penpot",
+ "fr" : "Votre Penpot",
"es" : "Tu Penpot"
}
},
@@ -769,7 +788,7 @@
"ds.button.save" : {
"translations" : {
"en" : "Save",
- "fr" : "Sauvegarder",
+ "fr" : "Enregistrer",
"ru" : "Сохранить",
"es" : "Guardar"
},
@@ -811,6 +830,12 @@
"es" : "Actualizado: %s"
}
},
+ "errors.google-auth-not-enabled" : {
+ "translations" : {
+ "en" : "Authentication with google disabled on backend",
+ "es" : "Autenticación con google esta dehabilitada en el servidor"
+ }
+ },
"errors.auth.unauthorized" : {
"used-in" : [ "src/app/main/ui/auth/login.cljs:89" ],
"translations" : {
@@ -824,7 +849,7 @@
"used-in" : [ "src/app/main/data/workspace.cljs:1394" ],
"translations" : {
"en" : "Your browser cannot do this operation",
- "fr" : "",
+ "fr" : "Votre navigateur ne peut pas effectuer cette opération",
"ru" : "",
"es" : "Tu navegador no puede realizar esta operación"
}
@@ -887,7 +912,7 @@
"used-in" : [ "src/app/main/data/media.cljs:78", "src/app/main/data/workspace/persistence.cljs:426" ],
"translations" : {
"en" : "Seems that the contents of the image does not match the file extension.",
- "fr" : "",
+ "fr" : "Il semble que le contenu de l'image ne correspond pas à l'extension de fichier.",
"ru" : "",
"es" : "Parece que el contenido de la imagen no coincide con la etensión del archivo."
}
@@ -896,7 +921,7 @@
"used-in" : [ "src/app/main/data/media.cljs:75", "src/app/main/data/workspace/persistence.cljs:423" ],
"translations" : {
"en" : "Seems that this is not a valid image.",
- "fr" : "",
+ "fr" : "Il semble que ce n'est pas une image valide.",
"ru" : "",
"es" : "Parece que no es una imagen válida."
}
@@ -950,16 +975,88 @@
"used-in" : [ "src/app/main/ui/settings/password.cljs:28" ],
"translations" : {
"en" : "Old password is incorrect",
- "fr" : "l'ancien mot de passe est incorrect",
+ "fr" : "L'ancien mot de passe est incorrect",
"ru" : "Старый пароль неверный",
"es" : "La contraseña anterior no es correcta"
}
},
+ "feedback.chat-start" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:112" ],
+ "translations" : {
+ "en" : "Join the chat",
+ "es" : "Unirse al chat"
+ }
+ },
+ "feedback.chat-subtitle" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:109" ],
+ "translations" : {
+ "en" : "Feeling like talking? Chat with us at Gitter",
+ "es" : "¿Deseas conversar? Entra al nuestro chat de la comunidad en Gitter"
+ }
+ },
+ "feedback.description" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:88" ],
+ "translations" : {
+ "en" : "Description",
+ "es" : "Descripción"
+ }
+ },
+ "feedback.discussions-go-to" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:104" ],
+ "translations" : {
+ "en" : "Go to discussions",
+ "es" : "Ir a las discussiones"
+ }
+ },
+ "feedback.discussions-subtitle1" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:99" ],
+ "translations" : {
+ "en" : "Join Penpot team collaborative communication forum.",
+ "es" : "Entra al foro colaborativo de Penpot"
+ }
+ },
+ "feedback.discussions-subtitle2" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:100" ],
+ "translations" : {
+ "en" : "You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.",
+ "es" : ""
+ }
+ },
+ "feedback.discussions-title" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:98" ],
+ "translations" : {
+ "en" : "Team discussions",
+ "es" : ""
+ }
+ },
+ "feedback.subject" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:84" ],
+ "translations" : {
+ "en" : "Subject",
+ "es" : "Asunto"
+ }
+ },
+ "feedback.subtitle" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:81" ],
+ "translations" : {
+ "en" : "Please describe the reason of your email, specifying if is an issue, an idea or a doubt. A member of our team will respond as soon as possible.",
+ "es" : ""
+ }
+ },
+ "feedback.title" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:80" ],
+ "translations" : {
+ "en" : "Email",
+ "fr" : "Adresse email",
+ "ru" : "Email",
+ "es" : "Correo electrónico"
+ }
+ },
"generic.error" : {
"used-in" : [ "src/app/main/ui/settings/password.cljs:31" ],
"translations" : {
"en" : "An error has occurred",
- "fr" : null,
+ "fr" : "Une erreur c'est produite",
"ru" : "Произошла ошибка",
"es" : "Ha ocurrido un error"
}
@@ -968,6 +1065,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs:34" ],
"translations" : {
"en" : "Blur",
+ "fr" : "Flou",
"es" : "Desenfocado"
}
},
@@ -975,6 +1073,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs:40" ],
"translations" : {
"en" : "Value",
+ "fr" : "Valeur",
"es" : "Valor"
}
},
@@ -982,6 +1081,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:70" ],
"translations" : {
"en" : "HEX",
+ "fr" : "HEX",
"es" : "HEX"
}
},
@@ -989,6 +1089,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:76" ],
"translations" : {
"en" : "HSLA",
+ "fr" : "HSLA",
"es" : "HSLA"
}
},
@@ -996,12 +1097,14 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:73" ],
"translations" : {
"en" : "RGBA",
+ "fr" : "RGBA",
"es" : "RGBA"
}
},
"handoff.attributes.content" : {
"translations" : {
"en" : "Content",
+ "fr" : "Contenu",
"es" : "Contenido"
},
"unused" : true
@@ -1010,6 +1113,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/fill.cljs:57" ],
"translations" : {
"en" : "Fill",
+ "fr" : "Remplir",
"es" : "Relleno"
}
},
@@ -1017,6 +1121,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:50" ],
"translations" : {
"en" : "Dowload source image",
+ "fr" : "Télécharger l'image source",
"es" : "Descargar imagen original"
}
},
@@ -1024,6 +1129,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:38" ],
"translations" : {
"en" : "Height",
+ "fr" : "Hauteur",
"es" : "Altura"
}
},
@@ -1031,6 +1137,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:33" ],
"translations" : {
"en" : "Width",
+ "fr" : "Largeur",
"es" : "Ancho"
}
},
@@ -1038,6 +1145,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:76" ],
"translations" : {
"en" : "Layout",
+ "fr" : "Disposition",
"es" : "Estructura"
}
},
@@ -1045,6 +1153,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:43" ],
"translations" : {
"en" : "Height",
+ "fr" : "Hauteur",
"es" : "Altura"
}
},
@@ -1052,6 +1161,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:49" ],
"translations" : {
"en" : "Left",
+ "fr" : "Gauche",
"es" : "Izquierda"
}
},
@@ -1059,6 +1169,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:61" ],
"translations" : {
"en" : "Radius",
+ "fr" : "Rayon",
"es" : "Derecha"
}
},
@@ -1066,6 +1177,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:67" ],
"translations" : {
"en" : "Rotation",
+ "fr" : "Rotation",
"es" : "Rotación"
}
},
@@ -1073,6 +1185,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:55" ],
"translations" : {
"en" : "Top",
+ "fr" : "Haut",
"es" : "Arriba"
}
},
@@ -1080,6 +1193,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:38" ],
"translations" : {
"en" : "Width",
+ "fr" : "Largeur",
"es" : "Ancho"
}
},
@@ -1087,6 +1201,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:71" ],
"translations" : {
"en" : "Shadow",
+ "fr" : "Ombre",
"es" : "Sombra"
}
},
@@ -1094,6 +1209,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:53" ],
"translations" : {
"en" : "B",
+ "fr" : "B",
"es" : "B"
}
},
@@ -1101,6 +1217,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:45" ],
"translations" : {
"en" : "X",
+ "fr" : "X",
"es" : "X"
}
},
@@ -1108,6 +1225,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:49" ],
"translations" : {
"en" : "Y",
+ "fr" : "Y",
"es" : "Y"
}
},
@@ -1115,12 +1233,14 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:57" ],
"translations" : {
"en" : "S",
+ "fr" : "S",
"es" : "S"
}
},
"handoff.attributes.shadow.style.drop-shadow" : {
"translations" : {
"en" : "Drop",
+ "fr" : "Portée",
"es" : "Arrojar"
},
"unused" : true
@@ -1128,6 +1248,7 @@
"handoff.attributes.shadow.style.inner-shadow" : {
"translations" : {
"en" : "Inner",
+ "fr" : "Interne",
"es" : "Interna"
},
"unused" : true
@@ -1136,12 +1257,14 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs:75" ],
"translations" : {
"en" : "Stroke",
+ "fr" : "Trait",
"es" : "Borde"
}
},
"handoff.attributes.stroke.alignment.center" : {
"translations" : {
"en" : "Center",
+ "fr" : "Centré",
"es" : "Centrado"
},
"unused" : true
@@ -1149,6 +1272,7 @@
"handoff.attributes.stroke.alignment.inner" : {
"translations" : {
"en" : "Inner",
+ "fr" : "Intérieur",
"es" : "Interno"
},
"unused" : true
@@ -1156,6 +1280,7 @@
"handoff.attributes.stroke.alignment.outer" : {
"translations" : {
"en" : "Outer",
+ "fr" : "Extérieur",
"es" : "Externo"
},
"unused" : true
@@ -1163,6 +1288,7 @@
"handoff.attributes.stroke.style.dashed" : {
"translations" : {
"en" : "Dashed",
+ "fr" : "Tiret",
"es" : "Discontinuo"
},
"unused" : true
@@ -1170,6 +1296,7 @@
"handoff.attributes.stroke.style.dotted" : {
"translations" : {
"en" : "Dotted",
+ "fr" : "Pointillé",
"es" : "Punteado"
},
"unused" : true
@@ -1177,6 +1304,7 @@
"handoff.attributes.stroke.style.mixed" : {
"translations" : {
"en" : "Mixed",
+ "fr" : "Mixte",
"es" : "Mixto"
},
"unused" : true
@@ -1184,6 +1312,7 @@
"handoff.attributes.stroke.style.none" : {
"translations" : {
"en" : "None",
+ "fr" : "Aucun",
"es" : "Ninguno"
},
"unused" : true
@@ -1191,6 +1320,7 @@
"handoff.attributes.stroke.style.solid" : {
"translations" : {
"en" : "Solid",
+ "fr" : "Solide",
"es" : "Sólido"
},
"unused" : true
@@ -1199,6 +1329,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs:63" ],
"translations" : {
"en" : "Width",
+ "fr" : "Largeur",
"es" : "Ancho"
}
},
@@ -1206,6 +1337,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:189" ],
"translations" : {
"en" : "Typography",
+ "fr" : "Typographie",
"es" : "Tipografía"
}
},
@@ -1213,6 +1345,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:120" ],
"translations" : {
"en" : "Font Family",
+ "fr" : "Police de caractères",
"es" : "Familia tipográfica"
}
},
@@ -1220,6 +1353,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:132" ],
"translations" : {
"en" : "Font Size",
+ "fr" : "Taille de police",
"es" : "Tamaño de fuente"
}
},
@@ -1227,6 +1361,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:126" ],
"translations" : {
"en" : "Font Style",
+ "fr" : "Style de police",
"es" : "Estilo de fuente"
}
},
@@ -1234,6 +1369,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:144" ],
"translations" : {
"en" : "Letter Spacing",
+ "fr" : "Espacement des lettres",
"es" : "Espaciado de letras"
}
},
@@ -1241,6 +1377,7 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:138" ],
"translations" : {
"en" : "Line Height",
+ "fr" : "Hauteur de ligne",
"es" : "Interlineado"
}
},
@@ -1248,12 +1385,14 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:150" ],
"translations" : {
"en" : "Text Decoration",
+ "fr" : "Décoration de texte",
"es" : "Decoración de texto"
}
},
"handoff.attributes.typography.text-decoration.none" : {
"translations" : {
"en" : "None",
+ "fr" : "Aucune",
"es" : "Ninguna"
},
"unused" : true
@@ -1261,6 +1400,7 @@
"handoff.attributes.typography.text-decoration.strikethrough" : {
"translations" : {
"en" : "Strikethrough",
+ "fr" : "Barré",
"es" : "Tachar"
},
"unused" : true
@@ -1268,6 +1408,7 @@
"handoff.attributes.typography.text-decoration.underline" : {
"translations" : {
"en" : "Underline",
+ "fr" : "Sousligné",
"es" : "Subrayar"
},
"unused" : true
@@ -1276,12 +1417,14 @@
"used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:156" ],
"translations" : {
"en" : "Text Transform",
+ "fr" : "Transformation de texte",
"es" : "Transformación de texto"
}
},
"handoff.attributes.typography.text-transform.lowercase" : {
"translations" : {
"en" : "Lower Case",
+ "fr" : "Minuscule",
"es" : "Minúsculas"
},
"unused" : true
@@ -1289,6 +1432,7 @@
"handoff.attributes.typography.text-transform.none" : {
"translations" : {
"en" : "None",
+ "fr" : "Aucune",
"es" : "Ninguna"
},
"unused" : true
@@ -1296,6 +1440,7 @@
"handoff.attributes.typography.text-transform.titlecase" : {
"translations" : {
"en" : "Title Case",
+ "fr" : "Première lettre en majuscule",
"es" : "Primera en mayúscula"
},
"unused" : true
@@ -1303,6 +1448,7 @@
"handoff.attributes.typography.text-transform.uppercase" : {
"translations" : {
"en" : "Upper Case",
+ "fr" : "Majuscule",
"es" : "Mayúsculas"
},
"unused" : true
@@ -1311,12 +1457,14 @@
"used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:65" ],
"translations" : {
"en" : "Code",
+ "fr" : "Code",
"es" : "Código"
}
},
"handoff.tabs.code.selected.circle" : {
"translations" : {
"en" : "Circle",
+ "fr" : "Cercle",
"es" : "Círculo"
},
"unused" : true
@@ -1324,6 +1472,7 @@
"handoff.tabs.code.selected.curve" : {
"translations" : {
"en" : "Curve",
+ "fr" : "Courbe",
"es" : "Curva"
},
"unused" : true
@@ -1331,6 +1480,7 @@
"handoff.tabs.code.selected.frame" : {
"translations" : {
"en" : "Artboard",
+ "fr" : "Plan de travail",
"es" : "Mesa de trabajo"
},
"unused" : true
@@ -1338,6 +1488,7 @@
"handoff.tabs.code.selected.group" : {
"translations" : {
"en" : "Group",
+ "fr" : "Groupe",
"es" : "Grupo"
},
"unused" : true
@@ -1345,6 +1496,7 @@
"handoff.tabs.code.selected.image" : {
"translations" : {
"en" : "Image",
+ "fr" : "Image",
"es" : "Imagen"
},
"unused" : true
@@ -1353,12 +1505,14 @@
"used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:48" ],
"translations" : {
"en" : "%s Selected",
+ "fr" : "%s Sélectionné",
"es" : "%s Seleccionado"
}
},
"handoff.tabs.code.selected.path" : {
"translations" : {
"en" : "Path",
+ "fr" : "Chemin",
"es" : "Trazado"
},
"unused" : true
@@ -1366,6 +1520,7 @@
"handoff.tabs.code.selected.rect" : {
"translations" : {
"en" : "Rectangle",
+ "fr" : "Rectangle",
"es" : "Rectángulo"
},
"unused" : true
@@ -1373,6 +1528,7 @@
"handoff.tabs.code.selected.svg-raw" : {
"translations" : {
"en" : "SVG",
+ "fr" : "SVG",
"es" : "SVG"
},
"unused" : true
@@ -1380,6 +1536,7 @@
"handoff.tabs.code.selected.text" : {
"translations" : {
"en" : "Text",
+ "fr" : "Texte",
"es" : "Texto"
},
"unused" : true
@@ -1388,6 +1545,7 @@
"used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:59" ],
"translations" : {
"en" : "Info",
+ "fr" : "Information",
"es" : "Información"
}
},
@@ -1404,6 +1562,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:85", "src/app/main/ui/dashboard/team.cljs:178", "src/app/main/ui/dashboard/team.cljs:194" ],
"translations" : {
"en" : "Admin",
+ "fr" : "Administration",
"es" : "Administración"
}
},
@@ -1411,6 +1570,7 @@
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:161" ],
"translations" : {
"en" : "All",
+ "fr" : "Tous",
"es" : "Todo"
}
},
@@ -1418,6 +1578,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:58" ],
"translations" : {
"en" : "Looks like you need to wait a bit and retry; we are performing small maintenance of our servers.",
+ "fr" : "Il semble que vous deviez attendre un peu et réessayer; nous effectuons une petite maintenance de nos serveurs.",
"es" : "Parece que necesitas esperar un poco y volverlo a intentar; estamos realizando operaciones de mantenimiento en nuestros servidores."
}
},
@@ -1425,6 +1586,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:57" ],
"translations" : {
"en" : "Bad Gateway",
+ "fr" : "Bad Gateway",
"es" : "Bad Gateway"
}
},
@@ -1441,6 +1603,7 @@
"used-in" : [ "src/app/main/ui/dashboard/comments.cljs:71" ],
"translations" : {
"en" : "Comments",
+ "fr" : "Commentaires",
"es" : "Comentarios"
}
},
@@ -1448,7 +1611,7 @@
"used-in" : [ "src/app/main/ui/settings/password.cljs:93" ],
"translations" : {
"en" : "Confirm password",
- "fr" : "Confirmez mot de passe",
+ "fr" : "Confirmer mot de passe",
"ru" : "Подтвердите пароль",
"es" : "Confirmar contraseña"
}
@@ -1457,6 +1620,7 @@
"used-in" : [ "src/app/main/ui/settings/sidebar.cljs:62" ],
"translations" : {
"en" : "Dashboard",
+ "fr" : "Tableau de bord",
"es" : "Panel"
}
},
@@ -1473,6 +1637,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:278" ],
"translations" : {
"en" : "Delete comment",
+ "fr" : "Supprimer commentaire",
"es" : "Eliminar comentario"
}
},
@@ -1480,6 +1645,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:277" ],
"translations" : {
"en" : "Delete thread",
+ "fr" : "Supprimer le fil",
"es" : "Eliminar hilo"
}
},
@@ -1496,6 +1662,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:275" ],
"translations" : {
"en" : "Edit",
+ "fr" : "Editer",
"es" : "Editar"
}
},
@@ -1503,6 +1670,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:86", "src/app/main/ui/dashboard/team.cljs:181", "src/app/main/ui/dashboard/team.cljs:195" ],
"translations" : {
"en" : "Editor",
+ "fr" : "Editeur",
"es" : "Editor"
}
},
@@ -1515,7 +1683,7 @@
"es" : "Correo electrónico"
}
},
- "labels.feedback" : {
+ "labels.give-feedback" : {
"used-in" : [ "src/app/main/ui/workspace/header.cljs:231", "src/app/main/ui/dashboard/sidebar.cljs:471" ],
"translations" : {
"en" : "Give feedback",
@@ -1528,6 +1696,7 @@
"used-in" : [ "src/app/main/ui/viewer/header.cljs:176", "src/app/main/ui/workspace/comments.cljs:129" ],
"translations" : {
"en" : "Hide resolved comments",
+ "fr" : "Masquer les commentaires résolus",
"es" : "Ocultar comentarios resueltos"
}
},
@@ -1535,6 +1704,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:92" ],
"translations" : {
"en" : "Something bad happened. Please retry the operation and if the problem persists, contact with support.",
+ "fr" : "Quelque chose d'étrange est arrivé. Veuillez réessayer l'opération, et si le problème persiste, contactez le service technique.",
"es" : "Ha ocurrido algo extraño. Por favor, reintenta la operación, y si el problema persiste, contacta con el servicio técnico."
}
},
@@ -1542,6 +1712,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:91" ],
"translations" : {
"en" : "Internal Error",
+ "fr" : "Erreur interne",
"es" : "Error interno"
}
},
@@ -1567,6 +1738,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:311", "src/app/main/ui/dashboard/team.cljs:60", "src/app/main/ui/dashboard/team.cljs:66" ],
"translations" : {
"en" : "Members",
+ "fr" : "Membres",
"es" : "Integrantes"
}
},
@@ -1592,6 +1764,7 @@
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:186", "src/app/main/ui/dashboard/comments.cljs:96" ],
"translations" : {
"en" : "You have no pending comment notifications",
+ "fr" : "Vous n'avez aucune notification de commentaire en attente",
"es" : "No tienes notificaciones de comentarios pendientes"
}
},
@@ -1599,6 +1772,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:42" ],
"translations" : {
"en" : "You’re signed in as",
+ "fr" : "Vous êtes connecté en tant que",
"es" : "Estás identificado como"
}
},
@@ -1606,6 +1780,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:40" ],
"translations" : {
"en" : "This page might not exist or you don’t have permissions to access to it.",
+ "fr" : "Cette page n'existe pas ou vous ne disposez pas des permissions nécessaires pour y accéder.",
"es" : "Esta página no existe o no tienes permisos para verla."
}
},
@@ -1613,6 +1788,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:39" ],
"translations" : {
"en" : "Oops!",
+ "fr" : "Oups!",
"es" : "¡Huy!"
}
},
@@ -1620,6 +1796,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:314" ],
"translations" : {
"en" : [ "1 file", "%s files" ],
+ "fr" : [ "1 fichier", "%s fichiers" ],
"es" : [ "1 archivo", "%s archivos" ]
}
},
@@ -1627,6 +1804,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:311" ],
"translations" : {
"en" : [ "1 project", "%s projects" ],
+ "fr" : [ "1 projet", "%s projets" ],
"es" : [ "1 proyecto", "%s proyectos" ]
}
},
@@ -1643,6 +1821,7 @@
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:162" ],
"translations" : {
"en" : "Only yours",
+ "fr" : "Seulement les votres",
"es" : "Sólo los tuyos"
}
},
@@ -1650,6 +1829,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:175", "src/app/main/ui/dashboard/team.cljs:302" ],
"translations" : {
"en" : "Owner",
+ "fr" : "Propriétaire",
"es" : "Dueño"
}
},
@@ -1666,6 +1846,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:222" ],
"translations" : {
"en" : "Permissions",
+ "fr" : "Permissions",
"es" : "Permisos"
}
},
@@ -1682,7 +1863,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:412" ],
"translations" : {
"en" : "Projects",
- "fr" : "Projetes",
+ "fr" : "Projets",
"ru" : "Проекты",
"es" : "Proyectos"
}
@@ -1691,7 +1872,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:92", "src/app/main/ui/dashboard/team.cljs:208" ],
"translations" : {
"en" : "Remove",
- "fr" : "",
+ "fr" : "Retirer",
"ru" : "",
"es" : "Quitar"
}
@@ -1700,6 +1881,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:314", "src/app/main/ui/dashboard/files.cljs:84", "src/app/main/ui/dashboard/grid.cljs:178" ],
"translations" : {
"en" : "Rename",
+ "fr" : "Renommer",
"es" : "Renombrar"
}
},
@@ -1707,6 +1889,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:62", "src/app/main/ui/static.cljs:79", "src/app/main/ui/static.cljs:96" ],
"translations" : {
"en" : "Retry",
+ "fr" : "Réessayer",
"es" : "Reintentar"
}
},
@@ -1714,13 +1897,46 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:84" ],
"translations" : {
"en" : "Role",
+ "fr" : "Rôle",
"es" : "Cargo"
}
},
+ "labels.send" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:93" ],
+ "translations" : {
+ "en" : "Send",
+ "es" : "Enviar"
+ }
+ },
+ "labels.sending" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs:93" ],
+ "translations" : {
+ "en" : "Sending...",
+ "es" : "Enviando..."
+ }
+ },
+
+ "labels.feedback-disabled" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ],
+ "translations" : {
+ "en" : "Feedback disabled",
+ "es" : "El modulo de recepción de opiniones esta deshabilitado."
+ }
+ },
+
+ "labels.feedback-sent" : {
+ "used-in" : [ "src/app/main/ui/settings/feedback.cljs" ],
+ "translations" : {
+ "en" : "Feedback sent",
+ "es" : "Opinión enviada"
+ }
+ },
+
"labels.service-unavailable.desc-message" : {
"used-in" : [ "src/app/main/ui/static.cljs:75" ],
"translations" : {
"en" : "We are in programmed maintenance of our systems.",
+ "fr" : "Nous sommes en maintenance planifiée de nos systèmes.",
"es" : "Estamos en una operación de mantenimiento programado de nuestros sistemas."
}
},
@@ -1728,6 +1944,7 @@
"used-in" : [ "src/app/main/ui/static.cljs:74" ],
"translations" : {
"en" : "Service Unavailable",
+ "fr" : "Service non disponible",
"es" : "El servicio no está disponible"
}
},
@@ -1735,7 +1952,7 @@
"used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/sidebar.cljs:312", "src/app/main/ui/dashboard/team.cljs:61", "src/app/main/ui/dashboard/team.cljs:68" ],
"translations" : {
"en" : "Settings",
- "fr" : "Settings",
+ "fr" : "Configuration",
"ru" : "Параметры",
"es" : "Configuración"
}
@@ -1744,7 +1961,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:421" ],
"translations" : {
"en" : "Shared Libraries",
- "fr" : "",
+ "fr" : "Bibliothèques Partagées",
"ru" : "",
"es" : "Bibliotecas Compartidas"
}
@@ -1753,6 +1970,7 @@
"used-in" : [ "src/app/main/ui/viewer/header.cljs:164", "src/app/main/ui/workspace/comments.cljs:117" ],
"translations" : {
"en" : "Show all comments",
+ "fr" : "Afficher tous les commentaires",
"es" : "Mostrar todos los comentarios"
}
},
@@ -1760,6 +1978,7 @@
"used-in" : [ "src/app/main/ui/viewer/header.cljs:169", "src/app/main/ui/workspace/comments.cljs:122" ],
"translations" : {
"en" : "Show only yours comments",
+ "fr" : "Afficher uniquement vos commentaires",
"es" : "Mostrar sólo tus comentarios"
}
},
@@ -1776,7 +1995,7 @@
"used-in" : [ "src/app/main/ui/settings/profile.cljs:104" ],
"translations" : {
"en" : "Update",
- "fr" : "Mettre a jour",
+ "fr" : "Actualiser",
"ru" : "Обновить",
"es" : "Actualizar"
}
@@ -1785,6 +2004,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:184" ],
"translations" : {
"en" : "Viewer",
+ "fr" : "Téléspectateur",
"es" : "Visualizador"
}
},
@@ -1792,6 +2012,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:154" ],
"translations" : {
"en" : "Write new comment",
+ "fr" : "Écrire un nouveau commentaire",
"es" : "Escribir un nuevo comentario"
}
},
@@ -1800,7 +2021,7 @@
"translations" : {
"en" : "Loading image...",
"fr" : "Chargement de l'image...",
- "ru" : "Загружаю изображение ...",
+ "ru" : "Загружаю изображение...",
"es" : "Cargando imagen..."
}
},
@@ -1817,7 +2038,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:114", "src/app/main/ui/dashboard/grid.cljs:116" ],
"translations" : {
"en" : "Add as Shared Library",
- "fr" : "",
+ "fr" : "Ajouter comme Bibliothèque Partagée",
"ru" : "",
"es" : "Añadir como Biblioteca Compartida"
}
@@ -1826,7 +2047,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:112", "src/app/main/ui/dashboard/grid.cljs:114" ],
"translations" : {
"en" : "Once added as Shared Library, the assets of this file library will be available to be used among the rest of your files.",
- "fr" : "",
+ "fr" : "Une fois ajoutés en tant que Bibliothèque Partagée, les ressources de cette bibliothèque de fichiers seront disponibles pour être utilisés parmi le reste de vos fichiers.",
"ru" : "",
"es" : "Una vez añadido como Biblioteca Compartida, los recursos de este archivo estarán disponibles para ser usado por el resto de tus archivos."
}
@@ -1835,7 +2056,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:111", "src/app/main/ui/dashboard/grid.cljs:113" ],
"translations" : {
"en" : "Add “%s” as Shared Library",
- "fr" : "",
+ "fr" : "Ajouter “%s” comme Bibliothèque Partagée",
"ru" : "",
"es" : "Añadir “%s” como Biblioteca Compartida"
}
@@ -1925,6 +2146,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:227" ],
"translations" : {
"en" : "Delete conversation",
+ "fr" : "Supprimer la conversation",
"es" : "Eliminar conversación"
}
},
@@ -1932,6 +2154,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:226" ],
"translations" : {
"en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted.",
+ "fr" : "Voulez-vous vraiment supprimer cette conversation? Tous les commentaires de ce fil seront supprimés.",
"es" : "¿Seguro que quieres eliminar esta conversación? Todos los comentarios en este hilo serán eliminados."
}
},
@@ -1939,6 +2162,7 @@
"used-in" : [ "src/app/main/ui/comments.cljs:225" ],
"translations" : {
"en" : "Delete conversation",
+ "fr" : "Supprimer la conversation",
"es" : "Eliminar conversación"
}
},
@@ -1946,6 +2170,7 @@
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:82" ],
"translations" : {
"en" : "Delete file",
+ "fr" : "Supprimer le fichier",
"es" : "Eliminar archivo"
}
},
@@ -1953,6 +2178,7 @@
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:81" ],
"translations" : {
"en" : "Are you sure you want to delete this file?",
+ "fr" : "Êtes-vous sûr de vouloir supprimer ce fichier?",
"es" : "¿Seguro que quieres eliminar este archivo?"
}
},
@@ -1960,6 +2186,7 @@
"used-in" : [ "src/app/main/ui/dashboard/grid.cljs:80" ],
"translations" : {
"en" : "Deleting file",
+ "fr" : "Supprimer le fichier",
"es" : "Eliminando archivo"
}
},
@@ -1967,6 +2194,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs:60" ],
"translations" : {
"en" : "Are you sure you want to delete this page?",
+ "fr" : "Êtes-vous sûr de vouloir supprimer cette page?",
"es" : "¿Seguro que quieres borrar esta página?"
}
},
@@ -1974,6 +2202,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs:59" ],
"translations" : {
"en" : "Delete page",
+ "fr" : "Supprimer la page",
"es" : "Borrar página"
}
},
@@ -1981,6 +2210,7 @@
"used-in" : [ "src/app/main/ui/dashboard/files.cljs:58" ],
"translations" : {
"en" : "Delete project",
+ "fr" : "Supprimer le projet",
"es" : "Eliminar proyecto"
}
},
@@ -1988,6 +2218,7 @@
"used-in" : [ "src/app/main/ui/dashboard/files.cljs:57" ],
"translations" : {
"en" : "Are you sure you want to delete this project?",
+ "fr" : "Êtes-vous sûr de vouloir supprimer ce projet?",
"es" : "¿Seguro que quieres eliminar este proyecto?"
}
},
@@ -1995,6 +2226,7 @@
"used-in" : [ "src/app/main/ui/dashboard/files.cljs:56" ],
"translations" : {
"en" : "Delete project",
+ "fr" : "Supprimer le projet",
"es" : "Eliminar proyecto"
}
},
@@ -2002,6 +2234,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:301" ],
"translations" : {
"en" : "Delete team",
+ "fr" : "Supprimer l'équipe",
"es" : "Eliminar equipo"
}
},
@@ -2009,6 +2242,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:300" ],
"translations" : {
"en" : "Are you sure you want to delete this team? All projects and files associated with team will be permanently deleted.",
+ "fr" : "Voulez-vous vraiment supprimer cette équipe? Tous les projets et fichiers associés à l'équipe seront définitivement supprimés.",
"es" : "¿Seguro que quieres eliminar este equipo? Todos los proyectos y archivos asociados con el equipo serán eliminados permamentemente."
}
},
@@ -2016,6 +2250,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:299" ],
"translations" : {
"en" : "Deleting team",
+ "fr" : "Suppression de l'équipe",
"es" : "Eliminando equipo"
}
},
@@ -2023,6 +2258,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:164" ],
"translations" : {
"en" : "Delete member",
+ "fr" : "Supprimer le membre",
"es" : "Eliminando miembro"
}
},
@@ -2030,6 +2266,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:163" ],
"translations" : {
"en" : "Are you sure you want to delete this member from the team?",
+ "fr" : "Voulez-vous vraiment supprimer ce membre de l'équipe?",
"es" : "¿Seguro que quieres eliminar este integrante del equipo?"
}
},
@@ -2037,6 +2274,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:162" ],
"translations" : {
"en" : "Delete team member",
+ "fr" : "Supprimer le membre de l'équipe",
"es" : "Eliminar integrante del equipo"
}
},
@@ -2044,6 +2282,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:112" ],
"translations" : {
"en" : "Invite to join the team",
+ "fr" : "Inviter à rejoindre l'équipe",
"es" : "Invitar a unirse al equipo"
}
},
@@ -2051,6 +2290,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:204" ],
"translations" : {
"en" : "You are %s owner.",
+ "fr" : "Vous êtes le propriétaire de %s.",
"es" : "Eres %s dueño."
}
},
@@ -2058,6 +2298,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:205" ],
"translations" : {
"en" : "Select other member to promote before leave",
+ "fr" : "Sélectionnez un autre membre à promouvoir avant de partir",
"es" : "Promociona otro miembro a dueño antes de abandonar el equipo"
}
},
@@ -2065,6 +2306,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:222" ],
"translations" : {
"en" : "Promote and leave",
+ "fr" : "Promouvoir et quitter",
"es" : "Promocionar y abandonar"
}
},
@@ -2072,6 +2314,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:182" ],
"translations" : {
"en" : "Select a member to promote",
+ "fr" : "Sélectionnez un membre à promouvoir",
"es" : "Selecciona un miembro a promocionar"
}
},
@@ -2079,6 +2322,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:199" ],
"translations" : {
"en" : "Select a member to promote",
+ "fr" : "Sélectionnez un membre à promouvoir",
"es" : "Selecciona un miembro a promocionar"
}
},
@@ -2086,6 +2330,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:276" ],
"translations" : {
"en" : "Leave team",
+ "fr" : "Quitter l'équipe",
"es" : "Abandonar el equipo"
}
},
@@ -2093,6 +2338,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:275" ],
"translations" : {
"en" : "Are you sure you want to leave this team?",
+ "fr" : "Êtes-vous sûr de vouloir quitter cette équipe?",
"es" : "¿Seguro que quieres abandonar este equipo?"
}
},
@@ -2100,6 +2346,7 @@
"used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:274" ],
"translations" : {
"en" : "Leaving team",
+ "fr" : "Quitter l'équipe",
"es" : "Abandonando el equipo"
}
},
@@ -2107,6 +2354,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:151" ],
"translations" : {
"en" : "Promote",
+ "fr" : "Promouvoir",
"es" : "Promocionar"
}
},
@@ -2114,6 +2362,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:150" ],
"translations" : {
"en" : "Are you sure you want to promote this user to owner?",
+ "fr" : "Voulez-vous vraiment promouvoir cet utilisateur en propriétaire?",
"es" : "¿Seguro que quieres promocionar este usuario a dueño?"
}
},
@@ -2121,6 +2370,7 @@
"used-in" : [ "src/app/main/ui/dashboard/team.cljs:149" ],
"translations" : {
"en" : "Promote to owner",
+ "fr" : "Promouvoir en propriétaire",
"es" : "Promocionar a dueño"
}
},
@@ -2128,7 +2378,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:127", "src/app/main/ui/dashboard/grid.cljs:132" ],
"translations" : {
"en" : "Remove as Shared Library",
- "fr" : "",
+ "fr" : "Supprimer en tant que Bibliothèque Partagée",
"ru" : "",
"es" : "Eliminar como Biblioteca Compartida"
}
@@ -2137,7 +2387,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:125", "src/app/main/ui/dashboard/grid.cljs:130" ],
"translations" : {
"en" : "Once removed as Shared Library, the File Library of this file will stop being available to be used among the rest of your files.",
- "fr" : "",
+ "fr" : "Une fois supprimée en tant que Bibliothèque Partagée, la Bibliothèque de ce fichier ne pourra plus être utilisée par le reste de vos fichiers.",
"ru" : "",
"es" : "Una vez eliminado como Biblioteca Compartida, la Biblioteca de este archivo dejará de estar disponible para ser usada por el resto de tus archivos."
}
@@ -2146,7 +2396,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:124", "src/app/main/ui/dashboard/grid.cljs:129" ],
"translations" : {
"en" : "Remove “%s” as Shared Library",
- "fr" : "",
+ "fr" : "Retirer “%s” en tant que Bibliothèque Partagée",
"ru" : "",
"es" : "Añadir “%s” como Biblioteca Compartida"
}
@@ -2155,7 +2405,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:95", "src/app/main/ui/workspace/sidebar/options/component.cljs:72" ],
"translations" : {
"en" : "Update component",
- "fr" : "",
+ "fr" : "Actualiser le composant",
"ru" : "",
"es" : "Actualizar componente"
}
@@ -2164,7 +2414,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:94", "src/app/main/ui/workspace/sidebar/options/component.cljs:71" ],
"translations" : {
"en" : "Cancel",
- "fr" : "",
+ "fr" : "Annuler",
"ru" : "",
"es" : "Cancelar"
}
@@ -2173,7 +2423,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:93", "src/app/main/ui/workspace/sidebar/options/component.cljs:70" ],
"translations" : {
"en" : "You are about to update a component in a shared library. This may affect other files that use it.",
- "fr" : "",
+ "fr" : "Vous êtes sur le point de mettre à jour un composant dans une bibliothèque partagée. Cela peut affecter d'autres fichiers qui l'utilisent.",
"ru" : "",
"es" : "Vas a actualizar un componente en una librería compartida. Esto puede afectar a otros archivos que la usen."
}
@@ -2182,7 +2432,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:92", "src/app/main/ui/workspace/sidebar/options/component.cljs:69" ],
"translations" : {
"en" : "Update a component in a shared library",
- "fr" : "",
+ "fr" : "Actualiser un composant dans une bibliothèque",
"ru" : "",
"es" : "Actualizar un componente en librería"
}
@@ -2209,6 +2459,7 @@
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:56", "src/app/main/ui/auth/register.cljs:54" ],
"translations" : {
"en" : "Verification email sent to %s. Check your email!",
+ "fr" : "E-mail de vérification envoyé à %s. Vérifiez votre email!",
"es" : "Verificación de email enviada a %s. Comprueba tu correo."
}
},
@@ -2216,7 +2467,7 @@
"used-in" : [ "src/app/main/ui/auth/recovery.cljs:95" ],
"translations" : {
"en" : "Go to login",
- "fr" : null,
+ "fr" : "Aller à la connexion",
"ru" : null,
"es" : null
}
@@ -2225,7 +2476,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:213", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:161", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:170", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162", "src/app/main/ui/workspace/sidebar/options/blur.cljs:79", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:145" ],
"translations" : {
"en" : "Mixed",
- "fr" : null,
+ "fr" : "Divers",
"ru" : "Смешаный",
"es" : "Varios"
}
@@ -2450,7 +2701,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:705" ],
"translations" : {
"en" : "Assets",
- "fr" : "",
+ "fr" : "Ressources",
"ru" : "",
"es" : "Recursos"
}
@@ -2459,7 +2710,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:725" ],
"translations" : {
"en" : "All assets",
- "fr" : "",
+ "fr" : "Toutes",
"ru" : "",
"es" : "Todos"
}
@@ -2467,7 +2718,7 @@
"workspace.assets.box-filter-colors" : {
"translations" : {
"en" : "Colors",
- "fr" : "",
+ "fr" : "Couleurs",
"ru" : "",
"es" : "Colores"
},
@@ -2476,7 +2727,7 @@
"workspace.assets.box-filter-graphics" : {
"translations" : {
"en" : "Graphics",
- "fr" : "",
+ "fr" : "Graphiques",
"ru" : "",
"es" : "Gráficos"
},
@@ -2486,7 +2737,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:399", "src/app/main/ui/workspace/sidebar/assets.cljs:728" ],
"translations" : {
"en" : "Colors",
- "fr" : "",
+ "fr" : "Couleurs",
"ru" : "",
"es" : "Colores"
}
@@ -2495,7 +2746,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:109", "src/app/main/ui/workspace/sidebar/assets.cljs:726" ],
"translations" : {
"en" : "Components",
- "fr" : "",
+ "fr" : "Composants",
"ru" : "",
"es" : "Componentes"
}
@@ -2504,7 +2755,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs:151", "src/app/main/ui/workspace/sidebar/assets.cljs:140", "src/app/main/ui/workspace/sidebar/assets.cljs:260", "src/app/main/ui/workspace/sidebar/assets.cljs:375", "src/app/main/ui/workspace/sidebar/assets.cljs:504" ],
"translations" : {
"en" : "Delete",
- "fr" : "",
+ "fr" : "Supprimer",
"ru" : "",
"es" : "Borrar"
}
@@ -2513,7 +2764,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs:155", "src/app/main/ui/workspace/sidebar/assets.cljs:139" ],
"translations" : {
"en" : "Duplicate",
- "fr" : "",
+ "fr" : "Dupliquer",
"ru" : "",
"es" : "Duplicar"
}
@@ -2522,7 +2773,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:374", "src/app/main/ui/workspace/sidebar/assets.cljs:503" ],
"translations" : {
"en" : "Edit",
- "fr" : "",
+ "fr" : "Éditer",
"ru" : "",
"es" : "Editar"
}
@@ -2531,16 +2782,16 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:602" ],
"translations" : {
"en" : "File library",
- "fr" : "",
+ "fr" : "Bibliothèque du fichier",
"ru" : "",
- "es" : "Bilioteca del archivo"
+ "es" : "Biblioteca del archivo"
}
},
"workspace.assets.graphics" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:221", "src/app/main/ui/workspace/sidebar/assets.cljs:727" ],
"translations" : {
"en" : "Graphics",
- "fr" : "",
+ "fr" : "Graphiques",
"ru" : "",
"es" : "Gráficos"
}
@@ -2549,7 +2800,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:708" ],
"translations" : {
"en" : "Libraries",
- "fr" : "",
+ "fr" : "Bibliothèques",
"ru" : "",
"es" : "Bibliotecas"
}
@@ -2558,7 +2809,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:666" ],
"translations" : {
"en" : "No assets found",
- "fr" : "",
+ "fr" : "Aucune ressource trouvée",
"ru" : "",
"es" : "No se encontraron recursos"
}
@@ -2567,7 +2818,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/sitemap.cljs:154", "src/app/main/ui/workspace/sidebar/assets.cljs:138", "src/app/main/ui/workspace/sidebar/assets.cljs:259", "src/app/main/ui/workspace/sidebar/assets.cljs:373", "src/app/main/ui/workspace/sidebar/assets.cljs:502" ],
"translations" : {
"en" : "Rename",
- "fr" : "",
+ "fr" : "Renommer",
"ru" : "",
"es" : "Renombrar"
}
@@ -2576,7 +2827,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:712" ],
"translations" : {
"en" : "Search assets",
- "fr" : "",
+ "fr" : "Chercher des ressources",
"ru" : "",
"es" : "Buscar recursos"
}
@@ -2585,7 +2836,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:604" ],
"translations" : {
"en" : "SHARED",
- "fr" : "",
+ "fr" : "PARTAGÉ",
"ru" : "",
"es" : "COMPARTIDA"
}
@@ -2594,6 +2845,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:491", "src/app/main/ui/workspace/sidebar/assets.cljs:729" ],
"translations" : {
"en" : "Typographies",
+ "fr" : "Typographies",
"es" : "Tipografías"
}
},
@@ -2601,6 +2853,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:276" ],
"translations" : {
"en" : "Font",
+ "fr" : "Police",
"es" : "Fuente"
}
},
@@ -2608,6 +2861,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:284" ],
"translations" : {
"en" : "Size",
+ "fr" : "Taille",
"es" : "Tamaño"
}
},
@@ -2615,6 +2869,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:280" ],
"translations" : {
"en" : "Variant",
+ "fr" : "Variante",
"es" : "Variante"
}
},
@@ -2622,6 +2877,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:301" ],
"translations" : {
"en" : "Go to style library file to edit",
+ "fr" : "Accéder au fichier de bibliothèque de styles à modifier",
"es" : "Ir al archivo de la biblioteca del estilo para editar"
}
},
@@ -2629,6 +2885,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:292" ],
"translations" : {
"en" : "Letter Spacing",
+ "fr" : "Espacement des lettres",
"es" : "Interletrado"
}
},
@@ -2636,6 +2893,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:288" ],
"translations" : {
"en" : "Line Height",
+ "fr" : "Hauteur de ligne",
"es" : "Interlineado"
}
},
@@ -2643,6 +2901,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:254", "src/app/main/ui/handoff/attributes/text.cljs:96", "src/app/main/ui/handoff/attributes/text.cljs:105" ],
"translations" : {
"en" : "Ag",
+ "fr" : "Ag",
"es" : "Ag"
}
},
@@ -2650,6 +2909,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:296" ],
"translations" : {
"en" : "Text Transform",
+ "fr" : "Transformer le texte",
"es" : "Transformar texto"
}
},
@@ -2657,6 +2917,7 @@
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:71", "src/app/main/ui/components/color_bullet.cljs:31" ],
"translations" : {
"en" : "Linear gradient",
+ "fr" : "Dégradé linéaire",
"es" : "Degradado lineal"
}
},
@@ -2664,6 +2925,7 @@
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:72", "src/app/main/ui/components/color_bullet.cljs:32" ],
"translations" : {
"en" : "Radial gradient",
+ "fr" : "Dégradé radial",
"es" : "Degradado radial"
}
},
@@ -2707,7 +2969,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:209" ],
"translations" : {
"en" : "Hide assets",
- "fr" : "",
+ "fr" : "Masquer les ressources",
"ru" : "",
"es" : "Ocultar recursos"
}
@@ -2752,7 +3014,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:214" ],
"translations" : {
"en" : "Select all",
- "fr" : "",
+ "fr" : "Sélectionner tout",
"ru" : "",
"es" : "Seleccionar todo"
}
@@ -2761,7 +3023,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:210" ],
"translations" : {
"en" : "Show assets",
- "fr" : "",
+ "fr" : "Montrer les ressources",
"ru" : "",
"es" : "Mostrar recursos"
}
@@ -2806,6 +3068,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:58" ],
"translations" : {
"en" : "Error on saving",
+ "fr" : "Erreur d'enregistrement",
"es" : "Error al guardar"
}
},
@@ -2813,6 +3076,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:53" ],
"translations" : {
"en" : "Saved",
+ "fr" : "Enregistré",
"es" : "Guardado"
}
},
@@ -2820,6 +3084,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:48" ],
"translations" : {
"en" : "Saving",
+ "fr" : "Enregistrement",
"es" : "Guardando"
}
},
@@ -2827,6 +3092,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:43" ],
"translations" : {
"en" : "Unsaved changes",
+ "fr" : "Modifications non sauvegardées",
"es" : "Cambios sin guardar"
}
},
@@ -2843,7 +3109,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:116" ],
"translations" : {
"en" : "Add",
- "fr" : "",
+ "fr" : "Ajouter",
"ru" : "",
"es" : "Añadir"
}
@@ -2852,7 +3118,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:44" ],
"translations" : {
"en" : "%s colors",
- "fr" : "",
+ "fr" : "%s couleurs",
"ru" : "",
"es" : "%s colors"
}
@@ -2861,6 +3127,7 @@
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:171" ],
"translations" : {
"en" : "Big thumbnails",
+ "fr" : "Grandes vignettes",
"es" : "Miniaturas grandes"
}
},
@@ -2868,6 +3135,7 @@
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:89", "src/app/main/ui/workspace/colorpalette.cljs:149" ],
"translations" : {
"en" : "File library",
+ "fr" : "Bibliothèque du fichier",
"es" : "Biblioteca del archivo"
}
},
@@ -2875,6 +3143,7 @@
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:88", "src/app/main/ui/workspace/colorpalette.cljs:159" ],
"translations" : {
"en" : "Recent colors",
+ "fr" : "Couleurs récentes",
"es" : "Colores recientes"
}
},
@@ -2882,6 +3151,7 @@
"used-in" : [ "src/app/main/ui/workspace/colorpicker.cljs:339" ],
"translations" : {
"en" : "Save color style",
+ "fr" : "Enregistrer le style de couleur",
"es" : "Guardar estilo de color"
}
},
@@ -2889,6 +3159,7 @@
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:176" ],
"translations" : {
"en" : "Small thumbnails",
+ "fr" : "Petites vignettes",
"es" : "Miniaturas pequeñas"
}
},
@@ -2896,7 +3167,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:38" ],
"translations" : {
"en" : "%s components",
- "fr" : "",
+ "fr" : "%s composants",
"ru" : "",
"es" : "%s componentes"
}
@@ -2905,7 +3176,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:85" ],
"translations" : {
"en" : "File library",
- "fr" : "",
+ "fr" : "Bibliothèque du fichier",
"ru" : "",
"es" : "Biblioteca de este archivo"
}
@@ -2914,7 +3185,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:41" ],
"translations" : {
"en" : "%s graphics",
- "fr" : "",
+ "fr" : "%s graphiques",
"ru" : "",
"es" : "%s gráficos"
}
@@ -2923,7 +3194,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:82" ],
"translations" : {
"en" : "LIBRARIES IN THIS FILE",
- "fr" : "",
+ "fr" : "BIBLIOTHÈQUES DANS CE FICHIER",
"ru" : "",
"es" : "BIBLIOTECAS EN ESTE ARCHIVO"
}
@@ -2932,7 +3203,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:177" ],
"translations" : {
"en" : "LIBRARIES",
- "fr" : "",
+ "fr" : "BIBLIOTHÈQUES",
"ru" : "",
"es" : "BIBLIOTECAS"
}
@@ -2941,7 +3212,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:136" ],
"translations" : {
"en" : "LIBRARY",
- "fr" : "",
+ "fr" : "BIBLIOTHÈQUE",
"ru" : "",
"es" : "BIBLIOTECA"
}
@@ -2950,7 +3221,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:134" ],
"translations" : {
"en" : "There are no Shared Libraries that need update",
- "fr" : "",
+ "fr" : "Aucune Bibliothèque Partagée n'a besoin d'être mise à jour",
"ru" : "",
"es" : "No hay bibliotecas que necesiten ser actualizadas"
}
@@ -2968,7 +3239,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:121" ],
"translations" : {
"en" : "There are no Shared Libraries available",
- "fr" : "",
+ "fr" : "Aucune bibliothèque partagée disponible",
"ru" : "",
"es" : "No hay bibliotecas compartidas disponibles"
}
@@ -2977,7 +3248,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:99" ],
"translations" : {
"en" : "Search shared libraries",
- "fr" : "",
+ "fr" : "Rechercher des bibliothèques partagées",
"ru" : "",
"es" : "Buscar bibliotecas compartidas"
}
@@ -2986,7 +3257,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:96" ],
"translations" : {
"en" : "SHARED LIBRARIES",
- "fr" : "",
+ "fr" : "BIBLIOTHÈQUES PARTAGÉES",
"ru" : "",
"es" : "BIBLIOTECAS COMPARTIDAS"
}
@@ -2995,6 +3266,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:264" ],
"translations" : {
"en" : "Multiple typographies",
+ "fr" : "Multiple typographies",
"es" : "Varias tipografías"
}
},
@@ -3002,6 +3274,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:266" ],
"translations" : {
"en" : "Unlink all typographies",
+ "fr" : "Dissocier toutes les typographies",
"es" : "Desvincular todas las tipografías"
}
},
@@ -3009,6 +3282,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:47" ],
"translations" : {
"en" : "%s typographies",
+ "fr" : "%s typographies",
"es" : "%s tipografías"
}
},
@@ -3016,7 +3290,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:143" ],
"translations" : {
"en" : "Update",
- "fr" : "",
+ "fr" : "Actualiser",
"ru" : "",
"es" : "Actualizar"
}
@@ -3025,7 +3299,7 @@
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:181" ],
"translations" : {
"en" : "UPDATES",
- "fr" : "",
+ "fr" : "MISES À JOUR",
"ru" : "",
"es" : "ACTUALIZACIONES"
}
@@ -3033,7 +3307,7 @@
"workspace.library.all" : {
"translations" : {
"en" : "All libraries",
- "fr" : "Toutes les librairies",
+ "fr" : "Toutes les bibliothèques",
"ru" : "Все библиотеки",
"es" : "Todas"
},
@@ -3060,7 +3334,7 @@
"workspace.library.libraries" : {
"translations" : {
"en" : "Libraries",
- "fr" : "Librairies",
+ "fr" : "Bibliothèques",
"ru" : "Библиотеки",
"es" : "Bibliotecas"
},
@@ -3069,7 +3343,7 @@
"workspace.library.own" : {
"translations" : {
"en" : "My libraries",
- "fr" : "Mes librairies",
+ "fr" : "Mes bibliothèques",
"ru" : "Мои библиотеки",
"es" : "Mis bibliotecas"
},
@@ -3087,6 +3361,7 @@
"workspace.options.blur-options.background-blur" : {
"translations" : {
"en" : "Background",
+ "fr" : "Fond",
"es" : "Fondo"
},
"unused" : true
@@ -3094,6 +3369,7 @@
"workspace.options.blur-options.layer-blur" : {
"translations" : {
"en" : "Layer",
+ "fr" : "Couche",
"es" : "Capa"
},
"unused" : true
@@ -3102,6 +3378,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/blur.cljs:62" ],
"translations" : {
"en" : "Blur",
+ "fr" : "Flou",
"es" : "Desenfoque"
}
},
@@ -3109,6 +3386,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/blur.cljs:61" ],
"translations" : {
"en" : "Group blur",
+ "fr" : "Flou de groupe",
"es" : "Desenfoque del grupo"
}
},
@@ -3116,6 +3394,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/blur.cljs:60" ],
"translations" : {
"en" : "Selection blur",
+ "fr" : "Flou de sélection",
"es" : "Desenfoque de la selección"
}
},
@@ -3132,6 +3411,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:81" ],
"translations" : {
"en" : "Component",
+ "fr" : "Composant",
"es" : "Componente"
}
},
@@ -3148,6 +3428,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:132", "src/app/main/ui/handoff/exports.cljs:96" ],
"translations" : {
"en" : "Export",
+ "fr" : "Export",
"ru" : "Экспорт",
"es" : "Exprotar"
}
@@ -3156,6 +3437,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:166", "src/app/main/ui/handoff/exports.cljs:131" ],
"translations" : {
"en" : "Export shape",
+ "fr" : "Exporter la forme",
"ru" : "Экспорт фигуры",
"es" : "Exportar forma"
}
@@ -3164,6 +3446,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:149" ],
"translations" : {
"en" : "Suffix",
+ "fr" : "Suffixe",
"es" : "Sufijo"
}
},
@@ -3171,6 +3454,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:165", "src/app/main/ui/handoff/exports.cljs:130" ],
"translations" : {
"en" : "Exporting...",
+ "fr" : "Export...",
"ru" : "Экспортирую...",
"es" : "Exportando"
}
@@ -3377,7 +3661,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:40" ],
"translations" : {
"en" : "Group fill",
- "fr" : null,
+ "fr" : "Remplissage de groupe",
"ru" : "Заливка для группы",
"es" : "Relleno de grupo"
}
@@ -3386,7 +3670,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:54" ],
"translations" : {
"en" : "Group stroke",
- "fr" : null,
+ "fr" : "Contour de groupe",
"ru" : "Обводка для группы",
"es" : "Borde de grupo"
}
@@ -3467,7 +3751,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:39" ],
"translations" : {
"en" : "Selection fill",
- "fr" : null,
+ "fr" : "Remplissage de sélection",
"ru" : "Заливка выбранного",
"es" : "Relleno de selección"
}
@@ -3476,7 +3760,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:53" ],
"translations" : {
"en" : "Selection stroke",
- "fr" : null,
+ "fr" : "Contour de sélection",
"ru" : "Обводка выбранного",
"es" : "Borde de selección"
}
@@ -3485,6 +3769,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:166" ],
"translations" : {
"en" : "Blur",
+ "fr" : "Flou",
"es" : "Desenfoque"
}
},
@@ -3492,6 +3777,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:135" ],
"translations" : {
"en" : "Drop shadow",
+ "fr" : "Ombre portée",
"es" : "Sombra arrojada"
}
},
@@ -3499,6 +3785,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:136" ],
"translations" : {
"en" : "Inner shadow",
+ "fr" : "Ombre intérieure",
"es" : "Sombra interior"
}
},
@@ -3506,6 +3793,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:146" ],
"translations" : {
"en" : "X",
+ "fr" : "X",
"es" : "X"
}
},
@@ -3513,6 +3801,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:155" ],
"translations" : {
"en" : "Y",
+ "fr" : "Y",
"es" : "Y"
}
},
@@ -3520,6 +3809,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:176" ],
"translations" : {
"en" : "Spread",
+ "fr" : "Diffusion",
"es" : "Difusión"
}
},
@@ -3527,6 +3817,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:204" ],
"translations" : {
"en" : "Shadow",
+ "fr" : "Ombre",
"es" : "Sombra"
}
},
@@ -3534,6 +3825,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:203" ],
"translations" : {
"en" : "Group shadow",
+ "fr" : "Ombre de groupe",
"es" : "Sombra del grupo"
}
},
@@ -3541,6 +3833,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/shadow.cljs:202" ],
"translations" : {
"en" : "Selection shadows",
+ "fr" : "Ombres de la sélection",
"es" : "Sombras de la seleccíón"
}
},
@@ -3710,6 +4003,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:137" ],
"translations" : {
"en" : "Auto height",
+ "fr" : "Hauteur automatique",
"es" : "Alto automático"
}
},
@@ -3717,6 +4011,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:132" ],
"translations" : {
"en" : "Auto width",
+ "fr" : "Largeur automatique",
"es" : "Ancho automático"
}
},
@@ -3724,6 +4019,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:127" ],
"translations" : {
"en" : "Fixed",
+ "fr" : "Fixe",
"es" : "Fijo"
}
},
@@ -3731,7 +4027,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:154" ],
"translations" : {
"en" : "Letter Spacing",
- "fr" : "Espacement de caractères",
+ "fr" : "Espacement des lettres",
"ru" : "Межсимвольный интервал",
"es" : "Espaciado entre letras"
}
@@ -3794,6 +4090,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:185" ],
"translations" : {
"en" : "Group text",
+ "fr" : "Texte de groupe",
"ru" : "Текст группы",
"es" : "Texto de grupo"
}
@@ -3802,6 +4099,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:184" ],
"translations" : {
"en" : "Selection text",
+ "fr" : "Texte de la sélection",
"ru" : "Выбранный текст",
"es" : "Texto de selección"
}
@@ -3855,6 +4153,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:124" ],
"translations" : {
"en" : "Send to back",
+ "fr" : "Mettre en arrière plan",
"es" : "Enviar al fondo"
}
},
@@ -3862,6 +4161,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:121" ],
"translations" : {
"en" : "Send backward",
+ "fr" : "Reculer",
"es" : "Enviar atrás"
}
},
@@ -3869,6 +4169,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:102" ],
"translations" : {
"en" : "Copy",
+ "fr" : "Copier",
"es" : "Copiar"
}
},
@@ -3876,6 +4177,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:168" ],
"translations" : {
"en" : "Create component",
+ "fr" : "Créer un composant",
"es" : "Crear componente"
}
},
@@ -3883,6 +4185,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:105" ],
"translations" : {
"en" : "Cut",
+ "fr" : "Couper",
"es" : "Cortar"
}
},
@@ -3890,6 +4193,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:200" ],
"translations" : {
"en" : "Delete",
+ "fr" : "Supprimer",
"es" : "Eliminar"
}
},
@@ -3897,6 +4201,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:180", "src/app/main/ui/workspace/context_menu.cljs:190", "src/app/main/ui/workspace/sidebar/options/component.cljs:95", "src/app/main/ui/workspace/sidebar/options/component.cljs:100" ],
"translations" : {
"en" : "Detach instance",
+ "fr" : "Détacher l'instance",
"es" : "Desacoplar instancia"
}
},
@@ -3904,6 +4209,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:111" ],
"translations" : {
"en" : "Duplicate",
+ "fr" : "Dupliquer",
"es" : "Duplicar"
}
},
@@ -3911,6 +4217,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:115" ],
"translations" : {
"en" : "Bring forward",
+ "fr" : "Avancer",
"es" : "Mover hacia delante"
}
},
@@ -3918,6 +4225,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:118" ],
"translations" : {
"en" : "Bring to front",
+ "fr" : "Mettre au premier plan",
"es" : "Mover al frente"
}
},
@@ -3925,6 +4233,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:194", "src/app/main/ui/workspace/sidebar/options/component.cljs:102" ],
"translations" : {
"en" : "Go to master component file",
+ "fr" : "Aller au fichier du composant principal",
"es" : "Ir al archivo del componente maestro"
}
},
@@ -3932,6 +4241,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:131" ],
"translations" : {
"en" : "Group",
+ "fr" : "Groupe",
"es" : "Grupo"
}
},
@@ -3939,6 +4249,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:154" ],
"translations" : {
"en" : "Hide",
+ "fr" : "Masquer",
"es" : "Ocultar"
}
},
@@ -3946,6 +4257,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:160" ],
"translations" : {
"en" : "Lock",
+ "fr" : "Bloquer",
"es" : "Bloquear"
}
},
@@ -3953,6 +4265,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:134", "src/app/main/ui/workspace/context_menu.cljs:147" ],
"translations" : {
"en" : "Mask",
+ "fr" : "Masque",
"es" : "Máscara"
}
},
@@ -3960,6 +4273,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:108", "src/app/main/ui/workspace/context_menu.cljs:209" ],
"translations" : {
"en" : "Paste",
+ "fr" : "Coller",
"es" : "Pegar"
}
},
@@ -3967,6 +4281,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:182", "src/app/main/ui/workspace/context_menu.cljs:192", "src/app/main/ui/workspace/sidebar/options/component.cljs:96", "src/app/main/ui/workspace/sidebar/options/component.cljs:101" ],
"translations" : {
"en" : "Reset overrides",
+ "fr" : "Annuler les modifications",
"es" : "Deshacer modificaciones"
}
},
@@ -3974,6 +4289,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:152" ],
"translations" : {
"en" : "Show",
+ "fr" : "Montrer",
"es" : "Mostrar"
}
},
@@ -3981,6 +4297,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:186", "src/app/main/ui/workspace/sidebar/options/component.cljs:98" ],
"translations" : {
"en" : "Show master component",
+ "fr" : "Afficher le composant principal",
"es" : "Ver componente maestro"
}
},
@@ -3988,6 +4305,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:140" ],
"translations" : {
"en" : "Ungroup",
+ "fr" : "Dégrouper",
"es" : "Desagrupar"
}
},
@@ -3995,6 +4313,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:158" ],
"translations" : {
"en" : "Unlock",
+ "fr" : "Débloquer",
"es" : "Desbloquear"
}
},
@@ -4002,6 +4321,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:144" ],
"translations" : {
"en" : "Unmask",
+ "fr" : "Supprimer le masque",
"es" : "Quitar máscara"
}
},
@@ -4009,6 +4329,7 @@
"used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:184", "src/app/main/ui/workspace/context_menu.cljs:196", "src/app/main/ui/workspace/sidebar/options/component.cljs:97", "src/app/main/ui/workspace/sidebar/options/component.cljs:103" ],
"translations" : {
"en" : "Update master component",
+ "fr" : "Actualiser le composant principal",
"es" : "Actualizar componente maestro"
}
},
@@ -4016,7 +4337,8 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:122" ],
"translations" : {
"en" : "History (%s)",
- "en" : "Historial (%s)"
+ "fr" : "Historique (%s)",
+ "es" : "Historial (%s)"
}
},
"workspace.sidebar.icons" : {
@@ -4032,6 +4354,7 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:112" ],
"translations" : {
"en" : "Layers (%s)",
+ "fr" : "Couches (%s)",
"es" : "Capas (%s)"
}
},
@@ -4048,7 +4371,7 @@
"used-in" : [ "src/app/main/ui/workspace/header.cljs:149" ],
"translations" : {
"en" : "Sitemap",
- "fr" : null,
+ "fr" : "Plan du site",
"ru" : "Карта сайта",
"es" : "Mapa del sitio"
}
@@ -4057,7 +4380,7 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:117" ],
"translations" : {
"en" : "Assets (%s)",
- "fr" : "",
+ "fr" : "Ressources (%s)",
"ru" : "",
"es" : "Recursos (%s)"
}
@@ -4075,6 +4398,7 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:105" ],
"translations" : {
"en" : "Comments (%s)",
+ "fr" : "Commentaires (%s)",
"es" : "Comentarios (%s)"
}
},
@@ -4091,7 +4415,7 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:81" ],
"translations" : {
"en" : "Ellipse (E)",
- "fr" : "",
+ "fr" : "Ellipse (E)",
"ru" : "",
"es" : "Elipse (E)"
}
@@ -4136,7 +4460,7 @@
"used-in" : [ "src/app/main/ui/workspace/left_toolbar.cljs:76" ],
"translations" : {
"en" : "Rectangle (R)",
- "fr" : "",
+ "fr" : "Rectangle (R)",
"ru" : "Прямоугольник (R)",
"es" : "Rectángulo (R)"
}
@@ -4154,6 +4478,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:293" ],
"translations" : {
"en" : "There are no history changes so far",
+ "fr" : "Il n'y a aucun changement dans l'historique",
"es" : "Todavía no hay cambios en el histórico"
}
},
@@ -4161,6 +4486,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:121" ],
"translations" : {
"en" : "Deleted %s",
+ "fr" : "Supprimé %s",
"es" : "%s eliminado"
}
},
@@ -4168,6 +4494,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:120" ],
"translations" : {
"en" : "Modified %s",
+ "fr" : "Modifié %s",
"es" : "%s modificado"
}
},
@@ -4175,12 +4502,14 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:122" ],
"translations" : {
"en" : "Moved objects",
+ "fr" : "Objets déplacés",
"es" : "Objetos movidos"
}
},
"workspace.undo.entry.multiple.circle" : {
"translations" : {
"en" : "circles",
+ "fr" : "cercles",
"es" : "círculos"
},
"unused" : true
@@ -4188,6 +4517,7 @@
"workspace.undo.entry.multiple.color" : {
"translations" : {
"en" : "color assets",
+ "fr" : "couleurs",
"es" : "colores"
},
"unused" : true
@@ -4195,6 +4525,7 @@
"workspace.undo.entry.multiple.component" : {
"translations" : {
"en" : "components",
+ "fr" : "composants",
"es" : "componentes"
},
"unused" : true
@@ -4202,6 +4533,7 @@
"workspace.undo.entry.multiple.curve" : {
"translations" : {
"en" : "curves",
+ "fr" : "courbes",
"es" : "curvas"
},
"unused" : true
@@ -4209,6 +4541,7 @@
"workspace.undo.entry.multiple.frame" : {
"translations" : {
"en" : "artboard",
+ "fr" : "plan de travail",
"es" : "mesa de trabajo"
},
"unused" : true
@@ -4216,6 +4549,7 @@
"workspace.undo.entry.multiple.group" : {
"translations" : {
"en" : "groups",
+ "fr" : "grouprs",
"es" : "grupos"
},
"unused" : true
@@ -4223,6 +4557,7 @@
"workspace.undo.entry.multiple.image" : {
"translations" : {
"en" : "images",
+ "fr" : "images",
"es" : "imágenes"
},
"unused" : true
@@ -4230,6 +4565,7 @@
"workspace.undo.entry.multiple.media" : {
"translations" : {
"en" : "graphic assets",
+ "fr" : "graphiques",
"es" : "gráficos"
},
"unused" : true
@@ -4237,6 +4573,7 @@
"workspace.undo.entry.multiple.multiple" : {
"translations" : {
"en" : "objects",
+ "fr" : "objets",
"es" : "objetos"
},
"unused" : true
@@ -4244,6 +4581,7 @@
"workspace.undo.entry.multiple.page" : {
"translations" : {
"en" : "pages",
+ "fr" : "pages",
"es" : "páginas"
},
"unused" : true
@@ -4251,6 +4589,7 @@
"workspace.undo.entry.multiple.path" : {
"translations" : {
"en" : "paths",
+ "fr" : "chemins",
"es" : "trazos"
},
"unused" : true
@@ -4258,6 +4597,7 @@
"workspace.undo.entry.multiple.rect" : {
"translations" : {
"en" : "rectangles",
+ "fr" : "rectangles",
"es" : "rectángulos"
},
"unused" : true
@@ -4265,6 +4605,7 @@
"workspace.undo.entry.multiple.shape" : {
"translations" : {
"en" : "shapes",
+ "fr" : "formes",
"es" : "formas"
},
"unused" : true
@@ -4272,6 +4613,7 @@
"workspace.undo.entry.multiple.text" : {
"translations" : {
"en" : "texts",
+ "fr" : "textes",
"es" : "textos"
},
"unused" : true
@@ -4279,6 +4621,7 @@
"workspace.undo.entry.multiple.typography" : {
"translations" : {
"en" : "typography assets",
+ "fr" : "typographie",
"es" : "tipografías"
},
"unused" : true
@@ -4287,12 +4630,14 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:119" ],
"translations" : {
"en" : "New %s",
+ "fr" : "Nouveau %s",
"es" : "Nuevo %s"
}
},
"workspace.undo.entry.single.circle" : {
"translations" : {
"en" : "circle",
+ "fr" : "cercle",
"es" : "círculo"
},
"unused" : true
@@ -4300,6 +4645,7 @@
"workspace.undo.entry.single.color" : {
"translations" : {
"en" : "color asset",
+ "fr" : "culeur",
"es" : "color"
},
"unused" : true
@@ -4307,6 +4653,7 @@
"workspace.undo.entry.single.component" : {
"translations" : {
"en" : "component",
+ "fr" : "composant",
"es" : "componente"
},
"unused" : true
@@ -4314,13 +4661,15 @@
"workspace.undo.entry.single.curve" : {
"translations" : {
"en" : "curve",
+ "fr" : "courbe",
"es" : "curva"
},
"unused" : true
},
"workspace.undo.entry.single.frame" : {
"translations" : {
- "en" : "frame",
+ "en" : "artboard",
+ "fr" : "plan de travail",
"es" : "mesa de trabajo"
},
"unused" : true
@@ -4328,6 +4677,7 @@
"workspace.undo.entry.single.group" : {
"translations" : {
"en" : "group",
+ "fr" : "groupe",
"es" : "grupo"
},
"unused" : true
@@ -4335,6 +4685,7 @@
"workspace.undo.entry.single.image" : {
"translations" : {
"en" : "image",
+ "fr" : "image",
"es" : "imagen"
},
"unused" : true
@@ -4342,6 +4693,7 @@
"workspace.undo.entry.single.media" : {
"translations" : {
"en" : "graphic asset",
+ "fr" : "graphique",
"es" : "gráfico"
},
"unused" : true
@@ -4349,6 +4701,7 @@
"workspace.undo.entry.single.multiple" : {
"translations" : {
"en" : "object",
+ "fr" : "objet",
"es" : "objeto"
},
"unused" : true
@@ -4356,6 +4709,7 @@
"workspace.undo.entry.single.page" : {
"translations" : {
"en" : "page",
+ "fr" : "page",
"es" : "página"
},
"unused" : true
@@ -4363,6 +4717,7 @@
"workspace.undo.entry.single.path" : {
"translations" : {
"en" : "path",
+ "fr" : "chemin",
"es" : "trazo"
},
"unused" : true
@@ -4370,6 +4725,7 @@
"workspace.undo.entry.single.rect" : {
"translations" : {
"en" : "rectangle",
+ "fr" : "Rectangle",
"es" : "rectángulo"
},
"unused" : true
@@ -4377,6 +4733,7 @@
"workspace.undo.entry.single.shape" : {
"translations" : {
"en" : "shape",
+ "fr" : "forme",
"es" : "forma"
},
"unused" : true
@@ -4384,6 +4741,7 @@
"workspace.undo.entry.single.text" : {
"translations" : {
"en" : "text",
+ "fr" : "texte",
"es" : "texto"
},
"unused" : true
@@ -4391,6 +4749,7 @@
"workspace.undo.entry.single.typography" : {
"translations" : {
"en" : "typography asset",
+ "fr" : "typographie",
"es" : "tipografía"
},
"unused" : true
@@ -4399,6 +4758,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:123" ],
"translations" : {
"en" : "Operation over %s",
+ "fr" : "Opération sur %s",
"es" : "Operación sobre %s"
}
},
@@ -4406,6 +4766,7 @@
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:289" ],
"translations" : {
"en" : "History",
+ "fr" : "Historique",
"es" : "Histórico"
}
},
@@ -4413,7 +4774,7 @@
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:690" ],
"translations" : {
"en" : "Dismiss",
- "fr" : "",
+ "fr" : "Ignorer",
"ru" : "",
"es" : "Ignorar"
}
@@ -4422,7 +4783,7 @@
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:686" ],
"translations" : {
"en" : "There are updates in shared libraries",
- "fr" : "",
+ "fr" : "Il y a des mises à jour dans les Bibliothèques Partagées",
"ru" : "",
"es" : "Hay actualizaciones en librerías compartidas"
}
@@ -4431,7 +4792,7 @@
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:688" ],
"translations" : {
"en" : "Update",
- "fr" : "",
+ "fr" : "Actualiser",
"ru" : "",
"es" : "Actualizar"
}
@@ -4444,5 +4805,19 @@
"es" : "Pulsar para cerrar la ruta"
},
"unused" : true
+ },
+ "workspace.shape.menu.flip-horizontal" : {
+ "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:146" ],
+ "translations" : {
+ "en" : "Flip horizontal",
+ "es" : "Voltear horizontal"
+ }
+ },
+ "workspace.shape.menu.flip-vertical" : {
+ "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:143" ],
+ "translations" : {
+ "en" : "Flip vertical",
+ "es" : "Voltear vertical"
+ }
}
}
diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss
index 6c1923b92..55089a604 100644
--- a/frontend/resources/styles/common/framework.scss
+++ b/frontend/resources/styles/common/framework.scss
@@ -20,6 +20,7 @@
min-width: 25px;
padding: 0 1rem;
transition: all .4s;
+ text-decoration: none !important;
svg {
height: 15px;
width: 15px;
diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss
index d39ec4e3f..e877491e3 100644
--- a/frontend/resources/styles/main/partials/dashboard-grid.scss
+++ b/frontend/resources/styles/main/partials/dashboard-grid.scss
@@ -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;
diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss
index 03d520195..9e2d68ac0 100644
--- a/frontend/resources/styles/main/partials/forms.scss
+++ b/frontend/resources/styles/main/partials/forms.scss
@@ -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,
diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs
index 482bed0d8..70e808686 100644
--- a/frontend/src/app/config.cljs
+++ b/frontend/src/app/config.cljs
@@ -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))
diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs
index 550b5ddb9..cc2a09ea6 100644
--- a/frontend/src/app/main/data/viewer.cljs
+++ b/frontend/src/app/main/data/viewer.cljs
@@ -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})))))))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index 8ab42f532..79d370c33 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -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
diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs
index d77c9ce8c..b4b8a128f 100644
--- a/frontend/src/app/main/data/workspace/common.cljs
+++ b/frontend/src/app/main/data/workspace/common.cljs
@@ -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
diff --git a/frontend/src/app/main/data/workspace/drawing/path.cljs b/frontend/src/app/main/data/workspace/drawing/path.cljs
index 7c69909ed..c73014f1e 100644
--- a/frontend/src/app/main/data/workspace/drawing/path.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/path.cljs
@@ -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))))
diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs
index feacd4fbf..0b8a9bd82 100644
--- a/frontend/src/app/main/data/workspace/grid.cljs
+++ b/frontend/src/app/main/data/workspace/grid.cljs
@@ -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
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 9e6433f9c..519774d4b 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -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)
diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
index d6f8bff60..44fd6c31f 100644
--- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
@@ -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)
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index 5bb09a96a..effd20f97 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -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
diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs
index 4b4f5fea2..c8b260b46 100644
--- a/frontend/src/app/main/data/workspace/persistence.cljs
+++ b/frontend/src/app/main/data/workspace/persistence.cljs
@@ -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")))
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index a1f994a7d..11d982eb6 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -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
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 245645fc2..2eda58376 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -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)}
diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index 7e0174599..27ef5ce4a 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -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)
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 519517c04..b102be822 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -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))))))
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 74d35c0d5..f2bf529bb 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -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))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index b2f5e67e9..81ac15d54 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -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
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 5893eb604..bbdc805e7 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -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
[]
diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs
index 913dab1d1..37b08f6c9 100644
--- a/frontend/src/app/main/ui/components/forms.cljs
+++ b/frontend/src/app/main/ui/components/forms.cljs
@@ -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])]]))
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index 7804168de..2d4fb98a3 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -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
diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs
index 6994f243a..05ec590a2 100644
--- a/frontend/src/app/main/ui/settings.cljs
+++ b/frontend/src/app/main/ui/settings.cljs
@@ -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}]
diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs
new file mode 100644
index 000000000..125303a23
--- /dev/null
+++ b/frontend/src/app/main/ui/settings/feedback.cljs
@@ -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]]])
diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs
index 744778388..dd7a49d05 100644
--- a/frontend/src/app/main/ui/settings/sidebar.cljs
+++ b/frontend/src/app/main/ui/settings/sidebar.cljs
@@ -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}]]])
diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs
index fe10056a9..cd8ab02e0 100644
--- a/frontend/src/app/main/ui/shapes/gradients.cljs
+++ b/frontend/src/app/main/ui/shapes/gradients.cljs
@@ -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))))
diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs
index c859d2c0c..208dbf65b 100644
--- a/frontend/src/app/main/ui/shapes/text/styles.cljs
+++ b/frontend/src/app/main/ui/shapes/text/styles.cljs
@@ -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
diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs
index 59310a1de..c97025f13 100644
--- a/frontend/src/app/main/ui/viewer/header.cljs
+++ b/frontend/src/app/main/ui/viewer/header.cljs
@@ -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)
diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs
index 13d3df116..01c1151d0 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/context_menu.cljs
@@ -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))
[:*
diff --git a/frontend/src/app/main/ui/workspace/gradients.cljs b/frontend/src/app/main/ui/workspace/gradients.cljs
index 007b8631d..c88ba43b4 100644
--- a/frontend/src/app/main/ui/workspace/gradients.cljs
+++ b/frontend/src/app/main/ui/workspace/gradients.cljs
@@ -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)
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index 158e07c58..e4507a5ec 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -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
diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs
index 68a143cfb..d279c16fa 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs
@@ -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?
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs
index 3c7d587cd..895131571 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs
@@ -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)))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs
index 0bbf925ab..3cdb576a0 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/text.cljs
@@ -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)
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index d6deeb4c9..0d7f2d6af 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -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
diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs
index 17e80068d..94e1a385e 100644
--- a/frontend/src/app/util/forms.cljs
+++ b/frontend/src/app/util/forms.cljs
@@ -57,7 +57,6 @@
form))
-
(defn- wrap-update-fn
[f {:keys [spec validators]}]
(fn [& args]
diff --git a/frontend/tests/app/test_components_basic.cljs b/frontend/tests/app/test_components_basic.cljs
new file mode 100644
index 000000000..5f46a3366
--- /dev/null
+++ b/frontend/tests/app/test_components_basic.cljs
@@ -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)))))
+
diff --git a/frontend/tests/app/test_components_sync.cljs b/frontend/tests/app/test_components_sync.cljs
new file mode 100644
index 000000000..a7ae79ce3
--- /dev/null
+++ b/frontend/tests/app/test_components_sync.cljs
@@ -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)))))
+
diff --git a/frontend/tests/app/test_helpers/libraries.cljs b/frontend/tests/app/test_helpers/libraries.cljs
index b4bd7aa0e..7e275f100 100644
--- a/frontend/tests/app/test_helpers/libraries.cljs
+++ b/frontend/tests/app/test_helpers/libraries.cljs
@@ -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]))
+
diff --git a/frontend/tests/app/test_library_sync.cljs b/frontend/tests/app/test_library_sync.cljs
deleted file mode 100644
index d98bdf57a..000000000
--- a/frontend/tests/app/test_library_sync.cljs
+++ /dev/null
@@ -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))))))
-
diff --git a/frontend/tests/app/test_shapes.cljs b/frontend/tests/app/test_shapes.cljs
new file mode 100644
index 000000000..157780071
--- /dev/null
+++ b/frontend/tests/app/test_shapes.cljs
@@ -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))))))
+