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] [![Gitter](https://badges.gitter.im/sereno-xyz/community.svg)](https://gitter.im/penpot/community) -[![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/uxboxproject/ "Managed with Taiga.io") +[![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](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)))))) +