diff --git a/.circleci/config.yml b/.circleci/config.yml index 0fc2473ce9..09e3e6a893 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ jobs: # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ # - image: circleci/postgres:9.4 - - image: circleci/postgres:13.1-ram + - image: circleci/postgres:13.3-ram environment: POSTGRES_USER: penpot_test POSTGRES_PASSWORD: penpot_test @@ -29,21 +29,30 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}} + - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} # fallback to using the latest cache if no exact match is found - v1-dependencies- - # run lint - run: - working_directory: "./backend" - name: backend lint - command: "clj-kondo --lint src/" + name: common lint + working_directory: "./common" + command: "clj-kondo --parallel --lint src/" - # run test - run: + name: frontend lint + working_directory: "./frontend" + command: "clj-kondo --parallel --lint src/" + + - run: + name: backend lint working_directory: "./backend" + command: "clj-kondo --parallel --lint src/" + + # run backend test + - run: name: backend test - command: "clojure -M:dev:tests" + working_directory: "./backend" + command: "clojure -X:dev:test" environment: PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test" PENPOT_TEST_DATABASE_USERNAME: penpot_test @@ -51,11 +60,26 @@ jobs: PENPOT_TEST_REDIS_URI: "redis://localhost/1" - run: - working_directory: "./frontend" name: frontend tests + working_directory: "./frontend" command: | yarn install - npx shadow-cljs compile tests + clojure -M:dev:shadow-cljs compile test + node target/tests.js + + environment: + JAVA_HOME: /usr/lib/jvm/openjdk16 + PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin + + - run: + working_directory: "./common" + name: common tests + command: | + yarn install + clojure -M:dev:shadow-cljs compile test + node target/tests.js + clojure -X:dev:test + environment: JAVA_HOME: /usr/lib/jvm/openjdk16 PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin @@ -63,5 +87,5 @@ jobs: - save_cache: paths: - ~/.m2 - key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}} + key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 7f0a4e788c..cbe153b340 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,14 +1,23 @@ -{:lint-as {potok.core/reify clojure.core/reify - promesa.core/let clojure.core/let - rumext.alpha/defc clojure.core/defn - app.common.data/export clojure.core/def - app.db/with-atomic clojure.core/with-open} +{:lint-as + {promesa.core/let clojure.core/let + rumext.alpha/defc clojure.core/defn + rumext.alpha/fnc clojure.core/fn + app.common.data/export clojure.core/def + app.db/with-atomic clojure.core/with-open} :hooks - {:analyze-call {app.common.data/export hooks.export/export}} + {:analyze-call + {app.common.data/export hooks.export/export + potok.core/reify hooks.export/potok-reify + cljs.core/specify! hooks.export/clojure-specify + app.util.services/defmethod hooks.export/service-defmethod + }} :output - {:exclude-files ["data_readers.clj"]} + {:exclude-files + ["data_readers.clj" + "app/util/perf.cljs" + "app/common/exceptions.cljc"]} :linters {:unsorted-required-namespaces @@ -21,12 +30,12 @@ :single-key-in {:level :warning} + :redundant-do + {:level :off} + :unused-binding {:exclude-destructured-as true :exclude-destructured-keys-in-fn-args false } - - :unresolved-symbol - {:exclude ['(app.util.services/defmethod) - ]}}} + }} diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj index bac6996ca6..f29cc1a156 100644 --- a/.clj-kondo/hooks/export.clj +++ b/.clj-kondo/hooks/export.clj @@ -9,3 +9,41 @@ (api/token-node (symbol (name (:value sname)))) sname])] {:node result})) + +(defn potok-reify + [{:keys [:node]}] + (let [[rnode rtype & other] (:children node) + result (api/list-node + (into [(api/token-node (symbol "deftype")) + (api/token-node (gensym (name (:k rtype)))) + (api/vector-node [])] + other))] + {:node result})) + +(defn clojure-specify + [{:keys [:node]}] + (let [[rnode rtype & other] (:children node) + result (api/list-node + (into [(api/token-node (symbol "extend-type")) + (api/token-node (gensym (:string-value rtype)))] + other))] + {:node result})) + + +(defn service-defmethod + [{:keys [:node]}] + (let [[rnode rtype & other] (:children node) + rsym (gensym (name (:k rtype))) + result (api/list-node + [(api/token-node (symbol "do")) + (api/list-node + [(api/token-node (symbol "declare")) + (api/token-node rsym)]) + (api/list-node + (into [(api/token-node (symbol "defmethod")) + (api/token-node rsym) + rtype] + other))])] + {:node result})) + + diff --git a/CHANGES.md b/CHANGES.md index b74b8ab84b..fbf3a026c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,46 @@ ### :sparkles: New features ### :bug: Bugs fixed + +### :arrow_up: Deps updates +### :boom: Breaking changes +### :heart: Community contributions by (Thank you!) + +## 1.7.0-alpha + +### :sparkles: New features + +- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716). +- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719). +- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721). +- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125). +- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519). +- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718). +- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663). +- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063). +- Add the ability to offload file data to a cheaper storage when file becomes inactive. +- Import/Export Penpot files from dashboard. +- Double click won't make a shape a path until you change a node [Taiga #1796](https://tree.taiga.io/project/penpot/us/1796) +- Incremental area selection [#779](https://github.com/penpot/penpot/discussions/779) + +### :bug: Bugs fixed + +- Process numeric input changes only if the value actually changed. +- Remove unnecesary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820). +- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850). +- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819). +- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817). +- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813). +- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795). +- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865). +- Fix problem with color picker and fonts [#1049](https://github.com/penpot/penpot/issues/1049) +- Fix negative values in blur [Taiga #1815](https://tree.taiga.io/project/penpot/issue/1815) +- Fix problem when editing color in group [Taiga #1816](https://tree.taiga.io/project/penpot/issue/1816) +- Fix resize/rotate with mouse buttons different than left [#1060](https://github.com/penpot/penpot/issues/1060) +- Fix header partialy visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875) +- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063) + + ### :arrow_up: Deps updates ### :boom: Breaking changes ### :heart: Community contributions by (Thank you!) diff --git a/README.md b/README.md index 29d90c1dd6..ebe1760c42 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,60 @@ [uri_license]: https://www.mozilla.org/en-US/MPL/2.0 [uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg -[![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/penpot/ "Managed with Taiga.io") -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/penpot/penpot) +

+
+ PENPOT +

+ +

License: MPL-2.0 +Gitter +Managed with Taiga.io +Gitpod ready-to-code

+ +![PENPOT](https://penpot.app/images/readme/home-ui.jpg) -# PENPOT # +## What is Penpot? ## -Penpot is the first Open Source design and prototyping platform meant -for cross-domain teams. Non dependent on operating systems, Penpot is -web based and works with open web standards (SVG). For all and -empowered by the community. +Penpot is the first **Open Source design** and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open web standards (SVG). For all and empowered by the community. -![PENPOT](https://penpot.app/images/workspace-ui.jpg) +- [How to use](#how-to-use) +- [Help center](#help-center) +- [Contributing](#contributing) +- [Give feedback](#give-feedback) +- [Tutorials](#tutorials) +- [License](#license) +## How to use ## + +Login or Register on our Penpot cloud app. Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** by your own. + +✏️ [Start using Penpot](https://design.penpot.app) + +You can also install Penpot in a local environment. This section details everything you need to know to get Penpot up and running in production environments. Although it can be installed in many ways, the recommended approach is using **docker** and **docker-compose**. + +🐳 [Install docker](https://help.penpot.app/technical-guide/getting-started/) + +## Help center ## + +In this documentation you will find (almost) everything you need to know about how to work with Penpot. From the interface basics to advanced functionality. + +📖 [User guide](https://help.penpot.app/user-guide/) + +❓ [FAQs](https://help.penpot.app/faqs/) + +🖥️ [Technical guide](https://help.penpot.app/technical-guide/) + +❤️ [Contributing guide](https://help.penpot.app/contributing-guide/) + +![User guide](https://penpot.app/images/readme/help-center.jpg) ## Contributing ## +

+ Open Source +

+ **Open to you!** We love the open source software community. Contributing is our @@ -28,11 +64,24 @@ and improve Penpot. All your awesome ideas and code are welcome! Please refer to the [Contributing Guide](./CONTRIBUTING.md) +## Give feedback ## -## Documentation ## +You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project. -Please refer to the [help center](https://help.penpot.app). +✉️ [Mail us](mailto:info@penpot.app) +💬 [Github discussions](https://github.com/penpot/penpot/discussions) + +🐞 [Github issues](mailto:info@penpot.apphttps://github.com/penpot/penpot/issues) + +✍️️ [Gitter](https://gitter.im/penpot/community) + +## Tutorials ## + +You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project. +Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials. + +🎞️ [Youtube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g) ## License ## diff --git a/backend/deps.edn b/backend/deps.edn index 8032825c08..0bee0c0c52 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -1,22 +1,14 @@ -{:mvn/repos - {"central" {:url "https://repo1.maven.org/maven2/"} - "clojars" {:url "https://clojars.org/repo"} - "jcenter" {:url "https://jcenter.bintray.com/"}} +{ + ;; :mvn/repos + ;; {"central" {:url "https://repo1.maven.org/maven2/"} + ;; "clojars" {:url "https://clojars.org/repo"} + ;; "jcenter" {:url "https://jcenter.bintray.com/"} + ;; } :deps - {org.clojure/clojure {:mvn/version "1.10.3"} - org.clojure/data.json {:mvn/version "2.2.3"} - org.clojure/core.async {:mvn/version "1.3.618"} - org.clojure/tools.cli {:mvn/version "1.0.206"} - org.clojure/clojurescript {:mvn/version "1.10.844"} + {penpot/common + {:local/root "../common"} ;; Logging - org.clojure/tools.logging {:mvn/version "1.1.0"} - org.apache.logging.log4j/log4j-api {:mvn/version "2.14.1"} - org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"} - org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"} - org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"} - org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"} - org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} org.zeromq/jeromq {:mvn/version "0.5.2"} com.taoensso/nippy {:mvn/version "3.1.1"} @@ -32,69 +24,57 @@ org.eclipse.jetty/jetty-servlet]} io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"} - selmer/selmer {:mvn/version "1.12.40"} - expound/expound {:mvn/version "0.8.9"} - com.cognitect/transit-clj {:mvn/version "1.0.324"} - io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.2"} info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"} com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"} metosin/reitit-ring {:mvn/version "0.5.13"} - metosin/jsonista {:mvn/version "0.3.3"} - org.postgresql/postgresql {:mvn/version "42.2.20"} com.zaxxer/HikariCP {:mvn/version "4.0.3"} funcool/datoteka {:mvn/version "2.0.0"} - funcool/promesa {:mvn/version "6.0.1"} - funcool/cuerdas {:mvn/version "2021.05.09-0"} buddy/buddy-core {:mvn/version "1.10.1"} buddy/buddy-hashers {:mvn/version "1.8.1"} buddy/buddy-sign {:mvn/version "3.4.1"} - lambdaisland/uri {:mvn/version "1.4.54" - :exclusions [org.clojure/data.json]} - - frankiesardo/linked {:mvn/version "1.3.0"} - danlentz/clj-uuid {:mvn/version "0.1.9"} org.jsoup/jsoup {:mvn/version "1.13.1"} org.im4java/im4java {:mvn/version "1.4.0"} org.lz4/lz4-java {:mvn/version "1.7.1"} - commons-io/commons-io {:mvn/version "2.8.0"} - com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} integrant/integrant {:mvn/version "0.8.0"} - software.amazon.awssdk/s3 {:mvn/version "2.16.62"} + software.amazon.awssdk/s3 {:mvn/version "2.16.62"}} - ;; exception printing - io.aviso/pretty {:mvn/version "0.1.37"} - environ/environ {:mvn/version "1.2.0"}} - :paths ["src" "resources" "../common" "common"] + :paths ["src" "resources"] :aliases {:dev {:extra-deps {com.bhauman/rebel-readline {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} + com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.0"} - fipp/fipp {:mvn/version "0.6.23"} - criterium/criterium {:mvn/version "0.4.6"} - mockery/mockery {:mvn/version "0.1.4"}} - :extra-paths ["tests" "dev"]} + criterium/criterium {:mvn/version "RELEASE"} + mockery/mockery {:mvn/version "RELEASE"}} + :extra-paths ["test" "dev"]} :fn-fixtures {:exec-fn app.cli.fixtures/run :args {}} - :tests + :kaocha {:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.829"}} :main-opts ["-m" "kaocha.runner"]} + :test + {:extra-deps {io.github.cognitect-labs/test-runner + {:git/url "https://github.com/cognitect-labs/test-runner.git" + :sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}} + :exec-fn cognitect.test-runner.api/test} + :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} :main-opts ["-m" "antq.core"]} diff --git a/backend/dev/user.clj b/backend/dev/user.clj index fc3fe94bd8..4f47934a8b 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -50,7 +50,7 @@ ;; --- Development Stuff (defn- run-tests - ([] (run-tests #"^app.tests.*")) + ([] (run-tests #"^app.*-test$")) ([o] (repl/refresh) (cond diff --git a/backend/scripts/build b/backend/scripts/build index f1d9e03a47..865fed48b0 100755 --- a/backend/scripts/build +++ b/backend/scripts/build @@ -49,6 +49,7 @@ ;; Create the application jar (spit "./target/dist/version.txt" version) + (-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check) (-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check) (run! (fn [item] diff --git a/backend/scripts/repl b/backend/scripts/repl index 1f434abcfb..cee9d8c5b6 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -2,7 +2,7 @@ export PENPOT_ASSERTS_ENABLED=true -export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m"; +export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-Djdk.attach.allowAttachSelf -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m"; # export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions"; # export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000"; diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 30ca83d38b..f7c587d487 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -58,11 +58,8 @@ :srepl-host "127.0.0.1" :srepl-port 6062 - :storage-backend :fs - - :storage-fs-directory "assets" - :storage-s3-region :eu-central-1 - :storage-s3-bucket "penpot-devenv-assets-pre" + :assets-storage-backend :fs + :storage-assets-fs-directory "assets" :feedback-destination "info@example.com" :feedback-enabled false @@ -175,10 +172,14 @@ (s/def ::smtp-username (s/nilable ::us/string)) (s/def ::srepl-host ::us/string) (s/def ::srepl-port ::us/integer) -(s/def ::storage-backend ::us/keyword) -(s/def ::storage-fs-directory ::us/string) -(s/def ::storage-s3-bucket ::us/string) -(s/def ::storage-s3-region ::us/keyword) +(s/def ::assets-storage-backend ::us/keyword) +(s/def ::fdata-storage-backend ::us/keyword) +(s/def ::storage-assets-fs-directory ::us/string) +(s/def ::storage-assets-s3-bucket ::us/string) +(s/def ::storage-assets-s3-region ::us/keyword) +(s/def ::storage-fdata-s3-bucket ::us/string) +(s/def ::storage-fdata-s3-region ::us/keyword) +(s/def ::storage-fdata-s3-prefix ::us/string) (s/def ::telemetry-enabled ::us/boolean) (s/def ::telemetry-uri ::us/string) (s/def ::telemetry-with-taiga ::us/boolean) @@ -257,12 +258,20 @@ ::smtp-ssl ::smtp-tls ::smtp-username + ::srepl-host ::srepl-port - ::storage-backend - ::storage-fs-directory - ::storage-s3-bucket - ::storage-s3-region + + ::assets-storage-backend + ::storage-assets-fs-directory + ::storage-assets-s3-bucket + ::storage-assets-s3-region + + ::fdata-storage-backend + ::storage-fdata-s3-bucket + ::storage-fdata-s3-region + ::storage-fdata-s3-prefix + ::telemetry-enabled ::telemetry-uri ::telemetry-referer diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index b9886ec7e4..6e2e086bb2 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -10,13 +10,14 @@ [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.spec :as us] + [app.common.transit :as t] + [app.common.uuid :as uuid] [app.db.sql :as sql] [app.metrics :as mtx] [app.util.json :as json] [app.util.logging :as l] [app.util.migrations :as mg] [app.util.time :as dt] - [app.util.transit :as t] [clojure.java.io :as io] [clojure.spec.alpha :as s] [integrant.core :as ig] @@ -221,14 +222,20 @@ (sql/delete table params opts) (assoc opts :return-keys true)))) +(defn- is-deleted? + [{:keys [deleted-at]}] + (and (dt/instant? deleted-at) + (< (inst-ms deleted-at) + (inst-ms (dt/now))))) + (defn get-by-params ([ds table params] (get-by-params ds table params nil)) ([ds table params {:keys [uncheked] :or {uncheked false} :as opts}] (let [res (exec-one! ds (sql/select table params opts))] - (when (and (not uncheked) - (or (:deleted-at res) (not res))) + (when (and (not uncheked) (or (not res) (is-deleted? res))) (ex/raise :type :not-found + :table table :hint "database object not found")) res))) @@ -245,8 +252,11 @@ (exec! ds (sql/select table params opts)))) (defn pgobject? - [v] - (instance? PGobject v)) + ([v] + (instance? PGobject v)) + ([v type] + (and (instance? PGobject v) + (= type (.getType ^PGobject v))))) (defn pginterval? [v] @@ -339,12 +349,18 @@ (.setType "inet") (.setValue (str ip-addr)))) +(defn decode-inet + [^PGobject o] + (if (= "inet" (.getType o)) + (.getValue o) + nil)) + (defn tjson "Encode as transit json." [data] (doto (org.postgresql.util.PGobject.) (.setType "jsonb") - (.setValue (t/encode-verbose-str data)))) + (.setValue (t/encode-str data {:type :json-verbose})))) (defn json "Encode as plain json." @@ -360,3 +376,25 @@ (defn pgarray->vector [v] (vec (.getArray ^PgArray v))) + + +;; --- Locks + +(defn- xact-check-param + [n] + (cond + (uuid? n) (uuid/get-word-high n) + (int? n) n + :else (throw (IllegalArgumentException. "uuid or number allowed")))) + +(defn xact-lock! + [conn n] + (let [n (xact-check-param n)] + (exec-one! conn ["select pg_advisory_xact_lock(?::bigint) as lock" n]) + true)) + +(defn xact-try-lock! + [conn n] + (let [n (xact-check-param n) + row (exec-one! conn ["select pg_try_advisory_xact_lock(?::bigint) as lock" n])] + (:lock row))) diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index 6ee5d3073c..0ce621f235 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -43,8 +43,8 @@ ([table where-params opts] (let [opts (merge default-opts opts) opts (cond-> opts - (:for-update opts) - (assoc :suffix "FOR UPDATE"))] + (:for-update opts) (assoc :suffix "FOR UPDATE") + (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] (sql/for-query table where-params opts)))) (defn update diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index 678c4bdddd..18c14462e9 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -49,7 +49,7 @@ {:status 200 :headers {"content-type" (:content-type mdata) "cache-control" (str "max-age=" (inst-ms cache-max-age))} - :body (sto/get-object-data storage obj)} + :body (sto/get-object-bytes storage obj)} :s3 (let [url (sto/get-object-url storage obj {:max-age signature-max-age})] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 16d7429edf..b979b8431a 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -6,13 +6,14 @@ (ns app.http.middleware (:require + [app.common.transit :as t] [app.metrics :as mtx] [app.util.json :as json] [app.util.logging :as l] - [app.util.transit :as t] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] [clojure.java.io :as io] + [ring.core.protocols :as rp] [ring.middleware.cookies :refer [wrap-cookies]] [ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.multipart-params :refer [wrap-multipart-params]] @@ -73,17 +74,28 @@ {:name ::parse-request-body :compile (constantly wrap-parse-request-body)}) +(defn- transit-streamable-body + [data opts] + (reify rp/StreamableResponseBody + (write-body-to-stream [_ response output-stream] + (try + (let [tw (t/writer output-stream opts)] + (t/write! tw data)) + (finally + (.close ^java.io.OutputStream output-stream)))))) + (defn- impl-format-response-body - [response] + [response request] (let [body (:body response) - type :json-verbose] + opts {:type :json-verbose}] (cond (coll? body) (-> response - (assoc :body (t/encode body {:type type})) - (update :headers assoc - "content-type" - "application/transit+json")) + (update :headers assoc "content-type" "application/transit+json") + (assoc :body + (if (= :post (:request-method request)) + (transit-streamable-body body opts) + (t/encode body opts)))) (nil? body) (assoc response :status 204 :body "") @@ -96,7 +108,7 @@ (fn [request] (let [response (handler request)] (cond-> response - (map? response) (impl-format-response-body))))) + (map? response) (impl-format-response-body request))))) (def format-response-body {:name ::format-response-body diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index ea8f10f4bf..528d90427f 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -6,10 +6,14 @@ (ns app.http.oauth (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] + [app.db :as db] + [app.loggers.audit :as audit] + [app.rpc.queries.profile :as profile] [app.util.http :as http] [app.util.logging :as l] [app.util.time :as dt] @@ -19,36 +23,6 @@ [cuerdas.core :as str] [integrant.core :as ig])) -(defn redirect-response - [uri] - {:status 302 - :headers {"location" (str uri)} - :body ""}) - -(defn generate-error-redirect-uri - [cfg] - (-> (u/uri (:public-uri cfg)) - (assoc :path "/#/auth/login") - (assoc :query (u/map->query-string {:error "unable-to-auth"})))) - -(defn register-profile - [{:keys [rpc] :as cfg} info] - (let [method-fn (get-in rpc [:methods :mutation :login-or-register]) - profile (method-fn info)] - (cond-> profile - (some? (:invitation-token info)) - (assoc :invitation-token (:invitation-token info))))) - -(defn generate-redirect-uri - [{:keys [tokens] :as cfg} profile] - (let [token (or (:invitation-token profile) - (tokens :generate {:iss :auth - :exp (dt/in-future "15m") - :profile-id (:id profile)}))] - (-> (u/uri (:public-uri cfg)) - (assoc :path "/#/auth/verify-token") - (assoc :query (u/map->query-string {:token token}))))) - (defn- build-redirect-uri [{:keys [provider] :as cfg}] (let [public (u/uri (:public-uri cfg))] @@ -146,6 +120,7 @@ (string? roles) (into #{} (str/words roles)) (vector? roles) (into #{} roles) :else #{}))] + ;; check if profile has a configured set of roles (when-not (set/subset? provider-roles profile-roles) (ex/raise :type :internal @@ -175,6 +150,63 @@ {} params)) +(defn- retrieve-profile + [{:keys [pool] :as cfg} info] + (with-open [conn (db/open pool)] + (some->> (:email info) + (profile/retrieve-profile-data-by-email conn) + (profile/populate-additional-data conn) + (profile/decode-profile-row)))) + +(defn- redirect-response + [uri] + {:status 302 + :headers {"location" (str uri)} + :body ""}) + +(defn- generate-error-redirect + [cfg error] + (let [uri (-> (u/uri (:public-uri cfg)) + (assoc :path "/#/auth/login") + (assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))] + (redirect-response uri))) + +(defn- generate-redirect + [{:keys [tokens session audit] :as cfg} request info profile] + (if profile + (let [sxf ((:create session) (:id profile)) + token (or (:invitation-token info) + (tokens :generate {:iss :auth + :exp (dt/in-future "15m") + :profile-id (:id profile)})) + params {:token token} + + uri (-> (u/uri (:public-uri cfg)) + (assoc :path "/#/auth/verify-token") + (assoc :query (u/map->query-string params)))] + + (when (fn? audit) + (audit :cmd :submit + :type "mutation" + :name "login" + :profile-id (:id profile) + :ip-addr (audit/parse-client-ip request) + :props (audit/profile->props profile))) + + (->> (redirect-response uri) + (sxf request))) + (let [info (assoc info + :iss :prepared-register + :exp (dt/in-future {:hours 48})) + token (tokens :generate info) + params (d/without-nils + {:token token + :fullname (:fullname info)}) + uri (-> (u/uri (:public-uri cfg)) + (assoc :path "/#/auth/register/validate") + (assoc :query (u/map->query-string params)))] + (redirect-response uri)))) + (defn- auth-handler [{:keys [tokens] :as cfg} {:keys [params] :as request}] (let [invitation (:invitation-token params) @@ -189,17 +221,15 @@ :body {:redirect-uri uri}})) (defn- callback-handler - [{:keys [session] :as cfg} request] + [cfg request] (try - (let [info (retrieve-info cfg request) - profile (register-profile cfg info) - uri (generate-redirect-uri cfg profile) - sxf ((:create session) (:id profile))] - (->> (redirect-response uri) - (sxf request))) - (catch Exception _e - (-> (generate-error-redirect-uri cfg) - (redirect-response))))) + (let [info (retrieve-info cfg request) + profile (retrieve-profile cfg info)] + (generate-redirect cfg request info profile)) + (catch Exception e + (l/warn :hint "error on oauth process" + :cause e) + (generate-error-redirect cfg e)))) ;; --- INIT @@ -210,8 +240,8 @@ (s/def ::tokens fn?) (s/def ::rpc map?) -(defmethod ig/pre-init-spec :app.http.oauth/handlers [_] - (s/keys :req-un [::public-uri ::session ::tokens ::rpc])) +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool])) (defn wrap-handler [cfg handler] @@ -225,7 +255,7 @@ (-> (assoc @cfg :provider provider) (handler request))))) -(defmethod ig/init-key :app.http.oauth/handlers +(defmethod ig/init-key ::handler [_ cfg] (let [cfg (initialize cfg)] {:handler (wrap-handler cfg auth-handler) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 5fd7f29ece..dbdff89208 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -7,8 +7,10 @@ (ns app.loggers.audit "Services related to the user activity (audit log)." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] + [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -16,13 +18,25 @@ [app.util.http :as http] [app.util.logging :as l] [app.util.time :as dt] - [app.util.transit :as t] [app.worker :as wrk] [clojure.core.async :as a] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [lambdaisland.uri :as u])) +(defn parse-client-ip + [{:keys [headers] :as request}] + (or (some-> (get headers "x-forwarded-for") (str/split ",") first) + (get headers "x-real-ip") + (get request :remote-addr))) + +(defn profile->props + [profile] + (-> profile + (select-keys [:is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang]) + (d/without-nils))) + (defn clean-props [{:keys [profile-id] :as event}] (letfn [(clean-common [props] @@ -50,6 +64,7 @@ (assoc k (name v)))) {} props))] + (update event :props #(-> % clean-common clean-profile-id clean-complex-data)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -87,11 +102,12 @@ :cause res))) (recur))) - (fn [& [cmd & params]] - (case cmd - :stop (a/close! input) - :submit (when-not (a/offer! input (first params)) - (l/warn :msg "activity channel is full"))))))) + (fn [& {:keys [cmd] :as params}] + (let [params (dissoc params :cmd)] + (case cmd + :stop (a/close! input) + :submit (when-not (a/offer! input params) + (l/warn :msg "activity channel is full")))))))) (defn- persist-events @@ -101,12 +117,13 @@ (:name event) (:type event) (:profile-id event) + (some-> (:ip-addr event) db/inet) (db/tjson (:props event))])] (aa/with-thread executor (db/with-atomic [conn pool] (db/insert-multi! conn :audit-log - [:id :name :type :profile-id :props] + [:id :name :type :profile-id :ip-addr :props] (sequence (map event->row) events)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -147,17 +164,22 @@ (defn archive-events [{:keys [pool uri tokens] :as cfg}] - (letfn [(decode-row [{:keys [props] :as row}] + (letfn [(decode-row [{:keys [props ip-addr] :as row}] (cond-> row (db/pgobject? props) - (assoc :props (db/decode-transit-pgobject props)))) + (assoc :props (db/decode-transit-pgobject props)) - (row->event [{:keys [name type created-at profile-id props]}] - {:type type - :name name - :timestamp created-at - :profile-id profile-id - :props props}) + (db/pgobject? ip-addr "inet") + (assoc :ip-addr (db/decode-inet ip-addr)))) + + (row->event [{:keys [name type created-at profile-id props ip-addr]}] + (cond-> {:type type + :name name + :timestamp created-at + :profile-id profile-id + :props props} + (some? ip-addr) + (update :context assoc :source-ip ip-addr))) (send [events] (let [token (tokens :generate {:iss "authentication" @@ -168,7 +190,7 @@ "origin" (cf/get :public-uri) "cookie" (u/map->query-string {:auth-token token})} params {:uri uri - :timeout 5000 + :timeout 6000 :method :post :headers headers :body body} @@ -187,7 +209,6 @@ (db/with-atomic [conn pool] (let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log]) - xform (comp (map decode-row) (map row->event)) events (into [] xform rows)] diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index ffc7305f28..34120a3604 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -40,22 +40,24 @@ (defmethod ig/init-key ::reporter [_ {:keys [receiver uri] :as cfg}] - (l/info :msg "intializing mattermost error reporter" :uri uri) - (let [output (a/chan (a/sliding-buffer 128) - (filter #(= (:level %) "error")))] - (receiver :sub output) - (a/go-loop [] - (let [msg (a/>'~:content-type' != fmo.mtype; + diff --git a/backend/src/app/notifications.clj b/backend/src/app/notifications.clj index 75b7de0c62..80afa560f8 100644 --- a/backend/src/app/notifications.clj +++ b/backend/src/app/notifications.clj @@ -8,12 +8,12 @@ "A websocket based notifications mechanism." (:require [app.common.spec :as us] + [app.common.transit :as t] [app.db :as db] [app.metrics :as mtx] [app.util.async :as aa] [app.util.logging :as l] [app.util.time :as dt] - [app.util.transit :as t] [app.worker :as wrk] [clojure.core.async :as a] [clojure.spec.alpha :as s] @@ -163,7 +163,7 @@ ;; when connection is closed (mtx-aconn :dec) - (mtx-sessions :observe (/ (inst-ms (dt/duration-between created-at (dt/now))) 1000.0)) + (mtx-sessions :observe (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)) ;; close subscription (a/close! sub-ch)))) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 45598854d7..09a2a636a9 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -32,9 +32,10 @@ [methods {:keys [profile-id] :as request}] (let [type (keyword (get-in request [:path-params :type])) - data (d/merge (:params request) - (:body-params request) - (:uploads request)) + data (merge (:params request) + (:body-params request) + (:uploads request) + {::request request}) data (if profile-id (assoc data :profile-id profile-id) @@ -50,12 +51,15 @@ (defn- rpc-mutation-handler [methods {:keys [profile-id] :as request}] (let [type (keyword (get-in request [:path-params :type])) - data (d/merge (:params request) - (:body-params request) - (:uploads request)) + data (merge (:params request) + (:body-params request) + (:uploads request) + {::request request}) + data (if profile-id (assoc data :profile-id profile-id) (dissoc data :profile-id)) + result ((get methods type default-handler) data) mdata (meta result)] (cond->> {:status 200 :body result} @@ -85,7 +89,6 @@ (rlm/execute rlinst (f cfg params)))) f)) - (defn- wrap-impl [{:keys [audit] :as cfg} f mdata] (let [f (wrap-with-rlimits cfg f mdata) @@ -95,23 +98,34 @@ (l/trace :action "register" :name (::sv/name mdata)) (fn [params] + + ;; Raise authentication error when rpc method requires auth but + ;; no profile-id is found in the request. (when (and auth? (not (uuid? (:profile-id params)))) (ex/raise :type :authentication :code :authentication-required :hint "authentication required for this endpoint")) - (let [params (us/conform spec params) - result (f cfg params) - resultm (meta result)] - (when (and (::type cfg) (fn? audit)) - (let [profile-id (or (:profile-id params) + + (let [params' (dissoc params ::request) + params' (us/conform spec params') + result (f cfg params')] + + ;; When audit log is enabled (default false). + (when (fn? audit) + (let [resultm (meta result) + request (::request params) + profile-id (or (:profile-id params') (:profile-id result) (::audit/profile-id resultm)) props (d/merge params (::audit/props resultm))] - (audit :submit {:type (::type cfg) - :name (or (::audit/name resultm) - (::sv/name mdata)) - :profile-id profile-id - :props props}))) + (audit :cmd :submit + :type (::type cfg) + :name (or (::audit/name resultm) + (::sv/name mdata)) + :profile-id profile-id + :ip-addr (audit/parse-client-ip request) + :props (audit/profile->props props)))) + result)))) (defn- process-method diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index a74f8b4f80..fc7f184bf9 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -15,7 +15,7 @@ [app.rpc.mutations.profile :as profile] [app.setup.initial-data :as sid] [app.util.services :as sv] - [app.worker :as wrk] + [app.util.time :as dt] [buddy.core.codecs :as bc] [buddy.core.nonce :as bn] [clojure.spec.alpha :as s])) @@ -35,6 +35,7 @@ :email email :fullname fullname :is-demo true + :deleted-at (dt/in-future cfg/deletion-delay) :password password :props {:onboarding-viewed true}}] @@ -48,12 +49,6 @@ (#'profile/create-profile-relations conn) (sid/load-initial-project! conn)) - ;; Schedule deletion of the demo profile - (wrk/submit! {::wrk/task :delete-profile - ::wrk/delay cfg/deletion-delay - ::wrk/conn conn - :profile-id id}) - (with-meta {:email email :password password} {::audit/profile-id id})))) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index d6fb24c595..3c266268d3 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -11,17 +11,19 @@ [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.files :as files] [app.rpc.queries.projects :as proj] + [app.storage.impl :as simpl] [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] [clojure.spec.alpha :as s])) +(declare create-file) + ;; --- Helpers & Specs (s/def ::id ::us/uuid) @@ -32,8 +34,6 @@ ;; --- Mutation: Create File -(declare create-file) - (s/def ::is-shared ::us/boolean) (s/def ::create-file (s/keys :req-un [::profile-id ::name ::project-id] @@ -45,7 +45,6 @@ (proj/check-edition-permissions! conn profile-id project-id) (create-file conn params))) - (defn create-file-role [conn {:keys [file-id profile-id role]}] (let [params {:file-id file-id @@ -54,21 +53,24 @@ (db/insert! conn :file-profile-rel)))) (defn create-file - [conn {:keys [id name project-id is-shared] - :or {is-shared false} + [conn {:keys [id name project-id is-shared data deleted-at] + :or {is-shared false + deleted-at nil} :as params}] - (let [id (or id (uuid/next)) - data (cp/make-file-data id) + (let [id (or id (:id data) (uuid/next)) + data (or data (cp/make-file-data id)) file (db/insert! conn :file {:id id :project-id project-id :name name :is-shared is-shared - :data (blob/encode data)})] + :data (blob/encode data) + :deleted-at deleted-at})] + (->> (assoc params :file-id id :role :owner) (create-file-role conn)) - (assoc file :data data))) + (assoc file :data data))) ;; --- Mutation: Rename File @@ -109,7 +111,6 @@ {:is-shared is-shared} {:id id})) - ;; --- Mutation: Delete File (declare mark-file-deleted) @@ -122,13 +123,6 @@ (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) - ;; Schedule object deletion - (wrk/submit! {::wrk/task :delete-object - ::wrk/delay cfg/deletion-delay - ::wrk/conn conn - :id id - :type :file}) - (mark-file-deleted conn params))) (defn mark-file-deleted @@ -175,7 +169,7 @@ (s/keys :req-un [::profile-id ::file-id ::library-id])) (sv/defmethod ::unlink-file-from-library - [{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (unlink-file-from-library conn params))) @@ -195,7 +189,7 @@ (s/keys :req-un [::profile-id ::file-id ::library-id])) (sv/defmethod ::update-sync - [{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (update-sync conn params))) @@ -207,7 +201,6 @@ {:file-id file-id :library-file-id library-id})) - ;; --- Mutation: Ignore updates in linked files (declare ignore-sync) @@ -216,7 +209,7 @@ (s/keys :req-un [::profile-id ::file-id ::date])) (sv/defmethod ::ignore-sync - [{:keys [pool] :as cfg} {:keys [profile-id file-id date] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (ignore-sync conn params))) @@ -278,15 +271,31 @@ (sv/defmethod ::update-file [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-update true})] + (db/xact-lock! conn id) + (let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-key-share true})] (files/check-edition-permissions! conn profile-id id) (update-file (assoc cfg :conn conn) (assoc params :file file))))) +(defn- take-snapshot? + "Defines the rule when file `data` snapshot should be saved." + [{:keys [revn modified-at] :as file}] + ;; The snapshot will be saved every 20 changes or if the last + ;; modification is older than 3 hour. + (or (zero? (mod revn 20)) + (> (inst-ms (dt/diff modified-at (dt/now))) + (inst-ms (dt/duration {:hours 3}))))) + +(defn- delete-from-storage + [{:keys [storage] :as cfg} file] + (when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))] + (simpl/del-object backend file))) + (defn- update-file [{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}] (when (> (:revn params) (:revn file)) + (ex/raise :type :validation :code :revn-conflict :hint "The incoming revision number is greater that stored version." @@ -297,7 +306,8 @@ (mapcat :changes changes-with-metadata) changes) - file (-> file + ts (dt/now) + file (-> (files/retrieve-data cfg file) (update :revn inc) (update :data (fn [data] (-> data @@ -311,26 +321,55 @@ {:id (uuid/next) :session-id session-id :profile-id profile-id + :created-at ts :file-id (:id file) :revn (:revn file) - :data (:data file) + :data (when (take-snapshot? file) + (:data file)) :changes (blob/encode changes)}) ;; Update file (db/update! conn :file {:revn (:revn file) :data (:data file) + :data-backend nil + :modified-at ts :has-media-trimmed false} {:id (:id file)}) - (let [params (-> params (assoc :file file - :changes changes))] + ;; We need to delete the data from external storage backend + (when-not (nil? (:data-backend file)) + (delete-from-storage cfg file)) + + (db/update! conn :project + {:modified-at ts} + {:id (:project-id file)}) + + (let [params (assoc params :file file :changes changes)] ;; Send asynchronous notifications (send-notifications cfg params) ;; Retrieve and return lagged data (retrieve-lagged-changes conn params)))) +(def ^:private + sql:lagged-changes + "select s.id, s.revn, s.file_id, + s.session_id, s.changes + from file_change as s + where s.file_id = ? + and s.revn > ? + order by s.created_at asc") + +(defn- retrieve-lagged-changes + [conn params] + (->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)]) + (into [] (comp (map files/decode-row) + (map (fn [row] + (cond-> row + (= (:revn row) (:revn (:file params))) + (assoc :changes [])))))))) + (defn- send-notifications [{:keys [msgbus conn] :as cfg} {:keys [file changes session-id] :as params}] (let [lchanges (filter library-change? changes)] @@ -362,17 +401,24 @@ [conn project-id] (:team-id (db/get-by-id conn :project project-id {:columns [:team-id]}))) -(def ^:private - sql:lagged-changes - "select s.id, s.revn, s.file_id, - s.session_id, s.changes - from file_change as s - where s.file_id = ? - and s.revn > ? - order by s.created_at asc") -(defn- retrieve-lagged-changes - [conn params] - (->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)]) - (mapv files/decode-row))) +;; TEMPORARY FILE CREATION +(s/def ::create-temp-file ::create-file) + +(sv/defmethod ::create-temp-file + [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] + (db/with-atomic [conn pool] + (proj/check-edition-permissions! conn profile-id project-id) + (create-file conn (assoc params :deleted-at (dt/in-future {:days 1}))))) + +(s/def ::persist-temp-file + (s/keys :req-un [::id ::profile-id])) + +(sv/defmethod ::persist-temp-file + [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + (db/with-atomic [conn pool] + (files/check-edition-permissions! conn profile-id id) + (db/update! conn :file + {:deleted-at nil} + {:id id}))) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index ca1d2263e7..9037550f13 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -104,21 +104,10 @@ (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) - (let [items (db/query conn :team-font-variant - {:font-id id :team-id team-id} - {:for-update true})] - (doseq [item items] - ;; Schedule object deletion - (wrk/submit! {::wrk/task :delete-object - ::wrk/delay cf/deletion-delay - ::wrk/conn conn - :id (:id item) - :type :team-font-variant})) - - (db/update! conn :team-font-variant - {:deleted-at (dt/now)} - {:font-id id :team-id team-id}) - nil))) + (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:font-id id :team-id team-id}) + nil)) ;; --- DELETE FONT VARIANT diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj index 8b5f93ff04..d327589d89 100644 --- a/backend/src/app/rpc/mutations/ldap.clj +++ b/backend/src/app/rpc/mutations/ldap.clj @@ -9,7 +9,10 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.config :as cfg] - [app.rpc.mutations.profile :refer [login-or-register]] + [app.db :as db] + [app.loggers.audit :as audit] + [app.rpc.mutations.profile :as profile-m] + [app.rpc.queries.profile :as profile-q] [app.util.services :as sv] [clj-ldap.client :as ldap] [clojure.spec.alpha :as s] @@ -34,6 +37,7 @@ ;; --- Mutation: login-with-ldap (declare authenticate) +(declare login-or-register) (s/def ::email ::us/email) (s/def ::password ::us/string) @@ -44,31 +48,37 @@ :opt-un [::invitation-token])) (sv/defmethod ::login-with-ldap {:auth false :rlimit :password} - [{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}] - (let [info (authenticate params) - cfg (assoc cfg :conn pool)] - (when-not info - (ex/raise :type :validation - :code :wrong-credentials)) - (let [profile (login-or-register cfg {:email (:email info) - :backend (:backend info) - :fullname (:fullname info)})] - (if-let [token (:invitation-token params)] - ;; If invitation token comes in params, this is because the - ;; user comes from team-invitation process; in this case, - ;; regenerate token and send back to the user a new invitation - ;; token (and mark current session as logged). - (let [claims (tokens :verify {:token token :iss :team-invitation}) - claims (assoc claims - :member-id (:id profile) - :member-email (:email profile)) - token (tokens :generate claims)] - (with-meta - {:invitation-token token} - {:transform-response ((:create session) (:id profile))})) + [{:keys [pool session tokens] :as cfg} params] + (db/with-atomic [conn pool] + (let [info (authenticate params) + cfg (assoc cfg :conn conn)] - (with-meta profile - {:transform-response ((:create session) (:id profile))}))))) + (when-not info + (ex/raise :type :validation + :code :wrong-credentials)) + + (let [profile (login-or-register cfg {:email (:email info) + :backend (:backend info) + :fullname (:fullname info)})] + (if-let [token (:invitation-token params)] + ;; If invitation token comes in params, this is because the + ;; user comes from team-invitation process; in this case, + ;; regenerate token and send back to the user a new invitation + ;; token (and mark current session as logged). + (let [claims (tokens :verify {:token token :iss :team-invitation}) + claims (assoc claims + :member-id (:id profile) + :member-email (:email profile)) + token (tokens :generate claims)] + (with-meta {:invitation-token token} + {:transform-response ((:create session) (:id profile)) + ::audit/props (:props profile) + ::audit/profile-id (:id profile)})) + + (with-meta profile + {:transform-response ((:create session) (:id profile)) + ::audit/props (:props profile) + ::audit/profile-id (:id profile)})))))) (defn- replace-several [s & {:as replacements}] (reduce-kv clojure.string/replace s replacements)) @@ -88,11 +98,25 @@ (first (ldap/search cpool base-dn params)))) (defn- authenticate - [{:keys [password] :as params}] + [{:keys [password email] :as params}] (with-open [conn (connect)] (when-let [{:keys [dn] :as luser} (get-ldap-user conn params)] (when (ldap/bind? conn dn password) {:photo (get luser (keyword (cfg/get :ldap-attrs-photo))) :fullname (get luser (keyword (cfg/get :ldap-attrs-fullname))) - :email (get luser (keyword (cfg/get :ldap-attrs-email))) + :email email :backend "ldap"})))) + +(defn- login-or-register + [{:keys [conn] :as cfg} info] + (or (some->> (:email info) + (profile-q/retrieve-profile-data-by-email conn) + (profile-q/populate-additional-data conn) + (profile-q/decode-profile-row)) + (let [params (-> info + (assoc :is-active true) + (assoc :is-demo false))] + (->> params + (profile-m/create-profile conn) + (profile-m/create-profile-relations conn) + (profile-q/strip-private-attrs))))) diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj index 7ef6207263..1cf1117292 100644 --- a/backend/src/app/rpc/mutations/management.clj +++ b/backend/src/app/rpc/mutations/management.clj @@ -167,7 +167,7 @@ :opt-un [::name])) (sv/defmethod ::duplicate-file - [{:keys [pool] :as cfg} {:keys [profile-id file-id name] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (let [file (db/get-by-id conn :file file-id) index {file-id (uuid/next)} @@ -187,7 +187,7 @@ :opt-un [::name])) (sv/defmethod ::duplicate-project - [{:keys [pool] :as cfg} {:keys [profile-id project-id name] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] (db/with-atomic [conn pool] (let [project (db/get-by-id conn :project project-id)] (teams/check-edition-permissions! conn profile-id (:team-id project)) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 62241a48bd..3467522f1d 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -92,7 +92,7 @@ (defn create-file-media-object - [{:keys [conn storage] :as cfg} {:keys [file-id is-local name content] :as params}] + [{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}] (media/validate-media-type (:content-type content)) (let [storage (assoc storage :conn conn) source-path (fs/path (:tempfile content)) @@ -118,7 +118,7 @@ (sto/put-object storage {:content (sto/content (:data thumb) (:size thumb)) :content-type (:mtype thumb)}))] (db/insert! conn :file-media-object - {:id (uuid/next) + {:id (or id (uuid/next)) :file-id file-id :is-local is-local :name name diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 71c2a0d900..17c52869a7 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -22,7 +22,6 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] [buddy.hashers :as hashers] [clojure.spec.alpha :as s] [cuerdas.core :as str])) @@ -37,106 +36,14 @@ (s/def ::password ::us/not-empty-string) (s/def ::old-password ::us/not-empty-string) (s/def ::theme ::us/string) - -;; --- Mutation: Register Profile +(s/def ::invitation-token ::us/not-empty-string) (declare annotate-profile-register) (declare check-profile-existence!) (declare create-profile) (declare create-profile-relations) -(declare email-domain-in-whitelist?) (declare register-profile) -(s/def ::invitation-token ::us/not-empty-string) -(s/def ::terms-privacy ::us/boolean) - -(s/def ::register-profile - (s/keys :req-un [::email ::password ::fullname ::terms-privacy] - :opt-un [::invitation-token])) - -(sv/defmethod ::register-profile {:auth false :rlimit :password} - [{:keys [pool tokens session] :as cfg} params] - (when-not (cfg/get :registration-enabled) - (ex/raise :type :restriction - :code :registration-disabled)) - - (when-let [domains (cfg/get :registration-domain-whitelist)] - (when-not (email-domain-in-whitelist? domains (:email params)) - (ex/raise :type :validation - :code :email-domain-is-not-allowed))) - - (when-not (:terms-privacy params) - (ex/raise :type :validation - :code :invalid-terms-and-privacy)) - - (db/with-atomic [conn pool] - (let [cfg (assoc cfg :conn conn)] - (register-profile cfg params)))) - -(defn- annotate-profile-register - "A helper for properly increase the profile-register metric once the - transaction is completed." - [metrics profile] - (fn [] - (when (::created profile) - ((get-in metrics [:definitions :profile-register]) :inc)))) - -(defn- register-profile - [{:keys [conn tokens session metrics] :as cfg} params] - (check-profile-existence! conn params) - (let [profile (->> (create-profile conn params) - (create-profile-relations conn)) - profile (assoc profile ::created true)] - - (sid/load-initial-project! conn profile) - - (if-let [token (:invitation-token params)] - ;; If invitation token comes in params, this is because the - ;; user comes from team-invitation process; in this case, - ;; regenerate token and send back to the user a new invitation - ;; token (and mark current session as logged). - (let [claims (tokens :verify {:token token :iss :team-invitation}) - claims (assoc claims - :member-id (:id profile) - :member-email (:email profile)) - token (tokens :generate claims) - resp {:invitation-token token}] - (with-meta resp - {:transform-response ((:create session) (:id profile)) - :before-complete (annotate-profile-register metrics profile) - ::audit/props (:props profile) - ::audit/profile-id (:id profile)})) - - ;; If no token is provided, send a verification email - (let [vtoken (tokens :generate - {:iss :verify-email - :exp (dt/in-future "48h") - :profile-id (:id profile) - :email (:email profile)}) - ptoken (tokens :generate-predefined - {:iss :profile-identity - :profile-id (:id profile)})] - - ;; Don't allow proceed in register page if the email is - ;; already reported as permanent bounced - (when (eml/has-bounce-reports? conn (:email profile)) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email has one or many bounces reported")) - - (eml/send! {::eml/conn conn - ::eml/factory eml/register - :public-uri (:public-uri cfg) - :to (:email profile) - :name (:fullname profile) - :token vtoken - :extra-data ptoken}) - - (with-meta profile - {:before-complete (annotate-profile-register metrics profile) - ::audit/props (:props profile) - ::audit/profile-id (:id profile)}))))) - (defn email-domain-in-whitelist? "Returns true if email's domain is in the given whitelist or if given whitelist is an empty string." @@ -178,28 +85,176 @@ {:update false :valid false}))) +(defn decode-profile-row + [{:keys [props] :as profile}] + (cond-> profile + (db/pgobject? props "jsonb") + (assoc :props (db/decode-transit-pgobject props)))) + +;; --- MUTATION: Prepare Register + +(s/def ::prepare-register-profile + (s/keys :req-un [::email ::password] + :opt-un [::invitation-token])) + +(sv/defmethod ::prepare-register-profile {:auth false} + [{:keys [pool tokens] :as cfg} params] + (when-not (cfg/get :registration-enabled) + (ex/raise :type :restriction + :code :registration-disabled)) + + (when-let [domains (cfg/get :registration-domain-whitelist)] + (when-not (email-domain-in-whitelist? domains (:email params)) + (ex/raise :type :validation + :code :email-domain-is-not-allowed))) + + ;; Don't allow proceed in preparing registration if the profile is + ;; already reported as spamer. + (when (eml/has-bounce-reports? pool (:email params)) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :hint "looks like the email has one or many bounces reported")) + + (check-profile-existence! pool params) + + (let [params (assoc params + :backend "penpot" + :iss :prepared-register + :exp (dt/in-future "48h")) + token (tokens :generate params)] + {:token token})) + +;; --- MUTATION: Register Profile + +(s/def ::accept-terms-and-privacy ::us/boolean) +(s/def ::accept-newsletter-subscription ::us/boolean) +(s/def ::token ::us/not-empty-string) + +(s/def ::register-profile + (s/keys :req-un [::token ::fullname + ::accept-terms-and-privacy] + :opt-un [::accept-newsletter-subscription])) + +(sv/defmethod ::register-profile {:auth false :rlimit :password} + [{:keys [pool] :as cfg} params] + (when-not (:accept-terms-and-privacy params) + (ex/raise :type :validation + :code :invalid-terms-and-privacy)) + + (db/with-atomic [conn pool] + (let [cfg (assoc cfg :conn conn)] + (register-profile cfg params)))) + +(defn- annotate-profile-register + "A helper for properly increase the profile-register metric once the + transaction is completed." + [metrics] + (fn [] + ((get-in metrics [:definitions :profile-register]) :inc))) + +(defn register-profile + [{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}] + (let [claims (tokens :verify {:token token :iss :prepared-register}) + params (merge params claims)] + (check-profile-existence! conn params) + (let [profile (->> params + (create-profile conn) + (create-profile-relations conn) + (decode-profile-row))] + (sid/load-initial-project! conn profile) + + (cond + ;; If invitation token comes in params, this is because the + ;; user comes from team-invitation process; in this case, + ;; regenerate token and send back to the user a new invitation + ;; token (and mark current session as logged). + (some? (:invitation-token params)) + (let [token (:invitation-token params) + claims (tokens :verify {:token token :iss :team-invitation}) + claims (assoc claims + :member-id (:id profile) + :member-email (:email profile)) + token (tokens :generate claims) + resp {:invitation-token token}] + (with-meta resp + {:transform-response ((:create session) (:id profile)) + :before-complete (annotate-profile-register metrics) + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)})) + + ;; If auth backend is different from "penpot" means user is + ;; registring using third party auth mechanism; in this case + ;; we need to mark this session as logged. + (not= "penpot" (:auth-backend profile)) + (with-meta (profile/strip-private-attrs profile) + {:transform-response ((:create session) (:id profile)) + :before-complete (annotate-profile-register metrics) + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}) + + ;; In all other cases, send a verification email. + :else + (let [vtoken (tokens :generate + {:iss :verify-email + :exp (dt/in-future "48h") + :profile-id (:id profile) + :email (:email profile)}) + ptoken (tokens :generate-predefined + {:iss :profile-identity + :profile-id (:id profile)})] + + (eml/send! {::eml/conn conn + ::eml/factory eml/register + :public-uri (:public-uri cfg) + :to (:email profile) + :name (:fullname profile) + :token vtoken + :extra-data ptoken}) + + (with-meta profile + {:before-complete (annotate-profile-register metrics) + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)})))))) + (defn create-profile - "Create the profile entry on the database with limited input - filling all the other fields with defaults." - [conn {:keys [id fullname email password is-active is-muted is-demo opts] - :or {is-active false is-muted false is-demo false} - :as params}] - (let [id (or id (uuid/next)) - is-active (if is-demo true is-active) - props (-> params extract-props db/tjson) - password (derive-password password) + "Create the profile entry on the database with limited input filling + all the other fields with defaults." + [conn params] + (let [id (or (:id params) (uuid/next)) + + props (-> (extract-props params) + (merge (:props params)) + (assoc :accept-terms-and-privacy (:accept-terms-and-privacy params true)) + (assoc :accept-newsletter-subscription (:accept-newsletter-subscription params false)) + (db/tjson)) + + password (if-let [password (:password params)] + (derive-password password) + "!") + + locale (as-> (:locale params) locale + (and (string? locale) (not (str/blank? locale)) locale)) + + backend (:backend params "penpot") + is-demo (:is-demo params false) + is-muted (:is-muted params false) + is-active (:is-active params (or (not= "penpot" backend) is-demo)) + email (str/lower (:email params)) + params {:id id - :fullname fullname - :email (str/lower email) - :auth-backend "penpot" + :fullname (:fullname params) + :email email + :auth-backend backend + :lang locale :password password + :deleted-at (:deleted-at params) :props props :is-active is-active :is-muted is-muted :is-demo is-demo}] (try - (-> (db/insert! conn :profile params opts) - (update :props db/decode-transit-pgobject)) + (-> (db/insert! conn :profile params) + (decode-profile-row)) (catch org.postgresql.util.PSQLException e (let [state (.getSQLState e)] (if (not= state "23505") @@ -231,7 +286,7 @@ (assoc :default-team-id (:id team)) (assoc :default-project-id (:id project))))) -;; --- Mutation: Login +;; --- MUTATION: Login (s/def ::email ::us/email) (s/def ::scope ::us/string) @@ -241,7 +296,7 @@ :opt-un [::scope ::invitation-token])) (sv/defmethod ::login {:auth false :rlimit :password} - [{:keys [pool session tokens] :as cfg} {:keys [email password scope] :as params}] + [{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}] (letfn [(check-password [profile password] (when (= (:password profile) "!") (ex/raise :type :validation @@ -264,7 +319,8 @@ (let [profile (->> (profile/retrieve-profile-data-by-email conn email) (validate-profile) (profile/strip-private-attrs) - (profile/populate-additional-data conn))] + (profile/populate-additional-data conn) + (decode-profile-row))] (if-let [token (:invitation-token params)] ;; If the request comes with an invitation token, this means ;; that user wants to accept it with different user. A very @@ -279,90 +335,25 @@ token (tokens :generate claims)] (with-meta {:invitation-token token} {:transform-response ((:create session) (:id profile)) + ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})) (with-meta profile {:transform-response ((:create session) (:id profile)) + ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))))) -;; --- Mutation: Logout +;; --- MUTATION: Logout (s/def ::logout (s/keys :req-un [::profile-id])) (sv/defmethod ::logout - [{:keys [pool session] :as cfg} {:keys [profile-id] :as params}] + [{:keys [session] :as cfg} _] (with-meta {} {:transform-response (:delete session)})) - -;; --- Mutation: Register if not exists - -(declare login-or-register) - -(s/def ::backend ::us/string) -(s/def ::login-or-register - (s/keys :req-un [::email ::fullname ::backend])) - -(sv/defmethod ::login-or-register {:auth false} - [{:keys [pool metrics] :as cfg} params] - (db/with-atomic [conn pool] - (let [profile (-> (assoc cfg :conn conn) - (login-or-register params)) - props (merge - (select-keys profile [:backend :fullname :email]) - (:props profile))] - (with-meta profile - {:before-complete (annotate-profile-register metrics profile) - ::audit/name (if (::created profile) "register" "login") - ::audit/props props - ::audit/profile-id (:id profile)})))) - -(defn login-or-register - [{:keys [conn] :as cfg} {:keys [email] :as params}] - (letfn [(info->lang [{:keys [locale] :as info}] - (when (and (string? locale) - (not (str/blank? locale))) - locale)) - - (create-profile [conn {:keys [fullname backend email props] :as info}] - (let [params {:id (uuid/next) - :fullname fullname - :email (str/lower email) - :lang (info->lang props) - :auth-backend backend - :is-active true - :password "!" - :props (db/tjson props) - :is-demo false}] - (-> (db/insert! conn :profile params) - (update :props db/decode-transit-pgobject)))) - - (update-profile [conn info profile] - (let [props (merge (:props profile) - (:props info))] - (db/update! conn :profile - {:props (db/tjson props) - :modified-at (dt/now)} - {:id (:id profile)}) - (assoc profile :props props))) - - (register-profile [conn params] - (let [profile (->> (create-profile conn params) - (create-profile-relations conn))] - (sid/load-initial-project! conn profile) - (assoc profile ::created true)))] - - (let [profile (profile/retrieve-profile-data-by-email conn email) - profile (if profile - (->> profile - (update-profile conn params) - (profile/populate-additional-data conn)) - (register-profile conn params))] - (profile/strip-private-attrs profile)))) - - -;; --- Mutation: Update Profile (own) +;; --- MUTATION: Update Profile (own) (defn- update-profile [conn {:keys [id fullname lang theme] :as params}] @@ -382,7 +373,7 @@ (update-profile conn params) nil)) -;; --- Mutation: Update Password +;; --- MUTATION: Update Password (declare validate-password!) (declare update-profile-password!) @@ -391,7 +382,7 @@ (s/keys :req-un [::profile-id ::password ::old-password])) (sv/defmethod ::update-profile-password {:rlimit :password} - [{:keys [pool] :as cfg} {:keys [password profile-id] :as params}] + [{:keys [pool] :as cfg} {:keys [password] :as params}] (db/with-atomic [conn pool] (let [profile (validate-password! conn params)] (update-profile-password! conn (assoc profile :password password)) @@ -411,7 +402,7 @@ {:password (derive-password password)} {:id id})) -;; --- Mutation: Update Photo +;; --- MUTATION: Update Photo (declare update-profile-photo) @@ -446,7 +437,7 @@ nil) -;; --- Mutation: Request Email Change +;; --- MUTATION: Request Email Change (declare request-email-change) (declare change-email-inmediatelly) @@ -514,7 +505,7 @@ [conn id] (db/get-by-id conn :profile id {:for-update true})) -;; --- Mutation: Request Profile Recovery +;; --- MUTATION: Request Profile Recovery (s/def ::request-profile-recovery (s/keys :req-un [::email])) @@ -563,7 +554,7 @@ (send-email-notification conn)))))) -;; --- Mutation: Recover Profile +;; --- MUTATION: Recover Profile (s/def ::token ::us/not-empty-string) (s/def ::recover-profile @@ -584,7 +575,7 @@ (update-password conn)) nil))) -;; --- Mutation: Update Profile Props +;; --- MUTATION: Update Profile Props (s/def ::props map?) (s/def ::update-profile-props @@ -606,7 +597,7 @@ nil))) -;; --- Mutation: Delete Profile +;; --- MUTATION: Delete Profile (declare check-can-delete-profile!) (declare mark-profile-as-deleted!) @@ -619,12 +610,6 @@ (db/with-atomic [conn pool] (check-can-delete-profile! conn profile-id) - ;; Schedule a complete deletion of profile - (wrk/submit! {::wrk/task :delete-profile - ::wrk/delay cfg/deletion-delay - ::wrk/conn conn - :profile-id profile-id}) - (db/update! conn :profile {:deleted-at (dt/now)} {:id profile-id}) diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index bc25ef8ff3..357b3841cb 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -8,14 +8,12 @@ (:require [app.common.spec :as us] [app.common.uuid :as uuid] - [app.config :as cfg] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as proj] [app.rpc.queries.teams :as teams] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] [clojure.spec.alpha :as s])) ;; --- Helpers & Specs @@ -123,14 +121,6 @@ [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] (proj/check-edition-permissions! conn profile-id id) - - ;; Schedule object deletion - (wrk/submit! {::wrk/task :delete-object - ::wrk/delay cfg/deletion-delay - ::wrk/conn conn - :id id - :type :project}) - (db/update! conn :project {:deleted-at (dt/now)} {:id id}) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 662d2dc354..3b71c43c48 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -10,7 +10,6 @@ [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 eml] [app.media :as media] @@ -21,7 +20,6 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] [clojure.spec.alpha :as s] [datoteka.core :as fs])) @@ -135,13 +133,6 @@ (ex/raise :type :validation :code :only-owner-can-delete-team)) - ;; Schedule object deletion - (wrk/submit! {::wrk/task :delete-object - ::wrk/delay cfg/deletion-delay - ::wrk/conn conn - :id id - :type :team}) - (db/update! conn :team {:deleted-at (dt/now)} {:id id}) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index dd36c64134..74938feb23 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -9,10 +9,12 @@ [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] [app.rpc.queries.teams :as teams] + [app.storage.impl :as simpl] [app.util.blob :as blob] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -171,11 +173,23 @@ ;; --- Query: File (By ID) +(defn- retrieve-data* + [{:keys [storage] :as cfg} file] + (when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))] + (simpl/get-object-bytes backend file))) + +(defn retrieve-data + [cfg file] + (if (bytes? (:data file)) + file + (assoc file :data (retrieve-data* cfg file)))) + (defn retrieve-file - [conn id] - (-> (db/get-by-id conn :file id) - (decode-row) - (pmg/migrate-file))) + [{:keys [conn] :as cfg} id] + (->> (db/get-by-id conn :file id) + (retrieve-data cfg) + (decode-row) + (pmg/migrate-file))) (s/def ::file (s/keys :req-un [::profile-id ::id])) @@ -183,8 +197,9 @@ (sv/defmethod ::file [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id id) - (retrieve-file conn id))) + (let [cfg (assoc cfg :conn conn)] + (check-edition-permissions! conn profile-id id) + (retrieve-file cfg id)))) (s/def ::page (s/keys :req-un [::profile-id ::file-id])) @@ -217,11 +232,11 @@ (update data :objects update-objects))) (sv/defmethod ::page - [{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}] - [{:keys [pool] :as cfg} {:keys [profile-id file-id]}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) - (let [file (retrieve-file conn file-id) + (let [cfg (assoc cfg :conn conn) + file (retrieve-file cfg file-id) page-id (get-in file [:data :pages 0])] (cond-> (get-in file [:data :pages-index page-id]) strip-thumbnails @@ -245,7 +260,7 @@ (s/keys :req-un [::profile-id ::team-id])) (sv/defmethod ::shared-files - [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [pool] :as cfg} {:keys [team-id] :as params}] (into [] decode-row-xf (db/exec! pool [sql:shared-files team-id]))) @@ -270,30 +285,43 @@ (s/keys :req-un [::profile-id ::team-id])) (sv/defmethod ::team-shared-files - [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [pool] :as cfg} {:keys [team-id] :as params}] (db/exec! pool [sql:team-shared-files team-id])) ;; --- Query: File Libraries used by a File (def ^:private sql:file-libraries - "select fl.*, - - flr.synced_at as synced_at - from file as fl - inner join file_library_rel as flr on (flr.library_file_id = fl.id) - where flr.file_id = ? - and fl.deleted_at is null") + "WITH RECURSIVE libs AS ( + SELECT fl.*, flr.synced_at + FROM file AS fl + JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) + WHERE flr.file_id = ?::uuid + UNION + SELECT fl.*, flr.synced_at + FROM file AS fl + JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) + JOIN libs AS l ON (flr.file_id = l.id) + ) + SELECT l.id, + l.data, + l.project_id, + l.created_at, + l.modified_at, + l.deleted_at, + l.name, + l.revn, + l.synced_at + FROM libs AS l + WHERE l.deleted_at IS NULL OR l.deleted_at > now();") (defn retrieve-file-libraries - [conn is-indirect file-id] - (let [libraries (->> (db/exec! conn [sql:file-libraries file-id]) - (map #(assoc % :is-indirect is-indirect)) - (into #{} decode-row-xf))] - (reduce #(into %1 (retrieve-file-libraries conn true %2)) - libraries - (map :id libraries)))) - + [{:keys [conn] :as cfg} is-indirect file-id] + (let [xform (comp + (map #(assoc % :is-indirect is-indirect)) + (map #(retrieve-data cfg %)) + (map decode-row))] + (into #{} xform (db/exec! conn [sql:file-libraries file-id])))) (s/def ::file-libraries (s/keys :req-un [::profile-id ::file-id])) @@ -301,8 +329,9 @@ (sv/defmethod ::file-libraries [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id file-id) - (retrieve-file-libraries conn false file-id))) + (let [cfg (assoc cfg :conn conn)] + (check-edition-permissions! conn profile-id file-id) + (retrieve-file-libraries cfg false file-id)))) ;; --- QUERY: team-recent-files @@ -334,7 +363,6 @@ (teams/check-read-permissions! conn profile-id team-id) (db/exec! conn [sql:team-recent-files team-id]))) - ;; --- Helpers (defn decode-row diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index ee974c3596..da36bb6647 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -11,7 +11,8 @@ [app.common.uuid :as uuid] [app.db :as db] [app.util.services :as sv] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [cuerdas.core :as str])) ;; --- Helpers & Specs @@ -72,7 +73,8 @@ (defn decode-profile-row [{:keys [props] :as row}] (cond-> row - (db/pgobject? props) (assoc :props (db/decode-transit-pgobject props)))) + (db/pgobject? props "jsonb") + (assoc :props (db/decode-transit-pgobject props)))) (defn retrieve-profile-data [conn id] @@ -90,16 +92,11 @@ profile)) -(def sql:retrieve-profile-by-email - "select p.* from profile as p - where p.email = lower(?) - and p.deleted_at is null") - (defn retrieve-profile-data-by-email [conn email] - (let [sql [sql:retrieve-profile-by-email email]] - (some-> (db/exec-one! conn sql) - (decode-profile-row)))) + (try + (db/get-by-params conn :profile {:email (str/lower email)}) + (catch Exception _e))) ;; --- Attrs Helpers diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index f346e53efa..dfe95314cc 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -42,12 +42,13 @@ (sv/defmethod ::viewer-bundle {:auth false} [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id token] :as params}] (db/with-atomic [conn pool] - (let [file (files/retrieve-file conn file-id) + (let [cfg (assoc cfg :conn conn) + file (files/retrieve-file cfg file-id) project (retrieve-project conn (:project-id file)) page (get-in file [:data :pages-index page-id]) file (merge (dissoc file :data) (select-keys (:data file) [:colors :media :typographies])) - libs (files/retrieve-file-libraries conn false file-id) + libs (files/retrieve-file-libraries cfg false file-id) users (teams/retrieve-users conn (:team-id project)) fonts (db/query conn :team-font-variant diff --git a/backend/src/app/setup/keys.clj b/backend/src/app/setup/keys.clj new file mode 100644 index 0000000000..372b83a201 --- /dev/null +++ b/backend/src/app/setup/keys.clj @@ -0,0 +1,29 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.setup.keys + "Keys derivation service." + (:require + [app.common.spec :as us] + [buddy.core.kdf :as bk] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(s/def ::secret-key ::us/string) +(s/def ::props (s/keys :req-un [::secret-key])) + +(defmethod ig/pre-init-spec :app.setup/keys [_] + (s/keys :req-un [::props])) + +(defmethod ig/init-key :app.setup/keys + [_ {:keys [props] :as cfg}] + (fn [& {:keys [salt _]}] + (let [engine (bk/engine {:key (:secret-key props) + :salt salt + :alg :hkdf + :digest :blake2b-512})] + (bk/get-bytes engine 32)))) + diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index 71ef20a6d7..9d7c6c8250 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -9,6 +9,7 @@ (:require [app.common.spec :as us] [app.srepl.main] + [app.util.logging :as l] [clojure.core.server :as ccs] [clojure.main :as cm] [clojure.spec.alpha :as s] @@ -41,14 +42,17 @@ (defmethod ig/init-key ::server [_ {:keys [port host name] :as cfg}] - (ccs/start-server {:address host - :port port - :name name - :accept 'app.srepl/repl}) - cfg) + (when (and port host name) + (l/info :msg "initializing server repl" :port port :host host :name name) + (ccs/start-server {:address host + :port port + :name name + :accept 'app.srepl/repl}) + cfg)) (defmethod ig/halt-key! ::server [_ cfg] - (ccs/stop-server (:name cfg))) + (when cfg + (ccs/stop-server (:name cfg)))) diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 0b1c1ffe15..f8a923a2de 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -5,7 +5,7 @@ ;; Copyright (c) UXBOX Labs SL (ns app.storage - "File Storage abstraction layer." + "Objects storage abstraction layer." (:require [app.common.data :as d] [app.common.exceptions :as ex] @@ -20,13 +20,9 @@ [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [datoteka.core :as fs] [integrant.core :as ig] - [promesa.exec :as px]) - (:import - java.io.InputStream)) - + [promesa.exec :as px])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Storage Module State @@ -39,7 +35,11 @@ (s/def ::db ::sdb/backend) (s/def ::backends - (s/keys :opt-un [::s3 ::fs ::db])) + (s/map-of ::us/keyword + (s/nilable + (s/or :s3 ::ss3/backend + :fs ::sfs/backend + :db ::sdb/backend)))) (defmethod ig/pre-init-spec ::storage [_] (s/keys :req-un [::backend ::wrk/executor ::db/pool ::backends])) @@ -50,8 +50,9 @@ (assoc :backends (d/without-nils backends)))) (defmethod ig/init-key ::storage - [_ cfg] - cfg) + [_ {:keys [backends] :as cfg}] + (-> (d/without-nils cfg) + (assoc :backends (d/without-nils backends)))) (s/def ::storage (s/keys :req-un [::backends ::wrk/executor ::db/pool ::backend])) @@ -151,8 +152,6 @@ ;; API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare resolve-backend) - (defn object->relative-path [{:keys [id] :as obj}] (impl/id->path id)) @@ -185,7 +184,7 @@ (px/run! executor #(register-recheck storage backend (:id object))) ;; Store the data finally on the underlying storage subsystem. - (-> (resolve-backend storage backend) + (-> (impl/resolve-backend storage backend) (impl/put-object object content)) object)) @@ -201,28 +200,37 @@ ;; if the source and destination backends are the same, we ;; proceed to use the fast path with specific copy ;; implementation on backend. - (-> (resolve-backend storage (:backend storage)) + (-> (impl/resolve-backend storage (:backend storage)) (impl/copy-object object object*)) ;; if the source and destination backends are different, we just ;; need to obtain the streams and proceed full copy of the data - (with-open [^InputStream input - (-> (resolve-backend storage (:backend object)) - (impl/get-object-data object))] - (-> (resolve-backend storage (:backend storage)) - (impl/put-object object* (impl/content input (:size object)))))) - + (with-open [is (-> (impl/resolve-backend storage (:backend object)) + (impl/get-object-data object))] + (-> (impl/resolve-backend storage (:backend storage)) + (impl/put-object object* (impl/content is (:size object)))))) object*)) (defn get-object-data + "Return an input stream instance of the object content." [{:keys [pool conn] :as storage} object] (us/assert ::storage storage) (when (or (nil? (:expired-at object)) (dt/is-after? (:expired-at object) (dt/now))) (-> (assoc storage :conn (or conn pool)) - (resolve-backend (:backend object)) + (impl/resolve-backend (:backend object)) (impl/get-object-data object)))) +(defn get-object-bytes + "Returns a byte array of object content." + [{:keys [pool conn] :as storage} object] + (us/assert ::storage storage) + (when (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) + (-> (assoc storage :conn (or conn pool)) + (impl/resolve-backend (:backend object)) + (impl/get-object-bytes object)))) + (defn get-object-url ([storage object] (get-object-url storage object nil)) @@ -231,14 +239,14 @@ (when (or (nil? (:expired-at object)) (dt/is-after? (:expired-at object) (dt/now))) (-> (assoc storage :conn (or conn pool)) - (resolve-backend (:backend object)) + (impl/resolve-backend (:backend object)) (impl/get-object-url object options))))) (defn get-object-path "Get the Path to the object. Only works with `:fs` type of storages." [storage object] - (let [backend (resolve-backend storage (:backend object))] + (let [backend (impl/resolve-backend storage (:backend object))] (when (not= :fs (:type backend)) (ex/raise :type :internal :code :operation-not-allowed @@ -254,16 +262,7 @@ (-> (assoc storage :conn (or conn pool)) (delete-database-object (if (uuid? id-or-obj) id-or-obj (:id id-or-obj))))) -;; --- impl - -(defn resolve-backend - [{:keys [conn pool] :as storage} backend-id] - (let [backend (get-in storage [:backends backend-id])] - (when-not backend - (ex/raise :type :internal - :code :backend-not-configured - :hint (str/fmt "backend '%s' not configured" backend-id))) - (assoc backend :conn (or conn pool)))) +(d/export impl/resolve-backend) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Garbage Collection: Permanently delete objects @@ -295,7 +294,7 @@ (some-> (seq rows) (group-by-backend)))) (delete-in-bulk [conn [backend ids]] - (let [backend (resolve-backend storage backend) + (let [backend (impl/resolve-backend storage backend) backend (assoc backend :conn conn)] (impl/del-objects-in-bulk backend ids)))] @@ -445,7 +444,7 @@ (some-> (seq rows) (group-results)))) (delete-group [conn [backend ids]] - (let [backend (resolve-backend storage backend) + (let [backend (impl/resolve-backend storage backend) backend (assoc backend :conn conn)] (impl/del-objects-in-bulk backend ids))) diff --git a/backend/src/app/storage/db.clj b/backend/src/app/storage/db.clj index a7ed7adbc1..0890f74552 100644 --- a/backend/src/app/storage/db.clj +++ b/backend/src/app/storage/db.clj @@ -46,12 +46,24 @@ (let [result (db/exec-one! conn ["select data from storage_data where id=?" id])] (ByteArrayInputStream. (:data result)))) +(defmethod impl/get-object-bytes :db + [{:keys [conn] :as backend} {:keys [id] :as object}] + (let [result (db/exec-one! conn ["select data from storage_data where id=?" id])] + (:data result))) + (defmethod impl/get-object-url :db [_ _] (throw (UnsupportedOperationException. "not supported"))) +(defmethod impl/del-object :db + [_ _] + ;; NOOP: because deleting the row already deletes the file data from + ;; the database. + nil) + (defmethod impl/del-objects-in-bulk :db [_ _] ;; NOOP: because deleting the row already deletes the file data from ;; the database. nil) + diff --git a/backend/src/app/storage/fs.clj b/backend/src/app/storage/fs.clj index 4f9d059693..e15bb7b0e3 100644 --- a/backend/src/app/storage/fs.clj +++ b/backend/src/app/storage/fs.clj @@ -79,6 +79,10 @@ :path (str full))) (io/input-stream full))) +(defmethod impl/get-object-bytes :fs + [backend object] + (fs/slurp-bytes (impl/get-object-data backend object))) + (defmethod impl/get-object-url :fs [{:keys [uri] :as backend} {:keys [id] :as object} _] (update uri :path @@ -87,6 +91,13 @@ (str existing (impl/id->path id)) (str existing "/" (impl/id->path id)))))) +(defmethod impl/del-object :fs + [backend {:keys [id] :as object}] + (let [base (fs/path (:directory backend)) + path (fs/path (impl/id->path id)) + path (fs/join base path)] + (Files/deleteIfExists ^Path path))) + (defmethod impl/del-objects-in-bulk :fs [backend ids] (let [base (fs/path (:directory backend))] @@ -94,3 +105,4 @@ (let [path (fs/path (impl/id->path id)) path (fs/join base path)] (Files/deleteIfExists ^Path path))))) + diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index a28184df5f..4c3a619009 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -8,10 +8,10 @@ "Storage backends abstraction layer." (:require [app.common.exceptions :as ex] - [app.common.spec :as us] [app.common.uuid :as uuid] [buddy.core.codecs :as bc] - [clojure.java.io :as io]) + [clojure.java.io :as io] + [cuerdas.core :as str]) (:import java.nio.ByteBuffer java.util.UUID @@ -45,6 +45,14 @@ :code :invalid-storage-backend :context cfg)) +(defmulti get-object-bytes (fn [cfg _] (:type cfg))) + +(defmethod get-object-bytes :default + [cfg _] + (ex/raise :type :internal + :code :invalid-storage-backend + :context cfg)) + (defmulti get-object-url (fn [cfg _ _] (:type cfg))) (defmethod get-object-url :default @@ -54,6 +62,14 @@ :context cfg)) +(defmulti del-object (fn [cfg _] (:type cfg))) + +(defmethod del-object :default + [cfg _] + (ex/raise :type :internal + :code :invalid-storage-backend + :context cfg)) + (defmulti del-objects-in-bulk (fn [cfg _] (:type cfg))) (defmethod del-objects-in-bulk :default @@ -62,7 +78,6 @@ :code :invalid-storage-backend :context cfg)) - ;; --- HELPERS (defn uuid->hex @@ -109,7 +124,10 @@ (make-output-stream [_ opts] (throw (UnsupportedOperationException. "not implemented"))) clojure.lang.Counted - (count [_] size)))) + (count [_] size) + + java.lang.AutoCloseable + (close [_])))) (defn string->content [^String v] @@ -129,7 +147,10 @@ clojure.lang.Counted (count [_] - (alength data))))) + (alength data)) + + java.lang.AutoCloseable + (close [_])))) (defn- input-stream->content [^InputStream is size] @@ -137,7 +158,7 @@ IContentObject io/IOFactory (make-reader [_ opts] - (io/make-reader is opts)) + (io/make-reader is opts)) (make-writer [_ opts] (throw (UnsupportedOperationException. "not implemented"))) (make-input-stream [_ opts] @@ -146,7 +167,11 @@ (throw (UnsupportedOperationException. "not implemented"))) clojure.lang.Counted - (count [_] size))) + (count [_] size) + + java.lang.AutoCloseable + (close [_] + (.close is)))) (defn content ([data] (content data nil)) @@ -179,10 +204,20 @@ (defn slurp-bytes [content] - (us/assert content? content) (with-open [input (io/input-stream content) output (java.io.ByteArrayOutputStream. (count content))] (io/copy input output) (.toByteArray output))) +(defn resolve-backend + [{:keys [conn pool] :as storage} backend-id] + (when backend-id + (let [backend (get-in storage [:backends backend-id])] + (when-not backend + (ex/raise :type :internal + :code :backend-not-configured + :hint (str/fmt "backend '%s' not configured" backend-id))) + (assoc backend + :conn (or conn pool) + :id backend-id)))) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index da7928bdff..2d42277a7c 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -5,7 +5,7 @@ ;; Copyright (c) UXBOX Labs SL (ns app.storage.s3 - "Storage backends abstraction layer." + "S3 Storage backend implementation." (:require [app.common.data :as d] [app.common.exceptions :as ex] @@ -18,25 +18,34 @@ [integrant.core :as ig]) (:import java.time.Duration + java.io.InputStream java.util.Collection software.amazon.awssdk.core.sync.RequestBody + software.amazon.awssdk.core.ResponseBytes + ;; software.amazon.awssdk.core.ResponseInputStream software.amazon.awssdk.regions.Region software.amazon.awssdk.services.s3.S3Client software.amazon.awssdk.services.s3.model.Delete software.amazon.awssdk.services.s3.model.CopyObjectRequest software.amazon.awssdk.services.s3.model.DeleteObjectsRequest software.amazon.awssdk.services.s3.model.DeleteObjectsResponse + software.amazon.awssdk.services.s3.model.DeleteObjectRequest software.amazon.awssdk.services.s3.model.GetObjectRequest software.amazon.awssdk.services.s3.model.ObjectIdentifier software.amazon.awssdk.services.s3.model.PutObjectRequest + ;; software.amazon.awssdk.services.s3.model.GetObjectResponse software.amazon.awssdk.services.s3.presigner.S3Presigner software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest - software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest)) + software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest + + )) (declare put-object) (declare copy-object) -(declare get-object) +(declare get-object-bytes) +(declare get-object-data) (declare get-object-url) +(declare del-object) (declare del-object-in-bulk) (declare build-s3-client) (declare build-s3-presigner) @@ -87,12 +96,20 @@ (defmethod impl/get-object-data :s3 [backend object] - (get-object backend object)) + (get-object-data backend object)) + +(defmethod impl/get-object-bytes :s3 + [backend object] + (get-object-bytes backend object)) (defmethod impl/get-object-url :s3 [backend object options] (get-object-url backend object options)) +(defmethod impl/del-object :s3 + [backend object] + (del-object backend object)) + (defmethod impl/del-objects-in-bulk :s3 [backend ids] (del-object-in-bulk backend ids)) @@ -104,19 +121,19 @@ (case region :eu-central-1 Region/EU_CENTRAL_1)) -(defn- build-s3-client +(defn build-s3-client [{:keys [region]}] (.. (S3Client/builder) (region (lookup-region region)) (build))) -(defn- build-s3-presigner +(defn build-s3-presigner [{:keys [region]}] (.. (S3Presigner/builder) (region (lookup-region region)) (build))) -(defn- put-object +(defn put-object [{:keys [client bucket prefix]} {:keys [id] :as object} content] (let [path (str prefix (impl/id->path id)) mdata (meta object) @@ -125,14 +142,15 @@ (bucket bucket) (contentType mtype) (key path) - (build)) - content (RequestBody/fromInputStream (io/input-stream content) - (count content))] - (.putObject ^S3Client client - ^PutObjectRequest request - ^RequestBody content))) + (build))] -(defn- copy-object + (with-open [^InputStream is (io/input-stream content)] + (let [content (RequestBody/fromInputStream is (count content))] + (.putObject ^S3Client client + ^PutObjectRequest request + ^RequestBody content))))) + +(defn copy-object [{:keys [client bucket prefix]} src-object dst-object] (let [source-path (str prefix (impl/id->path (:id src-object))) source-mdata (meta src-object) @@ -146,22 +164,33 @@ (contentType source-mtype) (build))] - (.copyObject ^S3Client client - ^CopyObjectRequest request))) + (.copyObject ^S3Client client ^CopyObjectRequest request))) -(defn- get-object +(defn get-object-data [{:keys [client bucket prefix]} {:keys [id]}] (let [gor (.. (GetObjectRequest/builder) (bucket bucket) (key (str prefix (impl/id->path id))) (build)) - obj (.getObject ^S3Client client ^GetObjectRequest gor)] + obj (.getObject ^S3Client client ^GetObjectRequest gor) + ;; rsp (.response ^ResponseInputStream obj) + ;; len (.contentLength ^GetObjectResponse rsp) + ] (io/input-stream obj))) +(defn get-object-bytes + [{:keys [client bucket prefix]} {:keys [id]}] + (let [gor (.. (GetObjectRequest/builder) + (bucket bucket) + (key (str prefix (impl/id->path id))) + (build)) + obj (.getObjectAsBytes ^S3Client client ^GetObjectRequest gor)] + (.asByteArray ^ResponseBytes obj))) + (def default-max-age (dt/duration {:minutes 10})) -(defn- get-object-url +(defn get-object-url [{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}] (us/assert dt/duration? max-age) (let [gor (.. (GetObjectRequest/builder) @@ -175,7 +204,16 @@ pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)] (u/uri (str (.url ^PresignedGetObjectRequest pgor))))) -(defn- del-object-in-bulk +(defn del-object + [{:keys [bucket client prefix]} {:keys [id] :as obj}] + (let [dor (.. (DeleteObjectRequest/builder) + (bucket bucket) + (key (str prefix (impl/id->path id))) + (build))] + (.deleteObject ^S3Client client + ^DeleteObjectRequest dor))) + +(defn del-object-in-bulk [{:keys [bucket client prefix]} ids] (let [oids (map (fn [id] (.. (ObjectIdentifier/builder) diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index d2d9420e89..2a30a63242 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -4,6 +4,9 @@ ;; ;; Copyright (c) UXBOX Labs SL +;; TODO: DEPRECATED +;; Should be removed in the 1.8.x + (ns app.tasks.delete-object "Generic task for permanent deletion of objects." (:require diff --git a/backend/src/app/tasks/delete_profile.clj b/backend/src/app/tasks/delete_profile.clj index 17e2facb4c..67a1733df6 100644 --- a/backend/src/app/tasks/delete_profile.clj +++ b/backend/src/app/tasks/delete_profile.clj @@ -14,6 +14,9 @@ [clojure.spec.alpha :as s] [integrant.core :as ig])) +;; TODO: DEPRECATED +;; Should be removed in the 1.8.x + (declare delete-profile-data) ;; --- INIT diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj index 8b8bc3d281..bc1675447f 100644 --- a/backend/src/app/tasks/file_media_gc.clj +++ b/backend/src/app/tasks/file_media_gc.clj @@ -100,6 +100,7 @@ :id (:id mobj) :media-id (:media-id mobj) :thumbnail-id (:thumbnail-id mobj)) + ;; NOTE: deleting the file-media-object in the database ;; automatically marks as toched the referenced storage ;; objects. The touch mechanism is needed because many files can diff --git a/backend/src/app/tasks/file_offload.clj b/backend/src/app/tasks/file_offload.clj new file mode 100644 index 0000000000..ee784d02d2 --- /dev/null +++ b/backend/src/app/tasks/file_offload.clj @@ -0,0 +1,63 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.tasks.file-offload + "A maintenance task that offloads file data to an external storage (S3)." + (:require + [app.common.spec :as us] + [app.db :as db] + [app.storage :as sto] + [app.storage.impl :as simpl] + [app.util.logging :as l] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def sql:offload-candidates-chunk + "select f.id, f.data from file as f + where f.data is not null + and f.modified_at < now() - ?::interval + order by f.modified_at + limit 10") + +(defn- retrieve-candidates + [{:keys [conn max-age]}] + (db/exec! conn [sql:offload-candidates-chunk max-age])) + +(defn- offload-candidate + [{:keys [storage conn backend] :as cfg} {:keys [id data] :as file}] + (l/debug :action "offload file data" :id id) + (let [backend (simpl/resolve-backend storage backend)] + (->> (simpl/content data) + (simpl/put-object backend file)) + (db/update! conn :file + {:data nil + :data-backend (name (:id backend))} + {:id id}))) + +;; ---- STATE INIT + +(s/def ::max-age ::dt/duration) +(s/def ::backend ::us/keyword) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req-un [::db/pool ::max-age ::sto/storage ::backend])) + +(defmethod ig/init-key ::handler + [_ {:keys [pool max-age] :as cfg}] + (fn [_] + (db/with-atomic [conn pool] + (let [max-age (db/interval max-age) + cfg (-> cfg + (assoc :conn conn) + (assoc :max-age max-age))] + (loop [n 0] + (let [candidates (retrieve-candidates cfg)] + (if (seq candidates) + (do + (run! (partial offload-candidate cfg) candidates) + (recur (+ n (count candidates)))) + (l/debug :hint "offload summary" :count n)))))))) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj new file mode 100644 index 0000000000..1125245607 --- /dev/null +++ b/backend/src/app/tasks/objects_gc.clj @@ -0,0 +1,171 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.tasks.objects-gc + "A maintenance task that performs a general purpose garbage collection + of deleted objects." + (:require + [app.config :as cf] + [app.db :as db] + [app.storage :as sto] + [app.storage.impl :as simpl] + [app.util.logging :as l] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig])) + +(def target-tables + ["profile" + "team" + "file" + "project" + "team_font_variant"]) + +(defmulti delete-objects :table) + +(def sql:delete-objects + "with deleted as ( + select id from %(table)s + where deleted_at is not null + and deleted_at < now() - ?::interval + order by deleted_at + limit %(limit)s + ) + delete from %(table)s + where id in (select id from deleted) + returning *") + +;; --- IMPL: generic object deletion + +(defmethod delete-objects :default + [{:keys [conn max-age table] :as cfg}] + (let [sql (str/fmt sql:delete-objects + {:table table :limit 50}) + result (db/exec! conn [sql max-age])] + + (doseq [{:keys [id] :as item} result] + (l/trace :action "delete object" :table table :id id)) + + (count result))) + + +;; --- IMPL: file deletion + +(defmethod delete-objects "file" + [{:keys [conn max-age table storage] :as cfg}] + (let [sql (str/fmt sql:delete-objects + {:table table :limit 50}) + result (db/exec! conn [sql max-age]) + backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))] + + (doseq [{:keys [id] :as item} result] + (l/trace :action "delete object" :table table :id id) + (when backend + (simpl/del-object backend item))) + + (count result))) + +;; --- IMPL: team-font-variant deletion + +(defmethod delete-objects "team_font_variant" + [{:keys [conn max-age storage table] :as cfg}] + (let [sql (str/fmt sql:delete-objects + {:table table :limit 50}) + fonts (db/exec! conn [sql max-age]) + storage (assoc storage :conn conn)] + (doseq [{:keys [id] :as font} fonts] + (l/trace :action "delete object" :table table :id id) + (some->> (:woff1-file-id font) (sto/del-object storage)) + (some->> (:woff2-file-id font) (sto/del-object storage)) + (some->> (:otf-file-id font) (sto/del-object storage)) + (some->> (:ttf-file-id font) (sto/del-object storage))) + (count fonts))) + +;; --- IMPL: team deletion + +(defmethod delete-objects "team" + [{:keys [conn max-age storage table] :as cfg}] + (let [sql (str/fmt sql:delete-objects + {:table table :limit 50}) + teams (db/exec! conn [sql max-age]) + storage (assoc storage :conn conn)] + + (doseq [{:keys [id] :as team} teams] + (l/trace :action "delete object" :table table :id id) + (some->> (:photo-id team) (sto/del-object storage))) + + (count teams))) + +;; --- IMPL: profile deletion + +(def sql:retrieve-deleted-profiles + "select id, photo_id from profile + where deleted_at is not null + and deleted_at < now() - ?::interval + order by deleted_at + limit %(limit)s + for update") + +(def sql:mark-owned-teams-deleted + "with owned as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.is_owner is true + and tpr.profile_id = ? + ) + update team set deleted_at = now() - ?::interval + where id in (select id from owned)") + +(defmethod delete-objects "profile" + [{:keys [conn max-age storage table] :as cfg}] + (let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50}) + profiles (db/exec! conn [sql max-age]) + storage (assoc storage :conn conn)] + + (doseq [{:keys [id] :as profile} profiles] + (l/trace :action "delete object" :table table :id id) + + ;; Mark the owned teams as deleted; this enables them to be procesed + ;; in the same transaction in the "team" table step. + (db/exec-one! conn [sql:mark-owned-teams-deleted id max-age]) + + ;; Mark as deleted the storage object related with the photo-id + ;; field. + (some->> (:photo-id profile) (sto/del-object storage)) + + ;; And finally, permanently delete the profile. + (db/delete! conn :profile {:id id})) + + (count profiles))) + +;; --- INIT + +(defn- process-table + [{:keys [table] :as cfg}] + (loop [n 0] + (let [res (delete-objects cfg)] + (if (pos? res) + (recur (+ n res)) + (l/debug :hint "table gc summary" :table table :deleted n))))) + +(s/def ::max-age ::dt/duration) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req-un [::db/pool ::sto/storage ::max-age])) + +(defmethod ig/init-key ::handler + [_ {:keys [pool max-age] :as cfg}] + (fn [task] + ;; Checking first on task argument allows properly testing it. + (let [max-age (get task :max-age max-age)] + (db/with-atomic [conn pool] + (let [max-age (db/interval max-age) + cfg (-> cfg + (assoc :max-age max-age) + (assoc :conn conn))] + (doseq [table target-tables] + (process-table (assoc cfg :table table)))))))) diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index bfa682ea6c..efff646d1e 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -9,21 +9,12 @@ (:require [app.common.exceptions :as ex] [app.common.spec :as us] + [app.common.transit :as t] [app.util.time :as dt] - [app.util.transit :as t] - [buddy.core.kdf :as bk] [buddy.sign.jwe :as jwe] [clojure.spec.alpha :as s] [integrant.core :as ig])) -(defn- derive-tokens-secret - [key] - (let [engine (bk/engine {:key key - :salt "tokens" - :alg :hkdf - :digest :blake2b-512})] - (bk/get-bytes engine 32))) - (defn- generate [cfg claims] (let [payload (t/encode claims)] @@ -50,13 +41,6 @@ :params params)) claims)) -(s/def ::secret-key ::us/string) -(s/def ::props - (s/keys :req-un [::secret-key])) - -(defmethod ig/pre-init-spec ::tokens [_] - (s/keys :req-un [::props])) - (defn- generate-predefined [cfg {:keys [iss profile-id] :as params}] (case iss @@ -70,9 +54,14 @@ :code :not-implemented :hint "no predefined token"))) +(s/def ::keys fn?) + +(defmethod ig/pre-init-spec ::tokens [_] + (s/keys :req-un [::keys])) + (defmethod ig/init-key ::tokens - [_ {:keys [props] :as cfg}] - (let [secret (derive-tokens-secret (:secret-key props)) + [_ {:keys [keys] :as cfg}] + (let [secret (keys :salt "tokens" :size 32) cfg (assoc cfg ::secret secret)] (fn [action params] (case action diff --git a/backend/src/app/util/blob.clj b/backend/src/app/util/blob.clj index d70d12ab0b..42539b9342 100644 --- a/backend/src/app/util/blob.clj +++ b/backend/src/app/util/blob.clj @@ -8,8 +8,8 @@ "A generic blob storage encoding. Mainly used for page data, page options and txlog payload storage." (:require + [app.common.transit :as t] [app.config :as cf] - [app.util.transit :as t] [taoensso.nippy :as n]) (:import java.io.ByteArrayInputStream @@ -108,7 +108,7 @@ cdata (byte-array mlen) clen (Zstd/compressByteArray ^bytes cdata 0 mlen ^bytes data 0 dlen - 4)] + 6)] (with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4)) ^DataOutputStream dos (DataOutputStream. baos)] (.writeShort dos (short 3)) ;; version number diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index c2839f483d..9d5d3d4d75 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -10,13 +10,14 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str]) (:import - java.time.Instant java.time.Duration - java.util.Date - java.time.ZonedDateTime + java.time.Instant + java.time.OffsetDateTime java.time.ZoneId + java.time.ZonedDateTime java.time.format.DateTimeFormatter java.time.temporal.TemporalAmount + java.util.Date org.apache.logging.log4j.core.util.CronExpression)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -54,28 +55,37 @@ (obj->duration ms-or-obj))) (defn duration-between + {:deprecated true} [t1 t2] (Duration/between t1 t2)) -(letfn [(conformer [v] - (cond - (duration? v) v +(defn diff + [t1 t2] + (Duration/between t1 t2)) - (string? v) - (try - (duration v) - (catch java.time.format.DateTimeParseException _e - ::s/invalid)) +(s/def ::duration + (s/conformer + (fn [v] + (cond + (duration? v) v - :else - ::s/invalid)) - (unformer [v] - (subs (str v) 2))] - (s/def ::duration (s/conformer conformer unformer))) + (string? v) + (try + (duration v) + (catch java.time.format.DateTimeParseException _e + ::s/invalid)) + + :else + ::s/invalid)) + (fn [v] + (subs (str v) 2)))) (extend-protocol clojure.core/Inst java.time.Duration - (inst-ms* [v] (.toMillis ^Duration v))) + (inst-ms* [v] (.toMillis ^Duration v)) + + OffsetDateTime + (inst-ms* [v] (.toEpochMilli (.toInstant ^OffsetDateTime v)))) (defmethod print-method Duration [mv ^java.io.Writer writer] diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 1e3e2dea9d..ed9d1982fd 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -436,7 +436,7 @@ (s/assert dt/cron? cron) (let [now (dt/now) next (dt/next-valid-instant-from cron now)] - (inst-ms (dt/duration-between now next)))) + (inst-ms (dt/diff now next)))) (defn- schedule-task [{:keys [scheduler] :as cfg} {:keys [cron] :as task}] diff --git a/backend/tests/app/tests/test_bounces_handling.clj b/backend/test/app/bounce_handling_test.clj similarity index 99% rename from backend/tests/app/tests/test_bounces_handling.clj rename to backend/test/app/bounce_handling_test.clj index 57e838af53..3d423f73f3 100644 --- a/backend/tests/app/tests/test_bounces_handling.clj +++ b/backend/test/app/bounce_handling_test.clj @@ -4,16 +4,16 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-bounces-handling +(ns app.bounce-handling-test (:require - [clojure.pprint :refer [pprint]] - [app.http.awsns :as awsns] - [app.emails :as emails] - [app.tests.helpers :as th] [app.db :as db] + [app.emails :as emails] + [app.http.awsns :as awsns] + [app.test-helpers :as th] [app.util.time :as dt] - [mockery.core :refer [with-mocks]] - [clojure.test :as t])) + [clojure.pprint :refer [pprint]] + [clojure.test :as t] + [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) diff --git a/backend/tests/app/tests/test_emails.clj b/backend/test/app/emails_test.clj similarity index 92% rename from backend/tests/app/tests/test_emails.clj rename to backend/test/app/emails_test.clj index 7cc62966ef..4e3e5bed98 100644 --- a/backend/tests/app/tests/test_emails.clj +++ b/backend/test/app/emails_test.clj @@ -4,13 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-emails +(ns app.emails-test (:require [clojure.test :as t] [promesa.core :as p] [app.db :as db] [app.emails :as emails] - [app.tests.helpers :as th])) + [app.test-helpers :as th])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) diff --git a/backend/tests/app/tests/test_services_files.clj b/backend/test/app/services_files_test.clj similarity index 82% rename from backend/tests/app/tests/test_services_files.clj rename to backend/test/app/services_files_test.clj index 248dc86f29..e22d8afc8d 100644 --- a/backend/tests/app/tests/test_services_files.clj +++ b/backend/test/app/services_files_test.clj @@ -4,13 +4,14 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-files +(ns app.services-files-test (:require [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] [app.storage :as sto] - [app.tests.helpers :as th] + [app.test-helpers :as th] + [app.util.time :as dt] [clojure.test :as t] [datoteka.core :as fs])) @@ -134,7 +135,7 @@ (t/deftest file-media-gc-task (letfn [(create-file-media-object [{:keys [profile-id file-id]}] (let [mfile {:filename "sample.jpg" - :tempfile (th/tempfile "app/tests/_files/sample.jpg") + :tempfile (th/tempfile "app/test_files/sample.jpg") :content-type "image/jpeg" :size 312043} params {::th/type :upload-file-media-object @@ -337,3 +338,69 @@ (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :not-found)))) +(t/deftest deletion-test + (let [task (:app.tasks.objects-gc/handler th/*system*) + profile1 (th/create-profile* 1) + file (th/create-file* 1 {:project-id (:default-project-id profile1) + :profile-id (:id profile1)})] + ;; file is not deleted because it does not meet all + ;; conditions to be deleted. + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of files + (let [data {::th/type :project-files + :project-id (:default-project-id profile1) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 1 (count result))))) + + ;; Request file to be deleted + (let [params {::th/type :delete-file + :id (:id file) + :profile-id (:id profile1)} + out (th/mutation! params)] + (t/is (nil? (:error out)))) + + ;; query the list of files after soft deletion + (let [data {::th/type :project-files + :project-id (:default-project-id profile1) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 0 (count result))))) + + ;; run permanent deletion (should be noop) + (let [result (task {:max-age (dt/duration {:minutes 1})})] + (t/is (nil? result))) + + ;; query the list of file libraries of a after hard deletion + (let [data {::th/type :file-libraries + :file-id (:id file) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 0 (count result))))) + + ;; run permanent deletion + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of file libraries of a after hard deletion + (let [data {::th/type :file-libraries + :file-id (:id file) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found)))) + )) diff --git a/backend/tests/app/tests/test_services_fonts.clj b/backend/test/app/services_fonts_test.clj similarity index 93% rename from backend/tests/app/tests/test_services_fonts.clj rename to backend/test/app/services_fonts_test.clj index 86836b71e4..71f217b6aa 100644 --- a/backend/tests/app/tests/test_services_fonts.clj +++ b/backend/test/app/services_fonts_test.clj @@ -4,13 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-fonts +(ns app.services-fonts-test (:require [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] [app.storage :as sto] - [app.tests.helpers :as th] + [app.test-helpers :as th] [clojure.java.io :as io] [clojure.test :as t] [datoteka.core :as fs])) @@ -24,7 +24,7 @@ proj-id (:default-project-id prof) font-id (uuid/custom 10 1) - ttfdata (-> (io/resource "app/tests/_files/font-1.ttf") + ttfdata (-> (io/resource "app/test_files/font-1.ttf") (fs/slurp-bytes)) params {::th/type :create-font-variant @@ -59,7 +59,7 @@ proj-id (:default-project-id prof) font-id (uuid/custom 10 1) - data (-> (io/resource "app/tests/_files/font-1.woff") + data (-> (io/resource "app/test_files/font-1.woff") (fs/slurp-bytes)) params {::th/type :create-font-variant diff --git a/backend/tests/app/tests/test_services_management.clj b/backend/test/app/services_management_test.clj similarity index 99% rename from backend/tests/app/tests/test_services_management.clj rename to backend/test/app/services_management_test.clj index f662b43810..7e75d434a5 100644 --- a/backend/tests/app/tests/test_services_management.clj +++ b/backend/test/app/services_management_test.clj @@ -4,13 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-management +(ns app.services-management-test (:require [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] [app.storage :as sto] - [app.tests.helpers :as th] + [app.test-helpers :as th] [clojure.test :as t] [buddy.core.bytes :as b] [datoteka.core :as fs])) diff --git a/backend/tests/app/tests/test_services_media.clj b/backend/test/app/services_media_test.clj similarity index 96% rename from backend/tests/app/tests/test_services_media.clj rename to backend/test/app/services_media_test.clj index 112eed7795..a0e9c9780f 100644 --- a/backend/tests/app/tests/test_services_media.clj +++ b/backend/test/app/services_media_test.clj @@ -4,12 +4,12 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-media +(ns app.services-media-test (:require [app.common.uuid :as uuid] [app.db :as db] [app.storage :as sto] - [app.tests.helpers :as th] + [app.test-helpers :as th] [clojure.test :as t] [datoteka.core :as fs])) @@ -57,7 +57,7 @@ :project-id (:default-project-id prof) :is-shared false}) mfile {:filename "sample.jpg" - :tempfile (th/tempfile "app/tests/_files/sample.jpg") + :tempfile (th/tempfile "app/test_files/sample.jpg") :content-type "image/jpeg" :size 312043} diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/test/app/services_profile_test.clj similarity index 68% rename from backend/tests/app/tests/test_services_profile.clj rename to backend/test/app/services_profile_test.clj index c3daf5c143..d5e6dac437 100644 --- a/backend/tests/app/tests/test_services_profile.clj +++ b/backend/test/app/services_profile_test.clj @@ -4,16 +4,17 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-profile +(ns app.services-profile-test (:require - [clojure.test :as t] - [clojure.java.io :as io] - [mockery.core :refer [with-mocks]] - [cuerdas.core :as str] - [datoteka.core :as fs] [app.db :as db] [app.rpc.mutations.profile :as profile] - [app.tests.helpers :as th])) + [app.test-helpers :as th] + [app.util.time :as dt] + [clojure.java.io :as io] + [clojure.test :as t] + [cuerdas.core :as str] + [datoteka.core :as fs] + [mockery.core :refer [with-mocks]])) ;; TODO: profile deletion with teams ;; TODO: profile deletion with owner teams @@ -108,7 +109,7 @@ :profile-id (:id profile) :file {:filename "sample.jpg" :size 123123 - :tempfile "tests/app/tests/_files/sample.jpg" + :tempfile (th/tempfile "app/test_files/sample.jpg") :content-type "image/jpeg"}} out (th/mutation! data)] @@ -117,7 +118,7 @@ )) (t/deftest profile-deletion-simple - (let [task (:app.tasks.delete-profile/handler th/*system*) + (let [task (:app.tasks.objects-gc/handler th/*system*) prof (th/create-profile* 1) file (th/create-file* 1 {:profile-id (:id prof) :project-id (:default-project-id prof) @@ -125,23 +126,14 @@ ;; profile is not deleted because it does not meet all ;; conditions to be deleted. - (let [result (task {:props {:profile-id (:id prof)}})] + (let [result (task {:max-age (dt/duration 0)})] (t/is (nil? result))) ;; Request profile to be deleted - (with-mocks [mock {:target 'app.worker/submit! :return nil}] - (let [params {::th/type :delete-profile - :profile-id (:id prof)} - out (th/mutation! params)] - (t/is (nil? (:error out))) - - ;; check the mock - (let [mock (deref mock) - mock-params (first (:call-args mock))] - (t/is (:called? mock)) - (t/is (= 1 (:call-count mock))) - (t/is (= :delete-profile (:app.worker/task mock-params))) - (t/is (= (:id prof) (:profile-id mock-params)))))) + (let [params {::th/type :delete-profile + :profile-id (:id prof)} + out (th/mutation! params)] + (t/is (nil? (:error out)))) ;; query files after profile soft deletion (let [params {::th/type :files @@ -153,8 +145,8 @@ (t/is (= 1 (count (:result out))))) ;; execute permanent deletion task - (let [result (task {:props {:profile-id (:id prof)}})] - (t/is (true? result))) + (let [result (task {:max-age (dt/duration "-1m")})] + (t/is (nil? result))) ;; query profile after delete (let [params {::th/type :profile @@ -165,17 +157,6 @@ error-data (ex-data error)] (t/is (th/ex-info? error)) (t/is (= (:type error-data) :not-found)))) - - ;; query files after profile soft deletion - (let [params {::th/type :files - :project-id (:default-project-id prof) - :profile-id (:id prof)} - out (th/query! params)] - ;; (th/print-result! out) - (let [error (:error out) - error-data (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type error-data) :not-found)))) )) (t/deftest registration-domain-whitelist @@ -187,126 +168,95 @@ (t/testing "not allowed email domain" (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) -(t/deftest test-register-with-no-terms-and-privacy - (let [data {::th/type :register-profile +(t/deftest prepare-register-and-register-profile + (let [data {::th/type :prepare-register-profile :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy nil} + :password "foobar"} out (th/mutation! data) - error (:error out) - edata (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type edata) :validation)) - (t/is (= (:code edata) :spec-validation)))) + token (get-in out [:result :token])] + (t/is (string? token)) -(t/deftest test-register-with-bad-terms-and-privacy - (let [data {::th/type :register-profile - :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy false} - out (th/mutation! data) - error (:error out) - edata (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type edata) :validation)) - (t/is (= (:code edata) :invalid-terms-and-privacy)))) -(t/deftest test-register-when-registration-disabled + ;; try register without accepting terms + (let [data {::th/type :register-profile + :token token + :fullname "foobar" + :accept-terms-and-privacy false} + out (th/mutation! data)] + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :invalid-terms-and-privacy)))) + + ;; try register without token + (let [data {::th/type :register-profile + :fullname "foobar" + :accept-terms-and-privacy true} + out (th/mutation! data)] + (let [error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :spec-validation)))) + + ;; try correct register + (let [data {::th/type :register-profile + :token token + :fullname "foobar" + :accept-terms-and-privacy true + :accept-newsletter-subscription true}] + (let [{:keys [result error]} (th/mutation! data)] + (t/is (nil? error)) + (t/is (true? (get-in result [:props :accept-newsletter-subscription]))) + (t/is (true? (get-in result [:props :accept-terms-and-privacy]))))) + )) + +(t/deftest prepare-register-with-registration-disabled (with-mocks [mock {:target 'app.config/get :return (th/mock-config-get-with {:registration-enabled false})}] - (let [data {::th/type :register-profile - :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy true} - out (th/mutation! data) - error (:error out) - edata (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type edata) :restriction)) - (t/is (= (:code edata) :registration-disabled))))) -(t/deftest test-register-existing-profile + (let [data {::th/type :prepare-register-profile + :email "user@example.com" + :password "foobar"}] + (let [{:keys [result error] :as out} (th/mutation! data)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :restriction)) + (t/is (th/ex-of-code? error :registration-disabled)))))) + +(t/deftest prepare-register-with-existing-user (let [profile (th/create-profile* 1) - data {::th/type :register-profile + data {::th/type :prepare-register-profile :email (:email profile) - :password "foobar" - :fullname "foobar" - :terms-privacy true} - out (th/mutation! data) - error (:error out) - edata (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type edata) :validation)) - (t/is (= (:code edata) :email-already-exists)))) - -(t/deftest test-register-profile - (with-mocks [mock {:target 'app.emails/send! - :return nil}] - (let [pool (:app.db/pool th/*system*) - data {::th/type :register-profile - :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy true} - out (th/mutation! data)] + :password "foobar"}] + (let [{:keys [result error] :as out} (th/mutation! data)] ;; (th/print-result! out) - (let [mock (deref mock) - [params] (:call-args mock)] - ;; (clojure.pprint/pprint params) - (t/is (:called? mock)) - (t/is (= (:email data) (:to params))) - (t/is (contains? params :extra-data)) - (t/is (contains? params :token))) - - (let [result (:result out)] - (t/is (false? (:is-demo result))) - (t/is (= (:email data) (:email result))) - (t/is (= "penpot" (:auth-backend result))) - (t/is (= "foobar" (:fullname result))) - (t/is (not (contains? result :password))))))) + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :email-already-exists))))) (t/deftest test-register-profile-with-bounced-email - (with-mocks [mock {:target 'app.emails/send! - :return nil}] - (let [pool (:app.db/pool th/*system*) - data {::th/type :register-profile - :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy true} - _ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) - out (th/mutation! data)] - ;; (th/print-result! out) + (let [pool (:app.db/pool th/*system*) + data {::th/type :prepare-register-profile + :email "user@example.com" + :password "foobar"}] - (let [mock (deref mock)] - (t/is (false? (:called? mock)))) + (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) - (let [error (:error out) - edata (ex-data error)] - (t/is (th/ex-info? error)) - (t/is (= (:type edata) :validation)) - (t/is (= (:code edata) :email-has-permanent-bounces)))))) + (let [{:keys [result error] :as out} (th/mutation! data)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :validation)) + (t/is (th/ex-of-code? error :email-has-permanent-bounces))))) (t/deftest test-register-profile-with-complained-email - (with-mocks [mock {:target 'app.emails/send! :return nil}] - (let [pool (:app.db/pool th/*system*) - data {::th/type :register-profile - :email "user@example.com" - :password "foobar" - :fullname "foobar" - :terms-privacy true} - _ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) - out (th/mutation! data)] + (let [pool (:app.db/pool th/*system*) + data {::th/type :prepare-register-profile + :email "user@example.com" + :password "foobar"}] - (let [mock (deref mock)] - (t/is (true? (:called? mock)))) - - (let [result (:result out)] - (t/is (= (:email data) (:email result))))))) + (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) + (let [{:keys [result error] :as out} (th/mutation! data)] + (t/is (nil? error)) + (t/is (string? (:token result)))))) (t/deftest test-email-change-request (with-mocks [email-send-mock {:target 'app.emails/send! :return nil} diff --git a/backend/tests/app/tests/test_services_projects.clj b/backend/test/app/services_projects_test.clj similarity index 69% rename from backend/tests/app/tests/test_services_projects.clj rename to backend/test/app/services_projects_test.clj index 5c791fe74e..5f23577f35 100644 --- a/backend/tests/app/tests/test_services_projects.clj +++ b/backend/test/app/services_projects_test.clj @@ -4,14 +4,14 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-projects +(ns app.services-projects-test (:require - [clojure.test :as t] - [promesa.core :as p] + [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] - [app.tests.helpers :as th] - [app.common.uuid :as uuid])) + [app.test-helpers :as th] + [app.util.time :as dt] + [clojure.test :as t])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -170,3 +170,71 @@ (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :not-found)))) + +(t/deftest test-deletion + (let [task (:app.tasks.objects-gc/handler th/*system*) + profile1 (th/create-profile* 1) + project (th/create-project* 1 {:team-id (:default-team-id profile1) + :profile-id (:id profile1)})] + + ;; project is not deleted because it does not meet all + ;; conditions to be deleted. + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of projects + (let [data {::th/type :projects + :team-id (:default-team-id profile1) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 2 (count result))))) + + ;; Request project to be deleted + (let [params {::th/type :delete-project + :id (:id project) + :profile-id (:id profile1)} + out (th/mutation! params)] + (t/is (nil? (:error out)))) + + ;; query the list of projects after soft deletion + (let [data {::th/type :projects + :team-id (:default-team-id profile1) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 1 (count result))))) + + ;; run permanent deletion (should be noop) + (let [result (task {:max-age (dt/duration {:minutes 1})})] + (t/is (nil? result))) + + ;; query the list of files of a after soft deletion + (let [data {::th/type :project-files + :project-id (:id project) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 0 (count result))))) + + ;; run permanent deletion + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of files of a after hard deletion + (let [data {::th/type :project-files + :project-id (:id project) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found)))) + )) diff --git a/backend/tests/app/tests/test_services_teams.clj b/backend/test/app/services_teams_test.clj similarity index 50% rename from backend/tests/app/tests/test_services_teams.clj rename to backend/test/app/services_teams_test.clj index c64f7922d2..2831ea4ee9 100644 --- a/backend/tests/app/tests/test_services_teams.clj +++ b/backend/test/app/services_teams_test.clj @@ -4,16 +4,17 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-teams +(ns app.services-teams-test (:require [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] [app.storage :as sto] - [app.tests.helpers :as th] - [mockery.core :refer [with-mocks]] + [app.test-helpers :as th] + [app.util.time :as dt] [clojure.test :as t] - [datoteka.core :as fs])) + [datoteka.core :as fs] + [mockery.core :refer [with-mocks]])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -80,6 +81,80 @@ ))) +(t/deftest test-deletion + (let [task (:app.tasks.objects-gc/handler th/*system*) + profile1 (th/create-profile* 1 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id profile1)}) + pool (:app.db/pool th/*system*) + data {::th/type :delete-team + :team-id (:id team) + :profile-id (:id profile1)}] + + ;; team is not deleted because it does not meet all + ;; conditions to be deleted. + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of teams + (let [data {::th/type :teams + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 2 (count result))) + (t/is (= (:id team) (get-in result [1 :id]))) + (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) + + ;; Request team to be deleted + (let [params {::th/type :delete-team + :id (:id team) + :profile-id (:id profile1)} + out (th/mutation! params)] + (t/is (nil? (:error out)))) + + ;; query the list of teams after soft deletion + (let [data {::th/type :teams + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 1 (count result))) + (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) + + ;; run permanent deletion (should be noop) + (let [result (task {:max-age (dt/duration {:minutes 1})})] + (t/is (nil? result))) + + ;; query the list of projects of a after hard deletion + (let [data {::th/type :projects + :team-id (:id team) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= 0 (count result))))) + + ;; run permanent deletion + (let [result (task {:max-age (dt/duration 0)})] + (t/is (nil? result))) + + ;; query the list of projects of a after hard deletion + (let [data {::th/type :projects + :team-id (:id team) + :profile-id (:id profile1)} + out (th/query! data)] + ;; (th/print-result! out) + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found)))) + )) + + diff --git a/backend/tests/app/tests/test_services_viewer.clj b/backend/test/app/services_viewer_test.clj similarity index 97% rename from backend/tests/app/tests/test_services_viewer.clj rename to backend/test/app/services_viewer_test.clj index ce638a19eb..d3176f81ac 100644 --- a/backend/tests/app/tests/test_services_viewer.clj +++ b/backend/test/app/services_viewer_test.clj @@ -4,13 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-services-viewer +(ns app.services-viewer-test (:require - [clojure.test :as t] - [datoteka.core :as fs] [app.common.uuid :as uuid] [app.db :as db] - [app.tests.helpers :as th])) + [app.test-helpers :as th] + [clojure.test :as t] + [datoteka.core :as fs])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) diff --git a/backend/tests/app/tests/test_storage.clj b/backend/test/app/storage_test.clj similarity index 97% rename from backend/tests/app/tests/test_storage.clj rename to backend/test/app/storage_test.clj index 7c0f49e8fb..05e471854b 100644 --- a/backend/tests/app/tests/test_storage.clj +++ b/backend/test/app/storage_test.clj @@ -4,12 +4,12 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-storage +(ns app.storage-test (:require [app.common.exceptions :as ex] [app.db :as db] [app.storage :as sto] - [app.tests.helpers :as th] + [app.test-helpers :as th] [app.util.time :as dt] [clojure.java.io :as io] [clojure.test :as t] @@ -22,7 +22,6 @@ th/database-reset th/clean-storage)) -;; TODO: add specific tests for DB backend. (t/deftest put-and-retrieve-object (let [storage (:app.storage/storage th/*system*) @@ -106,7 +105,7 @@ :project-id (:default-project-id prof) :is-shared false}) mfile {:filename "sample.jpg" - :tempfile (th/tempfile "app/tests/_files/sample.jpg") + :tempfile (th/tempfile "app/test_files/sample.jpg") :content-type "image/jpeg" :size 312043} @@ -167,7 +166,7 @@ :project-id (:default-project-id prof) :is-shared false}) mfile {:filename "sample.jpg" - :tempfile (th/tempfile "app/tests/_files/sample.jpg") + :tempfile (th/tempfile "app/test_files/sample.jpg") :content-type "image/jpeg" :size 312043} diff --git a/backend/tests/app/tests/_files/font-1.otf b/backend/test/app/test_files/font-1.otf similarity index 100% rename from backend/tests/app/tests/_files/font-1.otf rename to backend/test/app/test_files/font-1.otf diff --git a/backend/tests/app/tests/_files/font-1.ttf b/backend/test/app/test_files/font-1.ttf similarity index 100% rename from backend/tests/app/tests/_files/font-1.ttf rename to backend/test/app/test_files/font-1.ttf diff --git a/backend/tests/app/tests/_files/font-1.woff b/backend/test/app/test_files/font-1.woff similarity index 100% rename from backend/tests/app/tests/_files/font-1.woff rename to backend/test/app/test_files/font-1.woff diff --git a/backend/tests/app/tests/_files/font-2.otf b/backend/test/app/test_files/font-2.otf similarity index 100% rename from backend/tests/app/tests/_files/font-2.otf rename to backend/test/app/test_files/font-2.otf diff --git a/backend/tests/app/tests/_files/font-2.woff b/backend/test/app/test_files/font-2.woff similarity index 100% rename from backend/tests/app/tests/_files/font-2.woff rename to backend/test/app/test_files/font-2.woff diff --git a/backend/tests/app/tests/_files/sample.jpg b/backend/test/app/test_files/sample.jpg similarity index 100% rename from backend/tests/app/tests/_files/sample.jpg rename to backend/test/app/test_files/sample.jpg diff --git a/backend/tests/app/tests/_files/sample1.svg b/backend/test/app/test_files/sample1.svg similarity index 100% rename from backend/tests/app/tests/_files/sample1.svg rename to backend/test/app/test_files/sample1.svg diff --git a/backend/tests/app/tests/_files/sample2.svg b/backend/test/app/test_files/sample2.svg similarity index 100% rename from backend/tests/app/tests/_files/sample2.svg rename to backend/test/app/test_files/sample2.svg diff --git a/backend/tests/app/tests/helpers.clj b/backend/test/app/test_helpers.clj similarity index 99% rename from backend/tests/app/tests/helpers.clj rename to backend/test/app/test_helpers.clj index bf6de1ff11..c7541a4a03 100644 --- a/backend/tests/app/tests/helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.helpers +(ns app.test-helpers (:require [app.common.data :as d] [app.common.pages :as cp] diff --git a/backend/tests.edn b/backend/tests.edn index 5b277459dd..6d31eb3c65 100644 --- a/backend/tests.edn +++ b/backend/tests.edn @@ -1,5 +1,5 @@ #kaocha/v1 {:tests [{:id :unit - :test-paths ["tests" "src"] - :ns-patterns ["test-.*"]}]} + :test-paths ["test" "src"] + :ns-patterns [".*-test$"]}]} diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc deleted file mode 100644 index e35e6536e2..0000000000 --- a/common/app/common/geom/shapes/transforms.cljc +++ /dev/null @@ -1,424 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.common.geom.shapes.transforms - (:require - [app.common.attrs :as attrs] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.common :as gco] - [app.common.geom.shapes.path :as gpa] - [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth] - [app.common.data :as d] - [app.common.text :as txt])) - -;; --- Relative Movement - -(defn move-selrect [selrect {dx :x dy :y}] - (-> selrect - (d/update-when :x + dx) - (d/update-when :y + dy) - (d/update-when :x1 + dx) - (d/update-when :y1 + dy) - (d/update-when :x2 + dx) - (d/update-when :y2 + dy))) - -(defn move-points [points move-vec] - (->> points - (mapv #(gpt/add % move-vec)))) - -(defn move - "Move the shape relativelly to its current - position applying the provided delta." - [shape {dx :x dy :y}] - (let [dx (d/check-num dx) - dy (d/check-num dy) - move-vec (gpt/point dx dy)] - - (-> shape - (update :selrect move-selrect move-vec) - (update :points move-points move-vec) - (d/update-when :x + dx) - (d/update-when :y + dy) - (cond-> (= :path (:type shape)) - (update :content gpa/move-content move-vec))))) - -;; --- Absolute Movement - -(declare absolute-move-rect) - -(defn absolute-move - "Move the shape to the exactly specified position." - [shape {:keys [x y]}] - (let [dx (- (d/check-num x) (-> shape :selrect :x)) - dy (- (d/check-num y) (-> shape :selrect :y))] - (move shape (gpt/point dx dy)))) - - -(defn- modif-rotation [shape] - (let [cur-rotation (d/check-num (:rotation shape)) - delta-angle (d/check-num (get-in shape [:modifiers :rotation]))] - (mod (+ cur-rotation delta-angle) 360))) - -(defn transform-matrix - "Returns a transformation matrix without changing the shape properties. - The result should be used in a `transform` attribute in svg" - ([shape] (transform-matrix shape nil)) - ([shape params] (transform-matrix shape params (or (gco/center-shape shape) - (gpt/point 0 0)))) - ([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]} shape-center] - (-> (gmt/matrix) - (gmt/translate shape-center) - - (gmt/multiply (:transform shape (gmt/matrix))) - (cond-> - (and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1)) - (and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1))) - (gmt/translate (gpt/negate shape-center))))) - -(defn inverse-transform-matrix - ([shape] - (let [shape-center (or (gco/center-shape shape) - (gpt/point 0 0))] - (inverse-transform-matrix shape 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)))))) - -(defn transform-point-center - "Transform a point around the shape center" - [point center matrix] - (gpt/transform - point - (gmt/multiply (gmt/translate-matrix center) - matrix - (gmt/translate-matrix (gpt/negate center))))) - -(defn transform-points - ([points matrix] - (transform-points points nil matrix)) - ([points center matrix] - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)))) - -(defn transform-rect - "Transform a rectangles and changes its attributes" - [rect matrix] - - (let [points (-> (gpr/rect->points rect) - (transform-points matrix))] - (gpr/points->rect points))) - -(defn normalize-scale - "We normalize the scale so it's not too close to 0" - [scale] - (cond - (and (< scale 0) (> scale -0.01)) -0.01 - (and (>= scale 0) (< scale 0.01)) 0.01 - :else scale)) - -(defn modifiers->transform - [center modifiers] - (let [ds-modifier (:displacement modifiers (gmt/matrix)) - {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) - - ;; Normalize x/y vector coordinates because scale by 0 is infinite - res-x (normalize-scale res-x) - res-y (normalize-scale res-y) - resize (gpt/point res-x res-y) - - origin (:resize-origin modifiers (gpt/point 0 0)) - - resize-transform (:resize-transform modifiers (gmt/matrix)) - resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - rt-modif (or (:rotation modifiers) 0) - - center (gpt/transform center ds-modifier) - - transform (-> (gmt/matrix) - - ;; Applies the current resize transformation - (gmt/translate origin) - (gmt/multiply resize-transform) - (gmt/scale resize) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin)) - - ;; Applies the stacked transformations - (gmt/translate center) - (gmt/multiply (gmt/rotate-matrix rt-modif)) - (gmt/translate (gpt/negate center)) - - ;; Displacement - (gmt/multiply ds-modifier))] - transform)) - -(defn- calculate-skew-angle - "Calculates the skew angle of the paralelogram given by the points" - [[p1 _ p3 p4]] - (let [v1 (gpt/to-vec p3 p4) - v2 (gpt/to-vec p4 p1)] - ;; If one of the vectors is zero it's a rectangle with 0 height or width - ;; We don't skew these - (if (or (gpt/almost-zero? v1) - (gpt/almost-zero? v2)) - 0 - (- 90 (gpt/angle-with-other v1 v2))))) - -(defn- calculate-height - "Calculates the height of a paralelogram given by the points" - [[p1 _ _ p4]] - (-> (gpt/to-vec p4 p1) - (gpt/length))) - -(defn- calculate-width - "Calculates the width of a paralelogram given by the points" - [[p1 p2 _ _]] - (-> (gpt/to-vec p1 p2) - (gpt/length))) - -(defn- calculate-rotation - "Calculates the rotation between two shapes given the resize vector direction" - [center points-shape1 points-shape2 flip-x flip-y] - - (let [idx-1 0 - idx-2 (cond (and flip-x (not flip-y)) 1 - (and flip-x flip-y) 2 - (and (not flip-x) flip-y) 3 - :else 0) - p1 (nth points-shape1 idx-1) - p2 (nth points-shape2 idx-2) - v1 (gpt/to-vec center p1) - v2 (gpt/to-vec center p2) - - rot-angle (gpt/angle-with-other v1 v2) - rot-sign (gpt/angle-sign v1 v2)] - (* rot-sign rot-angle))) - -(defn- calculate-dimensions - [[p1 p2 p3 _]] - (let [width (gpt/distance p1 p2) - height (gpt/distance p2 p3)] - {:width width :height height})) - -(defn calculate-adjust-matrix - "Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that - after applying them the end result is the `shape-pathn-temp`. - This is compose of three transformations: skew, resize and rotation" - ([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false)) - ([points-temp points-rec flip-x flip-y] - (let [center (gco/center-points points-temp) - - stretch-matrix (gmt/matrix) - - skew-angle (calculate-skew-angle points-temp) - - ;; When one of the axis is flipped we have to reverse the skew - ;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle ) - skew-angle (if (and (or flip-x flip-y) - (not (and flip-x flip-y))) (- skew-angle) skew-angle ) - skew-angle (if (mth/nan? skew-angle) 0 skew-angle) - - stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) - - h1 (max 1 (calculate-height points-temp)) - h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix))) - h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1) - h3 (if (mth/nan? h3) 1 h3) - - w1 (max 1 (calculate-width points-temp)) - w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix))) - w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1) - w3 (if (mth/nan? w3) 1 w3) - - stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3))) - - rotation-angle (calculate-rotation - center - (transform-points points-rec (gco/center-points points-rec) stretch-matrix) - points-temp - flip-x - flip-y) - - stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) - - ;; This is the inverse to be able to remove the transformation - stretch-matrix-inverse (-> (gmt/matrix) - (gmt/scale (gpt/point (/ 1 w3) (/ 1 h3))) - (gmt/skew (- skew-angle) 0) - (gmt/rotate (- rotation-angle)))] - [stretch-matrix stretch-matrix-inverse rotation-angle]))) - -(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 round-coords?] - ;; - (let [points (-> shape :points (transform-points transform)) - center (gco/center-points points) - - ;; Reverse the current transformation stack to get the base rectangle - tr-inverse (:transform-inverse shape (gmt/matrix)) - - points-temp (transform-points points center tr-inverse) - points-temp-dim (calculate-dimensions points-temp) - - ;; This rectangle is the new data for the current rectangle. We want to change our rectangle - ;; to have this width, height, x, y - rect-shape (-> (gco/make-centered-rect - center - (:width points-temp-dim) - (:height points-temp-dim)) - (update :width max 1) - (update :height max 1)) - - rect-points (gpr/rect->points rect-shape) - - [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) - - rect-shape (cond-> rect-shape - round-coords? - (-> (update :x mth/round) - (update :y mth/round) - (update :width mth/round) - (update :height mth/round))) - - shape (cond - (= :path (:type shape)) - (-> shape - (update :content #(gpa/transform-content % transform))) - - :else - (-> shape - (merge rect-shape))) - - base-rotation (or (:rotation shape) 0) - modif-rotation (or (get-in shape [:modifiers :rotation]) 0)] - - (as-> shape $ - (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) - (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) - (assoc $ :points (into [] points)) - (assoc $ :selrect (gpr/rect->selrect rect-shape)) - (assoc $ :rotation (mod (+ base-rotation modif-rotation) 360))))) - -(defn set-flip [shape modifiers] - (let [rx (get-in modifiers [:resize-vector :x]) - ry (get-in modifiers [:resize-vector :y])] - (cond-> shape - (and rx (< rx 0)) (-> (update :flip-x not) - (update :rotation -)) - (and ry (< ry 0)) (-> (update :flip-y not) - (update :rotation -))))) - -(defn apply-displacement [shape] - (let [modifiers (:modifiers shape)] - (if (contains? modifiers :displacement) - (let [mov-vec (-> (gpt/point 0 0) - (gpt/transform (:displacement modifiers))) - shape (move shape mov-vec) - modifiers (dissoc modifiers :displacement)] - (-> shape - (assoc :modifiers modifiers) - (cond-> (empty? modifiers) - (dissoc :modifiers)))) - shape))) - -(defn apply-text-resize - [shape orig-shape modifiers] - (if (and (= (:type shape) :text) - (:resize-scale-text modifiers)) - (let [merge-attrs (fn [attrs] - (let [font-size (-> (get attrs :font-size 14) - (d/parse-double) - (* (-> modifiers :resize-vector :x)) - (str) - )] - (attrs/merge attrs {:font-size font-size})))] - (update shape :content #(txt/transform-nodes - txt/is-text-node? - merge-attrs - %))) - shape)) - -(defn transform-shape - ([shape] - (transform-shape shape nil)) - - ([shape {:keys [round-coords?] - :or {round-coords? true}}] - (let [shape (apply-displacement shape) - center (gco/center-shape shape) - modifiers (:modifiers shape)] - (if (and modifiers center) - (let [transform (modifiers->transform center modifiers)] - (-> shape - (set-flip modifiers) - (apply-transform transform round-coords?) - (apply-text-resize shape modifiers) - (dissoc :modifiers))) - shape)))) - -(defn update-group-viewbox - "Updates the viewbox for groups imported from SVG's" - [{:keys [selrect svg-viewbox] :as group} new-selrect] - (let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports) - deltas {:x (- (:x new-selrect 0) (:x selrect 0)) - :y (- (:y new-selrect 0) (:y selrect 0)) - :width (- (:width new-selrect 1) (:width selrect 1)) - :height (- (:height new-selrect 1) (:height selrect 1))}] - - (cond-> group - (and (some? svg-viewbox) (some? selrect) (some? new-selrect)) - (update :svg-viewbox - #(-> % - (update :x + (:x deltas)) - (update :y + (:y deltas)) - (update :width + (:width deltas)) - (update :height + (:height deltas))))))) - -(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)) - - ;; Invert to get the points minus the transforms applied to the group - base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix))) - - ;; Defines the new selection rect with its transformations - new-points (-> (gpr/points->selrect base-points) - (gpr/rect->points) - (transform-points shape-center (:transform group (gmt/matrix)))) - - ;; Calculte the new selrect - new-selrect (gpr/points->selrect base-points)] - - ;; Updates the shape and the applytransform-rect will update the other properties - (-> group - (update-group-viewbox new-selrect) - (assoc :selrect new-selrect) - (assoc :points new-points) - - ;; 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) true)))) - diff --git a/common/deps.edn b/common/deps.edn new file mode 100644 index 0000000000..2019e5504f --- /dev/null +++ b/common/deps.edn @@ -0,0 +1,72 @@ +{:deps + {org.clojure/clojure {:mvn/version "1.10.3"} + org.clojure/data.json {:mvn/version "2.3.1"} + org.clojure/core.async {:mvn/version "1.3.618"} + org.clojure/tools.cli {:mvn/version "1.0.206"} + metosin/jsonista {:mvn/version "0.3.3"} + org.clojure/clojurescript {:mvn/version "1.10.844"} + + ;; Logging + org.clojure/tools.logging {:mvn/version "1.1.0"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.14.1"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"} + org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"} + org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} + + selmer/selmer {:mvn/version "1.12.40"} + expound/expound {:mvn/version "0.8.9"} + com.cognitect/transit-clj {:mvn/version "1.0.324"} + com.cognitect/transit-cljs {:mvn/version "0.8.269"} + java-http-clj/java-http-clj {:mvn/version "0.4.2"} + + funcool/promesa {:mvn/version "6.0.1"} + funcool/cuerdas {:mvn/version "2021.05.29-0"} + + lambdaisland/uri {:mvn/version "1.4.70" + :exclusions [org.clojure/data.json]} + + frankiesardo/linked {:mvn/version "1.3.0"} + danlentz/clj-uuid {:mvn/version "0.1.9"} + commons-io/commons-io {:mvn/version "2.8.0"} + com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} + + ;; exception printing + io.aviso/pretty {:mvn/version "0.1.37"} + environ/environ {:mvn/version "1.2.0"}} + :paths ["src"] + :aliases + {:dev + {:extra-deps + {org.clojure/tools.namespace {:mvn/version "RELEASE"} + org.clojure/test.check {:mvn/version "RELEASE"} + org.clojure/tools.deps.alpha {:mvn/version "RELEASE"} + thheller/shadow-cljs {:mvn/version "2.12.6"} + criterium/criterium {:mvn/version "RELEASE"} + mockery/mockery {:mvn/version "RELEASE"}} + :extra-paths ["test" "dev"]} + + :repl + {:extra-deps + {com.bhauman/rebel-readline {:mvn/version "RELEASE"}} + :main-opts ["-m" "rebel-readline.main"]} + + :kaocha + {:extra-deps {lambdaisland/kaocha {:mvn/version "RELEASE"}} + :main-opts ["-m" "kaocha.runner"]} + + :test + {:extra-paths ["test"] + :extra-deps {io.github.cognitect-labs/test-runner + {:git/url "https://github.com/cognitect-labs/test-runner.git" + :sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}} + :exec-fn cognitect.test-runner.api/test} + + :shadow-cljs + {:main-opts ["-m" "shadow.cljs.devtools.cli"]} + + :outdated + {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} + :main-opts ["-m" "antq.core"]}}} + diff --git a/common/dev/user.clj b/common/dev/user.clj new file mode 100644 index 0000000000..10336b4f31 --- /dev/null +++ b/common/dev/user.clj @@ -0,0 +1,51 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns user + (:require + [clojure.java.io :as io] + [clojure.pprint :refer [pprint print-table]] + [clojure.repl :refer :all] + [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as sgen] + [clojure.test :as test] + [clojure.tools.namespace.repl :as repl] + [clojure.walk :refer [macroexpand-all]] + [criterium.core :refer [quick-bench bench with-progress-reporting]])) + +;; --- Benchmarking Tools + +(defmacro run-quick-bench + [& exprs] + `(with-progress-reporting (quick-bench (do ~@exprs) :verbose))) + +(defmacro run-quick-bench' + [& exprs] + `(quick-bench (do ~@exprs))) + +(defmacro run-bench + [& exprs] + `(with-progress-reporting (bench (do ~@exprs) :verbose))) + +(defmacro run-bench' + [& exprs] + `(bench (do ~@exprs))) + +;; --- Development Stuff + +(defn- run-tests + ([] (run-tests #"^app.common.tests.*")) + ([o] + (repl/refresh) + (cond + (instance? java.util.regex.Pattern o) + (test/run-all-tests o) + + (symbol? o) + (if-let [sns (namespace o)] + (do (require (symbol sns)) + (test/test-vars [(resolve o)])) + (test/test-ns o))))) diff --git a/common/package.json b/common/package.json new file mode 100644 index 0000000000..4344351821 --- /dev/null +++ b/common/package.json @@ -0,0 +1,13 @@ +{ + "name": "penpot-common", + "version": "1.0.0", + "main": "index.js", + "license": "MPL-2.0", + "dependencies": { + "luxon": "^1.27.0" + }, + "devDependencies": { + "source-map-support": "^0.5.19", + "ws": "^7.4.6" + } +} diff --git a/common/shadow-cljs.edn b/common/shadow-cljs.edn new file mode 100644 index 0000000000..e6bcd7175c --- /dev/null +++ b/common/shadow-cljs.edn @@ -0,0 +1,17 @@ +{:deps {:aliases [:dev]} + ;; :http {:port 3448} + ;; :nrepl {:port 3447} + :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] + + :builds + {:test + {:target :node-test + :output-to "target/tests.js" + :ns-regexp "^app.common.*-test$" + ;; :autorun true + + :compiler-options + {:output-feature-set :es-next + :output-wrapper false + :warnings {:fn-deprecated false}}}}} + diff --git a/common/app/common/attrs.cljc b/common/src/app/common/attrs.cljc similarity index 100% rename from common/app/common/attrs.cljc rename to common/src/app/common/attrs.cljc diff --git a/common/app/common/data.cljc b/common/src/app/common/data.cljc similarity index 87% rename from common/app/common/data.cljc rename to common/src/app/common/data.cljc index 649e290977..3d227cb380 100644 --- a/common/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -10,14 +10,16 @@ #?(:cljs (:require-macros [app.common.data])) (:require - [linked.set :as lks] [app.common.math :as mth] + [cljs.analyzer.api :as aapi] [clojure.set :as set] - #?(:clj [cljs.analyzer.api :as aapi]) + [cuerdas.core :as str] #?(:cljs [cljs.reader :as r] :clj [clojure.edn :as r]) #?(:cljs [cljs.core :as core] - :clj [clojure.core :as core])) + :clj [clojure.core :as core]) + [linked.set :as lks]) + #?(:clj (:import linked.set.LinkedSet))) @@ -274,7 +276,7 @@ ;; Data Parsing / Conversion ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- nan? +(defn nan? [v] (not= v v)) @@ -456,13 +458,18 @@ kw (if (keyword? kw) (name kw) kw)] (keyword (str prefix kw)))) - (defn tap "Simpilar to the tap in rxjs but for plain collections" [f coll] (f coll) coll) +(defn tap-r + "Same but with args reversed, for -> threads" + [coll f] + (f coll) + coll) + (defn map-diff "Given two maps returns the diff of its attributes in a map where the keys will be the attributes that change and the values the previous @@ -478,12 +485,11 @@ :var [2 nil]} :d [nil 10] } - If both maps are identical the result will be an empty map - " + If both maps are identical the result will be an empty map." [m1 m2] - (let [m1ks (keys m1) - m2ks (keys m2) + (let [m1ks (set (keys m1)) + m2ks (set (keys m2)) keys (set/union m1ks m2ks) diff-attr @@ -503,3 +509,65 @@ (->> keys (reduce diff-attr {})))) + +(defn- extract-numeric-suffix + [basename] + (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] + [p1 (+ 1 (parse-integer p2))] + [basename 1])) + +(defn unique-name + "A unique name generator" + ([basename used] + (unique-name basename used false)) + + ([basename used prefix-first?] + (assert (string? basename)) + (assert (set? used)) + + (let [[prefix initial] (extract-numeric-suffix basename)] + (if (and (not prefix-first?) + (not (contains? used basename))) + basename + (loop [counter initial] + (let [candidate (if (and (= 1 counter) prefix-first?) + (str prefix) + (str prefix "-" counter))] + (if (contains? used candidate) + (recur (inc counter)) + candidate))))))) + +(defn deep-mapm + "Applies a map function to an associative map and recurses over its children + when it's a vector or a map" + [mfn m] + (let [do-map + (fn [entry] + (let [[k v] (mfn entry)] + (cond + (or (vector? v) (map? v)) + [k (deep-mapm mfn v)] + + :else + (mfn [k v]))))] + (cond + (map? m) + (into {} (map do-map) m) + + (vector? m) + (into [] (map (partial deep-mapm mfn)) m) + + :else + m))) + +(defn not-empty? + [coll] + (boolean (seq coll))) + +(defn kebab-keys [m] + (->> m + (deep-mapm + (fn [[k v]] + (if (or (keyword? k) (string? k)) + [(keyword (str/kebab (name k))) v] + [k v]))))) diff --git a/common/app/common/data/undo_stack.cljc b/common/src/app/common/data/undo_stack.cljc similarity index 94% rename from common/app/common/data/undo_stack.cljc rename to common/src/app/common/data/undo_stack.cljc index 57f71d1286..09694aa01d 100644 --- a/common/app/common/data/undo_stack.cljc +++ b/common/src/app/common/data/undo_stack.cljc @@ -46,7 +46,7 @@ (assoc-in stack [:items index] value)) (defn undo - [{index :index items :items :as stack}] + [stack] (update stack :index dec)) (defn redo @@ -56,5 +56,5 @@ (update :index inc))) (defn size - [{index :index items :items :as stack}] + [{index :index :as stack}] (inc index)) diff --git a/common/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc similarity index 100% rename from common/app/common/exceptions.cljc rename to common/src/app/common/exceptions.cljc diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc new file mode 100644 index 0000000000..4a497cda5b --- /dev/null +++ b/common/src/app/common/file_builder.cljc @@ -0,0 +1,467 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.file-builder + "A version parsing helper." + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes :as gsh] + [app.common.pages.changes :as ch] + [app.common.pages.init :as init] + [app.common.pages.spec :as spec] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +(def root-frame uuid/zero) +(def conjv (fnil conj [])) +(def conjs (fnil conj #{})) + +;; This flag controls if we should execute spec validation after every commit +(def verify-on-commit? true) + +(defn- commit-change + ([file change] + (commit-change file change nil)) + + ([file change {:keys [add-container?] + :or {add-container? false}}] + (let [component-id (:current-component-id file) + change (cond-> change + (and add-container? (some? component-id)) + (assoc :component-id component-id) + + (and add-container? (nil? component-id)) + (assoc :page-id (:current-page-id file) + :frame-id (:current-frame-id file)))] + + (when verify-on-commit? + (us/assert ::spec/change change)) + (-> file + (update :changes conjv change) + (update :data ch/process-changes [change] verify-on-commit?))))) + +(defn- lookup-objects + ([file] + (if (some? (:current-component-id file)) + (get-in file [:data :components (:current-component-id file) :objects]) + (get-in file [:data :pages-index (:current-page-id file) :objects])))) + +(defn- lookup-shape [file shape-id] + (-> (lookup-objects file) + (get shape-id))) + +(defn- commit-shape [file obj] + (let [parent-id (-> file :parent-stack peek)] + (-> file + (commit-change + {:type :add-obj + :id (:id obj) + :obj obj + :parent-id parent-id} + + {:add-container? true})))) + +(defn setup-rect-selrect [obj] + (let [rect (select-keys obj [:x :y :width :height]) + center (gsh/center-rect rect) + transform (:transform obj (gmt/matrix)) + selrect (gsh/rect->selrect rect) + + points (-> (gsh/rect->points rect) + (gsh/transform-points center transform))] + + (-> obj + (assoc :selrect selrect) + (assoc :points points)))) + +(defn- setup-path-selrect + [obj] + (let [content (:content obj) + center (:center obj) + + transform-inverse + (->> (:transform-inverse obj (gmt/matrix)) + (gmt/transform-in center)) + + transform + (->> (:transform obj (gmt/matrix)) + (gmt/transform-in center)) + + content' (gsh/transform-content content transform-inverse) + selrect (gsh/content->selrect content') + points (-> (gsh/rect->points selrect) + (gsh/transform-points transform))] + + (-> obj + (dissoc :center) + (assoc :selrect selrect) + (assoc :points points)))) + +(defn- setup-selrect + [obj] + (if (= (:type obj) :path) + (setup-path-selrect obj) + (setup-rect-selrect obj))) + +(defn- generate-name + [type data] + (if (= type :svg-raw) + (let [tag (get-in data [:content :tag])] + (str "svg-" (cond (string? tag) tag + (keyword? tag) (d/name tag) + (nil? tag) "node" + :else (str tag)))) + (str/capital (d/name type)))) + +(defn- add-name + [file name] + (let [container-id (or (:current-component-id file) + (:current-page-id file))] + (-> file + (update-in [:unames container-id] conjs name)))) + +(defn- unique-name + [name file] + (let [container-id (or (:current-component-id file) + (:current-page-id file)) + unames (get-in file [:unames container-id])] + (d/unique-name name (or unames #{})))) + +(defn clear-names [file] + (dissoc file :unames)) + +(defn- check-name + "Given a tag returns its layer name" + [data file type] + + (cond-> data + (nil? (:name data)) + (assoc :name (generate-name type data)) + + :always + (update :name unique-name file))) + +;; PUBLIC API + +(defn create-file + ([name] + (create-file (uuid/next) name)) + + ([id name] + {:id id + :name name + :data (-> init/empty-file-data + (assoc :id id)) + + ;; We keep the changes so we can send them to the backend + :changes []})) + +(defn add-page + [file data] + + (assert (nil? (:current-component-id file))) + (let [page-id (or (:id data) (uuid/next)) + page (-> init/empty-page-data + (assoc :id page-id) + (d/deep-merge data))] + (-> file + (commit-change + {:type :add-page + :page page}) + + ;; Current page being edited + (assoc :current-page-id page-id) + + ;; Current frame-id + (assoc :current-frame-id root-frame) + + ;; Current parent stack we'll be nesting + (assoc :parent-stack [root-frame]) + + ;; Last object id added + (assoc :last-id nil)))) + +(defn close-page [file] + (assert (nil? (:current-component-id file))) + (-> file + (dissoc :current-page-id) + (dissoc :parent-stack) + (dissoc :last-id) + (clear-names))) + +(defn add-artboard [file data] + (assert (nil? (:current-component-id file))) + (let [obj (-> (init/make-minimal-shape :frame) + (merge data) + (check-name file :frame) + (setup-selrect) + (d/without-nils))] + (-> file + (commit-shape obj) + (assoc :current-frame-id (:id obj)) + (assoc :last-id (:id obj)) + (add-name (:name obj)) + (update :parent-stack conjv (:id obj))))) + +(defn close-artboard [file] + (assert (nil? (:current-component-id file))) + (-> file + (assoc :current-frame-id root-frame) + (update :parent-stack pop))) + +(defn add-group [file data] + (let [frame-id (:current-frame-id file) + selrect init/empty-selrect + name (:name data) + obj (-> (init/make-minimal-group frame-id selrect name) + (merge data) + (check-name file :group) + (d/without-nils))] + (-> file + (commit-shape obj) + (assoc :last-id (:id obj)) + (add-name (:name obj)) + (update :parent-stack conjv (:id obj))))) + +(defn close-group [file] + (let [group-id (-> file :parent-stack peek) + group (lookup-shape file group-id) + children (->> group :shapes (mapv #(lookup-shape file %))) + + file + (cond + (empty? children) + (commit-change + file + {:type :del-obj + :id group-id} + {:add-container? true}) + + (:masked-group? group) + (let [mask (first children)] + (commit-change + file + {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :x :val (-> mask :selrect :x)} + {:type :set :attr :y :val (-> mask :selrect :y)} + {:type :set :attr :width :val (-> mask :selrect :width)} + {:type :set :attr :height :val (-> mask :selrect :height)} + {:type :set :attr :flip-x :val (-> mask :flip-x)} + {:type :set :attr :flip-y :val (-> mask :flip-y)} + {:type :set :attr :selrect :val (-> mask :selrect)} + {:type :set :attr :points :val (-> mask :points)}]} + {:add-container? true})) + + :else + (let [group' (gsh/update-group-selrect group children)] + (commit-change + file + {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :selrect :val (:selrect group')} + {:type :set :attr :points :val (:points group')} + {:type :set :attr :x :val (-> group' :selrect :x)} + {:type :set :attr :y :val (-> group' :selrect :y)} + {:type :set :attr :width :val (-> group' :selrect :width)} + {:type :set :attr :height :val (-> group' :selrect :height)}]} + + {:add-container? true})))] + + (-> file + (update :parent-stack pop)))) + +(defn create-shape [file type data] + (let [frame-id (:current-frame-id file) + frame (when-not (= frame-id root-frame) + (lookup-shape file frame-id)) + obj (-> (init/make-minimal-shape type) + (merge data) + (check-name file :type) + (setup-selrect) + (d/without-nils)) + obj (cond-> obj + frame (gsh/translate-from-frame frame))] + (-> file + (commit-shape obj) + (assoc :last-id (:id obj)) + (add-name (:name obj))))) + +(defn create-rect [file data] + (create-shape file :rect data)) + +(defn create-circle [file data] + (create-shape file :circle data)) + +(defn create-path [file data] + (create-shape file :path data)) + +(defn create-text [file data] + (create-shape file :text data)) + +(defn create-image [file data] + (create-shape file :image data)) + +(declare close-svg-raw) + +(defn create-svg-raw [file data] + (let [file (as-> file $ + (create-shape $ :svg-raw data) + (update $ :parent-stack conjv (:last-id $))) + + create-child + (fn [file child] + (-> file + (create-svg-raw (assoc data + :id (uuid/next) + :content child)) + (close-svg-raw)))] + + ;; First :content is the the shape attribute, the other content is the + ;; XML children + (reduce create-child file (get-in data [:content :content])))) + +(defn close-svg-raw [file] + (-> file + (update :parent-stack pop))) + +(defn add-interaction + [file from-id {:keys [action-type event-type destination]}] + + (assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id)) + (assert (some? (lookup-shape file destination)) (str "Cannot locate shape with id " destination)) + + (let [interactions (->> (lookup-shape file from-id) + :interactions + (filterv #(or (not= (:action-type %) action-type) + (not= (:event-type %) event-type)))) + interactions (-> interactions + (conjv + {:action-type action-type + :event-type event-type + :destination destination}))] + (commit-change + file + {:type :mod-obj + :page-id (:current-page-id file) + :id from-id + + :operations + [{:type :set :attr :interactions :val interactions}]}))) + +(defn generate-changes + [file] + (:changes file)) + +(defn add-library-color + [file color] + + (let [id (or (:id color) (uuid/next))] + (-> file + (commit-change + {:type :add-color + :id id + :color (assoc color :id id)}) + (assoc :last-id id)))) + +(defn add-library-typography + [file typography] + (let [id (or (:id typography) (uuid/next))] + (-> file + (commit-change + {:type :add-typography + :id id + :typography (assoc typography :id id)}) + (assoc :last-id id)))) + +(defn add-library-media + [file media] + (let [id (or (:id media) (uuid/next))] + (-> file + (commit-change + {:type :add-media + :object (assoc media :id id)}) + (assoc :last-id id)))) + +(defn start-component + [file data] + + (let [selrect init/empty-selrect + name (:name data) + path (:path data) + obj (-> (init/make-minimal-group nil selrect name) + (merge data) + (check-name file :group) + (d/without-nils))] + (-> file + (commit-change + {:type :add-component + :id (:id obj) + :name name + :path path + :shapes [obj]}) + + (assoc :last-id (:id obj)) + (update :parent-stack conjv (:id obj)) + (assoc :current-component-id (:id obj))))) + +(defn finish-component + [file] + (let [component-id (:current-component-id file) + component (lookup-shape file component-id) + children (->> component :shapes (mapv #(lookup-shape file %))) + + file + (cond + (empty? children) + (commit-change + file + {:type :del-component + :id component-id}) + + (:masked-group? component) + (let [mask (first children)] + (commit-change + file + {:type :mod-obj + :id component-id + :operations + [{:type :set :attr :x :val (-> mask :selrect :x)} + {:type :set :attr :y :val (-> mask :selrect :y)} + {:type :set :attr :width :val (-> mask :selrect :width)} + {:type :set :attr :height :val (-> mask :selrect :height)} + {:type :set :attr :flip-x :val (-> mask :flip-x)} + {:type :set :attr :flip-y :val (-> mask :flip-y)} + {:type :set :attr :selrect :val (-> mask :selrect)} + {:type :set :attr :points :val (-> mask :points)}]} + + {:add-container? true})) + + :else + (let [component' (gsh/update-group-selrect component children)] + (commit-change + file + {:type :mod-obj + :id component-id + :operations + [{:type :set :attr :selrect :val (:selrect component')} + {:type :set :attr :points :val (:points component')} + {:type :set :attr :x :val (-> component' :selrect :x)} + {:type :set :attr :y :val (-> component' :selrect :y)} + {:type :set :attr :width :val (-> component' :selrect :width)} + {:type :set :attr :height :val (-> component' :selrect :height)}]} + + {:add-container? true})))] + + (-> file + (dissoc :current-component-id) + (update :parent-stack pop)))) + + diff --git a/common/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc similarity index 98% rename from common/app/common/geom/align.cljc rename to common/src/app/common/geom/align.cljc index 4a14e4b1e4..45d8b43210 100644 --- a/common/app/common/geom/align.cljc +++ b/common/src/app/common/geom/align.cljc @@ -6,9 +6,9 @@ (ns app.common.geom.align (:require - [clojure.spec.alpha :as s] + [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.data :as d])) + [clojure.spec.alpha :as s])) ;; --- Alignment diff --git a/common/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc similarity index 84% rename from common/app/common/geom/matrix.cljc rename to common/src/app/common/geom/matrix.cljc index b24736dff9..c04c05bb3b 100644 --- a/common/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -8,10 +8,9 @@ (:require #?(:cljs [cljs.pprint :as pp] :clj [clojure.pprint :as pp]) - [cuerdas.core :as str] [app.common.data :as d] - [app.common.math :as mth] - [app.common.geom.point :as gpt])) + [app.common.geom.point :as gpt] + [app.common.math :as mth])) ;; --- Matrix Impl @@ -27,6 +26,15 @@ ([a b c d e f] (Matrix. a b c d e f))) +(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") + +(defn str->matrix + [matrix-str] + (let [params (->> (re-seq number-regex matrix-str) + (filter #(-> % first seq)) + (map (comp d/parse-double first)))] + (apply matrix params))) + (defn multiply ([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] @@ -40,6 +48,21 @@ ([m1 m2 & others] (reduce multiply (multiply m1 m2) others))) +(defn add-translate + "Given two TRANSLATE matrixes (only e and f have significative + values), combine them. Quicker than multiplying them, for this + precise case." + ([{m1e :e m1f :f} {m2e :e m2f :f}] + (Matrix. + 1 + 0 + 0 + 1 + (+ m1e m2e) + (+ m1f m2f))) + ([m1 m2 & others] + (reduce add-translate (add-translate m1 m2) others))) + (defn substract [{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] diff --git a/common/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc similarity index 100% rename from common/app/common/geom/point.cljc rename to common/src/app/common/geom/point.cljc diff --git a/common/app/common/geom/proportions.cljc b/common/src/app/common/geom/proportions.cljc similarity index 100% rename from common/app/common/geom/proportions.cljc rename to common/src/app/common/geom/proportions.cljc diff --git a/common/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc similarity index 71% rename from common/app/common/geom/shapes.cljc rename to common/src/app/common/geom/shapes.cljc index db0d51e296..8771ff2e30 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -7,51 +7,12 @@ (ns app.common.geom.shapes (:require [app.common.data :as d] - [app.common.math :as mth] - [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] - [app.common.geom.shapes.transforms :as gtr] - [app.common.geom.shapes.intersect :as gin] - [app.common.spec :as us])) - - -;; --- Resize (Dimensions) -(defn resize-modifiers - [shape attr value] - (us/assert map? shape) - (us/assert #{:width :height} attr) - (us/assert number? value) - (let [{:keys [proportion proportion-lock]} shape - size (select-keys (:selrect shape) [:width :height]) - new-size (if-not proportion-lock - (assoc size attr value) - (if (= attr :width) - (-> size - (assoc :width value) - (assoc :height (/ value proportion))) - (-> size - (assoc :height value) - (assoc :width (* value proportion))))) - width (:width new-size) - height (:height new-size) - - shape-transform (:transform shape (gmt/matrix)) - shape-transform-inv (:transform-inverse shape (gmt/matrix)) - shape-center (gco/center-shape shape) - {sr-width :width sr-height :height} (:selrect shape) - - origin (-> (gpt/point (:selrect shape)) - (gtr/transform-point-center shape-center shape-transform)) - - scalev (gpt/divide (gpt/point width height) - (gpt/point sr-width sr-height))] - {:resize-vector scalev - :resize-origin origin - :resize-transform shape-transform - :resize-transform-inverse shape-transform-inv})) + [app.common.geom.shapes.transforms :as gtr])) ;; --- Setup (Initialize) ;; FIXME: Is this the correct place for these functions? @@ -101,6 +62,10 @@ [shape {:keys [x y]}] (gtr/move shape (gpt/negate (gpt/point x y))) ) +(defn translate-from-frame + [shape {:keys [x y]}] + (gtr/move shape (gpt/point x y)) ) + ;; --- Helpers (defn fully-contained? @@ -161,15 +126,6 @@ (assoc :selrect selrect :points points)))) -(defn rotation-modifiers - [shape center angle] - (let [displacement (let [shape-center (gco/center-shape shape)] - (-> (gmt/matrix) - (gmt/rotate angle center) - (gmt/rotate (- angle) shape-center)))] - {:rotation angle - :displacement displacement})) - ;; EXPORTS (d/export gco/center-shape) @@ -184,16 +140,20 @@ (d/export gpr/points->rect) (d/export gpr/center->rect) -(d/export gtr/transform-shape) +(d/export gtr/move) +(d/export gtr/absolute-move) (d/export gtr/transform-matrix) (d/export gtr/inverse-transform-matrix) (d/export gtr/transform-point-center) -(d/export gtr/transform-rect) -(d/export gtr/update-group-selrect) (d/export gtr/transform-points) +(d/export gtr/transform-rect) (d/export gtr/calculate-adjust-matrix) -(d/export gtr/move) -(d/export gtr/absolute-move) +(d/export gtr/update-group-selrect) +(d/export gtr/resize-modifiers) +(d/export gtr/rotation-modifiers) +(d/export gtr/merge-modifiers) +(d/export gtr/transform-shape) +(d/export gtr/calc-child-modifiers) ;; PATHS (d/export gsp/content->points) @@ -204,3 +164,4 @@ (d/export gin/overlaps?) (d/export gin/has-point?) (d/export gin/has-point-rect?) +(d/export gin/rect-contains-shape?) diff --git a/common/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc similarity index 100% rename from common/app/common/geom/shapes/common.cljc rename to common/src/app/common/geom/shapes/common.cljc diff --git a/common/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc similarity index 94% rename from common/app/common/geom/shapes/intersect.cljc rename to common/src/app/common/geom/shapes/intersect.cljc index 2b55cb3392..4b0593dc17 100644 --- a/common/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -6,9 +6,8 @@ (ns app.common.geom.shapes.intersect (:require - [app.common.data :as d] - [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gpp] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth])) @@ -113,11 +112,10 @@ ;; Even-odd algorithm ;; Cast a ray from the point in any direction and count the intersections ;; if it's odd the point is inside the polygon - (let [] - (->> lines - (filter #(intersect-ray? p %)) - (count) - (odd?)))) + (->> lines + (filter #(intersect-ray? p %)) + (count) + (odd?))) (defn- next-windup "Calculates the next windup number for the nonzero algorithm" @@ -173,7 +171,7 @@ (defn overlaps-path? "Checks if the given rect overlaps with the path in any point" [shape rect] - + (let [;; If paths are too complex the intersection is too expensive ;; we fallback to check its bounding box otherwise the performance penalty ;; is too big @@ -186,7 +184,7 @@ (points->lines (:points shape)) (gpp/path->lines shape)) start-point (-> shape :content (first) :params (gpt/point))] - + (or (is-point-inside-nonzero? (first rect-points) path-lines) (is-point-inside-nonzero? start-point rect-lines) (intersects-lines? rect-lines path-lines)))) @@ -197,14 +195,14 @@ (let [center (gpt/point cx cy) transform (gmt/transform-in center transform) - {px :x py :y} (gpt/transform point transform)] - ;; Ellipse inequality formula - ;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse - (let [v (+ (/ (mth/sq (- px cx)) - (mth/sq rx)) - (/ (mth/sq (- py cy)) - (mth/sq ry)))] - (<= v 1)))) + {px :x py :y} (gpt/transform point transform) + ;; Ellipse inequality formula + ;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse + v (+ (/ (mth/sq (- px cx)) + (mth/sq rx)) + (/ (mth/sq (- py cy)) + (mth/sq ry)))] + (<= v 1))) (defn intersects-line-ellipse? "Checks wether a single line intersects with the given ellipse" @@ -272,13 +270,13 @@ center (gpt/point (+ x (/ width 2)) (+ y (/ height 2))) - + ellipse-data {:cx (:x center) :cy (:y center) :rx (/ width 2) :ry (/ height 2) :transform (:transform-inverse shape)}] - + (or (is-point-inside-evenodd? center rect-lines) (is-point-inside-ellipse? (first rect-points) ellipse-data) (intersects-lines-ellipse? rect-lines ellipse-data)))) @@ -304,3 +302,9 @@ (let [lines (points->lines (:points shape))] ;; TODO: Will only work for simple shapes (is-point-inside-evenodd? point lines))) + +(defn rect-contains-shape? + [rect shape] + (->> shape + :points + (every? (partial has-point-rect? rect)))) diff --git a/common/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc similarity index 98% rename from common/app/common/geom/shapes/path.cljc rename to common/src/app/common/geom/shapes/path.cljc index 0339cd652e..ff39fa7db1 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -6,10 +6,10 @@ (ns app.common.geom.shapes.path (:require + [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.rect :as gpr] - [app.common.math :as mth] - [app.common.data :as d])) + [app.common.math :as mth])) (defn content->points [content] (->> content @@ -79,7 +79,7 @@ ;; When the term a is close to zero we have a linear equation [(/ (- c) b)] - ;; If a is not close to zero return the two roots for a cuadratic + ;; If a is not close to zero return the two roots for a cuadratic (not (mth/almost-zero? a)) [(/ (+ (- b) sqrt-b2-4ac) (* 2 a)) @@ -156,7 +156,8 @@ (mapv #(update % :params transform-params) content))) -(defn transform-content [content transform] +(defn transform-content + [content transform] (let [set-tr (fn [params px py] (let [tr-point (-> (gpt/point (get params px) (get params py)) (gpt/transform transform))] @@ -267,7 +268,7 @@ (and (< (d ht) (d t1)) (< (d ht) (d t2))) [ht1 ht2] - + (< (d t1) (d t2)) [t1 ht] @@ -324,7 +325,7 @@ (if (and (some? acc) (or (not cur) (<= min-dist cur-dist))) [min-p min-dist] [cur-p cur-dist]))] - + (->> (:content shape) (d/with-prev) (map point+distance) diff --git a/common/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc similarity index 100% rename from common/app/common/geom/shapes/rect.cljc rename to common/src/app/common/geom/shapes/rect.cljc diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc new file mode 100644 index 0000000000..0cf6fe8427 --- /dev/null +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -0,0 +1,693 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.geom.shapes.transforms + (:require + [app.common.attrs :as attrs] + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.path :as gpa] + [app.common.geom.shapes.rect :as gpr] + [app.common.math :as mth] + [app.common.pages.spec :as spec] + [app.common.spec :as us] + [app.common.text :as txt])) + + +;; --- Relative Movement + +(defn- move-selrect [selrect {dx :x dy :y}] + (-> selrect + (d/update-when :x + dx) + (d/update-when :y + dy) + (d/update-when :x1 + dx) + (d/update-when :y1 + dy) + (d/update-when :x2 + dx) + (d/update-when :y2 + dy))) + +(defn- move-points [points move-vec] + (->> points + (mapv #(gpt/add % move-vec)))) + +(defn move + "Move the shape relativelly to its current + position applying the provided delta." + [shape {dx :x dy :y}] + (let [dx (d/check-num dx) + dy (d/check-num dy) + move-vec (gpt/point dx dy)] + + (-> shape + (update :selrect move-selrect move-vec) + (update :points move-points move-vec) + (d/update-when :x + dx) + (d/update-when :y + dy) + (cond-> (= :path (:type shape)) + (update :content gpa/move-content move-vec))))) + + +;; --- Absolute Movement + +(defn absolute-move + "Move the shape to the exactly specified position." + [shape {:keys [x y]}] + (let [dx (- (d/check-num x) (-> shape :selrect :x)) + dy (- (d/check-num y) (-> shape :selrect :y))] + (move shape (gpt/point dx dy)))) + + +; ---- Geometric operations + +(defn- normalize-scale + "We normalize the scale so it's not too close to 0" + [scale] + (cond + (and (< scale 0) (> scale -0.01)) -0.01 + (and (>= scale 0) (< scale 0.01)) 0.01 + :else scale)) + +(defn- calculate-skew-angle + "Calculates the skew angle of the paralelogram given by the points" + [[p1 _ p3 p4]] + (let [v1 (gpt/to-vec p3 p4) + v2 (gpt/to-vec p4 p1)] + ;; If one of the vectors is zero it's a rectangle with 0 height or width + ;; We don't skew these + (if (or (gpt/almost-zero? v1) + (gpt/almost-zero? v2)) + 0 + (- 90 (gpt/angle-with-other v1 v2))))) + +(defn- calculate-height + "Calculates the height of a paralelogram given by the points" + [[p1 _ _ p4]] + (-> (gpt/to-vec p4 p1) + (gpt/length))) + +(defn- calculate-width + "Calculates the width of a paralelogram given by the points" + [[p1 p2 _ _]] + (-> (gpt/to-vec p1 p2) + (gpt/length))) + +(defn- calculate-rotation + "Calculates the rotation between two shapes given the resize vector direction" + [center points-shape1 points-shape2 flip-x flip-y] + + (let [idx-1 0 + idx-2 (cond (and flip-x (not flip-y)) 1 + (and flip-x flip-y) 2 + (and (not flip-x) flip-y) 3 + :else 0) + p1 (nth points-shape1 idx-1) + p2 (nth points-shape2 idx-2) + v1 (gpt/to-vec center p1) + v2 (gpt/to-vec center p2) + + rot-angle (gpt/angle-with-other v1 v2) + rot-sign (gpt/angle-sign v1 v2)] + (* rot-sign rot-angle))) + +(defn- calculate-dimensions + [[p1 p2 p3 _]] + (let [width (gpt/distance p1 p2) + height (gpt/distance p2 p3)] + {:width width :height height})) + + +;; --- Transformation matrix operations + +(defn transform-matrix + "Returns a transformation matrix without changing the shape properties. + The result should be used in a `transform` attribute in svg" + ([shape] (transform-matrix shape nil)) + ([shape params] (transform-matrix shape params (or (gco/center-shape shape) + (gpt/point 0 0)))) + ([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]} shape-center] + (-> (gmt/matrix) + (gmt/translate shape-center) + + (gmt/multiply (:transform shape (gmt/matrix))) + (cond-> + (and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1)) + (and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1))) + (gmt/translate (gpt/negate shape-center))))) + +(defn inverse-transform-matrix + ([shape] + (let [shape-center (or (gco/center-shape shape) + (gpt/point 0 0))] + (inverse-transform-matrix shape shape-center))) + ([{:keys [flip-x flip-y] :as shape} center] + (-> (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))))) + +(defn transform-point-center + "Transform a point around the shape center" + [point center matrix] + (gpt/transform + point + (gmt/multiply (gmt/translate-matrix center) + matrix + (gmt/translate-matrix (gpt/negate center))))) + +(defn transform-points + ([points matrix] + (transform-points points nil matrix)) + ([points center matrix] + (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) + post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) + + tr-point (fn [point] + (gpt/transform point (gmt/multiply prev matrix post)))] + (mapv tr-point points)))) + +(defn transform-rect + "Transform a rectangles and changes its attributes" + [rect matrix] + + (let [points (-> (gpr/rect->points rect) + (transform-points matrix))] + (gpr/points->rect points))) + +(defn calculate-adjust-matrix + "Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that + after applying them the end result is the `shape-pathn-temp`. + This is compose of three transformations: skew, resize and rotation" + ([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false)) + ([points-temp points-rec flip-x flip-y] + (let [center (gco/center-points points-temp) + + stretch-matrix (gmt/matrix) + + skew-angle (calculate-skew-angle points-temp) + + ;; When one of the axis is flipped we have to reverse the skew + ;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle ) + skew-angle (if (and (or flip-x flip-y) + (not (and flip-x flip-y))) (- skew-angle) skew-angle ) + skew-angle (if (mth/nan? skew-angle) 0 skew-angle) + + stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) + + h1 (max 1 (calculate-height points-temp)) + h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix))) + h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1) + h3 (if (mth/nan? h3) 1 h3) + + w1 (max 1 (calculate-width points-temp)) + w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix))) + w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1) + w3 (if (mth/nan? w3) 1 w3) + + stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3))) + + rotation-angle (calculate-rotation + center + (transform-points points-rec (gco/center-points points-rec) stretch-matrix) + points-temp + flip-x + flip-y) + + stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) + + ;; This is the inverse to be able to remove the transformation + stretch-matrix-inverse (-> (gmt/matrix) + (gmt/scale (gpt/point (/ 1 w3) (/ 1 h3))) + (gmt/skew (- skew-angle) 0) + (gmt/rotate (- rotation-angle)))] + [stretch-matrix stretch-matrix-inverse rotation-angle]))) + +(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 round-coords?] + ;; + (let [points (-> shape :points (transform-points transform)) + center (gco/center-points points) + + ;; Reverse the current transformation stack to get the base rectangle + tr-inverse (:transform-inverse shape (gmt/matrix)) + + points-temp (transform-points points center tr-inverse) + points-temp-dim (calculate-dimensions points-temp) + + ;; This rectangle is the new data for the current rectangle. We want to change our rectangle + ;; to have this width, height, x, y + rect-shape (-> (gco/make-centered-rect + center + (:width points-temp-dim) + (:height points-temp-dim)) + (update :width max 1) + (update :height max 1)) + + rect-points (gpr/rect->points rect-shape) + + [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) + + rect-shape (cond-> rect-shape + round-coords? + (-> (update :x mth/round) + (update :y mth/round) + (update :width mth/round) + (update :height mth/round))) + + shape (cond + (= :path (:type shape)) + (-> shape + (update :content #(gpa/transform-content % transform))) + + :else + (-> shape + (merge rect-shape))) + + base-rotation (or (:rotation shape) 0) + modif-rotation (or (get-in shape [:modifiers :rotation]) 0)] + + (as-> shape $ + (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) + (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) + (assoc $ :points (into [] points)) + (assoc $ :selrect (gpr/rect->selrect rect-shape)) + (assoc $ :rotation (mod (+ base-rotation modif-rotation) 360))))) + +(defn- update-group-viewbox + "Updates the viewbox for groups imported from SVG's" + [{:keys [selrect svg-viewbox] :as group} new-selrect] + (let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports) + deltas {:x (- (:x new-selrect 0) (:x selrect 0)) + :y (- (:y new-selrect 0) (:y selrect 0)) + :width (- (:width new-selrect 1) (:width selrect 1)) + :height (- (:height new-selrect 1) (:height selrect 1))}] + + (cond-> group + (and (some? svg-viewbox) (some? selrect) (some? new-selrect)) + (update :svg-viewbox + #(-> % + (update :x + (:x deltas)) + (update :y + (:y deltas)) + (update :width + (:width deltas)) + (update :height + (:height deltas))))))) + +(defn update-group-selrect [group children] + (let [shape-center (gco/center-shape group) + ;; Points for every shape inside the group + points (->> children (mapcat :points)) + + ;; Invert to get the points minus the transforms applied to the group + base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix))) + + ;; Defines the new selection rect with its transformations + new-points (-> (gpr/points->selrect base-points) + (gpr/rect->points) + (transform-points shape-center (:transform group (gmt/matrix)))) + + ;; Calculte the new selrect + new-selrect (gpr/points->selrect base-points)] + + ;; Updates the shape and the applytransform-rect will update the other properties + (-> group + (update-group-viewbox new-selrect) + (assoc :selrect new-selrect) + (assoc :points new-points) + + ;; 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) true)))) + + +;; --- Modifiers + +;; The `modifiers` structure contains a list of transformations to +;; do make to a shape, in this order: +;; +;; - resize-origin (gpt/point) + resize-vector (gpt/point) +;; apply a scale vector to all points of the shapes, starting +;; from the origin point. +;; +;; - resize-origin-2 + resize-vector-2 +;; same as the previous one, for cases in that we need to make +;; two vectors from different origin points. +;; +;; - displacement (gmt/matrix) +;; apply a translation matrix to the shape +;; +;; - rotation (gmt/matrix) +;; apply a rotation matrix to the shape +;; +;; - resize-transform (gmt/matrix) + resize-transform-inverse (gmt/matrix) +;; a copy of the rotation matrix currently applied to the shape; +;; this is needed temporarily to apply the resize vectors. +;; +;; - resize-scale-text (bool) +;; tells if the resize vectors must be applied to text shapes +;; or not. + +(defn resize-modifiers + [shape attr value] + (us/assert map? shape) + (us/assert #{:width :height} attr) + (us/assert number? value) + (let [{:keys [proportion proportion-lock]} shape + size (select-keys (:selrect shape) [:width :height]) + new-size (if-not proportion-lock + (assoc size attr value) + (if (= attr :width) + (-> size + (assoc :width value) + (assoc :height (/ value proportion))) + (-> size + (assoc :height value) + (assoc :width (* value proportion))))) + width (:width new-size) + height (:height new-size) + + shape-transform (:transform shape (gmt/matrix)) + shape-transform-inv (:transform-inverse shape (gmt/matrix)) + shape-center (gco/center-shape shape) + {sr-width :width sr-height :height} (:selrect shape) + + origin (-> (gpt/point (:selrect shape)) + (transform-point-center shape-center shape-transform)) + + scalev (gpt/divide (gpt/point width height) + (gpt/point sr-width sr-height))] + {:resize-vector scalev + :resize-origin origin + :resize-transform shape-transform + :resize-transform-inverse shape-transform-inv})) + +(defn rotation-modifiers + [shape center angle] + (let [displacement (let [shape-center (gco/center-shape shape)] + (-> (gmt/matrix) + (gmt/rotate angle center) + (gmt/rotate (- angle) shape-center)))] + {:rotation angle + :displacement displacement})) + +(defn merge-modifiers + [objects modifiers] + + (let [set-modifier + (fn [objects [id modifiers]] + (-> objects + (d/update-when id merge modifiers)))] + (->> modifiers + (reduce set-modifier objects)))) + +(defn- modifiers->transform + [center modifiers] + (let [ds-modifier (:displacement modifiers (gmt/matrix)) + {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) + {res-x-2 :x res-y-2 :y} (:resize-vector-2 modifiers (gpt/point 1 1)) + + ;; Normalize x/y vector coordinates because scale by 0 is infinite + res-x (normalize-scale res-x) + res-y (normalize-scale res-y) + resize (gpt/point res-x res-y) + + res-x-2 (normalize-scale res-x-2) + res-y-2 (normalize-scale res-y-2) + resize-2 (gpt/point res-x-2 res-y-2) + + origin (:resize-origin modifiers (gpt/point 0 0)) + origin-2 (:resize-origin-2 modifiers (gpt/point 0 0)) + + resize-transform (:resize-transform modifiers (gmt/matrix)) + resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) + rt-modif (or (:rotation modifiers) 0) + + center (gpt/transform center ds-modifier) + + transform (-> (gmt/matrix) + + ;; Applies the current resize transformation + (gmt/translate origin) + (gmt/multiply resize-transform) + (gmt/scale resize) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin)) + + (gmt/translate origin-2) + (gmt/multiply resize-transform) + (gmt/scale resize-2) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin-2)) + + ;; Applies the stacked transformations + (gmt/translate center) + (gmt/multiply (gmt/rotate-matrix rt-modif)) + (gmt/translate (gpt/negate center)) + + ;; Displacement + (gmt/multiply ds-modifier))] + transform)) + +(defn- set-flip [shape modifiers] + (let [rx (get-in modifiers [:resize-vector :x]) + ry (get-in modifiers [:resize-vector :y])] + (cond-> shape + (and rx (< rx 0)) (-> (update :flip-x not) + (update :rotation -)) + (and ry (< ry 0)) (-> (update :flip-y not) + (update :rotation -))))) + +(defn- apply-displacement [shape] + (let [modifiers (:modifiers shape)] + (if (contains? modifiers :displacement) + (let [mov-vec (-> (gpt/point 0 0) + (gpt/transform (:displacement modifiers))) + shape (move shape mov-vec) + modifiers (dissoc modifiers :displacement)] + (-> shape + (assoc :modifiers modifiers) + (cond-> (empty? modifiers) + (dissoc :modifiers)))) + shape))) + +(defn- apply-text-resize + [shape modifiers] + (if (and (= (:type shape) :text) + (:resize-scale-text modifiers)) + (let [merge-attrs (fn [attrs] + (let [font-size (-> (get attrs :font-size 14) + (d/parse-double) + (* (get-in modifiers [:resize-vector :x] 1)) + (* (get-in modifiers [:resize-vector-2 :x] 1)) + (str))] + (attrs/merge attrs {:font-size font-size})))] + (update shape :content #(txt/transform-nodes + txt/is-text-node? + merge-attrs + %))) + shape)) + +(defn transform-shape + ([shape] + (transform-shape shape nil)) + + ([shape {:keys [round-coords?] + :or {round-coords? true}}] + (let [shape (apply-displacement shape) + center (gco/center-shape shape) + modifiers (:modifiers shape)] + (if (and modifiers center) + (let [transform (modifiers->transform center modifiers)] + (-> shape + (set-flip modifiers) + (apply-transform transform round-coords?) + (apply-text-resize modifiers) + (dissoc :modifiers))) + shape)))) + +(defn calc-child-modifiers + "Given the modifiers to apply to the parent, calculate the corresponding + modifiers for the child, depending on the child constraints." + [parent child parent-modifiers] + (let [parent-rect (:selrect parent) + child-rect (:selrect child) + + ;; Apply the modifiers to the parent's selrect, to check the difference with + ;; the original, and calculate child transformations from this. + ;; + ;; Note that a shape's selrect is always "horizontal" (i.e. without applying + ;; the shape transform, that may include some rotation and skew). Thus, to + ;; apply the modifiers, we first apply to them the transform-inverse. + parent-displacement (-> (gpt/point 0 0) + (gpt/transform (get parent-modifiers :displacement (gmt/matrix))) + (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) + (gmt/translate-matrix)) + parent-origin (-> (:resize-origin parent-modifiers) + ((d/nilf transform-point-center) + (gco/center-shape parent) + (:resize-transform-inverse parent-modifiers (gmt/matrix)))) + parent-origin-2 (-> (:resize-origin-2 parent-modifiers) + ((d/nilf transform-point-center) + (gco/center-shape parent) + (:resize-transform-inverse parent-modifiers (gmt/matrix)))) + parent-vector (get parent-modifiers :resize-vector (gpt/point 1 1)) + parent-vector-2 (get parent-modifiers :resize-vector-2 (gpt/point 1 1)) + + transformed-parent-rect (-> parent-rect + (gpr/rect->points) + (transform-points parent-displacement) + (transform-points parent-origin (gmt/scale-matrix parent-vector)) + (transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) + (gpr/points->selrect)) + + ;; Calculate the modifiers in the horizontal and vertical directions + ;; depending on the child constraints. + constraints-h (get child :constraints-h (spec/default-constraints-h child)) + constraints-v (get child :constraints-v (spec/default-constraints-v child)) + + modifiers-h (case constraints-h + :left + (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect))] + + (if-not (mth/almost-zero? delta-left) + {:displacement (gpt/point delta-left 0)} ;; we convert to matrix below + {})) + + :right + (let [delta-right (- (:x2 transformed-parent-rect) (:x2 parent-rect))] + (if-not (mth/almost-zero? delta-right) + {:displacement (gpt/point delta-right 0)} + {})) + + :leftright + (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect)) + delta-width (- (:width transformed-parent-rect) (:width parent-rect))] + (if (or (not (mth/almost-zero? delta-left)) + (not (mth/almost-zero? delta-width))) + {:displacement (gpt/point delta-left 0) + :resize-origin (-> (gpt/point (+ (:x1 child-rect) delta-left) + (:y1 child-rect)) + (transform-point-center + (gco/center-rect child-rect) + (:transform child (gmt/matrix)))) + :resize-vector (gpt/point (/ (+ (:width child-rect) delta-width) + (:width child-rect)) 1)} + {})) + + :center + (let [parent-center (gco/center-rect parent-rect) + transformed-parent-center (gco/center-rect transformed-parent-rect) + delta-center (- (:x transformed-parent-center) (:x parent-center))] + (if-not (mth/almost-zero? delta-center) + {:displacement (gpt/point delta-center 0)} + {})) + + :scale + (cond-> {} + (and (:resize-vector parent-modifiers) + (not (mth/close? (:x (:resize-vector parent-modifiers)) 1))) + (assoc :resize-origin (:resize-origin parent-modifiers) + :resize-vector (gpt/point (:x (:resize-vector parent-modifiers)) 1)) + + (and (:resize-vector-2 parent-modifiers) + (not (mth/close? (:x (:resize-vector-2 parent-modifiers)) 1))) + (assoc :resize-origin-2 (:resize-origin-2 parent-modifiers) + :resize-vector-2 (gpt/point (:x (:resize-vector-2 parent-modifiers)) 1)) + + (:displacement parent-modifiers) + (assoc :displacement + (gpt/point (-> (gpt/point 0 0) + (gpt/transform (:displacement parent-modifiers)) + (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) + (:x)) + 0))) + {}) + + modifiers-v (case constraints-v + :top + (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect))] + (if-not (mth/almost-zero? delta-top) + {:displacement (gpt/point 0 delta-top)} ;; we convert to matrix below + {})) + + :bottom + (let [delta-bottom (- (:y2 transformed-parent-rect) (:y2 parent-rect))] + (if-not (mth/almost-zero? delta-bottom) + {:displacement (gpt/point 0 delta-bottom)} + {})) + + :topbottom + (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect)) + delta-height (- (:height transformed-parent-rect) (:height parent-rect))] + (if (or (not (mth/almost-zero? delta-top)) + (not (mth/almost-zero? delta-height))) + {:displacement (gpt/point 0 delta-top) + :resize-origin (-> (gpt/point (:x1 child-rect) + (+ (:y1 child-rect) delta-top)) + (transform-point-center + (gco/center-rect child-rect) + (:transform child (gmt/matrix)))) + :resize-vector (gpt/point 1 (/ (+ (:height child-rect) delta-height) + (:height child-rect)))} + {})) + + :center + (let [parent-center (gco/center-rect parent-rect) + transformed-parent-center (gco/center-rect transformed-parent-rect) + delta-center (- (:y transformed-parent-center) (:y parent-center))] + (if-not (mth/almost-zero? delta-center) + {:displacement (gpt/point 0 delta-center)} + {})) + + :scale + (cond-> {} + (and (:resize-vector parent-modifiers) + (not (mth/close? (:y (:resize-vector parent-modifiers)) 1))) + (assoc :resize-origin (:resize-origin parent-modifiers) + :resize-vector (gpt/point 1 (:y (:resize-vector parent-modifiers)))) + + (and (:resize-vector-2 parent-modifiers) + (not (mth/close? (:y (:resize-vector-2 parent-modifiers)) 1))) + (assoc :resize-origin-2 (:resize-origin-2 parent-modifiers) + :resize-vector-2 (gpt/point 1 (:y (:resize-vector-2 parent-modifiers)))) + + (:displacement parent-modifiers) + (assoc :displacement + (gpt/point 0 (-> (gpt/point 0 0) + (gpt/transform (:displacement parent-modifiers)) + (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) + (:y))))) + {})] + + ;; Build final child modifiers. Apply transform again to the result, to get the + ;; real modifiers that need to be applied to the child, including rotation as needed. + (cond-> {} + (or (:displacement modifiers-h) (:displacement modifiers-v)) + (assoc :displacement (gmt/translate-matrix + (-> (gpt/point (get (:displacement modifiers-h) :x 0) + (get (:displacement modifiers-v) :y 0)) + (gpt/transform + (:resize-transform parent-modifiers (gmt/matrix)))))) + + (:resize-vector modifiers-h) + (assoc :resize-origin (:resize-origin modifiers-h) + :resize-vector (gpt/point (get (:resize-vector modifiers-h) :x 1) + (get (:resize-vector modifiers-h) :y 1))) + + (:resize-vector modifiers-v) + (assoc :resize-origin-2 (:resize-origin modifiers-v) + :resize-vector-2 (gpt/point (get (:resize-vector modifiers-v) :x 1) + (get (:resize-vector modifiers-v) :y 1))) + + (:resize-transform parent-modifiers) + (assoc :resize-transform (:resize-transform parent-modifiers) + :resize-transform-inverse (:resize-transform-inverse parent-modifiers))))) + diff --git a/common/app/common/math.cljc b/common/src/app/common/math.cljc similarity index 100% rename from common/app/common/math.cljc rename to common/src/app/common/math.cljc diff --git a/common/app/common/media.cljc b/common/src/app/common/media.cljc similarity index 100% rename from common/app/common/media.cljc rename to common/src/app/common/media.cljc diff --git a/common/app/common/pages.cljc b/common/src/app/common/pages.cljc similarity index 97% rename from common/app/common/pages.cljc rename to common/src/app/common/pages.cljc index 4ef70babaa..fdf02cfa33 100644 --- a/common/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -60,11 +60,12 @@ (d/export helpers/get-base-shape) (d/export helpers/is-parent?) (d/export helpers/get-index-in-parent) +(d/export helpers/split-path) +(d/export helpers/join-path) (d/export helpers/parse-path-name) (d/export helpers/merge-path-item) (d/export helpers/compact-path) (d/export helpers/compact-name) -(d/export helpers/merge-modifiers) ;; Indices (d/export indices/calculate-z-index) @@ -82,6 +83,7 @@ (d/export init/make-file-data) (d/export init/make-minimal-shape) (d/export init/make-minimal-group) +(d/export init/empty-file-data) ;; Specs diff --git a/common/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc similarity index 95% rename from common/app/common/pages/changes.cljc rename to common/src/app/common/pages/changes.cljc index 80be617ec8..e3a5274f29 100644 --- a/common/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -9,12 +9,11 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] - [app.common.pages.spec :as ps] - [app.common.spec :as us] [app.common.pages.common :refer [component-sync-attrs]] + [app.common.pages.helpers :as cph] [app.common.pages.init :as init] - [app.common.pages.spec :as spec])) + [app.common.pages.spec :as spec] + [app.common.spec :as us])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Transformation Changes @@ -33,16 +32,13 @@ (when verify? (us/assert ::spec/changes items)) - (let [pages (into #{} (map :page-id) items) - result (->> items - (reduce #(or (process-change %1 %2) %1) data))] - + (let [result (reduce #(or (process-change %1 %2) %1) data items)] ;; Validate result shapes (only on the backend) #?(:clj - (doseq [page-id pages] + (doseq [page-id (into #{} (map :page-id) items)] (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])) + (when-not (= shape (get-in data [:pages-index page-id :objects id])) ;; If object has change verify is correct (us/verify ::spec/shape shape)))))) @@ -412,20 +408,22 @@ (let [attr (:attr op) val (:val op) ignore (:ignore-touched op) + ignore-geometry (:ignore-geometry op) shape-ref (:shape-ref shape) group (get component-sync-attrs attr) root-name? (and (= group :name-group) (:component-root? shape))] (cond-> shape + ;; Depending on the origin of the attribute change, we need or not to + ;; set the "touched" flag for the group the attribute belongs to. + ;; In some cases we need to ignore touched only if the attribute is + ;; geometric (position, width or transformation). (and shape-ref group (not ignore) (not= val (get shape attr)) (not root-name?) - ;; FIXME: it's difficult to tell if the geometry changes affect - ;; an individual shape inside the component, or are for - ;; the whole component (in which case we shouldn't set - ;; touched). For the moment we disable geometry touched - ;; except width and height that seems to work well. - (or (not= group :geometry-group) (#{:width :height} attr))) + (not (and ignore-geometry + (and (= group :geometry-group) + (not (#{:width :height} attr)))))) (-> (update :touched cph/set-touched-group group) (dissoc :remote-synced?)) diff --git a/common/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc similarity index 92% rename from common/app/common/pages/common.cljc rename to common/src/app/common/pages/common.cljc index 9a39105bf5..29319abef4 100644 --- a/common/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -56,5 +56,8 @@ :transform-inverse :geometry-group :shadow :shadow-group :blur :blur-group - :masked-group? :mask-group}) + :masked-group? :mask-group + :constraints-h :constraints-group + :constraints-v :constraints-group + :fixed-scroll :constraints-group}) diff --git a/common/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc similarity index 94% rename from common/app/common/pages/helpers.cljc rename to common/src/app/common/pages/helpers.cljc index 9eb194c271..2c7507243e 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -10,7 +10,6 @@ [app.common.geom.shapes :as gsh] [app.common.spec :as us] [app.common.uuid :as uuid] - [clojure.set :as set] [cuerdas.core :as str])) (defn walk-pages @@ -119,14 +118,14 @@ (conj! pending current) (first children) (rest children)) - [result pending]))] + [result pending])) - ;; If we have still pending, advance the iterator - (let [length (count pending)] - (if (pos? length) - (let [next (get pending (dec length))] - (recur result (pop! pending) next)) - (persistent! result)))))) + ;; If we have still pending, advance the iterator + length (count pending)] + (if (pos? length) + (let [next (get pending (dec length))] + (recur result (pop! pending) next)) + (persistent! result))))) (defn get-children-objects "Retrieve all children objects recursively for a given object" @@ -403,17 +402,23 @@ [objects shape-id] (let [shape (get objects shape-id) parent (get objects (:parent-id shape)) - [parent-idx _] (d/seek (fn [[idx child-id]] (= child-id shape-id)) + [parent-idx _] (d/seek (fn [[_idx child-id]] (= child-id shape-id)) (d/enumerate (:shapes parent)))] parent-idx)) (defn split-path - [path] "Decompose a string in the form 'one / two / three' into - an array of strings, normalizing spaces." - (->> (str/split path "/") - (map str/trim) - (remove str/empty?))) + a vector of strings, normalizing spaces." + [path] + (let [xf (comp (map str/trim) + (remove str/empty?))] + (->> (str/split path "/") + (into [] xf)))) + +(defn join-path + "Regenerate a path as a string, from a vector." + [path-vec] + (str/join " / " path-vec)) (defn parse-path-name "Parse a string in the form 'group / subgroup / name'. @@ -428,7 +433,9 @@ "Put the item at the end of the path." [path name] (if-not (empty? path) - (str path " / " name) + (if-not (empty? name) + (str path " / " name) + path) name)) (defn compact-path @@ -458,12 +465,3 @@ (let [path-split (split-path path)] (merge-path-item (first path-split) name))) -(defn merge-modifiers - [objects modifiers] - - (let [set-modifier - (fn [objects [id modifiers]] - (-> objects - (d/update-when id merge modifiers)))] - (->> modifiers - (reduce set-modifier objects)))) diff --git a/common/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc similarity index 98% rename from common/app/common/pages/indices.cljc rename to common/src/app/common/pages/indices.cljc index fa39937992..c1681fef19 100644 --- a/common/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -7,7 +7,6 @@ (ns app.common.pages.indices (:require [app.common.data :as d] - [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as helpers] [app.common.uuid :as uuid] [clojure.set :as set])) @@ -100,7 +99,8 @@ "Retrieves the mask information for an object" [objects parents-index] (let [retrieve-masks - (fn [id parents] + (fn [_ parents] + ;; TODO: use transducers? (->> parents (map #(get objects %)) (filter #(:masked-group? %)) diff --git a/common/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc similarity index 93% rename from common/app/common/pages/init.cljc rename to common/src/app/common/pages/init.cljc index 5ca2b85f12..ca42025f43 100644 --- a/common/app/common/pages/init.cljc +++ b/common/src/app/common/pages/init.cljc @@ -7,9 +7,9 @@ (ns app.common.pages.init (:require [app.common.data :as d] - [app.common.uuid :as uuid] [app.common.exceptions :as ex] - [app.common.pages.common :refer [file-version default-color]])) + [app.common.pages.common :refer [file-version default-color]] + [app.common.uuid :as uuid])) (def root uuid/zero) @@ -85,6 +85,12 @@ {:type :svg-raw}]) +(def empty-selrect + {:x 0 :y 0 + :x1 0 :y1 0 + :x2 1 :y2 1 + :width 1 :height 1}) + (defn make-minimal-shape [type] (let [type (cond (= type :curve) :path @@ -126,10 +132,11 @@ :height (:height selection-rect)}) (defn make-file-data - ([file-id] (make-file-data file-id(uuid/next))) + ([file-id] + (make-file-data file-id (uuid/next))) + ([file-id page-id] - (let [ - pd (assoc empty-page-data + (let [pd (assoc empty-page-data :id page-id :name "Page-1")] (-> empty-file-data diff --git a/common/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc similarity index 98% rename from common/app/common/pages/migrations.cljc rename to common/src/app/common/pages/migrations.cljc index 8e93da8043..7a12167807 100644 --- a/common/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -6,13 +6,13 @@ (ns app.common.pages.migrations (:require - [app.common.pages :as cp] + [app.common.data :as d] + [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gsp] - [app.common.geom.matrix :as gmt] [app.common.math :as mth] - [app.common.uuid :as uuid] - [app.common.data :as d])) + [app.common.pages :as cp] + [app.common.uuid :as uuid])) ;; TODO: revisit this and rename to file-migrations @@ -94,7 +94,7 @@ (= :curve (:type object)) (assoc :type :path) - (or (#{:curve :path} (:type object))) + (#{:curve :path} (:type object)) (migrate-path) (= :frame (:type object)) diff --git a/common/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc similarity index 94% rename from common/app/common/pages/spec.cljc rename to common/src/app/common/pages/spec.cljc index 24c523fd0c..9c445ce6de 100644 --- a/common/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -9,6 +9,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.spec :as us] + [app.common.uuid :as uuid] [clojure.spec.alpha :as s])) ;; --- Specs @@ -195,6 +196,30 @@ (s/def :internal.shape/interactions (s/coll-of :internal.shape/interaction :kind vector?)) +;; Size constraints + +(s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale}) +(s/def :internal.shape/constraints-v #{:top :bottom :topbottom :center :scale}) +(s/def :internal.shape/fixed-scroll boolean?) + +; Shapes in the top frame have no constraints. Shapes directly below some +; frame are left-top constrained. Else (shapes in a group) are scaled. +(defn default-constraints-h + [shape] + (if (= (:parent-id shape) uuid/zero) + nil + (if (= (:parent-id shape) (:frame-id shape)) + :left + :scale))) + +(defn default-constraints-v + [shape] + (if (= (:parent-id shape) uuid/zero) + nil + (if (= (:parent-id shape) (:frame-id shape)) + :top + :scale))) + ;; Page Data related (s/def :internal.shape/blocked boolean?) (s/def :internal.shape/collapsed boolean?) @@ -297,6 +322,9 @@ :internal.shape/locked :internal.shape/proportion :internal.shape/proportion-lock + :internal.shape/constraints-h + :internal.shape/constraints-v + :internal.shape/fixed-scroll :internal.shape/rx :internal.shape/ry :internal.shape/r1 @@ -367,11 +395,11 @@ :internal.media-object/mtype])) (s/def ::media-object-update - (s/keys :req-un [::id] - :req-opt [::name - :internal.media-object/width - :internal.media-object/height - :internal.media-object/mtype])) + (s/keys :req-un [::id] + :opt-un [::name + :internal.media-object/width + :internal.media-object/height + :internal.media-object/mtype])) (s/def :internal.file/colors (s/map-of ::uuid ::color)) diff --git a/common/app/common/spec.cljc b/common/src/app/common/spec.cljc similarity index 96% rename from common/app/common/spec.cljc rename to common/src/app/common/spec.cljc index 61c651136c..ca369fb516 100644 --- a/common/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -12,11 +12,14 @@ #?(:clj [clojure.spec.alpha :as s] :cljs [cljs.spec.alpha :as s]) - [expound.alpha :as expound] - [app.common.uuid :as uuid] + ;; NOTE: don't remove this, causes exception on advanced build + ;; because of some strange interaction with cljs.spec.alpha and + ;; modules spliting. [app.common.exceptions :as ex] [app.common.geom.point :as gpt] - [cuerdas.core :as str])) + [app.common.uuid :as uuid] + [cuerdas.core :as str] + [expound.alpha])) (s/check-asserts true) diff --git a/common/app/common/text.cljc b/common/src/app/common/text.cljc similarity index 96% rename from common/app/common/text.cljc rename to common/src/app/common/text.cljc index a3d1e97fde..9f184e7e9d 100644 --- a/common/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -6,10 +6,8 @@ (ns app.common.text (:require - [app.common.attrs :as attrs] - [app.common.uuid :as uuid] [app.common.data :as d] - [app.util.transit :as t] + [app.common.transit :as t] [clojure.walk :as walk] [cuerdas.core :as str])) @@ -82,13 +80,11 @@ (defn encode-style-value [v] - #?(:cljs (t/encode v) - :clj (t/encode-str v))) + (t/encode-str v)) (defn decode-style-value [v] - #?(:cljs (t/decode v) - :clj (t/decode-str v))) + (t/decode-str v)) (defn encode-style [key val] @@ -235,9 +231,9 @@ (remove empty?) (mapcat vec) (distinct)) - proc #(process-attr children %1 %2)] + f #(process-attr children %1 %2)] (persistent! - (transduce xform proc (transient []) children)))) + (transduce xform (completing f) (transient []) children)))) (build-block [{:keys [key children] :as paragraph}] {:key key diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc new file mode 100644 index 0000000000..7f1eddda54 --- /dev/null +++ b/common/src/app/common/transit.cljc @@ -0,0 +1,216 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.transit + (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [cognitect.transit :as t] + [linked.core :as lk] + [linked.set :as lks] + #?(:cljs ["luxon" :as lxn])) + #?(:clj + (:import + app.common.geom.matrix.Matrix + app.common.geom.point.Point + java.io.ByteArrayInputStream + java.io.ByteArrayOutputStream + java.io.File + java.time.Instant + java.time.Duration + java.time.OffsetDateTime + linked.set.LinkedSet))) + +;; --- MISC + +#?(:clj + (defn str->bytes + ([^String s] + (str->bytes s "UTF-8")) + ([^String s, ^String encoding] + (.getBytes s encoding)))) + +#?(:clj + (defn- bytes->str + ([^bytes data] + (bytes->str data "UTF-8")) + ([^bytes data, ^String encoding] + (String. data encoding)))) + +#?(:clj + (def ^:private file-write-handler + (t/write-handler + (constantly "file") + (fn [v] (str v))))) + +#?(:cljs + (def bigint-read-handler + (t/read-handler + (fn [value] + (js/parseInt value 10))))) + +#?(:cljs + (def uuid-read-handler + (t/read-handler uuid))) + +;; --- GEOM + +(def point-write-handler + (t/write-handler + (constantly "point") + (fn [v] (into {} v)))) + +(def point-read-handler + (t/read-handler gpt/map->Point)) + +(def matrix-write-handler + (t/write-handler + (constantly "matrix") + (fn [v] (into {} v)))) + +(def matrix-read-handler + (t/read-handler gmt/map->Matrix)) + +;; --- ORDERED SET + +(def ordered-set-write-handler + (t/write-handler + (constantly "ordered-set") + (fn [v] (vec v)))) + +(def ordered-set-read-handler + (t/read-handler #(into (lk/set) %))) + +;; --- DURATION + +(def duration-read-handler + #?(:cljs (t/read-handler #(.fromMillis ^js lxn/Duration %)) + :clj (t/read-handler #(Duration/ofMillis %)))) + +(def duration-write-handler + (t/write-handler + (constantly "duration") + (fn [v] (inst-ms v)))) + +;; --- TIME + +(def ^:private instant-read-handler + #?(:clj + (t/read-handler + (fn [v] (-> (Long/parseLong v) + (Instant/ofEpochMilli)))) + :cljs + (t/read-handler + (fn [v] + (let [ms (js/parseInt v 10)] + (.fromMillis ^js lxn/DateTime ms)))))) + +(def ^:private instant-write-handler + (t/write-handler + (constantly "m") + (fn [v] (str (inst-ms v))))) + +;; --- HANDLERS + +(def +read-handlers+ + {"matrix" matrix-read-handler + "ordered-set" ordered-set-read-handler + "point" point-read-handler + "duration" duration-read-handler + "m" instant-read-handler + #?@(:cljs ["n" bigint-read-handler + "u" uuid-read-handler]) + }) + +(def +write-handlers+ + #?(:clj + {Matrix matrix-write-handler + Point point-write-handler + Instant instant-write-handler + LinkedSet ordered-set-write-handler + + File file-write-handler + OffsetDateTime instant-write-handler} + :cljs + {gmt/Matrix matrix-write-handler + gpt/Point point-write-handler + lxn/DateTime instant-write-handler + lxn/Duration duration-write-handler + lks/LinkedSet ordered-set-write-handler} + )) + +;; --- Low-Level Api + +#?(:clj + (defn reader + ([istream] + (reader istream nil)) + ([istream {:keys [type] :or {type :json}}] + (t/reader istream type {:handlers +read-handlers+})))) + +#?(:clj + (defn writer + ([ostream] + (writer ostream nil)) + ([ostream {:keys [type] :or {type :json}}] + (t/writer ostream type {:handlers +write-handlers+})))) +#?(:clj + (defn read! + [reader] + (t/read reader))) + +#?(:clj + (defn write! + [writer data] + (t/write writer data))) + + +;; --- High-Level Api + +#?(:clj + (defn encode + ([data] (encode data nil)) + ([data opts] + (with-open [out (ByteArrayOutputStream.)] + (t/write (writer out opts) data) + (.toByteArray out))))) + +#?(:clj + (defn decode + ([data] (decode data nil)) + ([data opts] + (with-open [input (ByteArrayInputStream. ^bytes data)] + (t/read (reader input opts)))))) + +(defn encode-str + ([data] (encode-str data nil)) + ([data opts] + #?(:cljs + (let [t (:type opts :json) + w (t/writer t {:handlers +write-handlers+})] + (t/write w data)) + :clj + (->> (encode data opts) + (bytes->str))))) + +(defn decode-str + ([data] (decode-str data nil)) + ([data opts] + #?(:cljs + (let [t (:type opts :json) + r (t/reader t {:handlers +read-handlers+})] + (t/read r data)) + :clj + (-> (str->bytes data) + (decode opts))))) + +(defn transit? + "Checks if a string can be decoded with transit" + [v] + (try + (-> v decode-str nil? not) + (catch #?(:cljs js/SyntaxError :clj Exception) _e + false))) diff --git a/common/app/common/uri.cljc b/common/src/app/common/uri.cljc similarity index 94% rename from common/app/common/uri.cljc rename to common/src/app/common/uri.cljc index d381ee7b0a..dbe03bf79e 100644 --- a/common/app/common/uri.cljc +++ b/common/src/app/common/uri.cljc @@ -15,10 +15,7 @@ (d/export u/join) (d/export u/query-encode) (d/export un/percent-encode) - -(defn uri? - [o] - (instance? lambdaisland.uri.URI o)) +(d/export u/uri?) (defn query-string->map [s] diff --git a/common/app/common/uuid.cljc b/common/src/app/common/uuid.cljc similarity index 65% rename from common/app/common/uuid.cljc rename to common/src/app/common/uuid.cljc index 0cc0106dfd..4d0e483590 100644 --- a/common/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -6,13 +6,13 @@ (ns app.common.uuid (:refer-clojure :exclude [next uuid zero?]) - #?(:clj (:import java.util.UUID)) - #?(:clj - (:require [clj-uuid :as impl] - [clojure.core :as c]) - :cljs - (:require [app.common.uuid-impl :as impl] - [cljs.core :as c]))) + (:require + #?(:clj [app.common.data :as d]) + #?(:clj [clj-uuid :as impl]) + #?(:clj [clojure.core :as c]) + #?(:cljs [app.common.uuid-impl :as impl]) + #?(:cljs [cljs.core :as c])) + #?(:clj (:import java.util.UUID))) (def zero #uuid "00000000-0000-0000-0000-000000000000") @@ -42,7 +42,9 @@ #?(:clj (UUID/fromString s) :cljs (c/uuid s))) +(defn custom + ([a] #?(:clj (UUID. 0 a) :cljs (c/uuid (impl/custom 0 a)))) + ([b a] #?(:clj (UUID. b a) :cljs (c/uuid (impl/custom b a))))) + #?(:clj - (defn custom - ([a] (UUID. 0 a)) - ([b a] (UUID. b a)))) + (d/export impl/get-word-high)) diff --git a/common/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js similarity index 92% rename from common/app/common/uuid_impl.js rename to common/src/app/common/uuid_impl.js index 791dd58eab..e05f358536 100644 --- a/common/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -16,7 +16,8 @@ goog.scope(function() { const self = app.common.uuid_impl; const fill = (() => { - if (typeof global.crypto !== "undefined") { + if (typeof global.crypto !== "undefined" && + typeof global.crypto.getRandomValues !== "undefined") { return (buf) => { global.crypto.getRandomValues(buf); return buf; @@ -189,4 +190,10 @@ goog.scope(function() { self.v1 = v1; self.v4 = v4; + + self.custom = function formatAsUUID(mostSigBits, leastSigBits) { + const most = mostSigBits.toString("16").padStart(16, "0"); + const least = leastSigBits.toString("16").padStart(16, "0"); + return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring(12)}-${least.substring(0, 4)}-${least.substring(4)}`; + } }); diff --git a/common/app/common/version.cljc b/common/src/app/common/version.cljc similarity index 97% rename from common/app/common/version.cljc rename to common/src/app/common/version.cljc index aafad2d384..cb79885cfc 100644 --- a/common/app/common/version.cljc +++ b/common/src/app/common/version.cljc @@ -7,7 +7,6 @@ (ns app.common.version "A version parsing helper." (:require - [app.common.data :as d] [cuerdas.core :as str])) (def version-re #"^(([A-Za-z]+)\-?)?((\d+)\.(\d+)\.(\d+))(\-?((alpha|prealpha|beta|rc|dev)(\d+)?))?(\-?(\d+))?(\-?g(\w+))$") diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/common/test/app/common/geom_shapes_test.cljc similarity index 99% rename from backend/tests/app/tests/test_common_geom_shapes.clj rename to common/test/app/common/geom_shapes_test.cljc index 8019da9940..2b6a6fc57c 100644 --- a/backend/tests/app/tests/test_common_geom_shapes.clj +++ b/common/test/app/common/geom_shapes_test.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-common-geom-shapes +(ns app.common.geom-shapes-test (:require [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] diff --git a/backend/tests/app/tests/test_common_geom.clj b/common/test/app/common/geom_test.cljc similarity index 98% rename from backend/tests/app/tests/test_common_geom.clj rename to common/test/app/common/geom_test.cljc index 4656f716c3..fce01a0613 100644 --- a/backend/tests/app/tests/test_common_geom.clj +++ b/common/test/app/common/geom_test.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-common-geom +(ns app.common.geom-test (:require [clojure.test :as t] [app.common.geom.point :as gpt] diff --git a/backend/tests/app/tests/test_common_pages_migrations.clj b/common/test/app/common/pages_migrations_test.cljc similarity index 92% rename from backend/tests/app/tests/test_common_pages_migrations.clj rename to common/test/app/common/pages_migrations_test.cljc index 4e8adebd68..ab697886eb 100644 --- a/backend/tests/app/tests/test_common_pages_migrations.clj +++ b/common/test/app/common/pages_migrations_test.cljc @@ -4,17 +4,14 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-common-pages-migrations +(ns app.common.pages-migrations-test (:require [clojure.test :as t] [clojure.pprint :refer [pprint]] - [promesa.core :as p] - [mockery.core :refer [with-mock]] [app.common.data :as d] [app.common.pages :as cp] [app.common.pages.migrations :as cpm] - [app.common.uuid :as uuid] - [app.tests.helpers :as th])) + [app.common.uuid :as uuid])) (t/deftest test-migration-8-1 (let [page-id (uuid/custom 0 0) @@ -43,8 +40,8 @@ res (cpm/migrate-data data)] - (pprint data) - (pprint res) + ;; (pprint data) + ;; (pprint res) (t/is (= (dissoc data :version) (dissoc res :version))))) @@ -86,8 +83,8 @@ res (cpm/migrate-data data)] - (pprint res) - (pprint expct) + ;; (pprint res) + ;; (pprint expct) (t/is (= (dissoc expct :version) (dissoc res :version))) diff --git a/backend/tests/app/tests/test_common_pages.clj b/common/test/app/common/pages_test.cljc similarity index 99% rename from backend/tests/app/tests/test_common_pages.clj rename to common/test/app/common/pages_test.cljc index 80e3ea7e2f..abfa5f8f31 100644 --- a/backend/tests/app/tests/test_common_pages.clj +++ b/common/test/app/common/pages_test.cljc @@ -4,15 +4,12 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.tests.test-common-pages +(ns app.common.pages-test (:require [clojure.test :as t] [clojure.pprint :refer [pprint]] - [promesa.core :as p] - [mockery.core :refer [with-mock]] [app.common.pages :as cp] - [app.common.uuid :as uuid] - [app.tests.helpers :as th])) + [app.common.uuid :as uuid])) (t/deftest process-change-set-option (let [file-id (uuid/custom 2 2) diff --git a/common/test/app/common/setup_test.cljc b/common/test/app/common/setup_test.cljc new file mode 100644 index 0000000000..5b11a5af28 --- /dev/null +++ b/common/test/app/common/setup_test.cljc @@ -0,0 +1,10 @@ +(ns app.common.setup-test + (:require + [clojure.test :as t])) + +#?(:cljs + (defmethod t/report [:cljs.test/default :end-run-tests] + [m] + (if (t/successful? m) + (set! (.-exitCode js/process) 0) + (set! (.-exitCode js/process) 1)))) diff --git a/frontend/tests/app/test_draft_conversion.cljs b/common/test/app/common/text_test.cljc similarity index 89% rename from frontend/tests/app/test_draft_conversion.cljs rename to common/test/app/common/text_test.cljc index 72c143e6a0..9cd52a2531 100644 --- a/frontend/tests/app/test_draft_conversion.cljs +++ b/common/test/app/common/text_test.cljc @@ -1,9 +1,9 @@ -(ns app.test-draft-conversion +(ns app.common.text-test (:require [app.common.data :as d] [app.common.text :as txt] - [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]])) + [clojure.test :as t :include-macros true] + [clojure.pprint :refer [pprint]])) (t/deftest test-basic-conversion-roundtrip (let [text "qwqw 🠒" diff --git a/common/yarn.lock b/common/yarn.lock new file mode 100644 index 0000000000..425c4fc04e --- /dev/null +++ b/common/yarn.lock @@ -0,0 +1,31 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +luxon@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" + integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== + +source-map-support@^0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +ws@^7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index f7e2eebdcb..5508f5a7da 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v14.16.1 \ - CLOJURE_VERSION=1.10.3.822 \ - CLJKONDO_VERSION=2021.04.23 \ - BABASHKA_VERSION=0.4.0 \ +ENV NODE_VERSION=v14.17.2 \ + CLOJURE_VERSION=1.10.3.882 \ + CLJKONDO_VERSION=2021.06.18 \ + BABASHKA_VERSION=0.4.6 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index ee7e37bb0b..1b7530aac9 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -128,8 +128,10 @@ http { } location / { - add_header Cache-Control "no-cache, max-age=0"; - proxy_pass http://127.0.0.1:8888; + add_header Last-Modified $date_gmt; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + if_modified_since off; + expires off; } } } diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index 1770934fdf..6676425a8f 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -21,13 +21,13 @@ tmux -2 new-session -d -s penpot tmux new-window -t penpot:1 -n 'shadow watch' tmux select-window -t penpot:1 tmux send-keys -t penpot 'cd penpot/frontend' enter C-l -tmux send-keys -t penpot 'npx shadow-cljs watch main' enter +tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter tmux new-window -t penpot:2 -n 'exporter' tmux select-window -t penpot:2 tmux send-keys -t penpot 'cd penpot/exporter' enter C-l tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l -tmux send-keys -t penpot 'npx shadow-cljs watch main' enter +tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter tmux split-window -v tmux send-keys -t penpot 'cd penpot/exporter' enter C-l diff --git a/docker/images/config.env b/docker/images/config.env index 79f1472eef..6d1f76bd27 100644 --- a/docker/images/config.env +++ b/docker/images/config.env @@ -1,9 +1,7 @@ -# Should be set to the public domain when penpot is going to be -# served. +# Should be set to the public domain where penpot is going to be served. PENPOT_PUBLIC_URI=http://localhost:9001 -# Standard database connection parametes (only postgresql is -# supported): +# Standard database connection parameters (only postgresql is supported): PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot PENPOT_DATABASE_USERNAME=penpot PENPOT_DATABASE_PASSWORD=penpot @@ -11,22 +9,22 @@ PENPOT_DATABASE_PASSWORD=penpot # Redis is used for the websockets notifications. PENPOT_REDIS_URI=redis://penpot-redis/0 -# By default files upload by user are stored in local filesystem. But -# it can be configured to store in AWS S3 or completelly in de the -# database. Storing in the database makes the backups more easy but -# will make access to media less performant. +# By default, files uploaded by users are stored in local filesystem. But it +# can be configured to store in AWS S3 or completely in de the database. +# Storing in the database makes the backups more easy but will make access to +# media less performant. PENPOT_STORAGE_BACKEND=fs PENPOT_STORAGE_FS_DIRECTORY=/opt/data/assets -# Telemetry. When enabled, a periodical process will send annonymous -# data about this instance. Telemetry data will enable us to learn on -# how the application is used based on real scenarios. If you want to -# help us, please leave it enabled. +# Telemetry. When enabled, a periodical process will send anonymous data about +# this instance. Telemetry data will enable us to learn on how the application +# is used, based on real scenarios. If you want to help us, please leave it +# enabled. PENPOT_TELEMETRY_ENABLED=true -# Email sending configuration. By default emails are printed in -# console, but for production usage is recommeded to setup a real SMTP -# provider. Emails are used for confirm user registration. +# Email sending configuration. By default, emails are printed in the console, +# but for production usage is recommended to setup a real SMTP provider. Emails +# are used to confirm user registrations. PENPOT_SMTP_ENABLED=false PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com @@ -40,13 +38,12 @@ PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com # Enable or disable external user registration process. PENPOT_REGISTRATION_ENABLED=true -# Comma separated list of allowed domains to register. Empty for allow -# all. +# Comma separated list of allowed domains to register. Empty to allow all. # PENPOT_REGISTRATION_DOMAIN_WHITELIST="" # Penpot comes with the facility to create quick demo users that are -# automatically deleted after some time. This settings enables or -# disables the creation of demo users. +# automatically deleted after some time. This settings enables or disables the +# creation of demo users. PENPOT_ALLOW_DEMO_USERS=true ## Authentication providers @@ -55,11 +52,11 @@ PENPOT_ALLOW_DEMO_USERS=true # PENPOT_GOOGLE_CLIENT_ID= # PENPOT_GOOGLE_CLIENT_SECRET= -# Github +# GitHub # PENPOT_GITHUB_CLIENT_ID= # PENPOT_GITHUB_CLIENT_SECRET= -# Gitlab +# GitLab # PENPOT_GITLAB_BASE_URI=https://gitlab.com # PENPOT_GITLAB_CLIENT_ID= # PENPOT_GITLAB_CLIENT_SECRET= @@ -82,4 +79,3 @@ PENPOT_ALLOW_DEMO_USERS=true # PENPOT_LDAP_ATTRS_FULLNAME=cn # PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto # PENPOT_LOGIN_WITH_LDAP=true - diff --git a/docker/images/files/config.js b/docker/images/files/config.js index 8af727193e..ef86caa0e7 100644 --- a/docker/images/files/config.js +++ b/docker/images/files/config.js @@ -10,3 +10,4 @@ //var penpotLoginWithLDAP = ; //var penpotRegistrationEnabled = ; //var penpotAnalyticsEnabled = ; +//var penpotFlags = ""; diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 7543ab0e1b..cf292de0a9 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -105,6 +105,14 @@ update_analytics_enabled() { fi } +update_flags() { + if [ -n "$PENPOT_FLAGS" ]; then + sed -i \ + -e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|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 @@ -115,5 +123,5 @@ update_oidc_client_id /var/www/app/js/config.js update_login_with_ldap /var/www/app/js/config.js update_registration_enabled /var/www/app/js/config.js update_analytics_enabled /var/www/app/js/config.js - +update_flags /var/www/app/js/config.js exec "$@"; diff --git a/exporter/deps.edn b/exporter/deps.edn index 86c907f65c..2d1b03b6c0 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -1,9 +1,23 @@ -{:paths [] - :deps {} +{:paths ["src" "vendor" "resources" "test"] + :deps + {penpot/common {:local/root "../common"} + binaryage/devtools {:mvn/version "RELEASE"} + metosin/reitit-core {:mvn/version "0.5.13"} + lambdaisland/glogi {:mvn/version "1.0.106"} + funcool/beicon {:mvn/version "2021.04.29-0"} + } :aliases {:outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} :main-opts ["-m" "antq.core"]} + + :dev + {:extra-deps + {thheller/shadow-cljs {:mvn/version "2.14.1"}}} + + :shadow-cljs + {:main-opts ["-m" "shadow.cljs.devtools.cli"]} + }} diff --git a/exporter/package.json b/exporter/package.json index fb08208872..a7422dbf47 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -12,14 +12,15 @@ "inflation": "^2.0.0", "jszip": "^3.6.0", "koa": "^2.13.0", - "puppeteer": "^9.1.0", + "luxon": "^1.27.0", + "puppeteer": "^10.0.0", "puppeteer-cluster": "^0.22.0", "raw-body": "^2.4.1", "xml-js": "^1.6.11", "xregexp": "^5.0.2" }, "devDependencies": { - "shadow-cljs": "^2.12.5", + "shadow-cljs": "^2.14.2", "source-map-support": "^0.5.19" } } diff --git a/exporter/scripts/build b/exporter/scripts/build index 61cc021d1a..1df9a17070 100755 --- a/exporter/scripts/build +++ b/exporter/scripts/build @@ -8,7 +8,7 @@ rm -rf target export NODE_ENV=production; # Build the application -npx shadow-cljs release main +clojure -M:dev:shadow-cljs release main # Remove source rm -rf target/app diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn index 7a7ca5859c..da622bf434 100644 --- a/exporter/shadow-cljs.edn +++ b/exporter/shadow-cljs.edn @@ -1,14 +1,4 @@ -{:dependencies - [[com.cognitect/transit-cljs "0.8.269"] - [danlentz/clj-uuid "0.1.9"] - [frankiesardo/linked "1.3.0"] - [funcool/cuerdas "2021.05.02-0"] - [funcool/promesa "6.0.0"] - [integrant/integrant "0.8.0"] - [lambdaisland/glogi "1.0.106"] - [lambdaisland/uri "1.4.54"] - [metosin/reitit-core "0.5.13"]] - +{:deps {:aliases [:dev]} :source-paths ["src" "vendor" "../common"] :jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"] diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 90f7cd00bb..9df73c0663 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -71,6 +71,20 @@ :type (name type) :omitBackground omit-background?}))) +(defn pdf + ([page] (pdf page nil)) + ([page {:keys [viewport omit-background? prefer-css-page-size?] + :or {viewport {} + omit-background? true + prefer-css-page-size? true}}] + (let [viewport (d/merge default-viewport viewport)] + (.pdf ^js page #js {:width (:width viewport) + :height (:height viewport) + :scale (:scale viewport) + :omitBackground omit-background? + :printBackground (not omit-background?) + :preferCSSPageSize prefer-css-page-size?})))) + (defn eval! [frame f] (.evaluate ^js frame f)) diff --git a/exporter/src/app/http/export.cljs b/exporter/src/app/http/export.cljs index 8be2f54709..e8b56b6665 100644 --- a/exporter/src/app/http/export.cljs +++ b/exporter/src/app/http/export.cljs @@ -10,6 +10,7 @@ [app.common.spec :as us] [app.renderer.bitmap :as rb] [app.renderer.svg :as rs] + [app.renderer.pdf :as rp] [app.zipfile :as zip] [cljs.spec.alpha :as s] [cuerdas.core :as str] @@ -89,7 +90,8 @@ (case (:type params) :png (rb/render params) :jpeg (rb/render params) - :svg (rs/render params))) + :svg (rs/render params) + :pdf (rp/render params))) (defn- find-filename-candidate [params used] @@ -101,7 +103,8 @@ (case (:type params) :png ".png" :jpeg ".jpg" - :svg ".svg"))] + :svg ".svg" + :pdf ".pdf"))] (if (contains? used candidate) (recur (inc index)) candidate)))) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs new file mode 100644 index 0000000000..56efd4e46c --- /dev/null +++ b/exporter/src/app/renderer/pdf.cljs @@ -0,0 +1,79 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.renderer.pdf + "A pdf renderer." + (:require + [app.browser :as bw] + [app.common.exceptions :as ex :include-macros true] + [app.common.spec :as us] + [app.config :as cf] + [cljs.spec.alpha :as s] + [lambdaisland.uri :as u] + [lambdaisland.glogi :as log] + [promesa.core :as p])) + +(defn create-cookie + [uri token] + (let [domain (str (:host uri) + (when (:port uri) + (str ":" (:port uri))))] + {:domain domain + :key "auth-token" + :value token})) + +(defn pdf-from-object + [browser {:keys [file-id page-id object-id token scale type]}] + (letfn [(handle [page] + (let [path (str "/render-object/" file-id "/" page-id "/" object-id) + uri (-> (u/uri (cf/get :public-uri)) + (assoc :path "/") + (assoc :fragment path)) + cookie (create-cookie uri token)] + (pdf-from page (str uri) cookie))) + + (pdf-from [page uri cookie] + (log/info :uri uri) + (let [options {:cookie cookie}] + (p/do! + (bw/configure-page! page options) + (bw/navigate! page uri) + (bw/wait-for page "#screenshot") + (bw/pdf page))))] + + (bw/exec! browser handle))) + +(s/def ::name ::us/string) +(s/def ::suffix ::us/string) +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::object-id ::us/uuid) +(s/def ::scale ::us/number) +(s/def ::token ::us/string) +(s/def ::filename ::us/string) + +(s/def ::render-params + (s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id] + :opt-un [::filename])) + +(defn render + [params] + (us/assert ::render-params params) + (let [browser @bw/instance] + (when-not browser + (ex/raise :type :internal + :code :browser-not-ready + :hint "browser cluster is not initialized yet")) + + (p/let [content (pdf-from-object browser params)] + {:content content + :filename (or (:filename params) + (str (:name params) + (:suffix params "") + ".pdf")) + :length (alength content) + :mime-type "application/pdf"}))) + diff --git a/exporter/yarn.lock b/exporter/yarn.lock index 43907db232..e381d7f927 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -11,9 +11,9 @@ regenerator-runtime "^0.13.4" "@types/node@*": - version "15.0.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" - integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== + version "15.6.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.2.tgz#c61d49f38af70da32424b5322eee21f97e627175" + integrity sha512-dxcOx8801kMo3KlU+C+/ctWrzREAH7YvoF3aoVpRdqgs+Kf7flp+PJDN/EX5bME3suDUZHsxes9hpvBmzYlWbA== "@types/yauzl@^2.9.1": version "2.9.1" @@ -272,9 +272,9 @@ cookies@~0.8.0: keygrip "~1.1.0" core-js-pure@^3.0.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.12.1.tgz#934da8b9b7221e2a2443dc71dfa5bd77a7ea00b8" - integrity sha512-1cch+qads4JnDSWsvc7d6nzlKAippwjUlf6vykkTLW53VSV+NkE6muGBToAjEA8pG90cSfcud3JgVmW2ds5TaQ== + version "3.13.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1" + integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw== core-util-is@~1.0.0: version "1.0.2" @@ -329,7 +329,7 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -debug@4, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@4.3.1, debug@^4.1.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -376,10 +376,10 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -devtools-protocol@0.0.869402: - version "0.0.869402" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d" - integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA== +devtools-protocol@0.0.883894: + version "0.0.883894" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.883894.tgz#d403f2c75cd6d71c916aee8dde9258da988a4da9" + integrity sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg== diffie-hellman@^5.0.0: version "5.0.3" @@ -443,7 +443,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -extract-zip@^2.0.0: +extract-zip@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -564,7 +564,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^5.0.0: +https-proxy-agent@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== @@ -712,6 +712,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +luxon@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" + integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -734,17 +739,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.47.0: - version "1.47.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" - integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== +mime-db@1.48.0: + version "1.48.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== mime-types@^2.1.18, mime-types@~2.1.24: - version "2.1.30" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" - integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== + version "2.1.31" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== dependencies: - mime-db "1.47.0" + mime-db "1.48.0" minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" @@ -763,10 +768,17 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" ms@2.0.0: version "2.0.0" @@ -783,7 +795,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -node-fetch@^2.6.1: +node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -917,7 +929,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -pkg-dir@^4.2.0: +pkg-dir@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -934,12 +946,12 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +progress@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" + integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== -proxy-from-env@^1.1.0: +proxy-from-env@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -981,23 +993,23 @@ puppeteer-cluster@^0.22.0: dependencies: debug "^4.1.1" -puppeteer@^9.1.0: - version "9.1.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c" - integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw== +puppeteer@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-10.0.0.tgz#1b597c956103e2d989ca17f41ba4693b20a3640c" + integrity sha512-AxHvCb9IWmmP3gMW+epxdj92Gglii+6Z4sb+W+zc2hTTu10HF0yg6hGXot5O74uYkVqG3lfDRLfnRpi6WOwi5A== dependencies: - debug "^4.1.0" - devtools-protocol "0.0.869402" - extract-zip "^2.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.1" - pkg-dir "^4.2.0" - progress "^2.0.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - tar-fs "^2.0.0" - unbzip2-stream "^1.3.3" - ws "^7.2.3" + debug "4.3.1" + devtools-protocol "0.0.883894" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.1" + pkg-dir "4.2.0" + progress "2.0.1" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.0.0" + unbzip2-stream "1.3.3" + ws "7.4.6" querystring-es3@^0.2.0: version "0.2.1" @@ -1066,7 +1078,7 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== -rimraf@^3.0.2: +rimraf@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -1134,10 +1146,10 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@^2.12.5: - version "2.12.5" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4" - integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ== +shadow-cljs@^2.14.2: + version "2.14.2" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.14.2.tgz#dba651ea124028064aea6fa9a390f257cb6eede4" + integrity sha512-ficaYfBAATzJ6OGt/GbIl393+cqLchzNkdTrM2PY4ttbsAOyBfWd39t+PZcYpCqemXjkgfBdZt9DJda7WaHJGA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" @@ -1209,17 +1221,17 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -tar-fs@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== +tar-fs@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== dependencies: chownr "^1.1.1" - mkdirp-classic "^0.5.2" + mkdirp "^0.5.1" pump "^3.0.0" - tar-stream "^2.1.4" + tar-stream "^2.0.0" -tar-stream@^2.1.4: +tar-stream@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -1275,10 +1287,10 @@ ultron@~1.1.0: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== -unbzip2-stream@^1.3.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== +unbzip2-stream@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" + integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== dependencies: buffer "^5.2.1" through "^2.3.8" @@ -1337,6 +1349,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -1346,11 +1363,6 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^7.2.3: - version "7.4.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" - integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== - xml-js@^1.6.11: version "1.6.11" resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" diff --git a/frontend/deps.edn b/frontend/deps.edn index 4a3337c6a8..2695bf8460 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -1,9 +1,32 @@ -{:paths ["src" "vendor" "resources" "../common"] - :deps {} +{:paths ["src" "vendor" "resources" "test"] + :deps + {penpot/common + {:local/root "../common"} + + binaryage/devtools {:mvn/version "RELEASE"} + metosin/reitit-core {:mvn/version "0.5.13"} + + funcool/beicon {:mvn/version "2021.07.05-1"} + funcool/okulary {:mvn/version "2020.04.14-0"} + funcool/potok {:mvn/version "2021.06.07-0"} + funcool/rumext {:mvn/version "2021.05.12-1"} + funcool/tubax {:mvn/version "2021.05.20-0"} + + instaparse/instaparse {:mvn/version "1.4.10"} + } + :aliases {:outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} :main-opts ["-m" "antq.core"]} + + :dev + {:extra-deps + {thheller/shadow-cljs {:mvn/version "2.15.1"}}} + + :shadow-cljs + {:main-opts ["-m" "shadow.cljs.devtools.cli"]} + }} diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 428bbf9a7f..03c4f1c27a 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -8,7 +8,7 @@ const gulpGzip = require("gulp-gzip"); const gulpMustache = require("gulp-mustache"); const gulpPostcss = require("gulp-postcss"); const gulpRename = require("gulp-rename"); -const gulpSass = require("gulp-sass"); +const gulpSass = require("gulp-sass")(require("sass")); const svgSprite = require("gulp-svg-sprite"); const autoprefixer = require("autoprefixer") @@ -157,7 +157,7 @@ gulpSass.compiler = sass; gulp.task("scss", function() { return gulp.src(paths.resources + "styles/main-default.scss") - .pipe(gulpSass().on('error', gulpSass.logError)) + .pipe(gulpSass.sync().on('error', gulpSass.logError)) .pipe(gulpPostcss([ autoprefixer, // clean({format: "keep-breaks", level: 1}) diff --git a/frontend/package.json b/frontend/package.json index 9e583d58a6..e5f371b46f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,31 +23,32 @@ "gulp-mustache": "^5.0.0", "gulp-postcss": "^9.0.0", "gulp-rename": "^2.0.0", - "gulp-sass": "^4.1.0", + "gulp-sass": "^5.0.0", "gulp-sourcemaps": "^3.0.0", "gulp-svg-sprite": "^1.5.0", "map-stream": "0.0.7", - "marked": "^2.0.3", + "marked": "^2.1.1", "mkdirp": "^1.0.4", - "postcss": "^8.2.15", + "postcss": "^8.3.5", "postcss-clean": "^1.2.2", "rimraf": "^3.0.0", - "sass": "^1.32.8", - "shadow-cljs": "2.12.6" + "sass": "^1.35.1", + "shadow-cljs": "2.15.1" }, "dependencies": { - "date-fns": "^2.21.3", + "date-fns": "^2.22.1", "draft-js": "^0.11.7", - "highlight.js": "^10.6.0", - "js-beautify": "^1.13.5", + "highlight.js": "^11.0.1", + "js-beautify": "^1.14.0", + "jszip": "^3.6.0", "luxon": "^1.26.0", "mousetrap": "^1.6.5", "opentype.js": "^1.3.3", "randomcolor": "^0.6.2", - "react": "~17.0.1", - "react-dom": "~17.0.1", + "react": "~17.0.2", + "react-dom": "~17.0.2", "react-virtualized": "^9.22.3", - "rxjs": "~7.0.1", + "rxjs": "~7.2.0", "sax": "^1.2.4", "source-map-support": "^0.5.16", "tdigest": "^0.1.1", diff --git a/frontend/resources/images/features/constraints.gif b/frontend/resources/images/features/constraints.gif new file mode 100644 index 0000000000..e320861cec Binary files /dev/null and b/frontend/resources/images/features/constraints.gif differ diff --git a/frontend/resources/images/features/copy-paste.gif b/frontend/resources/images/features/copy-paste.gif new file mode 100644 index 0000000000..4f506ff130 Binary files /dev/null and b/frontend/resources/images/features/copy-paste.gif differ diff --git a/frontend/resources/images/features/export.gif b/frontend/resources/images/features/export.gif new file mode 100644 index 0000000000..a89874b213 Binary files /dev/null and b/frontend/resources/images/features/export.gif differ diff --git a/frontend/resources/images/features/group-components.gif b/frontend/resources/images/features/group-components.gif new file mode 100644 index 0000000000..59bb8d697b Binary files /dev/null and b/frontend/resources/images/features/group-components.gif differ diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 629cee8d1a..f74c58e25b 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -22,8 +22,8 @@ transition: all .4s; text-decoration: none !important; svg { - height: 15px; - width: 15px; + height: 16px; + width: 16px; } &.btn-large { font-size: $fs14; @@ -118,10 +118,10 @@ .btn-icon-basic { @extend %btn; background: transparent; - color: $color-gray-60; + color: $color-gray-20; padding: $x-small; svg { - fill: $color-gray-60; + fill: $color-gray-20; } &:hover { background: transparent; @@ -965,16 +965,8 @@ input[type=range]:focus::-ms-fill-upper { } } + // the default is the `right` &.tooltip-bottom { - &:hover { - &::after { - left: -100%; - top: 130%; - } - } - } - - &.tooltip-bottom-right { &:hover { &::after { left: 0; diff --git a/frontend/resources/styles/main/layouts/handoff.scss b/frontend/resources/styles/main/layouts/handoff.scss index c41ad640d0..027bbac0d9 100644 --- a/frontend/resources/styles/main/layouts/handoff.scss +++ b/frontend/resources/styles/main/layouts/handoff.scss @@ -21,9 +21,18 @@ $width-settings-bar: 16rem; .viewer-header { width: 100%; position: fixed; - top: -39px; + top: -48px; left: 0; transition: top 400ms ease 300ms; + + &::after { + content: " "; + position: absolute; + width: 100%; + height: 1rem; + left: 0; + top: 48px; + } } & .viewer-header:hover { diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 13be87951c..49aee2b0f0 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -54,6 +54,20 @@ justify-content: center; position: relative; + input { + margin-bottom: 0px; + } + + .buttons-stack { + display: flex; + flex-direction: column; + width: 100%; + + *:not(:last-child) { + margin-bottom: $medium; + } + } + .form-container { width: 412px; @@ -83,15 +97,47 @@ } .btn-github-auth { - margin-bottom: $medium; - text-decoration: none; + margin-bottom: $medium; + text-decoration: none; - .logo { - width: 20px; - height: 20px; - margin-right: 1rem; + .logo { + width: 20px; + height: 20px; + margin-right: 1rem; + } + } + + .separator { + display: flex; + justify-content: center; + width: 100%; + text-transform: uppercase; + } + + .links { + display: flex; + font-size: $fs14; + flex-direction: column; + justify-content: space-between; + margin-top: $medium; + margin-bottom: $medium; + + + &.demo { + justify-content: center; + margin-top: $big; + } + + .link-entry { + font-size: $fs14; + color: $color-gray-40; + margin-bottom: 10px; + a { + font-size: $fs14; + color: $color-primary-dark; } } + } } .terms-login { diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index cb4b1c7b2c..bc4ffcb331 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -44,3 +44,7 @@ } } +#screenshot { + display: flex; + flex-direction: column; +} diff --git a/frontend/resources/styles/main/layouts/viewer.scss b/frontend/resources/styles/main/layouts/viewer.scss index 8f120296fc..7e05a861b4 100644 --- a/frontend/resources/styles/main/layouts/viewer.scss +++ b/frontend/resources/styles/main/layouts/viewer.scss @@ -19,9 +19,19 @@ & .viewer-header { width: 100%; position: fixed; - top: -39px; + top: -48px; left: 0; transition: top 400ms ease 300ms; + + &::after { + content: " "; + position: absolute; + width: 100%; + height: 1rem; + left: 0; + top: 48px; + } + } & .viewer-header:hover { diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index 410437ae6e..e0b0ed1788 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -480,6 +480,7 @@ .color-data { align-items: center; display: flex; + margin-bottom: $small; position: relative; .color-name { diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss index 0f61a12a3a..2370def964 100644 --- a/frontend/resources/styles/main/partials/dashboard-fonts.scss +++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss @@ -121,11 +121,9 @@ fill: $color-gray-30; } } - } } - .filenames { display: flex; flex-direction: column; @@ -153,7 +151,6 @@ transform: rotate(45deg); } } - } } } @@ -164,7 +161,6 @@ display: flex; flex-direction: column; - .upload-button { width: 100px; } @@ -182,7 +178,7 @@ .banner { background-color: unset; - display: flex; + display: flex; .icon { display: flex; @@ -203,6 +199,10 @@ width: 80%; color: $color-gray-40; } + + .btn-primary { + flex-shrink: 0; + } } .fonts-placeholder { @@ -217,7 +217,6 @@ border: 1px dashed $color-gray-20; margin-top: 16px; - .icon { svg { fill: $color-gray-40; diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index a491f9c7e4..67aaa4ca5f 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -6,6 +6,7 @@ .dashboard-grid { font-size: $fs14; + height: 100%; .grid-row { display: flex; diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index c3af04cb22..3c5c7ab400 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -104,7 +104,12 @@ } } + .dashboard-header-actions { + display: flex; + } + .pin-icon { + margin: 0 $small 0 $big; svg { fill: $color-gray-20; } diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index f8423ce48c..6c90ee4f5d 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -34,7 +34,7 @@ height: 40px; .btn-secondary { - margin-left: $big; + border: none; height: 32px; } @@ -62,6 +62,7 @@ cursor: pointer; display: flex; align-items: center; + margin-left: 40px; margin-right: 10px; svg { width: 15px; @@ -165,3 +166,30 @@ } } } + +.import-file-btn { + align-items: center; + display: flex; + flex-direction: column; + height: 2rem; + justify-content: center; + overflow: hidden; + padding: 4px; + width: 2rem; + + background: none; + border: 1px solid $color-gray-20; + border-radius: 2px; + cursor: pointer; + transition: all 0.4s; + margin-left: 1rem; + + &:hover { + background: $color-primary; + } + + svg { + width: 16px; + height: 16px; + } +} diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss index 9ba80498e2..2c780c91c3 100644 --- a/frontend/resources/styles/main/partials/forms.scss +++ b/frontend/resources/styles/main/partials/forms.scss @@ -109,30 +109,6 @@ textarea { hr { border-color: $color-gray-20; } - - .links { - display: flex; - font-size: $fs14; - flex-direction: column; - justify-content: space-between; - margin-bottom: $medium; - - &.demo { - justify-content: center; - margin-top: $big; - } - } - - .link-entry { - font-size: $fs14; - color: $color-gray-40; - margin-bottom: 10px; - } - - .link-entry a { - font-size: $fs14; - color: $color-primary-dark; - } } .custom-input { diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index bb0b12751e..b6e34844ee 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -202,12 +202,332 @@ background: $color-primary; border: 1px solid $color-primary; color: $color-black; + &:hover { background: $color-primary-dark; } } } +} +.import-dialog, +.export-dialog { + background-color: $color-white; + border: 1px solid $color-gray-20; + width: 30rem; + min-height: 14rem; + + p { + font-size: $fs14; + color: $color-black; + } + + .detail { + font-size: $fs12; + } + + .detail, .explain { + padding: 0 1rem; + } + + .cancel-button { + border: 1px solid $color-gray-20; + background: $color-white; + border-radius: 3px; + padding: 0.3rem 1.25rem; + cursor: pointer; + margin-right: 8px; + + &:hover { + background: $color-gray-20; + } + } + + .accept-button { + background: $color-primary; + border-radius: 3px; + border: 1px solid $color-primary; + color: $color-black; + cursor: pointer; + padding: 0.3rem 1.25rem; + + &[disabled] { + border: 1px solid #E3E3E3; + } + + &:hover { + background: $color-primary-dark; + } + } + + .modal-content { + flex: 1; + overflow-y: auto; + max-height: calc(65vh); + } + + .modal-header-title { + padding-left: 2rem; + + h2 { + font-size: $fs14; + } + } + + .modal-content { + padding: 1rem; + } + + svg { + max-width: 18px; + max-height: 18px; + } + + .file-entry { + margin: 0.75rem 1rem; + user-select: none; + + &.editable:hover { + .file-name-label { + background-color: $color-primary-lighter; + } + .edit-entry-buttons { + display: flex; + background-color: $color-primary-lighter; + } + } + } + + .file-icon { + width: 18px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + + svg { + width: 18px; + height: 18px; + } + + #loader-pencil { + fill: $color-black; + } + + .icon-tick { + fill: $color-success; + } + + .icon-close { + transform: rotate(45deg); + fill: $color-danger; + } + } + + .file-name { + display: flex; + align-items: center; + color: $color-black; + + .file-name-label { + flex: 1; + white-space: nowrap; + display: flex; + align-items: center; + height: 2rem; + margin-left: -0.25rem; + padding-left: 0.25rem; + + .icon-library { + width: 14px; + fill: $color-gray-20; + margin-left: 0.5rem; + padding-top: 1px + } + } + + .file-name-edit { + width: 100%; + + input { + margin: 0; + border: none; + border-bottom: 1px solid $color-gray-20; + height: 2rem; + width: 100%; + } + } + } + + .feedback-banner { + color: $color-black; + background: $color-success-lighter; + height: 40px; + display: flex; + align-items: center; + margin: 0 1rem; + + .message { + padding: 0 1rem; + font-size: $fs12; + } + + .icon { + background: $color-success; + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 20px; + height: 20px; + fill: $color-white; + } + } + } + + .error-message { + margin: 0 2rem; + color: $color-danger; + font-size: $fs12; + font-style: italic; + } + + .linked-libraries { + display: flex; + flex-wrap: wrap; + margin-left: 2rem; + + .icon-chain, .icon-unchain { + width: 10px; + height: 10px; + margin-right: 2px; + } + + .linked-library-tag { + font-size: $fs10; + color: $color-black; + background: #d8f7fe; + border-radius: 3px; + padding: 2px 4px; + display: flex; + align-items: center; + margin: 0.25rem; + + &.error { + background-color: $color-danger-lighter; + } + } + } + + .edit-entry-buttons { + display: flex; + flex-direction: row; + font-size: $fs14; + height: 2rem; + display: none; + + button { + border: none; + background: none; + display: block; + cursor: pointer; + + svg { + width: 14px; + height: 14px; + } + + &:hover svg { + fill: $color-primary; + } + } + } +} + +.export-dialog { + .export-option { + border-radius: 4px; + border: 1px solid $color-gray-10; + margin-bottom: 0.5rem; + + h3 { + font-weight: 700; + } + + h3, p { + font-size: $fs12; + line-height: 1.5; + margin: 0; + color: $color-black; + padding: 0; + } + + &.selected { + border: 1px solid $color-primary; + } + } + + .option-container { + display: block; + position: relative; + padding-left: 40px; + padding-right: 1rem; + padding-top: 1rem; + padding-bottom: 1rem; + cursor: pointer; + user-select: none; + + input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + } + + .option-radio-check { + position: absolute; + top: 1rem; + left: 12px; + height: 18px; + width: 18px; + background-color: $color-white; + border: 1px solid $color-gray-10; + border-radius: 50%; + } + + &:hover input ~ .option-radio-check { + border-color: $color-primary; + } + + input:checked ~ .option-radio-check { + border-color: $color-primary; + background-color: $color-white; + } + + .option-radio-check:after { + content: ""; + position: absolute; + display: none; + } + + input:checked ~ .option-radio-check:after { + display: block; + background-color: $color-primary; + } + + .option-radio-check:after { + top: 3px; + left: 3px; + width: 10px; + height: 10px; + border-radius: 50%; + background: white; + } + } } .libraries-dialog { @@ -565,3 +885,6 @@ } } +.relnotes .onboarding { + height: 420px; +} diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 4389cf8b6e..122f5e8aa1 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -400,6 +400,15 @@ top: 10px; left: 10px; } + + .advanced-options { + border-color: $color-black; + background-color: $color-gray-60; + + .input-text, .input-select, .adv-typography-name { + background-color: $color-gray-60; + } + } } } diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index af2a9d22af..72faa15efd 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -543,7 +543,7 @@ padding: 0 $x-small; &:first-child { - justify-content: space-between; + justify-content: flex-start; margin-left: 0; } @@ -733,6 +733,16 @@ .grid-option { margin-bottom: 0.5rem; + .advanced-options { + .row-flex { + justify-content: flex-end; + } + .custom-button { + left: 0; + position: absolute; + top: 12px; + } + } } .element-set-content .custom-select.input-option { @@ -816,13 +826,13 @@ .advanced-options-wrapper { width: 100%; - padding-left: 0.25rem; } .advanced-options { - background-color: #303236; + border: 1px solid $color-gray-60; + background-color: $color-gray-50; border-radius: 4px; - padding: 0.5rem; + padding: 8px; position: relative; top: 2px; width: 100%; @@ -897,6 +907,22 @@ .download-button { margin-top: 10px; } + + .input-element { + width: 100%; + flex-shrink: initial; + } + + .row-grid-2 { + grid-column-gap: 1em; + } + + .color-info { + input { + margin-right: 1em; + width: 74px; + } + } } .shadow-options .color-row-wrap { @@ -920,6 +946,10 @@ &:hover svg { fill: $color-primary; } + &.actions-inside { + position: absolute; + right: 0; + } } .element-set-label { @@ -985,6 +1015,7 @@ .spacing-options { display: flex; + width: 100%; } .asset-section { @@ -1007,7 +1038,7 @@ width: 100%; max-width: none; margin: 0; - background: #303236; + background-color: #303236; border-top: none; border-left: none; border-right: none; @@ -1045,17 +1076,16 @@ .go-to-lib-button { transition: border 0.3s, color 0.3s; text-align: center; - background: $color-gray-60; + background: $color-gray-50; padding: 0.5rem; border-radius: 2px; cursor: pointer; font-size: 14px; margin-top: 1rem; - border: 1px solid $color-gray-60; &:hover { - border: 1px solid $color-primary; - color: $color-primary; + background: $color-primary; + color: $color-black; } } } @@ -1257,4 +1287,154 @@ } } +.row-flex.align-top { + align-items: flex-start; +} +.constraints-widget { + min-width: 72px; + min-height: 72px; + position: relative; + background-color: $color-gray-60; + flex-grow: 0; + + .constraints-box { + width: 28px; + height: 28px; + position: absolute; + top: 22px; + left: 22px; + border: 2px solid $color-gray-50; + } + + .constraint-button { + position: absolute; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + &::after { + content: ' '; + background-color: $color-gray-20; + } + + &.active, + &:hover { + &::after { + background-color: $color-primary; + } + } + + &.top, + &.bottom { + width: 28px; + height: 22px; + left: calc(50% - 14px); + + &::after { + width: 3px; + height: 15px; + } + } + + &.top { + top: 0; + } + + &.bottom { + bottom: 0; + } + + &.left, + &.right { + width: 22px; + height: 28px; + top: calc(50% - 14px); + + &::after { + width: 15px; + height: 3px; + } + } + + &.left { + left: 0; + } + + &.right { + right: 0; + } + + &.centerv { + width: 28px; + height: 28px; + left: calc(50% - 14px); + top: calc(50% - 14px); + + &::after { + width: 3px; + height: 15px; + } + } + + &.centerh { + width: 28px; + height: 15px; + left: calc(50% - 14px); + top: calc(50% - 7px); + + &::after { + width: 15px; + height: 3px; + } + } + } +} + +.constraints-form { + display: flex; + flex-grow: 1; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + + .input-select { + font-size: $fs11; + margin: 0 $x-small; + } + + svg { + width: 15px; + height: 15px; + margin-left: $medium; + fill: $color-gray-20; + } + + .left-right svg { + transform: rotate(45deg); + } + + .top-bottom svg { + transform: rotate(-45deg); + } + + .fix-when { + font-size: $fs11; + cursor: pointer; + + span { + margin-left: $small; + } + + &:hover, + &.active { + color: $color-primary; + + svg { + fill: $color-primary; + } + } + } + +} diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 9e8449c461..605a08ec3a 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -49,6 +49,12 @@ display: flex; justify-content: center; + svg { + fill: $color-gray-30; + height: 30px; + width: 28px; + } + &:hover { > svg { fill: $color-primary; @@ -56,16 +62,36 @@ } } - svg { - fill: $color-gray-30; - height: 16px; - width: 16px; + .dropdown { + min-width: 260px; + left: 0px; + top: 40px; } + .view-options-dropdown { + align-items: center; + cursor: pointer; + display: flex; + + span { + color: $color-gray-10; + font-size: $fs13; + margin-right: $x-small; + } + + svg { + fill: $color-gray-10; + height: 12px; + width: 12px; + } + } + } + + .file-menu { .dropdown { + min-width: 100px; + right: 0px; top: 40px; - left: 0px; - width: 260px; } } @@ -165,6 +191,10 @@ height: 20px; } } + + .btn-primary { + flex-shrink: 0; + } } .share-link-dropdown { diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss index e191a725f5..7c588f3587 100644 --- a/frontend/resources/styles/main/partials/workspace-header.scss +++ b/frontend/resources/styles/main/partials/workspace-header.scss @@ -179,7 +179,6 @@ .active-users { align-items: center; - cursor: pointer; display: flex; margin: 0; diff --git a/frontend/scripts/build b/frontend/scripts/build index 73b50e483b..1931f5431a 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -8,7 +8,7 @@ EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; yarn install || exit 1; npx gulp clean || exit 1; -npx shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 +clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 npx gulp build || exit 1; npx gulp dist:clean || exit 1; npx gulp dist:copy || exit 1; diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index d4b2b0a2e6..1d95275526 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -1,31 +1,9 @@ -{:http {:port 3448} +{:deps {:aliases [:dev]} + :http {:port 3448} :nrepl {:port 3447} :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] :dev-http {8888 "classpath:public"} - :source-paths ["src", "vendor", "resources", "../common", "tests", "dev"] - :dependencies - [[binaryage/devtools "RELEASE"] - - [environ/environ "1.2.0"] - [metosin/reitit-core "0.5.13"] - [expound/expound "0.8.9"] - - [danlentz/clj-uuid "0.1.9"] - [frankiesardo/linked "1.3.0"] - - [funcool/beicon "2021.04.29-0"] - [funcool/cuerdas "2021.05.09-0"] - [funcool/okulary "2020.04.14-0"] - [funcool/potok "4.0.0"] - [funcool/promesa "6.0.0"] - [funcool/rumext "2021.05.12-1"] - [funcool/tubax "2021.05.20-0"] - - [lambdaisland/uri "1.4.54" - :exclusions [org.clojure/data.json]] - - [instaparse/instaparse "1.4.10"]] :builds {:main @@ -58,14 +36,34 @@ :anon-fn-naming-policy :off :source-map-detail-level :all}}} - :tests + :lib-penpot + {:target :esm + :output-dir "resources/public/libs" + + :modules + {:penpot {:exports {:renderPage app.libs.render/render-page-export + :createFile app.libs.file-builder/create-file-export}}} + + :compiler-options + {:output-feature-set :es8 + :output-wrapper false + :warnings {:fn-deprecated false}} + + :release + {:compiler-options + {:fn-invoke-direct true + :source-map true + :elide-asserts true + :anon-fn-naming-policy :off + :source-map-detail-level :all}}} + + :test {:target :node-test :output-to "target/tests.js" - :ns-regexp "^app.test-" - :autorun true + :ns-regexp "^app.*-test$" + ;; :autorun true :compiler-options {:output-feature-set :es8 :output-wrapper false :warnings {:fn-deprecated false}}}}} - diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 87dcf44315..0cc0a34273 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -6,9 +6,8 @@ (ns app.config (:require - [app.common.data :as d] - [app.common.uri :as u] [app.common.spec :as us] + [app.common.uri :as u] [app.common.version :as v] [app.util.avatars :as avatars] [app.util.dom :as dom] @@ -54,6 +53,11 @@ :browser :webworker)) +(defn- parse-flags + [global] + (let [flags (obj/get global "penpotFlags" "")] + (into #{} (map keyword) (str/words flags)))) + (defn- parse-version [global] (-> (obj/get global "penpotVersion") @@ -78,6 +82,8 @@ (def themes (obj/get global "penpotThemes")) (def analytics (obj/get global "penpotAnalyticsEnabled" false)) +(def flags (delay (parse-flags global))) + (def version (delay (parse-version global))) (def target (delay (parse-target global))) (def browser (delay (parse-browser))) diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs new file mode 100644 index 0000000000..1dbee9c552 --- /dev/null +++ b/frontend/src/app/libs/file_builder.cljs @@ -0,0 +1,87 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.libs.file-builder + (:require + [app.common.data :as d] + [app.common.file-builder :as fb] + [cuerdas.core :as str])) + +(defn parse-data [data] + (as-> data $ + (js->clj $ :keywordize-keys true) + ;; Transforms camelCase to kebab-case + (d/deep-mapm + (fn [[key value]] + (let [value (if (= (type value) js/Symbol) + (keyword (js/Symbol.keyFor value)) + value) + key (-> key d/name str/kebab keyword)] + [key value])) $))) + +(deftype File [^:mutable file] + Object + + (addPage [_ name] + (set! file (fb/add-page file {:name name})) + (str (:current-page-id file))) + + (addPage [_ name options] + (set! file (fb/add-page file {:name name :options options})) + (str (:current-page-id file))) + + (closePage [_] + (set! file (fb/close-page file))) + + (addArtboard [_ data] + (set! file (fb/add-artboard file (parse-data data))) + (str (:last-id file))) + + (closeArtboard [_] + (set! file (fb/close-artboard file))) + + (addGroup [_ data] + (set! file (fb/add-group file (parse-data data))) + (str (:last-id file))) + + (closeGroup [_] + (set! file (fb/close-group file))) + + (createRect [_ data] + (set! file (fb/create-rect file (parse-data data))) + (str (:last-id file))) + + (createCircle [_ data] + (set! file (fb/create-circle file (parse-data data))) + (str (:last-id file))) + + (createPath [_ data] + (set! file (fb/create-path file (parse-data data))) + (str (:last-id file))) + + (createText [_ data] + (set! file (fb/create-text file (parse-data data))) + (str (:last-id file))) + + (createImage [_ data] + (set! file (fb/create-image file (parse-data data))) + (str (:last-id file))) + + (createSVG [_ data] + (set! file (fb/create-svg-raw file (parse-data data))) + (str (:last-id file))) + + (closeSVG [_] + (set! file (fb/close-svg-raw file))) + + (asMap [_] + (clj->js file))) + +(defn create-file-export [^string name] + (File. (fb/create-file name))) + +(defn exports [] + #js { :createFile create-file-export }) diff --git a/frontend/src/app/libs/render.cljs b/frontend/src/app/libs/render.cljs new file mode 100644 index 0000000000..73006a840d --- /dev/null +++ b/frontend/src/app/libs/render.cljs @@ -0,0 +1,28 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.libs.render + (:require + [app.common.uuid :as uuid] + [app.main.render :as r] + [beicon.core :as rx] + [promesa.core :as p])) + +(defn render-page-export + [file ^string page-id] + + ;; Better to expose the api as a promise to be consumed from JS + (let [page-id (uuid/uuid page-id) + file-data (.-file file) + data (get-in file-data [:data :pages-index page-id])] + (p/create + (fn [resolve reject] + (->> (r/render-page data) + (rx/take 1) + (rx/subs resolve reject))) ))) + +(defn exports [] + #js {:renderPage render-page-export}) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index dc2a58f961..ce862ff133 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -12,7 +12,6 @@ [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.users :as du] - [app.main.repo :as rp] [app.main.store :as st] [app.main.ui :as ui] [app.main.ui.confirm] @@ -21,11 +20,9 @@ [app.util.dom :as dom] [app.util.i18n :as i18n] [app.util.logging :as log] - [app.util.object :as obj] [app.util.router :as rt] [app.util.storage :refer [storage]] [app.util.theme :as theme] - [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk] @@ -81,7 +78,7 @@ (defn initialize [] - (letfn [(on-profile [profile] + (letfn [(on-profile [_profile] (rx/of (rt/initialize-router ui/routes) (rt/initialize-history on-navigate)))] (ptk/reify ::initialize @@ -90,7 +87,7 @@ (assoc state :session-id (uuid/next))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (rx/merge (rx/of (ptk/event ::ev/initialize) diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index aeda98fdcc..5250b7202d 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -6,29 +6,11 @@ (ns app.main.data.comments (:require - [cuerdas.core :as str] [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] - [app.common.math :as mth] - [app.common.pages :as cp] [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cfg] - [app.main.constants :as c] [app.main.repo :as rp] - [app.main.store :as st] - [app.main.streams :as ms] - [app.main.worker :as uw] - [app.util.router :as rt] - [app.util.timers :as ts] - [app.util.transit :as t] - [app.util.webapi :as wapi] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [potok.core :as ptk])) (s/def ::content ::us/string) @@ -92,7 +74,7 @@ (ptk/reify ::create-thread ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :create-comment-thread params) (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)})) (rx/map #(partial created %))))))) @@ -102,7 +84,7 @@ (us/assert ::comment-thread thread) (ptk/reify ::update-comment-thread-status ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)] (->> (rp/mutation :update-comment-thread-status {:id id}) (rx/map (constantly done))))))) @@ -118,7 +100,7 @@ (d/update-in-when state [:comment-threads id] assoc :is-resolved is-resolved)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved}) (rx/ignore))))) @@ -131,7 +113,7 @@ (update-in state [:comments (:id thread)] assoc (:id comment) comment))] (ptk/reify ::create-comment ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (rx/concat (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content}) (rx/map #(partial created %))) @@ -146,7 +128,7 @@ (d/update-in-when state [:comments thread-id id] assoc :content content)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :update-comment {:id id :content content}) (rx/ignore))))) @@ -161,7 +143,7 @@ (update :comment-threads dissoc id))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :delete-comment-thread {:id id}) (rx/ignore))))) @@ -174,7 +156,7 @@ (d/update-in-when state [:comments thread-id] dissoc id)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :delete-comment {:id id}) (rx/ignore))))) @@ -185,7 +167,7 @@ (assoc-in state [:comment-threads id] thread))] (ptk/reify ::refresh-comment-thread ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comment-thread {:file-id file-id :id id}) (rx/map #(partial fetched %))))))) @@ -196,7 +178,7 @@ (assoc state :comment-threads (d/index-by :id data)))] (ptk/reify ::retrieve-comment-threads ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comment-threads {:file-id file-id}) (rx/map #(partial fetched %))))))) @@ -207,7 +189,7 @@ (update state :comments assoc thread-id (d/index-by :id comments)))] (ptk/reify ::retrieve-comments ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comments {:thread-id thread-id}) (rx/map #(partial fetched %))))))) @@ -217,7 +199,7 @@ (us/assert ::us/uuid team-id) (ptk/reify ::retrieve-unread-comment-threads ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))] (->> (rp/query :unread-comment-threads {:team-id team-id}) (rx/map #(partial fetched %))))))) @@ -321,7 +303,7 @@ (defn apply-filters [cstate profile threads] - (let [{:keys [show mode open]} cstate] + (let [{:keys [show mode]} cstate] (cond->> threads (= :pending show) (filter (comp not :is-resolved)) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index eacc98c3ac..3879896a48 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -7,23 +7,16 @@ (ns app.main.data.dashboard (:require [app.common.data :as d] - [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.main.repo :as rp] - [app.main.data.events :as ev] - [app.main.data.users :as du] [app.main.data.fonts :as df] + [app.main.data.media :as di] + [app.main.data.users :as du] + [app.main.repo :as rp] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [app.util.time :as dt] - [app.util.timers :as ts] - [app.util.avatars :as avatars] - [app.main.data.media :as di] - [app.main.data.messages :as dm] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [potok.core :as ptk])) ;; --- Specs @@ -44,13 +37,12 @@ ::modified-at])) (s/def ::project - (s/keys ::req-un [::id - ::name - ::team-id - ::profile-id - ::created-at - ::modified-at - ::is-pinned])) + (s/keys :req-un [::id + ::name + ::team-id + ::created-at + ::modified-at + ::is-pinned])) (s/def ::file (s/keys :req-un [::id @@ -80,6 +72,7 @@ (-> (assoc :current-team-id id) (dissoc :dashboard-files) (dissoc :dashboard-projects) + (dissoc :dashboard-shared-files) (dissoc :dashboard-recent-files) (dissoc :dashboard-team-members) (dissoc :dashboard-team-stats))))) @@ -109,7 +102,7 @@ [] (ptk/reify ::fetch-team-members ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/query :team-members {:team-id team-id}) (rx/map team-members-fetched)))))) @@ -127,7 +120,7 @@ [] (ptk/reify ::fetch-team-stats ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/query :team-stats {:team-id team-id}) (rx/map team-stats-fetched)))))) @@ -146,7 +139,7 @@ [] (ptk/reify ::fetch-projects ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/query :projects {:team-id team-id}) (rx/map projects-fetched)))))) @@ -173,7 +166,7 @@ (dissoc state :dashboard-search-result)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state) params (assoc params :team-id team-id)] (->> (rp/query :search-files params) @@ -202,7 +195,7 @@ (us/assert ::us/uuid project-id) (ptk/reify ::fetch-files ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :project-files {:project-id project-id}) (rx/map #(files-fetched project-id %)))))) @@ -213,13 +206,16 @@ (ptk/reify ::shared-files-fetched ptk/UpdateEvent (update [_ state] - (assoc state :dashboard-shared-files (d/index-by :id files))))) + (let [files (d/index-by :id files)] + (-> state + (assoc :dashboard-shared-files files) + (update :dashboard-files d/merge files)))))) (defn fetch-shared-files [] (ptk/reify ::fetch-shared-files ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/query :team-shared-files {:team-id team-id}) (rx/map shared-files-fetched)))))) @@ -240,7 +236,7 @@ [] (ptk/reify ::fetch-recent-files ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/query :team-recent-files {:team-id team-id}) (rx/map recent-files-fetched)))))) @@ -293,7 +289,7 @@ (us/assert string? name) (ptk/reify ::create-team ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] @@ -313,7 +309,7 @@ (assoc-in state [:teams id :name] name)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation! :update-team params) (rx/ignore))))) @@ -322,7 +318,7 @@ (us/assert ::di/file file) (ptk/reify ::update-team-photo ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [on-success di/notify-finished-loading on-error #(do (di/notify-finished-loading) (di/process-error %)) @@ -344,7 +340,7 @@ (us/assert ::us/keyword role) (ptk/reify ::update-team-member-role ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state) params (assoc params :team-id team-id)] (->> (rp/mutation! :update-team-member-role params) @@ -357,7 +353,7 @@ (us/assert ::us/uuid member-id) (ptk/reify ::delete-team-member ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state) params (assoc params :team-id team-id)] (->> (rp/mutation! :delete-team-member params) @@ -370,7 +366,7 @@ (us/assert (s/nilable ::us/uuid) reassign-to) (ptk/reify ::leave-team ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) @@ -391,7 +387,7 @@ (us/assert ::us/keyword role) (ptk/reify ::invite-team-member ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) @@ -408,7 +404,7 @@ (us/assert ::team params) (ptk/reify ::delete-team ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] @@ -434,7 +430,7 @@ [] (ptk/reify ::create-project ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [name (name (gensym (str (tr "dashboard.new-project-prefix") " "))) team-id (:current-team-id state) params {:name name @@ -461,7 +457,7 @@ (us/assert ::us/uuid id) (ptk/reify ::duplicate-project ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) @@ -480,7 +476,7 @@ (us/assert ::us/uuid team-id) (ptk/reify ::move-project ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] @@ -491,7 +487,7 @@ (rx/catch on-error)))))) (defn toggle-project-pin - [{:keys [id is-pinned team-id] :as project}] + [{:keys [id is-pinned] :as project}] (us/assert ::project project) (ptk/reify ::toggle-project-pin ptk/UpdateEvent @@ -499,7 +495,7 @@ (assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [project (get-in state [:dashboard-projects id]) params (select-keys project [:id :is-pinned :team-id])] (->> (rp/mutation :update-project-pin params) @@ -508,7 +504,7 @@ ;; --- EVENT: rename-project (defn rename-project - [{:keys [id name team-id] :as params}] + [{:keys [id name] :as params}] (us/assert ::project params) (ptk/reify ::rename-project ptk/UpdateEvent @@ -518,7 +514,7 @@ (update :dashboard-local dissoc :project-for-edit))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [params {:id id :name name}] (->> (rp/mutation :rename-project params) (rx/ignore)))))) @@ -526,7 +522,7 @@ ;; --- EVENT: delete-project (defn delete-project - [{:keys [id team-id] :as params}] + [{:keys [id] :as params}] (us/assert ::project params) (ptk/reify ::delete-project ptk/UpdateEvent @@ -534,14 +530,14 @@ (update state :dashboard-projects dissoc id)) ptk/WatchEvent - (watch [_ state s] + (watch [_ _ _] (->> (rp/mutation :delete-project {:id id}) (rx/ignore))))) ;; --- EVENT: delete-file (defn file-deleted - [team-id project-id] + [_team-id project-id] (ptk/reify ::file-deleted ptk/UpdateEvent (update [_ state] @@ -555,10 +551,11 @@ (update [_ state] (-> state (d/update-when :dashboard-files dissoc id) + (d/update-when :dashboard-shared-files dissoc id) (d/update-when :dashboard-recent-files dissoc id))) ptk/WatchEvent - (watch [_ state s] + (watch [_ state _] (let [team-id (uuid/uuid (get-in state [:route :path-params :team-id]))] (->> (rp/mutation :delete-file {:id id}) (rx/map #(file-deleted team-id project-id))))))) @@ -573,10 +570,11 @@ (update [_ state] (-> state (d/update-in-when [:dashboard-files id :name] (constantly name)) + (d/update-in-when [:dashboard-shared-files id :name] (constantly name)) (d/update-in-when [:dashboard-recent-files id :name] (constantly name)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [params (select-keys params [:id :name])] (->> (rp/mutation :rename-file params) (rx/ignore)))))) @@ -591,10 +589,13 @@ (update [_ state] (-> state (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared)) - (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)))) + (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)) + (cond-> + (not is-shared) + (d/update-when :dashboard-shared-files dissoc id)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [params {:id id :is-shared is-shared}] (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) @@ -621,7 +622,7 @@ (us/assert ::us/uuid project-id) (ptk/reify ::create-file ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) @@ -642,7 +643,7 @@ (us/assert ::name name) (ptk/reify ::duplicate-file ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) @@ -663,7 +664,7 @@ (us/assert ::us/uuid project-id) (ptk/reify ::move-files ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] @@ -682,26 +683,32 @@ (us/assert ::file file) (ptk/reify ::go-to-workspace ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [pparams {:project-id project-id :file-id id}] (rx/of (rt/nav :workspace pparams)))))) (defn go-to-files - [project-id] - (ptk/reify ::go-to-files - ptk/WatchEvent - (watch [_ state stream] - (let [team-id (:current-team-id state)] - (rx/of (rt/nav :dashboard-files {:team-id team-id - :project-id project-id})))))) + ([project-id] + (ptk/reify ::go-to-files + ptk/WatchEvent + (watch [_ state _] + (let [team-id (:current-team-id state)] + (rx/of (rt/nav :dashboard-files {:team-id team-id + :project-id project-id})))))) + ([team-id project-id] + (ptk/reify ::go-to-files + ptk/WatchEvent + (watch [_ _ _] + (rx/of (rt/nav :dashboard-files {:team-id team-id + :project-id project-id})))))) (defn go-to-search ([] (go-to-search nil)) ([term] (ptk/reify ::go-to-search ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (if (empty? term) (rx/of (rt/nav :dashboard-search @@ -714,13 +721,13 @@ ([] (ptk/reify ::go-to-projects ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) ([team-id] (ptk/reify ::go-to-projects ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (du/set-current-team! team-id) (rx/of (rt/nav :dashboard-projects {:team-id team-id})))))) @@ -728,7 +735,7 @@ [] (ptk/reify ::go-to-team-members ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (rx/of (rt/nav :dashboard-team-members {:team-id team-id})))))) @@ -736,6 +743,6 @@ [] (ptk/reify ::go-to-team-settings ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (rx/of (rt/nav :dashboard-team-settings {:team-id team-id})))))) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index ca4b99a619..83c2a835f5 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -8,14 +8,14 @@ (:require ["ua-parser-js" :as UAParser] [app.common.data :as d] - [app.main.repo :as rp] [app.config :as cf] + [app.main.repo :as rp] [app.util.globals :as g] [app.util.http :as http] + [app.util.i18n :as i18n] [app.util.object :as obj] [app.util.storage :refer [storage]] [app.util.time :as dt] - [app.util.i18n :as i18n] [beicon.core :as rx] [lambdaisland.uri :as u] [potok.core :as ptk])) @@ -139,7 +139,7 @@ :project-id (:project-id data)}})) (defn- event->generic-action - [event name] + [_ name] {:type "action" :name name :props {}}) @@ -176,7 +176,7 @@ [_ {:keys [buffer] :as params}] (ptk/reify ::persistence ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (let [profile-id (:profile-id state) events (into [] (take max-buffer-size) @buffer)] (when (seq events) @@ -191,7 +191,7 @@ (let [buffer (atom #queue [])] (ptk/reify ::initialize ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (->> (rx/merge (->> (rx/from-atom buffer) (rx/filter #(pos? (count %))) @@ -202,7 +202,7 @@ (rx/map #(ptk/event ::persistence {:buffer buffer})))) ptk/EffectEvent - (effect [_ state stream] + (effect [_ _ stream] (let [events (methods process-event) session (atom nil) diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index 41ca36c9ac..74872995b0 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -8,16 +8,14 @@ (:require ["opentype.js" :as ot] [app.common.data :as d] - [app.common.spec :as us] [app.common.media :as cm] + [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.fonts :as fonts] [app.main.repo :as rp] - [app.util.i18n :as i18n :refer [tr]] [app.util.logging :as log] - [beicon.core :as rx] - [cljs.spec.alpha :as s] [app.util.webapi :as wa] + [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk])) @@ -62,7 +60,7 @@ (assoc state :dashboard-fonts (d/index-by :id fonts))) ptk/EffectEvent - (effect [_ state stream] + (effect [_ _ _] (let [fonts (->> fonts (map adapt-font-id) (group-by :font-id) @@ -73,7 +71,7 @@ [team-id] (ptk/reify ::load-team-fonts ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :font-variants {:team-id team-id}) (rx/map fonts-fetched))))) @@ -121,7 +119,7 @@ (parse-font [{:keys [data] :as params}] (try (assoc params :font (ot/parse data)) - (catch :default e + (catch :default _e (log/warn :msg (str/fmt "skiping file %s, unsupported format" (:name params))) nil))) @@ -204,7 +202,7 @@ fonts)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/mutation! :update-font {:id id :name name :team-id team-id}) (rx/ignore)))))) @@ -218,10 +216,10 @@ (update [_ state] (update state :dashboard-fonts (fn [variants] - (d/removem (fn [[id variant]] + (d/removem (fn [[_id variant]] (= (:font-id variant) font-id)) variants)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/mutation! :delete-font {:id font-id :team-id team-id}) (rx/ignore)))))) @@ -238,7 +236,7 @@ (= (:id variant) id)) variants)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (->> (rp/mutation! :delete-font-variant {:id id :team-id team-id}) (rx/ignore)))))) diff --git a/frontend/src/app/main/data/history.cljs b/frontend/src/app/main/data/history.cljs deleted file mode 100644 index 8da91d3874..0000000000 --- a/frontend/src/app/main/data/history.cljs +++ /dev/null @@ -1,270 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.data.history - (:require - [beicon.core :as rx] - [cljs.spec.alpha :as s] - [potok.core :as ptk] - [app.common.spec :as us] - [app.common.pages :as cp] - [app.main.repo :as rp] - [app.util.data :refer [replace-by-id index-by]])) - -;; --- Schema - -(s/def ::pinned boolean?) -(s/def ::id uuid?) -(s/def ::label string?) -(s/def ::project uuid?) -(s/def ::created-at inst?) -(s/def ::modified-at inst?) -(s/def ::version number?) -(s/def ::user uuid?) - -(s/def ::shapes - (s/every ::cp/minimal-shape :kind vector?)) - -(s/def ::data - (s/keys :req-un [::shapes])) - -(s/def ::history-entry - (s/keys :req-un [::id - ::pinned - ::label - ::project - ::created-at - ::modified-at - ::version - ::user - ::data])) - -(s/def ::history-entries - (s/every ::history-entry)) - -;; --- Initialize History State - -(declare fetch-history) -(declare fetch-pinned-history) - -(defn initialize - [id] - (us/verify ::us/uuid id) - (ptk/reify ::initialize - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace id] - assoc :history {:selected nil - :pinned #{} - :items #{} - :byver {}})) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (fetch-history id) - (fetch-pinned-history id))))) - -;; --- Watch Page Changes - -(defn watch-page-changes - [id] - (us/verify ::us/uuid id) - (reify - ptk/WatchEvent - (watch [_ state stream] - #_(let [stopper (rx/filter #(= % ::stop-page-watcher) stream)] - (->> stream - (rx/filter dp/page-persisted?) - (rx/debounce 1000) - (rx/flat-map #(rx/of (fetch-history id) - (fetch-pinned-history id))) - (rx/take-until stopper)))))) - -;; --- Pinned Page History Fetched - -(defn pinned-history-fetched - [items] - (us/verify ::history-entries items) - (ptk/reify ::pinned-history-fetched - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - items-map (index-by :version items) - items-set (into #{} items)] - (update-in state [:workspace pid :history] - (fn [history] - (-> history - (assoc :pinned items-set) - (update :byver merge items-map)))))))) - -;; --- Fetch Pinned Page History - -(defn fetch-pinned-history - [id] - (us/verify ::us/uuid id) - (ptk/reify ::fetch-pinned-history - ptk/WatchEvent - (watch [_ state s] - (let [params {:page id :pinned true}] - #_(->> (rp/req :fetch/page-history params) - (rx/map :payload) - (rx/map pinned-history-fetched)))))) - -;; --- Page History Fetched - -(defn history-fetched - [items] - (us/verify ::history-entries items) - (ptk/reify ::history-fetched - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - versions (into #{} (map :version) items) - items-map (index-by :version items) - min-version (apply min versions) - max-version (apply max versions)] - (update-in state [:workspace pid :history] - (fn [history] - (-> history - (assoc :min-version min-version) - (assoc :max-version max-version) - (update :byver merge items-map) - (update :items #(reduce conj % items))))))))) - -;; --- Fetch Page History - -(defn fetch-history - ([id] - (fetch-history id nil)) - ([id {:keys [since max]}] - (us/verify ::us/uuid id) - (ptk/reify ::fetch-history - ptk/WatchEvent - (watch [_ state s] - (let [params (merge {:page id - :max (or max 20)} - (when since - {:since since}))] - #_(->> (rp/req :fetch/page-history params) - (rx/map :payload) - (rx/map history-fetched))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Context Aware Events -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; --- Select Section - -(deftype SelectSection [section] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace :history :section] section))) - -(defn select-section - [section] - {:pre [(keyword? section)]} - (SelectSection. section)) - -;; --- Load More - -(def load-more - (ptk/reify ::load-more - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - since (get-in state [:workspace pid :history :min-version])] - (rx/of (fetch-history pid {:since since})))))) - -;; --- Select Page History - -(defn select - [version] - (us/verify int? version) - (ptk/reify ::select - ptk/UpdateEvent - (update [_ state] - #_(let [pid (get-in state [:workspace :current]) - item (get-in state [:workspace pid :history :byver version]) - page (-> (get-in state [:pages pid]) - (assoc :history true - :data (:data item)))] - (-> state - (dp/unpack-page page) - (assoc-in [:workspace pid :history :selected] version)))))) - -;; --- Apply Selected History - -(def apply-selected - (ptk/reify ::apply-selected - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current])] - (-> state - (update-in [:pages pid] dissoc :history) - (assoc-in [:workspace pid :history :selected] nil)))) - - ptk/WatchEvent - (watch [_ state s] - #_(let [pid (get-in state [:workspace :current])] - (rx/of (dp/persist-page pid)))))) - -;; --- Deselect Page History - -(def deselect - (ptk/reify ::deselect - ptk/UpdateEvent - (update [_ state] - #_(let [pid (get-in state [:workspace :current]) - packed (get-in state [:packed-pages pid])] - (-> (dp/unpack-page state packed) - (assoc-in [:workspace pid :history :selected] nil)))))) - - ;; --- Refresh Page History - -(def refres-history - (ptk/reify ::refresh-history - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - history (get-in state [:workspace pid :history]) - maxitems (count (:items history))] - (rx/of (fetch-history pid {:max maxitems}) - (fetch-pinned-history pid)))))) - -;; --- History Item Updated - -(defn history-updated - [item] - (us/verify ::history-entry item) - (ptk/reify ::history-item-updated - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :history] - (fn [history] - (-> history - (update :items #(into #{} (replace-by-id item) %)) - (update :pinned #(into #{} (replace-by-id item) %)) - (assoc-in [:byver (:version item)] item)))))))) - -(defn history-updated? - [v] - (= ::history-item-updated (ptk/type v))) - -;; --- Update History Item - -(defn update-history-item - [item] - (ptk/reify ::update-history-item - ptk/WatchEvent - (watch [_ state stream] - (rx/concat - #_(->> (rp/req :update/page-history item) - (rx/map :payload) - (rx/map history-updated)) - (->> (rx/filter history-updated? stream) - (rx/take 1) - (rx/map (constantly refres-history))))))) diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs index 695807cc28..ba9ad47df4 100644 --- a/frontend/src/app/main/data/media.cljs +++ b/frontend/src/app/main/data/media.cljs @@ -6,22 +6,14 @@ (ns app.main.data.media (:require - [app.common.data :as d] - [app.common.media :as cm] - [app.common.spec :as us] - [app.common.uuid :as uuid] [app.common.exceptions :as ex] + [app.common.media :as cm] [app.main.data.messages :as dm] - [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :refer [tr]] - [app.util.router :as r] - [app.util.router :as rt] - [app.util.time :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [potok.core :as ptk])) + [cuerdas.core :as str])) ;; --- Predicates diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index 41b8f6db93..1a834fa3c6 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -7,10 +7,7 @@ (ns app.main.data.messages (:require [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.pages :as cp] [app.common.spec :as us] - [app.config :as cfg] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -54,7 +51,7 @@ (assoc state :message message))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (when (:timeout data) (let [stoper (rx/filter (ptk/type? ::show) stream)] (->> (rx/of hide) @@ -68,7 +65,7 @@ (d/update-when state :message assoc :status :hide)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stoper (rx/filter (ptk/type? ::show) stream)] (->> (rx/of #(dissoc % :message)) (rx/delay default-animation-timeout) @@ -78,7 +75,7 @@ [tag] (ptk/reify ::hide-tag ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [message (get state :message)] (when (= (:tag message) tag) (rx/of hide)))))) @@ -127,7 +124,7 @@ :tag tag}))) (defn assign-exception - [{:keys [type] :as error}] + [error] (ptk/reify ::assign-exception ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index b77f4ec159..daf3bb1b45 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -7,10 +7,10 @@ (ns app.main.data.modal (:refer-clojure :exclude [update]) (:require - [potok.core :as ptk] - [app.main.store :as st] [app.common.uuid :as uuid] - [cljs.core :as c])) + [app.main.store :as st] + [cljs.core :as c] + [potok.core :as ptk])) (defonce components (atom {})) @@ -29,7 +29,7 @@ :allow-click-outside false}))))) (defn update-props - ([type props] + ([_type props] (ptk/reify ::update-modal-props ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index e3eb7810d0..69c9596bce 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -8,7 +8,6 @@ (:refer-clojure :exclude [meta reset!]) (:require ["mousetrap" :as mousetrap] - [app.common.data :as d] [app.common.spec :as us] [app.config :as cfg] [app.util.logging :as log] @@ -164,8 +163,8 @@ (update :shortcuts (fnil conj '()) [key shortcuts]))) ptk/EffectEvent - (effect [_ state stream] - (let [[key shortcuts] (peek (:shortcuts state))] + (effect [_ state _] + (let [[_key shortcuts] (peek (:shortcuts state))] (reset! shortcuts))))) (defn pop-shortcuts @@ -179,7 +178,7 @@ (pop shortcuts) shortcuts))))) ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (let [[key* shortcuts] (peek (:shortcuts state))] (when (not= key key*) (reset! shortcuts)))))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 906df6b8d2..b1fc5b7964 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -6,24 +6,20 @@ (ns app.main.data.users (:require - [app.config :as cf] [app.common.data :as d] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.events :as ev] [app.main.data.media :as di] [app.main.data.modal :as modal] - [app.main.data.messages :as dm] [app.main.repo :as rp] - [app.main.store :as st] - [app.util.avatars :as avatars] - [app.util.i18n :as i18n :refer [tr]] + [app.util.i18n :as i18n] [app.util.router :as rt] [app.util.storage :refer [storage]] [app.util.theme :as theme] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [potok.core :as ptk])) ;; --- COMMON SPECS @@ -75,7 +71,7 @@ [] (ptk/reify ::fetch-teams ptk/WatchEvent - (watch [_ state s] + (watch [_ _ _] (->> (rp/query! :teams) (rx/map teams-fetched))))) @@ -95,7 +91,7 @@ (assoc :profile profile))) ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (let [profile (:profile state)] (when (not= uuid/zero (:id profile)) (swap! storage assoc :profile profile) @@ -107,7 +103,7 @@ [] (ptk/reify ::fetch-profile ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query! :profile) (rx/map profile-fetched))))) @@ -120,7 +116,7 @@ [] (ptk/reify ::initialize-profile ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (rx/merge (rx/of (fetch-profile)) (->> stream @@ -141,10 +137,8 @@ (-deref [_] profile) ptk/WatchEvent - (watch [this state stream] - (let [team-id (get-current-team-id profile) - profile (with-meta profile - {::ev/source "login"})] + (watch [_ _ _] + (let [team-id (get-current-team-id profile)] (->> (rx/concat (rx/of (profile-fetched profile) (fetch-teams)) @@ -166,7 +160,7 @@ (us/verify ::login-params data) (ptk/reify ::login ptk/WatchEvent - (watch [this state s] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data) @@ -186,11 +180,30 @@ [{:keys [profile] :as tdata}] (ptk/reify ::login-from-token ptk/WatchEvent - (watch [this state s] + (watch [_ _ _] (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))))) +(defn login-from-register + "Event used mainly for mark current session as logged-in in after the + user sucessfully registred using third party auth provider (in this + case we dont need to verify the email)." + [] + (ptk/reify ::login-from-register + ptk/WatchEvent + (watch [_ _ stream] + (rx/merge + (rx/of (fetch-profile)) + (->> stream + (rx/filter (ptk/type? ::profile-fetched)) + (rx/take 1) + (rx/map deref) + (rx/map (fn [profile] + (with-meta profile + {::ev/source "register"}))) + (rx/map logged-in)))))) + ;; --- EVENT: logout (defn logged-out @@ -201,11 +214,11 @@ (select-keys state [:route :router :session-id :history])) ptk/WatchEvent - (watch [_ state s] + (watch [_ _ _] (rx/of (rt/nav :auth-login))) ptk/EffectEvent - (effect [_ state s] + (effect [_ _ _] (reset! storage {}) (i18n/reset-locale)))) @@ -213,7 +226,7 @@ [] (ptk/reify ::logout ptk/WatchEvent - (watch [_ state s] + (watch [_ _ _] (->> (rp/mutation :logout) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) @@ -221,6 +234,7 @@ ;; --- EVENT: register +;; TODO: remove (s/def ::invitation-token ::us/not-empty-string) (s/def ::register @@ -233,7 +247,7 @@ (s/assert ::register data) (ptk/reify ::register ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error identity on-success identity}} (meta data)] @@ -248,7 +262,7 @@ (us/assert ::profile data) (ptk/reify ::update-profile ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [mdata (meta data) on-success (:on-success mdata identity) on-error (:on-error mdata #(rx/throw %))] @@ -272,7 +286,7 @@ (us/assert ::us/email email) (ptk/reify ::request-email-change ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error identity on-success identity}} (meta data)] @@ -285,7 +299,7 @@ (def cancel-email-change (ptk/reify ::cancel-email-change ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :cancel-email-change {}) (rx/map (constantly (fetch-profile))))))) @@ -301,7 +315,7 @@ (us/verify ::update-password data) (ptk/reify ::update-password ptk/WatchEvent - (watch [_ state s] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error identity on-success identity}} (meta data) @@ -320,7 +334,7 @@ ([{:keys [version]}] (ptk/reify ::mark-oboarding-as-viewed ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [version (or version (:main @cf/version)) props (-> (get-in state [:profile :props]) (assoc :onboarding-viewed true) @@ -335,7 +349,7 @@ (us/verify ::di/blob file) (ptk/reify ::update-photo ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [on-success di/notify-finished-loading on-error #(do (di/notify-finished-loading) (di/process-error %)) @@ -363,7 +377,7 @@ (assoc state :users)))] (ptk/reify ::fetch-team-users ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :team-users {:team-id team-id}) (rx/map #(partial fetched %))))))) @@ -373,7 +387,7 @@ [params] (ptk/reify ::request-account-deletion ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta params)] @@ -394,7 +408,7 @@ (us/verify ::request-profile-recovery data) (ptk/reify ::request-profile-recovery ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] @@ -410,19 +424,17 @@ (s/keys :req-un [::password ::token])) (defn recover-profile - [{:keys [token password] :as data}] + [data] (us/verify ::recover-profile data) (ptk/reify ::recover-profile ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] (->> (rp/mutation :recover-profile data) (rx/tap on-success) - (rx/catch (fn [err] - (on-error) - (rx/empty)))))))) + (rx/catch on-error)))))) ;; --- EVENT: crete-demo-profile @@ -430,7 +442,7 @@ [] (ptk/reify ::create-demo-profile ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation :create-demo-profile {}) (rx/map login))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index c4eb652924..c6dc46a37d 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -7,7 +7,6 @@ (ns app.main.data.viewer (:require [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -15,8 +14,6 @@ [app.main.data.comments :as dcm] [app.main.data.fonts :as df] [app.main.repo :as rp] - [app.main.store :as st] - [app.util.avatars :as avatars] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -27,7 +24,7 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::project (s/keys ::req-un [::id ::name])) +(s/def ::project (s/keys :req-un [::id ::name])) (s/def ::file (s/keys :req-un [::id ::name])) (s/def ::page ::cp/page) @@ -60,10 +57,10 @@ (s/def ::initialize-params (s/keys :req-un [::page-id ::file-id] - :opt-in [::token])) + :opt-un [::token])) (defn initialize - [{:keys [page-id file-id token] :as params}] + [{:keys [page-id file-id] :as params}] (us/assert ::initialize-params params) (ptk/reify ::initialize ptk/UpdateEvent @@ -78,7 +75,7 @@ lstate))))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (rx/of (fetch-bundle params) (fetch-comment-threads params))))) @@ -86,14 +83,14 @@ (s/def ::fetch-bundle-params (s/keys :req-un [::page-id ::file-id] - :opt-in [::token])) + :opt-un [::token])) (defn fetch-bundle [{:keys [page-id file-id token] :as params}] (us/assert ::fetch-bundle-params params) (ptk/reify ::fetch-file ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [params (cond-> {:page-id page-id :file-id file-id} (string? token) (assoc :token token))] @@ -145,7 +142,7 @@ (ptk/reify ::fetch-comment-threads ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comment-threads {:file-id file-id}) (rx/map #(partial fetched %)) (rx/catch on-error)))))) @@ -156,7 +153,7 @@ (assoc-in state [:comment-threads id] thread))] (ptk/reify ::refresh-comment-thread ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comment-thread {:file-id file-id :id id}) (rx/map #(partial fetched %))))))) @@ -167,7 +164,7 @@ (update state :comments assoc thread-id (d/index-by :id comments)))] (ptk/reify ::retrieve-comments ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/query :comments {:thread-id thread-id}) (rx/map #(partial fetched %))))))) @@ -175,7 +172,7 @@ [] (ptk/reify ::create-share-link ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [file-id (:current-file-id state) page-id (:current-page-id state)] (->> (rp/mutation! :create-file-share-token {:file-id file-id @@ -187,7 +184,7 @@ [] (ptk/reify ::delete-share-link ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [file-id (:current-file-id state) page-id (:current-page-id state) token (get-in state [:viewer-data :token]) @@ -246,7 +243,7 @@ (def select-prev-frame (ptk/reify ::select-prev-frame ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) @@ -260,7 +257,7 @@ (def select-next-frame (ptk/reify ::select-prev-frame ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) @@ -296,7 +293,7 @@ (assoc-in state [:viewer-local :interactions-show?] true)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stopper (rx/filter (ptk/type? ::flash-interactions) stream)] (->> (rx/of flash-done) (rx/delay 500) @@ -314,7 +311,7 @@ [index] (ptk/reify ::go-to-frame ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) @@ -326,7 +323,7 @@ (us/verify ::us/uuid frame-id) (ptk/reify ::go-to-frame ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [frames (get-in state [:viewer-data :frames]) index (d/index-of-pred frames #(= (:id %) frame-id))] (when index @@ -337,13 +334,11 @@ [section] (ptk/reify ::go-to-section ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [route (:route state) - screen (-> route :data :name keyword) pparams (:path-params route) qparams (:query-params route)] - (rx/of - (rt/nav :viewer pparams (assoc qparams :section section))))))) + (rx/of (rt/nav :viewer pparams (assoc qparams :section section))))))) (defn set-current-frame [frame-id] @@ -422,6 +417,6 @@ ([{:keys [team-id]}] (ptk/reify ::go-to-dashboard ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (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/viewer/shortcuts.cljs b/frontend/src/app/main/data/viewer/shortcuts.cljs index 91ea3b3008..86f1ad8800 100644 --- a/frontend/src/app/main/data/viewer/shortcuts.cljs +++ b/frontend/src/app/main/data/viewer/shortcuts.cljs @@ -6,15 +6,9 @@ (ns app.main.data.viewer.shortcuts (:require - [app.config :as cfg] - [app.main.data.workspace.colors :as mdc] [app.main.data.shortcuts :as ds] - [app.main.data.shortcuts :refer [c-mod]] [app.main.data.viewer :as dv] - [app.main.store :as st] - [app.util.dom :as dom] - [beicon.core :as rx] - [potok.core :as ptk])) + [app.main.store :as st])) (def shortcuts {:increase-zoom {:tooltip "+" diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 39f452e49f..7c0a57c7d9 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -15,7 +15,9 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.pages.spec :as spec] [app.common.spec :as us] + [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.messages :as dm] @@ -37,9 +39,7 @@ [app.main.worker :as uw] [app.util.http :as http] [app.util.i18n :as i18n] - [app.util.logging :as log] [app.util.router :as rt] - [app.util.transit :as t] [app.util.webapi :as wapi] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -134,7 +134,7 @@ (or layout default-layout)))) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (if (and layout-name (contains? layout-names layout-name)) (rx/of (ensure-layout layout-name)) (rx/of (ensure-layout :layers)))))) @@ -153,7 +153,7 @@ :workspace-presence {})) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ stream] (rx/merge (rx/of (dwp/fetch-bundle project-id file-id)) @@ -162,7 +162,7 @@ (rx/filter (ptk/type? ::dwp/bundle-fetched)) (rx/take 1) (rx/map deref) - (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/mapcat (fn [bundle] (rx/merge (rx/of (dwn/initialize file-id) (dwp/initialize-file-persistence file-id) @@ -188,7 +188,7 @@ :workspace-libraries (d/index-by :id libraries))) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [file-id (:id file) ignore-until (:ignore-sync-until file) needs-update? (some #(and (> (:modified-at %) (:synced-at %)) @@ -199,7 +199,7 @@ (rx/of (dwl/notify-sync-file file-id))))))) (defn finalize-file - [project-id file-id] + [_project-id file-id] (ptk/reify ::finalize ptk/UpdateEvent (update [_ state] @@ -210,12 +210,10 @@ :workspace-persistence)) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/of (dwn/finalize file-id) ::dwp/finalize)))) -(declare go-to-page) - (defn initialize-page [page-id] (us/assert ::us/uuid page-id) @@ -264,7 +262,7 @@ {:id id :file-id file-id}) ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [pages (get-in state [:workspace-data :pages-index]) unames (dwc/retrieve-used-names pages) name (dwc/generate-unique-name unames "Page") @@ -282,7 +280,7 @@ [page-id] (ptk/reify ::duplicate-page ptk/WatchEvent - (watch [this state stream] + (watch [this state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) unames (dwc/retrieve-used-names pages) @@ -308,7 +306,7 @@ (us/verify string? name) (ptk/reify ::rename-page ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page (get-in state [:workspace-data :pages-index id]) rchg {:type :mod-page :id id @@ -329,7 +327,7 @@ [id] (ptk/reify ::delete-page ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page (get-in state [:workspace-data :pages-index id]) rchg {:type :del-page :id id} @@ -355,7 +353,7 @@ (assoc-in state [:workspace-file :name] name)) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [params {:id id :name name}] (->> (rp/mutation :rename-file params) (rx/ignore)))))) @@ -370,7 +368,7 @@ (defn initialize-viewport [{:keys [width height] :as size}] - (letfn [(update* [{:keys [vbox vport] :as local}] + (letfn [(update* [{:keys [vport] :as local}] (let [wprop (/ (:width vport) width) hprop (/ (:height vport) height)] (-> local @@ -435,7 +433,7 @@ ptk/UpdateEvent (update [_ state] (update state :workspace-local - (fn [{:keys [vbox vport left-sidebar? zoom] :as local}] + (fn [{:keys [vport left-sidebar? zoom] :as local}] (if (or (mth/almost-zero? width) (mth/almost-zero? height)) ;; If we have a resize to zero just keep the old value local @@ -454,7 +452,7 @@ (defn start-panning [] (ptk/reify ::start-panning ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning))) zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)] (when-not (get-in state [:workspace-local :panning]) @@ -582,7 +580,7 @@ (mth/nan? (:height srect))) state (update state :workspace-local - (fn [{:keys [vbox vport] :as local}] + (fn [{:keys [vport] :as local}] (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) zoom (/ (:width vport) (:width srect))] (-> local @@ -602,7 +600,7 @@ (map #(get objects %)) (gsh/selection-rect))] (update state :workspace-local - (fn [{:keys [vbox vport] :as local}] + (fn [{:keys [vport] :as local}] (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) zoom (/ (:width vport) (:width srect))] (-> local @@ -617,7 +615,7 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::update-shape ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/of (dch/update-shapes [id] #(merge % attrs)))))) (defn start-rename-shape @@ -633,7 +631,7 @@ (ptk/reify ::end-rename-shape ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-local] dissoc :shape-for-rename)))) + (update state :workspace-local dissoc :shape-for-rename)))) ;; --- Update Selected Shapes attrs @@ -642,45 +640,17 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::update-selected-shapes ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/from (map #(update-shape % attrs) selected)))))) -;; --- Shape Movement (using keyboard shorcuts) - -(declare initial-selection-align) - -(defn- get-displacement-with-grid - "Retrieve the correct displacement delta point for the - provided direction speed and distances thresholds." - [shape direction options] - (let [grid-x (:grid-x options 10) - grid-y (:grid-y options 10) - x-mod (mod (:x shape) grid-x) - y-mod (mod (:y shape) grid-y)] - (case direction - :up (gpt/point 0 (- (if (zero? y-mod) grid-y y-mod))) - :down (gpt/point 0 (- grid-y y-mod)) - :left (gpt/point (- (if (zero? x-mod) grid-x x-mod)) 0) - :right (gpt/point (- grid-x x-mod) 0)))) - -(defn- get-displacement - "Retrieve the correct displacement delta point for the - provided direction speed and distances thresholds." - [shape direction] - (case direction - :up (gpt/point 0 (- 1)) - :down (gpt/point 0 1) - :left (gpt/point (- 1) 0) - :right (gpt/point 1 0))) - ;; --- Delete Selected (def delete-selected "Deselect all and remove all selected shapes." (ptk/reify ::delete-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/of (dwc/delete-shapes selected) (dws/deselect-all)))))) @@ -694,7 +664,7 @@ (us/verify ::loc loc) (ptk/reify ::vertical-order-selected ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -733,7 +703,9 @@ ;; --- 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] +(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 shapes-to-unconstraint] (let [;; Changes to the shapes that are being move r-mov-change [{:type :mov-objects @@ -866,6 +838,43 @@ :val nil}]}) shapes-to-reroot) + + ;; Changes resetting constraints + + r-unconstraint-change + (map (fn [id] + (let [obj (get objects id) + parent (get objects parent-id) + frame-id (if (= (:type parent) :frame) + (:id parent) + (:frame-id parent))] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :constraints-h + :val (spec/default-constraints-h + (assoc obj :parent-id parent-id :frame-id frame-id))} + {:type :set + :attr :constraints-v + :val (spec/default-constraints-v + (assoc obj :parent-id parent-id :frame-id frame-id))}]})) + shapes-to-unconstraint) + + u-unconstraint-change + (map (fn [id] + (let [obj (get objects id)] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :constraints-h + :val (:constraints-h obj)} + {:type :set + :attr :constraints-v + :val (:constraints-v obj)}]})) + shapes-to-unconstraint) + r-reg-change [{:type :reg-objects :page-id page-id @@ -883,6 +892,7 @@ r-detach-change r-deroot-change r-reroot-change + r-unconstraint-change r-reg-change) uchanges (d/concat [] @@ -892,6 +902,7 @@ u-detach-change u-mask-change u-mov-change + u-unconstraint-change u-reg-change)] [rchanges uchanges])) @@ -903,7 +914,7 @@ (ptk/reify ::relocate-shapes ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -1002,9 +1013,9 @@ groups-to-unmask shapes-to-detach shapes-to-reroot - shapes-to-deroot) + shapes-to-deroot + ids)] - ] (rx/of (dch/commit-changes {:redo-changes rchanges :undo-changes uchanges :origin it}) @@ -1014,7 +1025,7 @@ [parent-id to-index] (ptk/reify ::relocate-selected-shapes ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/of (relocate-shapes selected parent-id to-index)))))) @@ -1023,7 +1034,7 @@ [] (ptk/reify ::start-editing-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [selected (wsh/lookup-selected state)] (if-not (= 1 (count selected)) (rx/empty) @@ -1035,14 +1046,11 @@ :text (rx/of (dwc/start-edition-mode id)) - :path - (rx/of (dwc/start-edition-mode id) - (dwdp/start-path-edit id)) - :group (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) - (rx/empty)))))))) + (rx/of (dwc/start-edition-mode id) + (dwdp/start-path-edit id))))))))) ;; --- Change Page Order (D&D Ordering) @@ -1051,7 +1059,7 @@ [id index] (ptk/reify ::relocate-pages ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [cidx (-> (get-in state [:workspace-data :pages]) (d/index-of id)) rchg {:type :mov-page @@ -1074,7 +1082,7 @@ (us/verify ::gal/align-axis axis) (ptk/reify :align-objects ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -1105,7 +1113,7 @@ (us/verify ::gal/dist-axis axis) (ptk/reify :align-objects ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -1123,7 +1131,7 @@ [id lock] (ptk/reify ::set-shape-proportion-lock ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (letfn [(assign-proportions [shape] (if-not lock (assoc shape :proportion-lock false) @@ -1131,33 +1139,6 @@ (gpr/assign-proportions))))] (rx/of (dch/update-shapes [id] assign-proportions)))))) -;; --- Update Shape Position - -(s/def ::x number?) -(s/def ::y number?) -(s/def ::position - (s/keys :opt-un [::x ::y])) - -(defn update-position - [id position] - (us/verify ::us/uuid id) - (us/verify ::position position) - (ptk/reify ::update-position - ptk/WatchEvent - (watch [it state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shape (get objects id) - - bbox (-> shape :points gsh/points->selrect) - - cpos (gpt/point (:x bbox) (:y bbox)) - pos (gpt/point (or (:x position) (:x bbox)) - (or (:y position) (:y bbox))) - displ (gmt/translate-matrix (gpt/subtract pos cpos))] - (rx/of (dwt/set-modifiers [id] {:displacement displ}) - (dwt/apply-modifiers [id])))))) - ;; --- Update Shape Flags (defn update-shape-flags @@ -1166,7 +1147,7 @@ (s/assert ::shape-attrs flags) (ptk/reify ::update-shape-flags ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [update-fn (fn [obj] (cond-> obj @@ -1186,29 +1167,28 @@ [project-id] (ptk/reify ::navigate-to-project ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [page-ids (get-in state [:projects project-id :pages]) params {:project project-id :page (first page-ids)}] (rx/of (rt/nav :workspace/page params)))))) (defn go-to-page ([] - (ptk/reify ::go-to-page ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [project-id (:current-project-id state) file-id (:current-file-id state) page-id (get-in state [:workspace-data :pages 0]) pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id}] - (rx/of (rt/nav :workspace pparams qparams)))))) + (rx/of (rt/nav' :workspace pparams qparams)))))) ([page-id] (us/verify ::us/uuid page-id) (ptk/reify ::go-to-page ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [project-id (:current-project-id state) file-id (:current-file-id state) pparams {:file-id file-id :project-id project-id} @@ -1220,10 +1200,10 @@ (us/verify ::layout-flag layout) (ptk/reify ::go-to-layout ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [project-id (get-in state [:workspace-project :id]) file-id (get-in state [:workspace-file :id]) - page-id (get-in state [:current-page-id]) + page-id (get state :current-page-id) pparams {:file-id file-id :project-id project-id} qparams {:page-id page-id :layout (name layout)}] (rx/of (rt/nav :workspace pparams qparams)))))) @@ -1231,7 +1211,7 @@ (def go-to-file (ptk/reify ::go-to-file ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [{:keys [id project-id data] :as file} (:workspace-file state) page-id (get-in data [:pages 0]) pparams {:project-id project-id :file-id id} @@ -1243,19 +1223,19 @@ ([{:keys [file-id page-id]}] (ptk/reify ::go-to-viewer ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [{:keys [current-file-id current-page-id]} state params {:file-id (or file-id current-file-id) :page-id (or page-id current-page-id)}] (rx/of ::dwp/force-persist - (rt/nav :viewer params {:index 0}))))))) + (rt/nav-new-window :viewer params {:index 0}))))))) (defn go-to-dashboard ([] (go-to-dashboard nil)) ([{:keys [team-id]}] (ptk/reify ::go-to-dashboard ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (when-let [team-id (or team-id (:current-team-id state))] (rx/of ::dwp/force-persist (rt/nav :dashboard-projects {:team-id team-id}))))))) @@ -1264,7 +1244,7 @@ [] (ptk/reify ::go-to-dashboard ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [team-id (:current-team-id state)] (rx/of ::dwp/force-persist (rt/nav :dashboard-fonts {:team-id team-id})))))) @@ -1294,7 +1274,7 @@ (us/verify ::cp/minimal-shape shape) (ptk/reify ::show-shape-context-menu ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/concat (when-not (selected (:id shape)) @@ -1386,7 +1366,7 @@ (ptk/reify ::copy-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [objects (wsh/lookup-page-objects state) selected (->> (wsh/lookup-selected state) (cp/clean-loops objects)) @@ -1400,7 +1380,7 @@ (rx/merge-map (partial prepare-object objects selected)) (rx/reduce collect-data initial) (rx/mapcat (partial sort-selected state)) - (rx/map t/encode) + (rx/map t/encode-str) (rx/map wapi/write-to-clipboard) (rx/catch on-copy-error) (rx/ignore))))))) @@ -1413,14 +1393,14 @@ (def paste (ptk/reify ::paste ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (try (let [clipboard-str (wapi/read-from-clipboard) paste-transit-str (->> clipboard-str (rx/filter t/transit?) - (rx/map t/decode) + (rx/map t/decode-str) (rx/filter #(= :copied-shapes (:type %))) (rx/map #(select-keys % [:selected :objects])) (rx/map paste-shape)) @@ -1452,14 +1432,14 @@ [event in-viewport?] (ptk/reify ::paste-from-event ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (try (let [objects (wsh/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-str text-data)) edit-id (get-in state [:workspace-local :edition]) is-editing-text? (and edit-id (= :text (get-in objects [edit-id :type])))] @@ -1490,8 +1470,8 @@ (defn selected-frame? [state] (let [selected (wsh/lookup-selected state) objects (wsh/lookup-page-objects state)] - (and (and (= 1 (count selected)) - (= :frame (get-in objects [(first selected) :type])))))) + (and (= 1 (count selected)) + (= :frame (get-in objects [(first selected) :type]))))) (defn- paste-shape [{:keys [selected objects images] :as data} in-viewport?] @@ -1569,10 +1549,18 @@ (assoc change :index (get map-ids (:old-id change))) change))) + ;; Check if the shape is an instance whose master is defined in a + ;; library that is not linked to the current file + (foreign-instance? [shape objects state] + (let [root (cph/get-root-shape shape objects) + root-file-id (:component-file root)] + (and (some? root) + (not= root-file-id (:current-file-id state)) + (nil? (get-in state [:workspace-libraries root-file-id]))))) + ;; Procceed with the standard shape paste procediment. (do-paste [it state mouse-pos media] (let [media-idx (d/index-by :prev-id media) - page-id (:current-page-id state) ;; Calculate position for the pasted elements [frame-id parent-id delta index] (calculate-paste-position state mouse-pos in-viewport?) @@ -1584,8 +1572,8 @@ (assoc :parent-id parent-id) (cond-> - ;; Pasting from another file, we deattach components - (not= (:current-file-id state) (:file-id data)) + ;; if foreign instance, detach the shape + (foreign-instance? shape objects state) (dissoc :component-id :component-file :component-root? @@ -1622,7 +1610,7 @@ (dwc/select-shapes selected))))] (ptk/reify ::paste-shape ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [file-id (:current-file-id state) mouse-pos (deref ms/mouse-position)] (if (= file-id (:file-id data)) @@ -1646,7 +1634,7 @@ (s/assert string? text) (ptk/reify ::paste-text ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [id (uuid/next) {:keys [x y]} @ms/mouse-position width (max 8 (min (* 7 (count text)) 700)) @@ -1675,7 +1663,7 @@ (s/assert string? text) (ptk/reify ::paste-svg ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [position (deref ms/mouse-position) file-id (:current-file-id state)] (->> (dwp/parse-svg ["svg" text]) @@ -1685,7 +1673,7 @@ [image] (ptk/reify ::paste-bin-impl ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [file-id (get-in state [:workspace-file :id]) params {:file-id file-id :blobs [image] @@ -1710,7 +1698,7 @@ [] (ptk/reify ::start-create-interaction ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) stopper (rx/filter ms/mouse-up? stream)] @@ -1747,7 +1735,7 @@ (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [position @ms/mouse-position page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -1775,7 +1763,7 @@ [color] (ptk/reify ::change-canvas-color ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (get state :current-page-id) options (wsh/lookup-page-options state page-id) previus-color (:background options)] @@ -1790,25 +1778,21 @@ :value previus-color}] :origin it})))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Exports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Transform -(d/export dwt/start-rotate) (d/export dwt/start-resize) +(d/export dwt/update-dimensions) +(d/export dwt/start-rotate) +(d/export dwt/increase-rotation) (d/export dwt/start-move-selected) (d/export dwt/move-selected) -(d/export dwt/set-rotation) -(d/export dwt/increase-rotation) -(d/export dwt/set-modifiers) -(d/export dwt/apply-modifiers) -(d/export dwt/update-dimensions) +(d/export dwt/update-position) (d/export dwt/flip-horizontal-selected) (d/export dwt/flip-vertical-selected) -(d/export dwt/selected-to-path) ;; Persistence @@ -1830,7 +1814,7 @@ (d/export dwc/select-shapes) (d/export dws/shift-select-shapes) (d/export dws/duplicate-selected) -(d/export dws/handle-selection) +(d/export dws/handle-area-selection) (d/export dws/select-inside-group) (d/export dwd/select-for-drawing) (d/export dwc/clear-edition-mode) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index b3ffc2d76d..4ca6c4f0b0 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -10,14 +10,13 @@ [app.common.pages :as cp] [app.common.pages.spec :as spec] [app.common.spec :as us] - [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.state-helpers :as wsh] - [app.main.worker :as uw] + [app.main.data.workspace.undo :as dwu] [app.main.store :as st] + [app.main.worker :as uw] [app.util.logging :as log] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [potok.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module @@ -35,45 +34,45 @@ (defn- generate-operation "Given an object old and new versions and an attribute will append into changes the set and undo operations" - [changes attr old new] + [changes attr old new ignore-geometry?] (let [old-val (get old attr) new-val (get new attr)] (if (= old-val new-val) changes (-> changes - (update :rops conj {:type :set :attr attr :val new-val}) + (update :rops conj {:type :set :attr attr :val new-val :ignore-geometry ignore-geometry?}) (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true}))))) (defn- update-shape-changes "Calculate the changes and undos to be done when a function is applied to a single object" - [changes page-id objects update-fn attrs id] + [changes page-id objects update-fn attrs id ignore-geometry?] (let [old-obj (get objects id) new-obj (update-fn old-obj) attrs (or attrs (d/concat #{} (keys old-obj) (keys new-obj))) {rops :rops uops :uops} - (reduce #(generate-operation %1 %2 old-obj new-obj) + (reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?) {:rops [] :uops []} attrs) uops (cond-> uops - (not (empty? uops)) + (seq uops) (conj {:type :set-touched :touched (:touched old-obj)})) change {:type :mod-obj :page-id page-id :id id}] (cond-> changes - (not (empty? rops)) + (seq rops) (update :redo-changes conj (assoc change :operations rops)) - (not (empty? uops)) + (seq uops) (update :undo-changes conj (assoc change :operations uops))))) (defn update-shapes ([ids f] (update-shapes ids f nil)) - ([ids f {:keys [reg-objects? save-undo? keys] + ([ids f {:keys [reg-objects? save-undo? attrs ignore-tree] :or {reg-objects? false save-undo? true attrs nil}}] (us/assert ::coll-of-uuid ids) @@ -81,7 +80,7 @@ (ptk/reify ::update-shapes ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) changes {:redo-changes [] @@ -91,7 +90,9 @@ ids (into [] (filter some?) ids) - changes (reduce #(update-shape-changes %1 page-id objects f keys %2) changes ids)] + changes (reduce + #(update-shape-changes %1 page-id objects f attrs %2 (get ignore-tree %2)) + changes ids)] (when-not (empty? (:redo-changes changes)) (let [reg-objs {:type :reg-objects @@ -107,7 +108,7 @@ [page-id changes] (ptk/reify ::update-indices ptk/EffectEvent - (effect [_ state stream] + (effect [_ _ _] (uw/ask! {:cmd :update-page-indices :page-id page-id :changes changes})))) @@ -147,7 +148,7 @@ state)))) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (when-not @error (let [;; adds page-id to page changes (that have the `id` field instead) add-page-id @@ -163,7 +164,7 @@ (group-by :page-id)) process-page-changes - (fn [[page-id changes]] + (fn [[page-id _changes]] (update-indices page-id redo-changes))] (rx/concat (rx/from (map process-page-changes changes-by-pages)) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 2e40b5b98b..1813fa7120 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -7,23 +7,13 @@ (ns app.main.data.workspace.colors (:require [app.common.data :as d] - [app.common.pages :as cp] - [app.common.spec :as us] - [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] [app.main.repo :as rp] - [app.main.store :as st] - [app.main.streams :as ms] - [app.util.color :as color] - [app.util.i18n :refer [tr]] - [app.util.router :as rt] - [app.util.time :as dt] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [potok.core :as ptk])) (def clear-color-for-rename @@ -38,17 +28,16 @@ [file-id color-id name] (ptk/reify ::rename-color ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (->> (rp/mutation! :rename-color {:id color-id :name name}) (rx/map (partial rename-color-result file-id)))))) (defn rename-color-result - [file-id color] + [_file-id color] (ptk/reify ::rename-color-result ptk/UpdateEvent (update [_ state] - (-> state - (update-in [:workspace-file :colors] #(d/replace-by-id % color)))))) + (update-in state [:workspace-file :colors] #(d/replace-by-id % color))))) (defn change-palette-size [size] @@ -125,7 +114,7 @@ [ids color] (ptk/reify ::change-fill ptk/WatchEvent - (watch [_ state s] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -157,32 +146,29 @@ [ids color] (ptk/reify ::change-stroke ptk/WatchEvent - (watch [_ state s] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (watch [_ _ _] + (let [attrs (cond-> {} + (contains? color :color) + (assoc :stroke-color (:color color)) - attrs (cond-> {} - (contains? color :color) - (assoc :stroke-color (:color color)) + (contains? color :id) + (assoc :stroke-color-ref-id (:id color)) - (contains? color :id) - (assoc :stroke-color-ref-id (:id color)) + (contains? color :file-id) + (assoc :stroke-color-ref-file (:file-id color)) - (contains? color :file-id) - (assoc :stroke-color-ref-file (:file-id color)) + (contains? color :gradient) + (assoc :stroke-color-gradient (:gradient color)) - (contains? color :gradient) - (assoc :stroke-color-gradient (:gradient color)) + (contains? color :opacity) + (assoc :stroke-opacity (:opacity color)))] - (contains? color :opacity) - (assoc :stroke-opacity (:opacity color)))] - - (rx/of (dch/update-shapes ids (fn [shape] - (cond-> (d/merge shape attrs) - (= (:stroke-style shape) :none) - (assoc :stroke-style :solid - :stroke-width 1 - :stroke-opacity 1))))))))) + (rx/of (dch/update-shapes ids (fn [shape] + (cond-> (d/merge shape attrs) + (= (:stroke-style shape) :none) + (assoc :stroke-style :solid + :stroke-width 1 + :stroke-opacity 1))))))))) (defn picker-for-selected-shape diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index 08f70ed2fa..d83b51455f 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -6,19 +6,14 @@ (ns app.main.data.workspace.comments (:require - [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.math :as mth] [app.common.spec :as us] - [app.main.constants :as c] + [app.main.data.comments :as dcm] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] - [app.main.data.comments :as dcm] - [app.main.store :as st] [app.main.streams :as ms] [app.util.router :as rt] [beicon.core :as rx] - [cljs.spec.alpha :as s] [potok.core :as ptk])) (declare handle-interrupt) @@ -29,7 +24,7 @@ (us/assert ::us/uuid file-id) (ptk/reify ::initialize-comments ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stoper (rx/filter #(= ::finalize %) stream)] (rx/merge (rx/of (dcm/retrieve-comment-threads file-id)) @@ -47,8 +42,8 @@ [] (ptk/reify ::handle-interrupt ptk/WatchEvent - (watch [_ state stream] - (let [local (:comments-local state)] + (watch [_ state _] + (let [local (:comments-local state)] (cond (:draft local) (rx/of (dcm/close-thread)) (:open local) (rx/of (dcm/close-thread)) @@ -62,7 +57,7 @@ [position] (ptk/reify ::handle-comment-layer-click ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [local (:comments-local state)] (if (some? (:open local)) (rx/of (dcm/close-thread)) @@ -74,14 +69,13 @@ (rx/of (dcm/create-draft params)))))))) (defn center-to-comment-thread - [{:keys [id position] :as thread}] + [{:keys [position] :as thread}] (us/assert ::dcm/comment-thread thread) (ptk/reify :center-to-comment-thread ptk/UpdateEvent (update [_ state] (update state :workspace-local - (fn [{:keys [vbox vport zoom] :as local}] - (prn "center-to-comment-thread" vbox) + (fn [{:keys [vbox zoom] :as local}] (let [pw (/ 50 zoom) ph (/ 200 zoom) nw (mth/round (- (/ (:width vbox) 2) pw)) @@ -93,11 +87,11 @@ (update local :vbox assoc :x nx :y ny))))))) (defn navigate - [{:keys [project-id file-id page-id] :as thread}] + [thread] (us/assert ::dcm/comment-thread thread) (ptk/reify ::navigate ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [pparams {:project-id (:project-id thread) :file-id (:file-id thread)} qparams {:page-id (:page-id thread)}] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 3bffa6bdb9..75e9ce724e 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -40,15 +40,13 @@ [{:keys [file] :as bundle}] (ptk/reify ::setup-selection-index ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [msg {:cmd :initialize-indices :file-id (:id file) :data (:data file)}] (->> (uw/ask! msg) (rx/map (constantly ::index-initialized))))))) - - ;; --- Common Helpers & Events (defn get-frame-at-point @@ -59,7 +57,7 @@ (defn- extract-numeric-suffix [basename] - (if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] + (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] [p1 (+ 1 (d/parse-integer p2))] [basename 1])) @@ -112,7 +110,7 @@ (def undo (ptk/reify ::undo ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] ;; Editors handle their own undo's @@ -131,7 +129,7 @@ (def redo (ptk/reify ::redo ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [edition (get-in state [:workspace-local :edition]) drawing (get state :workspace-drawing)] (when-not (or (some? edition) (not-empty drawing)) @@ -180,10 +178,10 @@ (assoc-in state [:workspace-local :selected] ids)) ptk/WatchEvent - (watch [it state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id)] - (rx/of (expand-all-parents ids objects)))))) + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id)] + (rx/of (expand-all-parents ids objects)))))) (declare clear-edition-mode) @@ -202,12 +200,11 @@ state))) ptk/WatchEvent - (watch [it state stream] - (let [objects (wsh/lookup-page-objects state)] - (->> stream - (rx/filter interrupt?) - (rx/take 1) - (rx/map (constantly clear-edition-mode))))))) + (watch [_ _ stream] + (->> stream + (rx/filter interrupt?) + (rx/take 1) + (rx/map (constantly clear-edition-mode)))))) ;; If these event change modules review /src/app/main/data/workspace/path/undo.cljs (def clear-edition-mode @@ -282,7 +279,7 @@ (us/verify ::shape-attrs attrs) (ptk/reify ::add-shape ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -313,7 +310,7 @@ (defn move-shapes-into-frame [frame-id shapes] (ptk/reify ::move-shapes-into-frame ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) to-move-shapes (->> (cp/select-toplevel-shapes objects {:include-frames? false}) @@ -341,26 +338,18 @@ :undo-changes uchanges :origin it})))))) +(s/def ::set-of-uuid + (s/every ::us/uuid :kind set?)) + (defn delete-shapes [ids] - (us/assert (s/coll-of ::us/uuid) ids) + (us/assert ::set-of-uuid ids) (ptk/reify ::delete-shapes ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - get-empty-parents - (fn [parents] - (->> parents - (map (fn [id] - (let [obj (get objects id)] - (when (and (= :group (:type obj)) - (= 1 (count (:shapes obj)))) - obj)))) - (take-while (complement nil?)) - (map :id))) - groups-to-unmask (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, @@ -381,90 +370,117 @@ (some ids (map :destination interactions)))) (vals objects)) - rchanges - (d/concat - (reduce (fn [res id] - (let [children (cp/get-children id objects) - parents (cp/get-parents id objects) - del-change #(array-map - :type :del-obj - :page-id page-id - :id %)] - (d/concat res - (map del-change (reverse children)) - [(del-change id)] - (map del-change (get-empty-parents parents)) - [{:type :reg-objects - :page-id page-id - :shapes (vec parents)}]))) - [] - ids) - (map #(array-map - :type :mod-obj - :page-id page-id - :id % - :operations [{:type :set - :attr :masked-group? - :val false}]) - groups-to-unmask) - (map #(array-map - :type :mod-obj - :page-id page-id - :id (:id %) - :operations [{:type :set - :attr :interactions - :val (vec (remove (fn [interaction] - (contains? ids (:destination interaction))) - (:interactions %)))}]) - interacting-shapes)) + empty-parents-xform + (comp + (map (fn [id] (get objects id))) + (map (fn [{:keys [shapes type] :as obj}] + (when (and (= :group type) + (zero? (count (remove #(contains? ids %) shapes)))) + obj))) + (take-while some?) + (map :id)) + all-parents + (reduce (fn [res id] + (into res (cp/get-parents id objects))) + (d/ordered-set) + ids) + + all-children + (reduce (fn [res id] + (into res (cp/get-children id objects))) + (d/ordered-set) + ids) + + empty-parents + (into (d/ordered-set) empty-parents-xform all-parents) + + mk-del-obj-xf + (map (fn [id] + {:type :del-obj + :page-id page-id + :id id})) + + mk-add-obj-xf + (map (fn [id] + (let [item (get objects id)] + {:type :add-obj + :id (:id item) + :page-id page-id + :index (cp/position-on-parent id objects) + :frame-id (:frame-id item) + :parent-id (:parent-id item) + :obj item}))) + + mk-mod-touched-xf + (map (fn [id] + (let [parent (get objects id)] + {:type :mod-obj + :page-id page-id + :id (:id parent) + :operations [{:type :set-touched + :touched (:touched parent)}]}))) + + mk-mod-int-del-xf + (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :interactions + :val (vec (remove (fn [interaction] + (contains? ids (:destination interaction))) + (:interactions obj)))}]})) + mk-mod-int-add-xf + (map (fn [obj] + {:type :mod-obj + :page-id page-id + :id (:id obj) + :operations [{:type :set + :attr :interactions + :val (:interactions obj)}]})) + + mk-mod-unmask-xf + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :masked-group? + :val false}]})) + + mk-mod-mask-xf + (map (fn [id] + {:type :mod-obj + :page-id page-id + :id id + :operations [{:type :set + :attr :masked-group? + :val true}]})) + + rchanges + (-> [] + (into mk-del-obj-xf all-children) + (into mk-del-obj-xf ids) + (into mk-del-obj-xf empty-parents) + (conj {:type :reg-objects + :page-id page-id + :shapes (vec all-parents)}) + (into mk-mod-unmask-xf groups-to-unmask) + (into mk-mod-int-del-xf interacting-shapes)) uchanges - (d/concat - (reduce (fn [res id] - (let [children (cp/get-children id objects) - parents (cp/get-parents id objects) - parent (get objects (first parents)) - add-change (fn [id] - (let [item (get objects id)] - {:type :add-obj - :id (:id item) - :page-id page-id - :index (cp/position-on-parent id objects) - :frame-id (:frame-id item) - :parent-id (:parent-id item) - :obj item}))] - (d/concat res - (map add-change (reverse (get-empty-parents parents))) - [(add-change id)] - (map add-change children) - [{:type :reg-objects - :page-id page-id - :shapes (vec parents)}] - (when (some? parent) - [{:type :mod-obj - :page-id page-id - :id (:id parent) - :operations [{:type :set-touched - :touched (:touched parent)}]}])))) - [] - ids) - (map #(array-map - :type :mod-obj - :page-id page-id - :id % - :operations [{:type :set - :attr :masked-group? - :val true}]) - groups-to-unmask) - (map #(array-map - :type :mod-obj - :page-id page-id - :id (:id %) - :operations [{:type :set - :attr :interactions - :val (:interactions %)}]) - interacting-shapes))] + (-> [] + (into mk-add-obj-xf (reverse empty-parents)) + (into mk-add-obj-xf (reverse ids)) + (into mk-add-obj-xf (reverse all-children)) + (conj {:type :reg-objects + :page-id page-id + :shapes (vec all-parents)}) + (into mk-mod-touched-xf (reverse all-parents)) + (into mk-mod-mask-xf groups-to-unmask) + (into mk-mod-int-add-xf interacting-shapes)) + ] ;; (println "================ rchanges") ;; (cljs.pprint/pprint rchanges) @@ -485,7 +501,7 @@ [type frame-x frame-y data] (ptk/reify ::create-and-add-shape ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [{:keys [width height]} data [vbc-x vbc-y] (viewport-center state) @@ -505,7 +521,7 @@ [image {:keys [x y]}] (ptk/reify ::image-uploaded ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [{:keys [name width height id mtype]} image shape {:name name :width width diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index 503d6bee97..14ec47a2ac 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -7,17 +7,15 @@ (ns app.main.data.workspace.drawing "Drawing interactions." (:require - [beicon.core :as rx] - [potok.core :as ptk] - [app.common.spec :as us] [app.common.pages :as cp] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.path :as path] + [app.main.data.workspace.drawing.box :as box] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.drawing.curve :as curve] - [app.main.data.workspace.drawing.box :as box])) + [app.main.data.workspace.path :as path] + [beicon.core :as rx] + [potok.core :as ptk])) (declare start-drawing) (declare handle-drawing) @@ -38,7 +36,7 @@ (update :workspace-layout disj :scale-text))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stoper (rx/filter (ptk/type? ::clear-drawing) stream)] (rx/merge (when (= tool :path) @@ -88,16 +86,17 @@ (update-in state [:workspace-drawing :object] merge data))) ptk/WatchEvent - (watch [_ state stream] - (rx/of (case type - :path - (path/handle-new-shape) + (watch [_ _ _] + (rx/of + (case type + :path + (path/handle-new-shape) - :curve - (curve/handle-drawing-curve) + :curve + (curve/handle-drawing-curve) - ;; default - (box/handle-drawing-box)))))) + ;; default + (box/handle-drawing-box)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index d9e3879a7b..01632a744c 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -21,7 +21,7 @@ (defn truncate-zero [num default] (if (mth/almost-zero? num) default num)) -(defn resize-shape [{:keys [x y width height transform transform-inverse] :as shape} point lock?] +(defn resize-shape [{:keys [x y width height] :as shape} point lock?] (let [;; The new shape behaves like a resize on the bottom-right corner initial (gpt/point (+ x width) (+ y height)) shapev (gpt/point width height) @@ -53,9 +53,7 @@ (ptk/reify ::handle-drawing-box ptk/WatchEvent (watch [_ state stream] - (let [{:keys [flags]} (:workspace-local state) - - stoper? #(or (ms/mouse-up? %) (= % :interrupt)) + (let [stoper? #(or (ms/mouse-up? %) (= % :interrupt)) stoper (rx/filter stoper? stream) initial @ms/mouse-position diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 92db099600..fb93c6f5cc 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -6,15 +6,13 @@ (ns app.main.data.workspace.drawing.common (:require - [beicon.core :as rx] - [potok.core :as ptk] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.selection :as dws] [app.main.data.workspace.undo :as dwu] - [app.main.streams :as ms] - [app.main.worker :as uw])) + [app.main.worker :as uw] + [beicon.core :as rx] + [potok.core :as ptk])) (def clear-drawing (ptk/reify ::clear-drawing @@ -25,7 +23,7 @@ (def handle-finish-drawing (ptk/reify ::handle-finish-drawing ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [shape (get-in state [:workspace-drawing :object])] (rx/concat (when (:initialized? shape) diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index 333404214f..463c1e9ead 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -6,7 +6,6 @@ (ns app.main.data.workspace.drawing.curve (:require - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gsp] [app.common.pages :as cp] @@ -19,7 +18,7 @@ (def simplify-tolerance 0.3) -(defn stoper-event? [{:keys [type shift] :as event}] +(defn stoper-event? [{:keys [type] :as event}] (ms/mouse-event? event) (= type :up)) (defn initialize-drawing [state] @@ -73,9 +72,8 @@ (defn handle-drawing-curve [] (ptk/reify ::handle-drawing-curve ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [flags]} (:workspace-local state) - stoper (rx/filter stoper-event? stream) + (watch [_ _ stream] + (let [stoper (rx/filter stoper-event? stream) mouse (rx/sample 10 ms/mouse-position)] (rx/concat (rx/of initialize-drawing) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index e405c23646..b04b99f6aa 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -6,11 +6,11 @@ (ns app.main.data.workspace.grid (:require - [beicon.core :as rx] - [potok.core :as ptk] [app.common.data :as d] [app.common.spec :as us] - [app.main.data.workspace.changes :as dch])) + [app.main.data.workspace.changes :as dch] + [beicon.core :as rx] + [potok.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Grid @@ -40,7 +40,7 @@ (us/assert ::us/uuid frame-id) (ptk/reify ::add-frame-grid ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [page-id (:current-page-id state) data (get-in state [:workspace-data :pages-index page-id]) params (or (get-in data [:options :saved-grids :square]) @@ -56,21 +56,21 @@ [frame-id index] (ptk/reify ::set-frame-grid ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) (defn set-frame-grid [frame-id index data] (ptk/reify ::set-frame-grid ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) (defn set-default-grid [type params] (ptk/reify ::set-default-grid ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [pid (:current-page-id state) prev-value (get-in state [:workspace-data :pages-index pid :options :saved-grids type])] (rx/of (dch/commit-changes diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 849fdde94a..fc88141b31 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -1,3 +1,9 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + (ns app.main.data.workspace.groups (:require [app.common.data :as d] @@ -5,7 +11,6 @@ [app.common.pages :as cp] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [beicon.core :as rx] [potok.core :as ptk])) @@ -22,7 +27,6 @@ [shapes prefix keep-name] (let [selrect (gsh/selection-rect shapes) frame-id (-> shapes first :frame-id) - parent-id (-> shapes first :parent-id) group-name (if (and keep-name (= (count shapes) 1) (= (:type (first shapes)) :group)) @@ -166,7 +170,7 @@ (def group-selected (ptk/reify ::group-selected ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -183,7 +187,7 @@ (def ungroup-selected (ptk/reify ::ungroup-selected ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -200,7 +204,7 @@ (def mask-group (ptk/reify ::mask-group ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) @@ -257,7 +261,7 @@ (def unmask-group (ptk/reify ::unmask-group ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state)] diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 57e654c430..b267fb3885 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -27,7 +27,7 @@ [beicon.core :as rx] [potok.core :as ptk])) -;; Change this to :info :debug or :trace to debug this module +;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) (defn- log-changes @@ -46,7 +46,7 @@ (:component-id change) :objects (:id change)]) - :default nil)) + :else nil)) prefix (if (:component-id change) "[C] " "[P] ") @@ -68,6 +68,13 @@ (update [_ state] (assoc-in state [:workspace-local :assets-files-open file-id box] open?)))) +(defn set-assets-group-open + [file-id box path open?] + (ptk/reify ::set-assets-group-open + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :assets-files-open file-id :groups box path] open?)))) + (defn default-color-name [color] (or (:color color) (case (get-in color [:gradient :type]) @@ -83,7 +90,7 @@ (us/assert ::cp/color color) (ptk/reify ::add-color ptk/WatchEvent - (watch [it state s] + (watch [it _ _] (let [rchg {:type :add-color :color color} uchg {:type :del-color @@ -97,7 +104,7 @@ (us/assert ::cp/recent-color color) (ptk/reify ::add-recent-color ptk/WatchEvent - (watch [it state s] + (watch [it _ _] (let [rchg {:type :add-recent-color :color color}] (rx/of (dch/commit-changes {:redo-changes [rchg] @@ -116,7 +123,7 @@ (us/assert ::us/uuid file-id) (ptk/reify ::update-color ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [[path name] (cp/parse-path-name (:name color)) color (assoc color :path path :name name) prev (get-in state [:workspace-data :colors id]) @@ -134,7 +141,7 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-color ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [prev (get-in state [:workspace-data :colors id]) rchg {:type :del-color :id id} @@ -149,7 +156,7 @@ (us/assert ::cp/media-object media) (ptk/reify ::add-media ptk/WatchEvent - (watch [it state stream] + (watch [it _ _] (let [obj (select-keys media [:id :name :width :height :mtype]) rchg {:type :add-media :object obj} @@ -165,7 +172,7 @@ (us/assert ::us/string new-name) (ptk/reify ::rename-media ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [object (get-in state [:workspace-data :media id]) [path name] (cp/parse-path-name new-name) @@ -188,7 +195,7 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-media ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [prev (get-in state [:workspace-data :media id]) rchg {:type :del-media :id id} @@ -205,7 +212,7 @@ (us/assert ::cp/typography typography) (ptk/reify ::add-typography ptk/WatchEvent - (watch [it state s] + (watch [it _ _] (let [rchg {:type :add-typography :typography typography} uchg {:type :del-typography @@ -223,7 +230,7 @@ (us/assert ::us/uuid file-id) (ptk/reify ::update-typography ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [[path name] (cp/parse-path-name (:name typography)) typography (assoc typography :path path :name name) prev (get-in state [:workspace-data :typographies (:id typography)]) @@ -241,7 +248,7 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-typography ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [prev (get-in state [:workspace-data :typographies id]) rchg {:type :del-typography :id id} @@ -255,7 +262,7 @@ "Add a new component to current file library, from the currently selected shapes." (ptk/reify ::add-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [file-id (:current-file-id state) page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -278,7 +285,7 @@ (us/assert ::us/string new-name) (ptk/reify ::rename-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [[path name] (cp/parse-path-name new-name) component (get-in state [:workspace-data :components id]) objects (get component :objects) @@ -308,7 +315,7 @@ [{:keys [id] :as params}] (ptk/reify ::duplicate-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [component (cp/get-component id (:current-file-id state) (dwlh/get-local-file state) @@ -317,7 +324,7 @@ unames (set (map :name all-components)) new-name (dwc/generate-unique-name unames (:name component)) - [new-shape new-shapes updated-shapes] + [new-shape new-shapes _updated-shapes] (dwlh/duplicate-component component) rchanges [{:type :add-component @@ -339,7 +346,7 @@ (us/assert ::us/uuid id) (ptk/reify ::delete-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [component (get-in state [:workspace-data :components id]) rchanges [{:type :del-component @@ -364,7 +371,7 @@ (us/assert ::us/point position) (ptk/reify ::instantiate-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [local-library (dwlh/get-local-file state) libraries (get state :workspace-libraries) component (cp/get-component component-id file-id local-library libraries) @@ -442,58 +449,14 @@ (us/assert ::us/uuid id) (ptk/reify ::detach-component ptk/WatchEvent - (watch [it state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shapes (cp/get-object-with-children id objects) + (watch [it state _] + (let [local-library (dwlh/get-local-file state) + container (cp/get-container (get state :current-page-id) + :page + local-library) - rchanges (mapv (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :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) - - uchanges (mapv (fn [obj] - {:type :mod-obj - :page-id page-id - :id (:id obj) - :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)] + [rchanges uchanges] + (dwlh/generate-detach-instance id container)] (rx/of (dch/commit-changes {:redo-changes rchanges :undo-changes uchanges @@ -504,13 +467,13 @@ (us/assert ::us/uuid file-id) (ptk/reify ::nav-to-component-file ptk/WatchEvent - (watch [it state stream] - (let [file (get-in state [:workspace-libraries file-id]) + (watch [_ state _] + (let [file (get-in state [:workspace-libraries file-id]) pparams {:project-id (:project-id file) :file-id (:id file)} qparams {:page-id (first (get-in file [:data :pages])) :layout :assets}] - (st/emit! (rt/nav-new-window :workspace pparams qparams)))))) + (rx/of (rt/nav-new-window :workspace pparams qparams)))))) (defn ext-library-changed [file-id modified-at revn changes] @@ -533,7 +496,7 @@ (us/assert ::us/uuid id) (ptk/reify ::reset-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (log/info :msg "RESET-COMPONENT of shape" :id (str id)) (let [local-library (dwlh/get-local-file state) libraries (dwlh/get-libraries state) @@ -567,7 +530,7 @@ (us/assert ::us/uuid id) (ptk/reify ::update-component ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (log/info :msg "UPDATE-COMPONENT of shape" :id (str id)) (let [page-id (get state :current-page-id) local-library (dwlh/get-local-file state) @@ -635,7 +598,7 @@ state)) ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (log/info :msg "SYNC-FILE" :file (dwlh/pretty-file file-id state) :library (dwlh/pretty-file library-id state)) @@ -695,7 +658,7 @@ (us/assert ::us/uuid library-id) (ptk/reify ::sync-file-2nd-stage ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (log/info :msg "SYNC-FILE (2nd stage)" :file (dwlh/pretty-file file-id state) :library (dwlh/pretty-file library-id state)) @@ -720,7 +683,7 @@ (assoc-in state [:workspace-file :ignore-sync-until] (dt/now))) ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (rp/mutation :ignore-sync {:file-id (get-in state [:workspace-file :id]) :date (dt/now)})))) @@ -730,7 +693,7 @@ (us/assert ::us/uuid file-id) (ptk/reify ::notify-sync-file ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) (vals (get state :workspace-libraries))) do-update #(do (apply st/emit! (map (fn [library] @@ -740,6 +703,7 @@ (st/emit! dm/hide)) do-dismiss #(do (st/emit! ignore-sync) (st/emit! dm/hide))] + (rx/of (dm/info-dialog (tr "workspace.updates.there-are-updates") :inline-actions diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 738491e7d8..788b2453a0 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -17,19 +17,19 @@ [cljs.spec.alpha :as s] [clojure.set :as set])) -;; Change this to :info :debug or :trace to debug this module +;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) (defonce empty-changes [[] []]) (defonce color-sync-attrs - [[:fill-color-ref-id :color :fill-color] - [:fill-color-ref-id :gradient :fill-color-gradient] - [:fill-color-ref-id :opacity :fill-opacity] + [[:fill-color-ref-id :fill-color-ref-file :color :fill-color] + [:fill-color-ref-id :fill-color-ref-file :gradient :fill-color-gradient] + [:fill-color-ref-id :fill-color-ref-file :opacity :fill-opacity] - [:stroke-color-ref-id :color :stroke-color] - [:stroke-color-ref-id :gradient :stroke-color-gradient] - [:stroke-color-ref-id :opacity :stroke-opacity]]) + [:stroke-color-ref-id :stroke-color-ref-file :color :stroke-color] + [:stroke-color-ref-id :stroke-color-ref-file :gradient :stroke-color-gradient] + [:stroke-color-ref-id :stroke-color-ref-file :opacity :stroke-opacity]]) (declare generate-sync-container) (declare generate-sync-shape) @@ -90,7 +90,7 @@ (assert (nil? (:shape-ref shape))) (let [;; Ensure that the component root is not an instance and ;; it's no longer tied to a frame. - update-new-shape (fn [new-shape original-shape] + update-new-shape (fn [new-shape _original-shape] (cond-> new-shape true (-> (assoc :frame-id nil) @@ -200,6 +200,63 @@ (get component :objects) identity))) +(defn generate-detach-instance + "Generate changes to remove the links between a shape and all its children + with a component." + [shape-id container] + (let [shapes (cp/get-object-with-children shape-id (:objects container)) + rchanges (mapv (fn [obj] + (make-change + container + {:type :mod-obj + :id (:id obj) + :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) + + uchanges (mapv (fn [obj] + (make-change + container + {:type :mod-obj + :id (:id obj) + :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)] + + [rchanges uchanges])) + ;; ---- General library synchronization functions ---- @@ -216,26 +273,20 @@ :file (pretty-file file-id state) :library (pretty-file library-id state)) - (let [file (get-file state file-id) - library (get-file state library-id) - library-items (get library asset-type)] - - (if (empty? library-items) - empty-changes - - (loop [pages (vals (get file :pages-index)) - rchanges [] - uchanges []] - (if-let [page (first pages)] - (let [[page-rchanges page-uchanges] - (generate-sync-container asset-type - library-id - state - (cp/make-container page :page))] - (recur (next pages) - (d/concat rchanges page-rchanges) - (d/concat uchanges page-uchanges))) - [rchanges uchanges]))))) + (let [file (get-file state file-id)] + (loop [pages (vals (get file :pages-index)) + rchanges [] + uchanges []] + (if-let [page (first pages)] + (let [[page-rchanges page-uchanges] + (generate-sync-container asset-type + library-id + state + (cp/make-container page :page))] + (recur (next pages) + (d/concat rchanges page-rchanges) + (d/concat uchanges page-uchanges))) + [rchanges uchanges])))) (defn generate-sync-library "Generate changes to synchronize all shapes in all components of the @@ -248,27 +299,21 @@ :file (pretty-file file-id state) :library (pretty-file library-id state)) - (let [file (get-file state file-id) - library (get-file state library-id) - library-items (get library asset-type)] - - (if (empty? library-items) - empty-changes - - (loop [local-components (vals (get file :components)) - rchanges [] - uchanges []] - (if-let [local-component (first local-components)] - (let [[comp-rchanges comp-uchanges] - (generate-sync-container asset-type - library-id - state - (cp/make-container local-component - :component))] - (recur (next local-components) - (d/concat rchanges comp-rchanges) - (d/concat uchanges comp-uchanges))) - [rchanges uchanges]))))) + (let [file (get-file state file-id)] + (loop [local-components (vals (get file :components)) + rchanges [] + uchanges []] + (if-let [local-component (first local-components)] + (let [[comp-rchanges comp-uchanges] + (generate-sync-container asset-type + library-id + state + (cp/make-container local-component + :component))] + (recur (next local-components) + (d/concat rchanges comp-rchanges) + (d/concat uchanges comp-uchanges))) + [rchanges uchanges])))) (defn- generate-sync-container "Generate changes to synchronize all shapes in a particular container (a page @@ -323,7 +368,7 @@ attr-ref-file (keyword (str attr "-ref-file"))] (and (get shape attr-ref-id) (= library-id (get shape attr-ref-file)))) - (map #(nth % 2) color-sync-attrs)))) + (map #(nth % 3) color-sync-attrs)))) :typographies (fn [shape] @@ -338,10 +383,10 @@ (defmulti generate-sync-shape "Generate changes to synchronize one shape with all assets of the given type that is using, in the given library." - (fn [type library-id state container shape] type)) + (fn [type _library-id _state _container _shape] type)) (defmethod generate-sync-shape :components - [_ library-id state container shape] + [_ _ state container shape] (generate-sync-shape-direct container (:id shape) (get-local-file state) @@ -385,12 +430,14 @@ :fill-color (:color color) :fill-opacity (:opacity color) :fill-color-gradient (:gradient color)) - node))] + (assoc node + :fill-color-ref-id nil + :fill-color-ref-file nil)))] (generate-sync-text-shape shape container update-node)) (loop [attrs (seq color-sync-attrs) roperations [] uoperations []] - (let [[attr-ref-id color-attr attr] (first attrs)] + (let [[attr-ref-id attr-ref-file color-attr attr] (first attrs)] (if (nil? attr) (if (empty? roperations) empty-changes @@ -410,17 +457,37 @@ roperations uoperations) (let [color (get colors (get shape attr-ref-id)) - roperation {:type :set - :attr attr - :val (color-attr color) - :ignore-touched true} - uoperation {:type :set - :attr attr - :val (get shape attr) - :ignore-touched true}] + roperations' (if color + [{:type :set + :attr attr + :val (color-attr color) + :ignore-touched true}] + ;; If the referenced color does no longer exist in the library, + ;; we must unlink the color in the shape + [{:type :set + :attr attr-ref-id + :val nil + :ignore-touched true} + {:type :set + :attr attr-ref-file + :val nil + :ignore-touched true}]) + uoperations' (if color + [{:type :set + :attr attr + :val (get shape attr) + :ignore-touched true}] + [{:type :set + :attr attr-ref-id + :val (get shape attr-ref-id) + :ignore-touched true} + {:type :set + :attr attr-ref-file + :val (get shape attr-ref-file) + :ignore-touched true}])] (recur (next attrs) - (conj roperations roperation) - (conj uoperations uoperation)))))))))) + (concat roperations roperations') + (concat uoperations uoperations')))))))))) (defmethod generate-sync-shape :typographies [_ library-id state container shape] @@ -432,7 +499,8 @@ update-node (fn [node] (if-let [typography (get typographies (:typography-ref-id node))] (merge node (d/without-keys typography [:name :id])) - node))] + (dissoc node :typography-ref-id + :typography-ref-file)))] (generate-sync-text-shape shape container update-node))) (defn- get-assets @@ -568,7 +636,9 @@ root-main reset? initial-root?) - empty-changes))) + ; If the component is not found, because the master component has been + ; deleted or the library unlinked, detach the instance. + (generate-detach-instance shape-id container)))) (defn- generate-sync-shape-direct-recursive [container shape-inst component shape-main root-inst root-main reset? initial-root?] @@ -631,20 +701,14 @@ set-remote-synced?))) both (fn [child-inst child-main] - (let [sub-root? (and (:component-id shape-inst) - (not (:component-root? shape-inst)))] - (generate-sync-shape-direct-recursive container - child-inst - component - child-main - (if sub-root? - shape-inst - root-inst) - (if sub-root? - shape-main - root-main) - reset? - initial-root?))) + (generate-sync-shape-direct-recursive container + child-inst + component + child-main + root-inst + root-main + reset? + initial-root?)) moved (fn [child-inst child-main] (move-shape @@ -666,7 +730,7 @@ [(d/concat rchanges child-rchanges) (d/concat uchanges child-uchanges)])) -(defn- generate-sync-shape-inverse +(defn generate-sync-shape-inverse "Generate changes to update the component a shape is linked to, from the values in the shape and all its children." [page-id shape-id local-library libraries] @@ -752,20 +816,13 @@ false)) both (fn [child-inst child-main] - (let [sub-root? (and (:component-id shape-inst) - (not (:component-root? shape-inst)))] - - (generate-sync-shape-inverse-recursive container - child-inst - component - child-main - (if sub-root? - shape-inst - root-inst) - (if sub-root? - shape-main - root-main) - initial-root?))) + (generate-sync-shape-inverse-recursive container + child-inst + component + child-main + root-inst + root-main + initial-root?)) moved (fn [child-inst child-main] (move-shape @@ -886,10 +943,10 @@ set-remote-synced? (assoc :remote-synced? true)))) - update-original-shape (fn [original-shape new-shape] + update-original-shape (fn [original-shape _new-shape] original-shape) - [new-shape new-shapes _] + [_ new-shapes _] (cp/clone-object component-shape (:id parent-shape) (get component :objects) @@ -939,7 +996,7 @@ (cp/get-parents (:id component-parent-shape) (:objects component)))) - update-new-shape (fn [new-shape original-shape] + update-new-shape (fn [new-shape _original-shape] (reposition-shape new-shape root-instance root-main)) @@ -950,7 +1007,7 @@ :shape-ref (:id new-shape)) original-shape)) - [new-shape new-shapes updated-shapes] + [_new-shape new-shapes updated-shapes] (cp/clone-object shape (:id component-parent-shape) (get page :objects) @@ -1141,33 +1198,6 @@ :remote-synced? (:remote-synced? shape)}]})]] [rchanges uchanges])))) -(defn- set-touched-shapes-group - [shape container] - (if-not (:shape-ref shape) - empty-changes - (do - (log/info :msg (str "SET-TOUCHED-SHAPES-GROUP " - (if (cp/page? container) "[P] " "[C] ") - (:name shape))) - (let [rchanges [(make-change - container - {:type :mod-obj - :id (:id shape) - :operations - [{:type :set-touched - :touched (cp/set-touched-group - (:touched shape) - :shapes-group)}]})] - - uchanges [(make-change - container - {:type :mod-obj - :id (:id shape) - :operations - [{:type :set-touched - :touched (:touched shape)}]})]] - [rchanges uchanges])))) - (defn- update-attrs "The main function that implements the attribute sync algorithm. Copy attributes that have changed in the origin shape to the dest shape. @@ -1187,6 +1217,8 @@ ; sync only the position relative to the origin of the component. ; We solve this by moving the origin shape so it is aligned with ; the dest root before syncing. + ; In case of subinstances, the comparison is always done with the + ; near component, because this is that we are syncing with. origin-shape (reposition-shape origin-shape origin-root dest-root) touched (get dest-shape :touched #{})] diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 95afceb6d1..93dcf92291 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -10,18 +10,14 @@ [app.common.geom.point :as gpt] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.transit :as t] [app.common.uri :as u] [app.config :as cf] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.persistence :as dwp] - [app.main.repo :as rp] - [app.main.store :as st] [app.main.streams :as ms] - [app.util.avatars :as avatars] - [app.util.i18n :as i18n :refer [tr]] [app.util.time :as dt] - [app.util.transit :as t] [app.util.websockets :as ws] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -74,7 +70,7 @@ ;; Process all incoming messages. (->> (ws/-stream wsession) (rx/filter ws/message?) - (rx/map (comp t/decode :payload)) + (rx/map (comp t/decode-str :payload)) (rx/filter #(s/valid? ::message %)) (rx/map process-message)) @@ -104,7 +100,7 @@ [file-id] (ptk/reify ::send-keepalive ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (when-let [ws (get-in state [:ws file-id])] (ws/send! ws {:type :keepalive}))))) @@ -112,9 +108,8 @@ [file-id point] (ptk/reify ::handle-pointer-update ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (let [ws (get-in state [:ws file-id]) - sid (:session-id state) pid (:current-page-id state) msg {:type :pointer-update :page-id pid @@ -128,7 +123,7 @@ [file-id] (ptk/reify ::finalize ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (when-let [ws (get-in state [:ws file-id])] (ws/-close ws)) (rx/of ::finalize)))) @@ -187,7 +182,7 @@ (update state :workspace-presence update-presence)))))) (defn handle-pointer-update - [{:keys [page-id profile-id session-id x y] :as msg}] + [{:keys [page-id session-id x y] :as msg}] (ptk/reify ::handle-pointer-update ptk/UpdateEvent (update [_ state] @@ -213,7 +208,7 @@ (us/assert ::file-change-event msg) (ptk/reify ::handle-file-change ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (let [changes-by-pages (group-by :page-id changes) process-page-changes (fn [[page-id changes]] @@ -239,7 +234,7 @@ (us/assert ::library-change-event msg) (ptk/reify ::handle-library-change ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (when (contains? (:workspace-libraries state) file-id) (rx/of (dwl/ext-library-changed file-id modified-at revn changes) (dwl/notify-sync-file file-id)))))) diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index 57f2af43b4..ada2f22a82 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -28,7 +28,7 @@ (d/export edition/move-selected) ;; Selection -(d/export selection/handle-selection) +(d/export selection/handle-area-selection) (d/export selection/select-node) (d/export selection/path-handler-enter) (d/export selection/path-handler-leave) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index cddf0a849f..ba0700bf3f 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -76,20 +76,21 @@ (ptk/reify ::save-path-content ptk/UpdateEvent (update [_ state] - (let [content (get-in state (st/get-path state :content)) + (let [content (st/get-path state :content) content (if (and (not preserve-move-to) (= (-> content last :command) :move-to)) (into [] (take (dec (count content)) content)) content)] - (assoc-in state (st/get-path state :content) content))) + (-> state + (st/set-content content)))) ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [objects (wsh/lookup-page-objects state) page-id (:current-page-id state) id (get-in state [:workspace-local :edition]) old-content (get-in state [:workspace-local :edit-path id :old-content]) - shape (get-in state (st/get-path state))] + shape (st/get-path state)] (if (and (some? old-content) (some? (:id shape))) (let [[rch uch] (generate-path-changes objects page-id shape old-content (:content shape))] (rx/of (dch/commit-changes {:redo-changes rch diff --git a/frontend/src/app/main/data/workspace/path/common.cljs b/frontend/src/app/main/data/workspace/path/common.cljs index d8e6e19cbf..f9313126db 100644 --- a/frontend/src/app/main/data/workspace/path/common.cljs +++ b/frontend/src/app/main/data/workspace/path/common.cljs @@ -16,7 +16,8 @@ [state] (dissoc state :last-point :prev-handler :drag-handler :preview)) -(defn finish-path [source] +(defn finish-path + [_source] (ptk/reify ::finish-path ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 75c2f2202c..202266e798 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -9,6 +9,7 @@ [app.common.geom.point :as gpt] [app.common.pages :as cp] [app.common.spec :as us] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.path.changes :as changes] @@ -17,12 +18,12 @@ [app.main.data.workspace.path.spec :as spec] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] - [app.main.data.workspace.path.tools :as tools] [app.main.data.workspace.path.undo :as undo] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] [app.util.path.commands :as upc] [app.util.path.geom :as upg] + [app.util.path.shapes-to-path :as upsp] [beicon.core :as rx] [potok.core :as ptk])) @@ -37,7 +38,7 @@ last-point (get-in state [:workspace-local :edit-path id :last-point]) position (cond-> (gpt/point x y) fix-angle? (helpers/position-fixed-angle last-point)) - shape (get-in state (st/get-path state)) + shape (st/get-path state) {:keys [last-point prev-handler]} (get-in state [:workspace-local :edit-path id]) command (helpers/next-node shape position last-point prev-handler)] (assoc-in state [:workspace-local :edit-path id :preview] command))))) @@ -56,19 +57,18 @@ (assoc-in [:workspace-local :edit-path id :last-point] position) (update-in [:workspace-local :edit-path id] dissoc :prev-handler) (update-in [:workspace-local :edit-path id] dissoc :preview) - (update-in (st/get-path state) helpers/append-node position last-point prev-handler)) + (update-in (st/get-path-location state) helpers/append-node position last-point prev-handler)) state))))) (defn drag-handler - ([{:keys [x y alt? shift?] :as position}] + ([position] (drag-handler nil nil :c1 position)) - ([position index prefix {:keys [x y alt? shift?]}] (ptk/reify ::drag-handler ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) index (or index (count content)) prefix (or prefix :c1) @@ -98,19 +98,19 @@ (let [id (st/get-path-id state) modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) - content (-> (get-in state (st/get-path state :content)) + content (-> (st/get-path state :content) (upc/apply-content-modifiers modifiers)) handler (get-in state [:workspace-local :edit-path id :drag-handler])] (-> state - (assoc-in (st/get-path state :content) content) + (st/set-content content) (update-in [:workspace-local :edit-path id] dissoc :drag-handler) (update-in [:workspace-local :edit-path id] dissoc :content-modifiers) (assoc-in [:workspace-local :edit-path id :prev-handler] handler) - (update-in (st/get-path state) helpers/update-selrect)))) + (update-in (st/get-path-location state) helpers/update-selrect)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [id (st/get-path-id state) handler (get-in state [:workspace-local :edit-path id :prev-handler])] ;; Update the preview because can be outdated after the dragging @@ -124,14 +124,11 @@ ptk/WatchEvent (watch [_ state stream] (let [id (st/get-path-id state) - zoom (get-in state [:workspace-local :zoom]) - start-position @ms/mouse-position - stop-stream (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) points (upg/content->points content) @@ -166,11 +163,9 @@ (ptk/reify ::start-path-from-point ptk/WatchEvent (watch [_ state stream] - (let [start-point @ms/mouse-position - zoom (get-in state [:workspace-local :zoom]) - mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) + (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) points (upg/content->points content) id (st/get-path-id state) @@ -195,7 +190,7 @@ (rx/merge-map #(rx/empty)))) (defn make-drag-stream - [stream snap-toggled zoom points down-event] + [stream snap-toggled _zoom points down-event] (let [mouse-up (->> stream (rx/filter #(or (helpers/end-path-event? %) (ms/mouse-up? %)))) @@ -211,7 +206,7 @@ (rx/of (finish-drag))))))) (defn handle-drawing-path - [id] + [_id] (ptk/reify ::handle-drawing-path ptk/UpdateEvent (update [_ state] @@ -225,7 +220,7 @@ mouse-down (->> stream (rx/filter ms/mouse-down?)) end-path-events (->> stream (rx/filter helpers/end-path-event?)) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) points (upg/content->points content) id (st/get-path-id state) @@ -278,11 +273,11 @@ state))) ptk/WatchEvent - (watch [_ state stream] - (->> (rx/of (setup-frame-path) - dwdc/handle-finish-drawing - (dwc/start-edition-mode shape-id) - (change-edit-mode :draw)))))) + (watch [_ _ _] + (rx/of (setup-frame-path) + dwdc/handle-finish-drawing + (dwc/start-edition-mode shape-id) + (change-edit-mode :draw))))) (defn handle-new-shape "Creates a new path shape" @@ -323,6 +318,7 @@ edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])] (if (= :draw edit-mode) (rx/concat + (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (rx/of (handle-drawing-path id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) @@ -333,9 +329,9 @@ (defn check-changed-content [] (ptk/reify ::check-changed-content ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [id (st/get-path-id state) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) old-content (get-in state [:workspace-local :edit-path id :old-content]) mode (get-in state [:workspace-local :edit-path id :edit-mode])] @@ -354,7 +350,7 @@ id (assoc-in [:workspace-local :edit-path id :edit-mode] mode)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [id (st/get-path-id state)] (cond (and id (= :move mode)) (rx/of (common/finish-path "change-edit-mode")) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 4a93e57dd3..a7ae00efe9 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -8,11 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.math :as mth] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] - [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.drawing :as drawing] [app.main.data.workspace.path.helpers :as helpers] [app.main.data.workspace.path.selection :as selection] @@ -23,6 +21,7 @@ [app.main.streams :as ms] [app.util.path.commands :as upc] [app.util.path.geom :as upg] + [app.util.path.shapes-to-path :as upsp] [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] @@ -33,8 +32,7 @@ ptk/UpdateEvent (update [_ state] - (let [content (get-in state (st/get-path state :content)) - + (let [content (st/get-path state :content) modifiers (helpers/move-handler-modifiers content index prefix false match-opposite? dx dy) [cx cy] (if (= prefix :c1) [:c1x :c1y] [:c2x :c2y]) point (gpt/point (+ (get-in content [index :params cx]) dx) @@ -47,12 +45,12 @@ (defn apply-content-modifiers [] (ptk/reify ::apply-content-modifiers ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [objects (wsh/lookup-page-objects state) id (st/get-path-id state) page-id (:current-page-id state) - shape (get-in state (st/get-path state)) + shape (st/get-path state) content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers]) content (:content shape) @@ -102,7 +100,7 @@ ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) modifiers-reducer (partial modify-content-point content move-modifier) content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) content-modifiers (->> points @@ -116,7 +114,7 @@ ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) delta (gpt/subtract to-point from-point) modifiers-reducer (partial modify-content-point content delta) @@ -137,28 +135,29 @@ [position shift?] (ptk/reify ::start-move-path-point ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [id (get-in state [:workspace-local :edition]) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) selected? (contains? selected-points position)] (streams/drag-stream (rx/of - (when-not selected? (selection/select-node position shift? "drag")) + (dch/update-shapes [id] upsp/convert-to-path) + (when-not selected? (selection/select-node position shift?)) (drag-selected-points @ms/mouse-position)) - (rx/of (selection/select-node position shift? "click"))))))) + (rx/of (selection/select-node position shift?))))))) (defn drag-selected-points [start-position] (ptk/reify ::drag-selected-points ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [stopper (->> stream (rx/filter ms/mouse-up?)) id (get-in state [:workspace-local :edition]) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) points (upg/content->points content)] (rx/concat @@ -206,7 +205,7 @@ state))) ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [id (get-in state [:workspace-local :edition]) current-move (get-in state [:workspace-local :edit-path id :current-move])] (if (= same-event current-move) @@ -223,6 +222,7 @@ mov-vec (gpt/multiply (get-displacement direction) scale)] (rx/concat + (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (rx/merge (->> move-events (rx/take-until stopper) @@ -240,7 +240,7 @@ [index prefix] (ptk/reify ::start-move-handler ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [id (get-in state [:workspace-local :edition]) cx (d/prefix-keyword prefix :x) cy (d/prefix-keyword prefix :y) @@ -249,7 +249,7 @@ start-delta-x (get-in modifiers [index cx] 0) start-delta-y (get-in modifiers [index cy] 0) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) points (upg/content->points content) point (-> content (get (if (= prefix :c1) (dec index) index)) (upc/command->point)) @@ -262,6 +262,7 @@ (streams/drag-stream (rx/concat + (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (->> (streams/move-handler-stream snap-toggled start-point point handler opposite points) (rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %) (streams/finish-edition? %))))) @@ -286,7 +287,8 @@ ptk/UpdateEvent (update [_ state] (let [edit-path (get-in state [:workspace-local :edit-path id]) - state (update-in state (st/get-path state :content) ups/close-subpaths)] + content (st/get-path state :content) + state (st/set-content state (ups/close-subpaths content))] (cond-> state (or (not edit-path) (= :draw (:edit-mode edit-path))) (assoc-in [:workspace-local :edit-path id] {:edit-mode :move @@ -297,7 +299,7 @@ (assoc-in [:workspace-local :edit-path id :edit-mode] :draw)))) ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] (rx/concat (rx/of (undo/start-path-undo)) @@ -315,17 +317,26 @@ (let [id (get-in state [:workspace-local :edition])] (update state :workspace-local dissoc :edit-path id))))) -(defn create-node-at-position +(defn split-segments [{:keys [from-p to-p t]}] - (ptk/reify ::create-node-at-position + (ptk/reify ::split-segments ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) - old-content (get-in state (st/get-path state :content))] + content (st/get-path state :content)] (-> state - (assoc-in [:workspace-local :edit-path id :old-content] old-content) - (update-in (st/get-path state :content) upt/split-segments #{from-p to-p} t)))) + (assoc-in [:workspace-local :edit-path id :old-content] content) + (st/set-content (-> content (upt/split-segments #{from-p to-p} t)))))) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/of (changes/save-path-content {:preserve-move-to true}))))) + +(defn create-node-at-position + [event] + (ptk/reify ::create-node-at-position + ptk/WatchEvent + (watch [_ state _] + (let [id (st/get-path-id state)] + (rx/of (dch/update-shapes [id] upsp/convert-to-path) + (split-segments event)))))) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index cbf9d383ca..deff63dce1 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -6,25 +6,23 @@ (ns app.main.data.workspace.path.helpers (:require - [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.main.data.workspace.path.common :as common] - [app.main.data.workspace.path.state :refer [get-path]] [app.main.streams :as ms] [app.util.path.commands :as upc] [app.util.path.subpaths :as ups] [potok.core :as ptk])) -(defn end-path-event? [{:keys [type shift] :as event}] +(defn end-path-event? [event] (or (= (ptk/type event) ::common/finish-path) (= (ptk/type event) :esc-pressed) (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event)) (= :app.main.data.workspace/finalize-page (ptk/type event)) (= event :interrupt) ;; ESC - (and (ms/mouse-double-click? event)))) + (ms/mouse-double-click? event))) (defn content-center [content] @@ -35,7 +33,6 @@ (defn content->points+selrect "Given the content of a shape, calculate its points and selrect" [shape content] - (let [{:keys [flip-x flip-y]} shape transform (cond-> (:transform shape (gmt/matrix)) diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index dec4027ea3..3fed3f3651 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -48,7 +48,7 @@ (update [_ state] (let [selrect (get-in state [:workspace-local :selrect]) id (get-in state [:workspace-local :edition]) - content (get-in state (st/get-path state :content)) + content (st/get-path state :content) selected-point? #(gsh/has-point-rect? selrect %) selected-points (get-in state [:workspace-local :edit-path id :selected-points]) positions (into (if shift? selected-points #{}) @@ -60,7 +60,7 @@ (some? id) (assoc-in [:workspace-local :edit-path id :selected-points] positions)))))) -(defn select-node [position shift? kk] +(defn select-node [position shift?] (ptk/reify ::select-node ptk/UpdateEvent (update [_ state] @@ -79,38 +79,6 @@ (some? id) (assoc-in [:workspace-local :edit-path id :selected-points] selected-points)))))) -(defn deselect-node [position shift?] - (ptk/reify ::deselect-node - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (update-in [:workspace-local :edit-path id :selected-points] (fnil disj #{}) position)))))) - -(defn add-to-selection-handler [index type] - (ptk/reify ::add-to-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - -(defn add-to-selection-node [index] - (ptk/reify ::add-to-selection-node - ptk/UpdateEvent - (update [_ state] - state))) - -(defn remove-from-selection-handler [index] - (ptk/reify ::remove-from-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - -(defn remove-from-selection-node [index] - (ptk/reify ::remove-from-selection-handler - ptk/UpdateEvent - (update [_ state] - state))) - (defn deselect-all [] (ptk/reify ::deselect-all ptk/UpdateEvent @@ -133,14 +101,14 @@ (update [_ state] (update state :workspace-local dissoc :selrect)))) -(defn handle-selection +(defn handle-area-selection [shift?] (letfn [(valid-rect? [{width :width height :height}] (or (> width 10) (> height 10)))] - (ptk/reify ::handle-selection + (ptk/reify ::handle-area-selection ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) stoper (->> stream (rx/filter stop?)) from-p @ms/mouse-position] diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index b88ee5268a..55dbcd592d 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -22,7 +22,7 @@ (defn esc-pressed [] (ptk/reify :esc-pressed ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] ;; Not interrupt when we're editing a path (let [edition-id (or (get-in state [:workspace-drawing :object :id]) (get-in state [:workspace-local :edition])) diff --git a/frontend/src/app/main/data/workspace/path/spec.cljs b/frontend/src/app/main/data/workspace/path/spec.cljs index 96ad24fa0a..434a971170 100644 --- a/frontend/src/app/main/data/workspace/path/spec.cljs +++ b/frontend/src/app/main/data/workspace/path/spec.cljs @@ -40,8 +40,8 @@ (s/def ::content-entry (s/keys :req-un [::command] - :req-opt [::params - ::relative?])) + :opt-un [::params + ::relative?])) (s/def ::content (s/coll-of ::content-entry :kind vector?)) diff --git a/frontend/src/app/main/data/workspace/path/state.cljs b/frontend/src/app/main/data/workspace/path/state.cljs index 6bb59c4c64..229e46256f 100644 --- a/frontend/src/app/main/data/workspace/path/state.cljs +++ b/frontend/src/app/main/data/workspace/path/state.cljs @@ -6,7 +6,8 @@ (ns app.main.data.workspace.path.state (:require - [app.common.data :as d])) + [app.common.data :as d] + [app.util.path.shapes-to-path :as upsp])) (defn get-path-id "Retrieves the currently editing path id" @@ -14,16 +15,30 @@ (or (get-in state [:workspace-local :edition]) (get-in state [:workspace-drawing :object :id]))) -(defn get-path - "Retrieves the location of the path object and additionaly can pass - the arguments. This location can be used in get-in, assoc-in... functions" - [state & path] - (let [edit-id (get-in state [:workspace-local :edition]) - page-id (:current-page-id state)] +(defn get-path-location + [state & ks] + (let [edit-id (get-in state [:workspace-local :edition]) + page-id (:current-page-id state)] (d/concat (if edit-id [:workspace-data :pages-index page-id :objects edit-id] [:workspace-drawing :object]) - path))) + ks))) +(defn get-path + "Retrieves the location of the path object and additionaly can pass + the arguments. This location can be used in get-in, assoc-in... functions" + [state & ks] + (let [path-loc (get-path-location state) + shape (-> (get-in state path-loc) + (upsp/convert-to-path))] + (if (empty? ks) + shape + (get-in shape ks)))) + +(defn set-content + [state content] + (let [path-loc (get-path-location state :content)] + (-> state + (assoc-in path-loc content)))) diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index f8059c483a..b67607ae07 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -6,17 +6,16 @@ (ns app.main.data.workspace.path.streams (:require - [app.main.data.workspace.path.helpers :as helpers] - [app.main.data.workspace.path.state :as state] [app.common.geom.point :as gpt] + [app.common.math :as mth] + [app.main.data.workspace.path.state :as state] + [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] + [app.util.path.geom :as upg] [beicon.core :as rx] - [potok.core :as ptk] - [app.common.math :as mth] - [app.main.snap :as snap] [okulary.core :as l] - [app.util.path.geom :as upg])) + [potok.core :as ptk])) (defonce drag-threshold 5) @@ -50,7 +49,7 @@ (if (= value ::empty) not-drag-stream (rx/empty))))) - + (->> position-stream (rx/merge-map (fn [] to-stream))))))) @@ -107,7 +106,7 @@ (<= (- 180 rot-angle) 5))] (cond - snap-opposite-angle? + snap-opposite-angle? (let [rot-handler (gpt/rotate handler node (- 180 (* rot-sign rot-angle))) snap (gpt/to-vec handler rot-handler)] (merge position (gpt/add position snap))) @@ -122,11 +121,10 @@ (rx/map check-path-snap)))) (defn position-stream - [snap-toggled points] + [snap-toggled _points] (let [zoom (get-in @st/state [:workspace-local :zoom] 1) - ;; ranges (snap/create-ranges points) d-pos (/ snap/snap-path-accuracy zoom) - get-content (fn [state] (get-in state (state/get-path state :content))) + get-content #(state/get-path % :content) content-stream (-> (l/derived get-content st/state) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 610897cd23..18f262743f 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -6,13 +6,12 @@ (ns app.main.data.workspace.path.tools (:require - [app.common.geom.point :as gpt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.changes :as changes] - [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] + [app.util.path.shapes-to-path :as upsp] [app.util.path.subpaths :as ups] [app.util.path.tools :as upt] [beicon.core :as rx] @@ -25,23 +24,25 @@ ([points tool-fn] (ptk/reify ::process-path-tool ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [objects (wsh/lookup-page-objects state) id (st/get-path-id state) page-id (:current-page-id state) - shape (get-in state (st/get-path state)) - + shape (st/get-path state) selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{}) points (or points selected-points)] - (when (and (not (empty? points)) (some? shape)) + (when (and (seq points) (some? shape)) (let [new-content (-> (tool-fn (:content shape) points) (ups/close-subpaths)) [rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)] - (rx/of (dch/commit-changes {:redo-changes rch - :undo-changes uch - :origin it}) - (when (empty? new-content) - dwc/clear-edition-mode))))))))) + + (rx/concat + (rx/of (dch/update-shapes [id] upsp/convert-to-path)) + (rx/of (dch/commit-changes {:redo-changes rch + :undo-changes uch + :origin it}) + (when (empty? new-content) + dwc/clear-edition-mode)))))))))) (defn make-corner ([] diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs index 74c8fcb348..43dcb09313 100644 --- a/frontend/src/app/main/data/workspace/path/undo.cljs +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -9,8 +9,8 @@ [app.common.data :as d] [app.common.data.undo-stack :as u] [app.common.uuid :as uuid] - [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.changes :as changes] + [app.main.data.workspace.path.state :as st] [app.main.store :as store] [beicon.core :as rx] [okulary.core :as l] @@ -25,21 +25,22 @@ (= :app.main.data.workspace.common/redo (ptk/type event))) (defn- make-entry [state] - (let [id (st/get-path-id state)] - {:content (get-in state (st/get-path state :content)) - :selrect (get-in state (st/get-path state :selrect)) - :points (get-in state (st/get-path state :points)) + (let [id (st/get-path-id state) + shape (st/get-path state)] + {:content (:content shape) + :selrect (:selrect shape) + :points (:points shape) :preview (get-in state [:workspace-local :edit-path id :preview]) :last-point (get-in state [:workspace-local :edit-path id :last-point]) :prev-handler (get-in state [:workspace-local :edit-path id :prev-handler])})) (defn- load-entry [state {:keys [content selrect points preview last-point prev-handler]}] (let [id (st/get-path-id state) - old-content (get-in state (st/get-path state :content))] + old-content (st/get-path state :content)] (-> state - (d/assoc-in-when (st/get-path state :content) content) - (d/assoc-in-when (st/get-path state :selrect) selrect) - (d/assoc-in-when (st/get-path state :points) points) + (d/assoc-in-when (st/get-path-location state :content) content) + (d/assoc-in-when (st/get-path-location state :selrect) selrect) + (d/assoc-in-when (st/get-path-location state :points) points) (d/update-in-when [:workspace-local :edit-path id] assoc @@ -64,7 +65,7 @@ undo-stack))))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (rx/of (changes/save-path-content {:preserve-move-to true}))))) (defn redo-path [] @@ -82,7 +83,7 @@ undo-stack)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] (rx/of (changes/save-path-content))))) (defn merge-head @@ -92,10 +93,9 @@ (ptk/reify ::add-undo-entry ptk/UpdateEvent (update [_ state] - (let [id (st/get-path-id state) - entry (make-entry state) + (let [id (st/get-path-id state) stack (get-in state [:workspace-local :edit-path id :undo-stack]) - head (u/peek stack) + head (u/peek stack) stack (-> stack (u/undo) (u/fixup head))] (-> state (d/assoc-in-when @@ -129,7 +129,7 @@ (def path-content-ref (letfn [(selector [state] - (get-in state (st/get-path state :content)))] + (st/get-path state :content))] (l/derived selector store/state))) (defn start-path-undo @@ -145,7 +145,7 @@ assoc :undo-lock lock :undo-stack (u/make-stack))))) - + ptk/WatchEvent (watch [_ state stream] (let [undo-lock (get-in state [:workspace-local :edit-path (st/get-path-id state) :undo-lock])] diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 3228e0e5ed..76609c41a9 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -8,8 +8,6 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.geom.point :as gpt] - [app.common.media :as cm] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -26,13 +24,10 @@ [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] - [app.util.avatars :as avatars] [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] - [app.util.router :as rt] [app.util.time :as dt] - [app.util.transit :as t] [app.util.uri :as uu] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -53,7 +48,7 @@ [file-id] (ptk/reify ::initialize-persistence ptk/EffectEvent - (effect [_ state stream] + (effect [_ _ stream] (let [stoper (rx/filter #(= ::finalize %) stream) forcer (rx/filter #(= ::force-persist %) stream) notifier (->> stream @@ -121,12 +116,11 @@ (ptk/reify ::persist-changes ptk/UpdateEvent (update [_ state] - (let [conj (fnil conj []) - into* (fnil into [])] + (let [into* (fnil into [])] (update-in state [:workspace-persistence :queue] into* changes))) ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [sid (:session-id state) file (get state :workspace-file) queue (get-in state [:workspace-persistence :queue] []) @@ -177,7 +171,7 @@ (us/verify ::us/uuid file-id) (ptk/reify ::persist-synchronous-changes ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [sid (:session-id state) file (get-in state [:workspace-libraries file-id]) @@ -256,11 +250,11 @@ (declare fetch-libraries-content) (declare bundle-fetched) -(defn- fetch-bundle +(defn fetch-bundle [project-id file-id] (ptk/reify ::fetch-bundle ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (->> (rx/zip (rp/query :file {:id file-id}) (rp/query :team-users {:file-id file-id}) (rp/query :project {:id project-id}) @@ -286,7 +280,7 @@ (assoc-in state [:workspace-file :is-shared] is-shared)) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [params {:id id :is-shared is-shared}] (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) @@ -301,7 +295,7 @@ (us/assert ::us/uuid team-id) (ptk/reify ::fetch-shared-files ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (->> (rp/query :team-shared-files {:team-id team-id}) (rx/map shared-files-fetched))))) @@ -321,7 +315,7 @@ [file-id library-id] (ptk/reify ::link-file-to-library ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1) params {:file-id file-id :library-id library-id}] @@ -333,7 +327,7 @@ [file-id library-id] (ptk/reify ::unlink-file-from-library ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [unlinked #(d/dissoc-in % [:workspace-libraries library-id]) params {:file-id file-id :library-id library-id}] @@ -349,7 +343,7 @@ (->> (rx/of (-> (tubax/xml->clj text) (assoc :name name)))) - (catch :default err + (catch :default _err (rx/throw {:type :svg-parser})))) (defn fetch-svg [name uri] @@ -459,7 +453,7 @@ (s/def ::process-media-objects (s/and (s/keys :req-un [::file-id ::local?] - :opt-in [::name ::data ::uris ::mtype]) + :opt-un [::name ::data ::uris ::mtype]) (fn [props] (or (contains? props :blobs) (contains? props :uris))))) @@ -469,7 +463,7 @@ (us/assert ::process-media-objects params) (ptk/reify ::process-media-objects ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (rx/concat (rx/of (dm/show {:content (tr "media.loading") :type :info @@ -516,7 +510,7 @@ (us/assert ::clone-media-objects-params params) (ptk/reify ::clone-media-objects ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error identity}} (meta params) @@ -549,7 +543,7 @@ [ids] (ptk/reify ::remove-thumbnails ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ _] ;; Removes the thumbnail while it's regenerated (rx/of (dch/update-shapes ids diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 84964f8bd0..386e8f3b9f 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -45,7 +45,7 @@ (update [_ state] (assoc-in state [:workspace-local :selrect] selrect)))) -(defn handle-selection +(defn handle-area-selection [preserve?] (letfn [(data->selrect [data] (let [start (:start data) @@ -59,10 +59,11 @@ :y start-y :width (mth/abs (- end-x start-x)) :height (mth/abs (- end-y start-y))}))] - (ptk/reify ::handle-selection + (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) + (let [zoom (get-in state [:workspace-local :zoom] 1) + stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) stoper (->> stream (rx/filter stop?))] (rx/concat (when-not preserve? @@ -74,11 +75,16 @@ {:start pos :stop pos})) nil) (rx/map data->selrect) - (rx/filter #(or (> (:width %) 10) - (> (:height %) 10))) - (rx/map update-selrect) + (rx/filter #(or (> (:width %) (/ 10 zoom)) + (> (:height %) (/ 10 zoom)))) + + (rx/flat-map + (fn [selrect] + (rx/of (update-selrect selrect) + (select-shapes-by-current-selrect preserve?)))) + (rx/take-until stoper)) - (rx/of (select-shapes-by-current-selrect preserve?)))))))) + (rx/of (update-selrect nil)))))))) ;; --- Toggle shape's selection status (selected or deselected) @@ -100,7 +106,7 @@ (conj selected id)))))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id)] (rx/of (dwc/expand-all-parents [id] objects))))))) @@ -136,7 +142,7 @@ (assoc-in state [:workspace-local :selected] ids)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [objects (wsh/lookup-page-objects state)] (rx/of (dwc/expand-all-parents ids objects)))))) @@ -144,7 +150,7 @@ [] (ptk/reify ::select-all ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) new-selected (let [selected-objs @@ -204,7 +210,7 @@ [preserve?] (ptk/reify ::select-shapes-by-current-selrect ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state) @@ -214,34 +220,36 @@ selrect (get-in state [:workspace-local :selrect]) blocked? (fn [id] (get-in objects [id :blocked] false))] (rx/merge - (rx/of (update-selrect nil)) + (when selrect (->> (uw/ask! {:cmd :selection/query :page-id page-id - :rect selrect}) + :rect selrect + :include-frames? true + :full-frame? true}) (rx/map #(cp/clean-loops objects %)) (rx/map #(into initial-set (filter (comp not blocked?)) %)) (rx/map select-shapes)))))))) (defn select-inside-group - ([group-id position] (select-inside-group group-id position false)) - ([group-id position deep-children] - (ptk/reify ::select-inside-group - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - group (get objects group-id) - children (map #(get objects %) (:shapes group)) + [group-id position] - ;; We need to reverse the children because if two children - ;; overlap we want to select the one that's over (and it's - ;; in the later vector position - selected (->> children - reverse + (ptk/reify ::select-inside-group + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + group (get objects group-id) + children (map #(get objects %) (:shapes group)) + + ;; We need to reverse the children because if two children + ;; overlap we want to select the one that's over (and it's + ;; in the later vector position + selected (->> children + reverse (d/seek #(geom/has-point? % position)))] - (when selected - (rx/of (select-shape (:id selected))))))))) + (when selected + (rx/of (select-shape (:id selected)))))))) ;; --- Duplicate Shapes @@ -322,7 +330,6 @@ name (dwc/generate-unique-name names (:name obj)) renamed-obj (assoc obj :id id :name name) moved-obj (geom/move renamed-obj delta) - frames (cp/select-frames objects) parent-id (or parent-id frame-id) children-changes @@ -379,7 +386,7 @@ (def duplicate-selected (ptk/reify ::duplicate-selected ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) selected (wsh/lookup-selected state) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 7d2fe9bf4f..9d8ce96180 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -10,14 +10,13 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.undo :as dwu] [app.main.store :as st] - [app.util.dom :as dom] - [potok.core :as ptk])) + [app.util.dom :as dom])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index f2828eb3fa..048e4bb965 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.uuid :as uuid] @@ -18,14 +17,12 @@ [app.main.data.workspace.state-helpers :as wsh] [app.main.repo :as rp] [app.util.color :as uc] - [app.util.object :as obj] [app.util.path.parser :as upp] [app.util.svg :as usvg] [app.util.uri :as uu] [beicon.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk] - [promesa.core :as p])) + [potok.core :as ptk])) (defonce default-rect {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0}) (defonce default-circle {:r 0 :cx 0 :cy 0}) @@ -163,7 +160,7 @@ (gsh/setup-selrect)))) (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] - (when (and (contains? attrs :d) (not (empty? (:d attrs)) )) + (when (and (contains? attrs :d) (seq (:d attrs))) (let [svg-transform (usvg/parse-transform (:transform attrs)) path-content (upp/parse-path (:d attrs)) content (cond-> path-content @@ -387,7 +384,7 @@ [svg-data file-id position] (ptk/reify ::svg-uploaded ptk/WatchEvent - (watch [it state stream] + (watch [_ _ _] ;; Once the SVG is uploaded, we need to extract all the bitmap ;; images and upload them separatelly, then proceed to create ;; all shapes. @@ -414,7 +411,7 @@ [svg-data {:keys [x y] :as position}] (ptk/reify ::create-svg-shapes ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (try (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index f789a50ff7..f928c01cec 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -16,17 +16,11 @@ [app.main.data.workspace.common :as dwc] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] - [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] - [app.main.fonts :as fonts] - [app.util.object :as obj] + [app.util.router :as rt] [app.util.text-editor :as ted] [app.util.timers :as ts] - [app.util.router :as rt] [beicon.core :as rx] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [goog.object :as gobj] [potok.core :as ptk])) (defn update-editor @@ -42,7 +36,7 @@ [] (ptk/reify ::focus-editor ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (when-let [editor (:workspace-editor state)] (ts/schedule #(.focus ^js editor)))))) @@ -59,7 +53,7 @@ [{:keys [id] :as shape}] (ptk/reify ::finalize-editor-state ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [content (-> (get-in state [:workspace-editor-state id]) (ted/get-editor-current-content))] @@ -74,7 +68,7 @@ (dch/update-shapes [id] #(assoc % :content content)) (dwu/commit-undo-transaction))))) (rx/of (dws/deselect-shape id) - (dwc/delete-shapes [id]))))))) + (dwc/delete-shapes #{id}))))))) (defn initialize-editor-state [{:keys [id content] :as shape} decorator] @@ -88,7 +82,7 @@ decorator)))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] ;; We need to finalize editor on two main events: (1) when user ;; explicitly navigates to other section or page; (2) when user ;; leaves the editor. @@ -149,7 +143,7 @@ [{:keys [id attrs]}] (ptk/reify ::update-root-attrs ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [objects (wsh/lookup-page-objects state) shape (get objects id) @@ -168,7 +162,7 @@ (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-block-data attrs)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (when-not (some? (get-in state [:workspace-editor-state id])) (let [objects (wsh/lookup-page-objects state) shape (get objects id) @@ -195,7 +189,7 @@ (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles attrs)) ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (when-not (some? (get-in state [:workspace-editor-state id])) (let [objects (wsh/lookup-page-objects state) shape (get objects id) @@ -232,7 +226,7 @@ (defn resize-text-batch [changes] (ptk/reify ::resize-text-batch ptk/WatchEvent - (watch [_ state stream] + (watch [_ state _] (let [page-id (:current-page-id state) objects (get-in state [:workspace-data :pages-index page-id :objects])] (if-not (every? #(contains? objects(first %)) changes) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 0e2c7f230b..713371c7ed 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -11,7 +11,6 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.math :as mth] [app.common.pages :as cp] [app.common.spec :as us] [app.main.data.workspace.changes :as dch] @@ -19,28 +18,19 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [app.main.refs :as refs] [app.main.snap :as snap] - [app.main.store :as st] [app.main.streams :as ms] - [app.util.path.shapes-to-path :as ups] - [beicon.core :as rx] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) -;; -- Declarations -(declare set-modifiers) -(declare set-rotation) -(declare apply-modifiers) +;; -- Helpers -------------------------------------------------------- -;; -- Helpers - -;; For each of the 8 handlers gives the modifier for resize +;; For each of the 8 handlers gives the multiplier for resize ;; for example, right will only grow in the x coordinate and left ;; will grow in the inverse of the x coordinate -(def ^:private handler-modifiers +(def ^:private handler-multipliers {:right [ 1 0] :bottom [ 0 1] :left [-1 0] @@ -50,13 +40,16 @@ :bottom-right [ 1 1] :bottom-left [-1 1]}) -;; Given a handler returns the coordinate origin for resizes -;; this is the opposite of the handler so for right we want the -;; left side as origin of the resize -;; sx, sy => start x/y -;; mx, my => middle x/y -;; ex, ey => end x/y -(defn- handler-resize-origin [{sx :x sy :y :keys [width height]} handler] +(defn- handler-resize-origin + "Given a handler, return the coordinate origin for resizes. + This is the opposite of the handler so for right we want the + left side as origin of the resize. + + sx, sy => start x/y + mx, my => middle x/y + ex, ey => end x/y + " + [{sx :x sy :y :keys [width height]} handler] (let [mx (+ sx (/ width 2)) my (+ sy (/ height 2)) ex (+ sx width) @@ -100,10 +93,195 @@ (update [_ state] (update state :workspace-local dissoc :transform)))) -;; -- RESIZE + +;; -- Temporary modifiers ------------------------------------------- + +;; During an interactive transformation of shapes (e.g. when resizing or rotating +;; a group with the mouse), there are a lot of objects that need to be modified +;; (in this case, the group and all its children). +;; +;; To avoid updating the shapes theirselves, and forcing redraw of all components +;; that depend on the "objects" global state, we set a "modifiers" structure, with +;; the changes that need to be applied, and store it in :workspace-modifiers global +;; variable. The viewport reads this and merges it into the objects list it uses to +;; paint the viewport content, redrawing only the objects that have new modifiers. +;; +;; When the interaction is finished (e.g. user releases mouse button), the +;; apply-modifiers event is done, that consolidates all modifiers into the base +;; geometric attributes of the shapes. + +(declare set-modifiers-recursive) +(declare check-delta) +(declare set-local-displacement) +(declare clear-local-transform) + +(defn- set-modifiers + ([ids] (set-modifiers ids nil)) + ([ids modifiers] + (us/verify (s/coll-of uuid?) ids) + (ptk/reify ::set-modifiers + ptk/UpdateEvent + (update [_ state] + (let [modifiers (or modifiers (get-in state [:workspace-local :modifiers] {})) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + + ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false))))] + + (reduce (fn [state id] + (update state :workspace-modifiers + #(set-modifiers-recursive % + objects + (get objects id) + modifiers + nil + nil))) + state + ids)))))) + +;; Rotation use different algorithm to calculate children modifiers (and do not use child constraints). +(defn- set-rotation-modifiers + ([angle shapes] + (set-rotation-modifiers angle shapes (-> shapes gsh/selection-rect gsh/center-selrect))) + + ([angle shapes center] + (ptk/reify ::set-rotation-modifiers + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + id->obj #(get objects %) + get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects))) + + shapes (->> shapes (into [] (remove #(get % :blocked false)))) + + shapes (->> shapes (mapcat get-children) (concat shapes)) + + update-shape + (fn [modifiers shape] + (let [rotate-modifiers (gsh/rotation-modifiers shape center angle)] + (assoc-in modifiers [(:id shape) :modifiers] rotate-modifiers)))] + (-> state + (update :workspace-modifiers + #(reduce update-shape % shapes)))))))) + +(defn- apply-modifiers + [ids] + (us/verify (s/coll-of uuid?) ids) + (ptk/reify ::apply-modifiers + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + children-ids (->> ids (mapcat #(cp/get-children % objects))) + ids-with-children (d/concat [] children-ids ids) + object-modifiers (get state :workspace-modifiers) + ignore-tree (d/mapm #(get-in %2 [:modifiers :ignore-geometry?]) object-modifiers)] + + (rx/of (dwu/start-undo-transaction) + (dch/update-shapes + ids-with-children + (fn [shape] + (-> shape + (merge (get object-modifiers (:id shape))) + (gsh/transform-shape))) + {:reg-objects? true + :ignore-tree ignore-tree + ;; Attributes that can change in the transform. This way we don't have to check + ;; all the attributes + :attrs [:selrect :points + :x :y + :width :height + :content + :transform + :transform-inverse + :rotation + :flip-x + :flip-y]}) + (clear-local-transform) + (dwu/commit-undo-transaction)))))) + +(defn- set-modifiers-recursive + [modif-tree objects shape modifiers root transformed-root] + (let [children (->> (get shape :shapes []) + (map #(get objects %))) + + transformed-shape (gsh/transform-shape (assoc shape :modifiers modifiers)) + + [root transformed-root ignore-geometry?] + (check-delta shape root transformed-shape transformed-root objects) + + modifiers (assoc modifiers :ignore-geometry? ignore-geometry?) + + set-child (fn [modif-tree child] + (let [child-modifiers (gsh/calc-child-modifiers shape + child + modifiers)] + (set-modifiers-recursive modif-tree + objects + child + child-modifiers + root + transformed-root)))] + (reduce set-child + (update-in modif-tree [(:id shape) :modifiers] #(merge % modifiers)) + children))) + +(defn- check-delta + "If the shape is a component instance, check its relative position respect the + root of the component, and see if it changes after applying a transformation." + [shape root transformed-shape transformed-root objects] + (let [root (cond + (:component-root? shape) + shape + + (nil? root) + (cp/get-root-shape shape objects) + + :else root) + + transformed-root (cond + (:component-root? transformed-shape) + transformed-shape + + (nil? transformed-root) + (cp/get-root-shape transformed-shape objects) + + :else transformed-root) + + shape-delta (when root + (gpt/point (- (:x shape) (:x root)) + (- (:y shape) (:y root)))) + + transformed-shape-delta (when transformed-root + (gpt/point (- (:x transformed-shape) (:x transformed-root)) + (- (:y transformed-shape) (:y transformed-root)))) + + ignore-geometry? (= shape-delta transformed-shape-delta)] + + [root transformed-root ignore-geometry?])) + +(defn- set-local-displacement [point] + (ptk/reify ::start-local-displacement + ptk/UpdateEvent + (update [_ state] + (let [mtx (gmt/translate-matrix point)] + (-> state + (assoc-in [:workspace-local :modifiers] {:displacement mtx})))))) + +(defn- clear-local-transform [] + (ptk/reify ::clear-local-transform + ptk/UpdateEvent + (update [_ state] + (-> state + (dissoc :workspace-modifiers) + (update :workspace-local dissoc :modifiers :current-move-selected))))) + + +;; -- Resize -------------------------------------------------------- + (defn start-resize - [handler initial ids shape] - (letfn [(resize [shape initial resizing-shapes layout [point lock? point-snap]] + "Enter mouse resize mode, until mouse button is released." + [handler ids shape] + (letfn [(resize [shape initial layout [point lock? point-snap]] (let [{:keys [width height]} (:selrect shape) {:keys [rotation]} shape rotation (or rotation 0) @@ -118,12 +296,12 @@ lock? (or lock? scale-text) ;; Vector modifiers depending on the handler - handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y)) + handler-mult (let [[x y] (handler-multipliers handler)] (gpt/point x y)) ;; Difference between the origin point in the coordinate system of the rotation deltav (-> (gpt/to-vec initial (if (= rotation 0) point-snap point)) (gpt/transform (gmt/rotate-matrix (- rotation))) - (gpt/multiply handler-modif)) + (gpt/multiply handler-mult)) ;; Resize vector scalev (gpt/divide (gpt/add shapev deltav) shapev) @@ -151,8 +329,7 @@ :resize-origin origin :resize-transform shape-transform :resize-scale-text scale-text - :resize-transform-inverse shape-transform-inverse} - false)))) + :resize-transform-inverse shape-transform-inverse})))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Shift key and the shapes own proportion @@ -186,13 +363,48 @@ (rx/switch-map (fn [[point :as current]] (->> (snap/closest-snap-point page-id resizing-shapes layout zoom point) (rx/map #(conj current %))))) - (rx/mapcat (partial resize shape initial-position resizing-shapes layout)) + (rx/mapcat (partial resize shape initial-position layout)) (rx/take-until stoper)) (rx/of (apply-modifiers ids) (finish-transform)))))))) +(defn update-dimensions + "Change size of shapes, from the sideber options form." + [ids attr value] + (us/verify (s/coll-of ::us/uuid) ids) + (us/verify #{:width :height} attr) + (us/verify ::us/number value) + (ptk/reify ::update-dimensions + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data :pages-index page-id :objects])] + + (reduce (fn [state id] + (let [shape (get objects id) + modifiers (gsh/resize-modifiers shape attr value)] + (update state :workspace-modifiers + #(set-modifiers-recursive % + objects + shape + modifiers + nil + nil)))) + state + ids))) + + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))] + (rx/of (apply-modifiers ids)))))) + + +;; -- Rotate -------------------------------------------------------- (defn start-rotate + "Enter mouse rotate mode, until mouse button is released." [shapes] (ptk/reify ::start-rotate ptk/UpdateEvent @@ -201,7 +413,7 @@ (assoc-in [:workspace-local :transform] :rotate))) ptk/WatchEvent - (watch [it state stream] + (watch [_ _ stream] (let [stoper (rx/filter ms/mouse-up? stream) group (gsh/selection-rect shapes) group-center (gsh/center-selrect group) @@ -224,23 +436,41 @@ (rx/with-latest vector ms/mouse-position-ctrl) (rx/map (fn [[pos ctrl?]] (let [delta-angle (calculate-angle pos ctrl?)] - (set-rotation delta-angle shapes group-center)))) + (set-rotation-modifiers delta-angle shapes group-center)))) (rx/take-until stoper)) (rx/of (apply-modifiers (map :id shapes)) (finish-transform))))))) -;; -- MOVE +(defn increase-rotation + "Rotate shapes a fixed angle, from a keyboard action." + [ids rotation] + (ptk/reify ::increase-rotation + ptk/WatchEvent + (watch [_ state _] + + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + rotate-shape (fn [shape] + (let [delta (- rotation (:rotation shape))] + (set-rotation-modifiers delta [shape])))] + (rx/concat + (rx/from (->> ids (map #(get objects %)) (map rotate-shape))) + (rx/of (apply-modifiers ids))))))) + + +;; -- Move ---------------------------------------------------------- (declare start-move) (declare start-move-duplicate) -(declare start-local-displacement) -(declare clear-local-transform) +(declare calculate-frame-for-move) +(declare get-displacement) (defn start-move-selected + "Enter mouse move mode, until mouse button is released." [] (ptk/reify ::start-move-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state stream] (let [initial (deref ms/mouse-position) selected (wsh/lookup-selected state {:omit-blocked? true}) stopper (rx/filter ms/mouse-up? stream)] @@ -261,19 +491,141 @@ ;; Otherwise just plain old move (rx/of (start-move initial selected))))))))))) -(defn start-move-duplicate [from-position] +(defn- start-move-duplicate + [from-position] (ptk/reify ::start-move-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ _ stream] (->> stream (rx/filter (ptk/type? ::dws/duplicate-selected)) (rx/first) (rx/map #(start-move from-position)))))) -(defn calculate-frame-for-move [ids] +(defn- start-move + ([from-position] (start-move from-position nil)) + ([from-position ids] + (ptk/reify ::start-move + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :transform] :move))) + + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (wsh/lookup-selected state {:omit-blocked? true}) + ids (if (nil? ids) selected ids) + shapes (mapv #(get objects %) ids) + stopper (rx/filter ms/mouse-up? stream) + layout (get state :workspace-layout) + zoom (get-in state [:workspace-local :zoom] 1) + + + position (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(gpt/to-vec from-position %))) + + snap-delta (rx/concat + ;; We send the nil first so the stream is not waiting for the first value + (rx/of nil) + (->> position + (rx/throttle 20) + (rx/switch-map + (fn [pos] + (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) + (rx/map #(vector pos %)))))))] + (if (empty? shapes) + (rx/empty) + (rx/concat + (->> position + (rx/with-latest vector snap-delta) + (rx/map snap/correct-snap-point) + (rx/map set-local-displacement)) + + (rx/of (set-modifiers ids) + (apply-modifiers ids) + (calculate-frame-for-move ids) + (finish-transform))))))))) + +(s/def ::direction #{:up :down :right :left}) + +(defn move-selected + "Move shapes a fixed increment in one direction, from a keyboard action." + [direction shift?] + (us/verify ::direction direction) + (us/verify boolean? shift?) + + (let [same-event (js/Symbol "same-event")] + (ptk/reify ::move-selected + IDeref + (-deref [_] direction) + + ptk/UpdateEvent + (update [_ state] + (if (nil? (get-in state [:workspace-local :current-move-selected])) + (-> state + (assoc-in [:workspace-local :transform] :move) + (assoc-in [:workspace-local :current-move-selected] same-event)) + state)) + + ptk/WatchEvent + (watch [_ state stream] + (if (= same-event (get-in state [:workspace-local :current-move-selected])) + (let [selected (wsh/lookup-selected state {:omit-blocked? true}) + move-events (->> stream + (rx/filter (ptk/type? ::move-selected)) + (rx/filter #(= direction (deref %)))) + stopper (->> move-events + (rx/debounce 100) + (rx/first)) + scale (if shift? (gpt/point 10) (gpt/point 1)) + mov-vec (gpt/multiply (get-displacement direction) scale)] + + (rx/concat + (rx/merge + (->> move-events + (rx/take-until stopper) + (rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0)) + (rx/map set-local-displacement)) + (rx/of (move-selected direction shift?))) + + (rx/of (set-modifiers selected) + (apply-modifiers selected) + (finish-transform)))) + (rx/empty)))))) + +(s/def ::x number?) +(s/def ::y number?) +(s/def ::position + (s/keys :opt-un [::x ::y])) + +(defn update-position + "Move shapes to a new position, from the sidebar options form." + [id position] + (us/verify ::us/uuid id) + (us/verify ::position position) + (ptk/reify ::update-position + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (get objects id) + + bbox (-> shape :points gsh/points->selrect) + + cpos (gpt/point (:x bbox) (:y bbox)) + pos (gpt/point (or (:x position) (:x bbox)) + (or (:y position) (:y bbox))) + displ (gmt/translate-matrix (gpt/subtract pos cpos))] + (rx/of (set-modifiers [id] {:displacement displ}) + (apply-modifiers [id])))))) + +(defn- calculate-frame-for-move + [ids] (ptk/reify ::calculate-frame-for-move ptk/WatchEvent - (watch [it state stream] + (watch [it state _] (let [position @ms/mouse-position page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -308,66 +660,6 @@ (dwu/commit-undo-transaction) (dwc/expand-collapse frame-id))))))) -(defn start-move - ([from-position] (start-move from-position nil)) - ([from-position ids] - (ptk/reify ::start-move - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-local :transform] :move))) - - ptk/WatchEvent - (watch [it state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state {:omit-blocked? true}) - ids (if (nil? ids) selected ids) - shapes (mapv #(get objects %) ids) - stopper (rx/filter ms/mouse-up? stream) - layout (get state :workspace-layout) - zoom (get-in state [:workspace-local :zoom] 1) - - - position (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(gpt/to-vec from-position %))) - - snap-delta (rx/concat - ;; We send the nil first so the stream is not waiting for the first value - (rx/of nil) - (->> position - (rx/throttle 20) - (rx/switch-map - (fn [pos] - (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) - (rx/map #(vector pos %)))))))] - (if (empty? shapes) - (rx/empty) - (rx/concat - (->> position - (rx/with-latest vector snap-delta) - (rx/map snap/correct-snap-point) - (rx/map start-local-displacement)) - - (rx/of (apply-modifiers ids {:set-modifiers? true}) - (calculate-frame-for-move ids) - (finish-transform))))))))) - -(defn- get-displacement-with-grid - "Retrieve the correct displacement delta point for the - provided direction speed and distances thresholds." - [shape direction options] - (let [grid-x (:grid-x options 10) - grid-y (:grid-y options 10) - x-mod (mod (:x shape) grid-x) - y-mod (mod (:y shape) grid-y)] - (case direction - :up (gpt/point 0 (- (if (zero? y-mod) grid-y y-mod))) - :down (gpt/point 0 (- grid-y y-mod)) - :left (gpt/point (- (if (zero? x-mod) grid-x x-mod)) 0) - :right (gpt/point (- grid-x x-mod) 0)))) - (defn- get-displacement "Retrieve the correct displacement delta point for the provided direction speed and distances thresholds." @@ -378,216 +670,13 @@ :left (gpt/point (- 1) 0) :right (gpt/point 1 0))) -(s/def ::direction #{:up :down :right :left}) -(defn move-selected - [direction shift?] - (us/verify ::direction direction) - (us/verify boolean? shift?) - - (let [same-event (js/Symbol "same-event")] - (ptk/reify ::move-selected - IDeref - (-deref [_] direction) - - ptk/UpdateEvent - (update [_ state] - (if (nil? (get-in state [:workspace-local :current-move-selected])) - (-> state - (assoc-in [:workspace-local :transform] :move) - (assoc-in [:workspace-local :current-move-selected] same-event)) - state)) - - ptk/WatchEvent - (watch [it state stream] - (if (= same-event (get-in state [:workspace-local :current-move-selected])) - (let [selected (wsh/lookup-selected state {:omit-blocked? true}) - move-events (->> stream - (rx/filter (ptk/type? ::move-selected)) - (rx/filter #(= direction (deref %)))) - stopper (->> move-events - (rx/debounce 100) - (rx/first)) - scale (if shift? (gpt/point 10) (gpt/point 1)) - mov-vec (gpt/multiply (get-displacement direction) scale)] - - (rx/concat - (rx/merge - (->> move-events - (rx/take-until stopper) - (rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0)) - (rx/map start-local-displacement)) - (rx/of (move-selected direction shift?))) - - (rx/of (apply-modifiers selected {:set-modifiers? true}) - (finish-transform)))) - (rx/empty)))))) - - -;; -- Apply modifiers - -(defn set-modifiers - ([ids] (set-modifiers ids nil true)) - ([ids modifiers] (set-modifiers ids modifiers true)) - ([ids modifiers recurse-frames?] - (us/verify (s/coll-of uuid?) ids) - (ptk/reify ::set-modifiers - ptk/UpdateEvent - (update [_ state] - (let [modifiers (or modifiers (get-in state [:workspace-local :modifiers] {})) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - - ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false)))) - - not-frame-id? - (fn [shape-id] - (let [shape (get objects shape-id)] - (or recurse-frames? (not (= :frame (:type shape)))))) - - ;; For each shape updates the modifiers given as arguments - update-shape - (fn [objects shape-id] - (update-in objects [shape-id :modifiers] #(merge % modifiers))) - - ;; ID's + Children but remove frame children if the flag is set to false - ids-with-children (concat ids (mapcat #(cp/get-children % objects) - (filter not-frame-id? ids)))] - - (update state :workspace-modifiers - #(reduce update-shape % ids-with-children))))))) - - -;; Set-rotation is custom because applies different modifiers to each -;; shape adjusting their position. - -(defn set-rotation - ([angle shapes] - (set-rotation angle shapes (-> shapes gsh/selection-rect gsh/center-selrect))) - - ([angle shapes center] - (ptk/reify ::set-rotation - ptk/UpdateEvent - (update [_ state] - (let [objects (wsh/lookup-page-objects state) - id->obj #(get objects %) - get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects))) - - shapes (->> shapes (into [] (remove #(get % :blocked false)))) - - shapes (->> shapes (mapcat get-children) (concat shapes)) - - update-shape - (fn [modifiers shape] - (let [rotate-modifiers (gsh/rotation-modifiers shape center angle)] - (assoc-in modifiers [(:id shape) :modifiers] rotate-modifiers)))] - (-> state - (update :workspace-modifiers - #(reduce update-shape % shapes)))))))) - -(defn increase-rotation [ids rotation] - (ptk/reify ::increase-rotation - ptk/WatchEvent - (watch [it state stream] - - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - rotate-shape (fn [shape] - (let [delta (- rotation (:rotation shape))] - (set-rotation delta [shape])))] - (rx/concat - (rx/from (->> ids (map #(get objects %)) (map rotate-shape))) - (rx/of (apply-modifiers ids))))))) - -(defn apply-modifiers - ([ids] - (apply-modifiers ids nil)) - - ([ids {:keys [set-modifiers?] - :or {set-modifiers? false}}] - (us/verify (s/coll-of uuid?) ids) - (ptk/reify ::apply-modifiers - ptk/WatchEvent - (watch [it state stream] - (let [objects (wsh/lookup-page-objects state) - children-ids (->> ids (mapcat #(cp/get-children % objects))) - ids-with-children (d/concat [] children-ids ids) - - state (if set-modifiers? - (ptk/update (set-modifiers ids) state) - state) - object-modifiers (get state :workspace-modifiers)] - - (rx/of (dwu/start-undo-transaction) - (dch/update-shapes - ids-with-children - (fn [shape] - (-> shape - (merge (get object-modifiers (:id shape))) - (gsh/transform-shape))) - {:reg-objects? true - ;; Attributes that can change in the transform. This way we don't have to check - ;; all the attributes - :attrs [:selrect :points - :x :y - :width :height - :content - :transform - :transform-inverse - :rotation - :flip-x - :flip-y] - }) - (clear-local-transform) - (dwu/commit-undo-transaction))))))) - -;; --- Update Dimensions - -;; Event mainly used for handling user modification of the size of the -;; object from workspace sidebar options inputs. - -(defn update-dimensions - [ids attr value] - (us/verify (s/coll-of ::us/uuid) ids) - (us/verify #{:width :height} attr) - (us/verify ::us/number value) - (ptk/reify ::update-dimensions - ptk/UpdateEvent - (update [_ state] - - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - - update-children - (fn [objects ids modifiers] - (reduce #(assoc-in %1 [%2 :modifiers] modifiers) objects ids)) - - ;; For each shape updates the modifiers given as arguments - update-shape - (fn [objects shape-id] - (let [shape (get objects shape-id) - modifier (gsh/resize-modifiers shape attr value)] - (-> objects - (assoc-in [shape-id :modifiers] modifier) - (cond-> (not (= :frame (:type shape))) - (update-children (cp/get-children shape-id objects) modifier)))))] - - (d/update-in-when - state - [:workspace-data :pages-index page-id :objects] - #(reduce update-shape % ids)))) - - ptk/WatchEvent - (watch [it state stream] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))] - (rx/of (apply-modifiers ids)))))) +;; -- Flip ---------------------------------------------------------- (defn flip-horizontal-selected [] (ptk/reify ::flip-horizontal-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) @@ -597,14 +686,13 @@ (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) + :displacement (gmt/translate-matrix (gpt/point (- (:width selrect)) 0))}) (apply-modifiers selected)))))) (defn flip-vertical-selected [] (ptk/reify ::flip-vertical-selected ptk/WatchEvent - (watch [it state stream] + (watch [_ state _] (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) shapes (map #(get objects %) selected) @@ -614,30 +702,9 @@ (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) + :displacement (gmt/translate-matrix (gpt/point 0 (- (:height selrect))))}) (apply-modifiers selected)))))) -(defn start-local-displacement [point] - (ptk/reify ::start-local-displacement - ptk/UpdateEvent - (update [_ state] - (let [mtx (gmt/translate-matrix point)] - (-> state - (assoc-in [:workspace-local :modifiers] {:displacement mtx})))))) -(defn clear-local-transform [] - (ptk/reify ::clear-local-transform - ptk/UpdateEvent - (update [_ state] - (-> state - (dissoc :workspace-modifiers) - (update :workspace-local dissoc :modifiers :current-move-selected))))) +;; -- Transform to path --------------------------------------------- -(defn selected-to-path - [] - (ptk/reify ::selected-to-path - ptk/WatchEvent - (watch [_ state stream] - (let [ids (wsh/lookup-selected state {:omit-blocked? true})] - (rx/of (dch/update-shapes ids ups/convert-to-path)))))) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index a63225870b..85651e0d88 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -6,21 +6,10 @@ (ns app.main.data.workspace.undo (:require - [app.common.data :as d] - [app.common.geom.proportions :as gpr] - [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.spec :as spec] [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.main.worker :as uw] - [app.main.streams :as ms] - [app.util.logging :as log] - [app.util.timers :as ts] - [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] - [cuerdas.core :as str] [potok.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -42,7 +31,7 @@ (subvec undo (- cnt MAX-UNDO-SIZE)) undo))) -(defn- materialize-undo +(defn materialize-undo [changes index] (ptk/reify ::materialize-undo ptk/UpdateEvent @@ -51,15 +40,6 @@ (update :workspace-data cp/process-changes changes) (assoc-in [:workspace-undo :index] index))))) -(defn- reset-undo - [index] - (ptk/reify ::reset-undo - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-undo dissoc :undo-index) - (update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue)))))))) - (defn- add-undo-entry [state entry] (if (and entry @@ -81,7 +61,7 @@ (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)))) -(defn- append-undo +(defn append-undo [entry] (us/assert ::undo-entry entry) (ptk/reify ::append-undo diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 77ab799186..d48e5eebea 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -15,7 +15,8 @@ [app.common.pages :as cp] [app.common.uuid :as uuid] [app.main.ui.shapes.circle :as circle] - [app.main.ui.shapes.filters :as filters] + [app.main.ui.shapes.embed :as embed] + [app.main.ui.shapes.export :as use] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.image :as image] @@ -24,6 +25,8 @@ [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] + [app.main.ui.shapes.text.fontfaces :as ff] + [app.util.object :as obj] [app.util.timers :as ts] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -43,8 +46,9 @@ [{:keys [objects] :as data} vport] (let [shapes (cp/select-toplevel-shapes objects {:include-frames? true}) to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val)) - rect (->> (gsh/selection-rect shapes) - (gal/adjust-to-viewport vport))] + rect (cond->> (gsh/selection-rect shapes) + (some? vport) + (gal/adjust-to-viewport vport))] (-> rect (update :x to-finite 0) (update :y to-finite 0) @@ -83,9 +87,17 @@ (mf/fnc svg-raw-wrapper [{:keys [shape frame] :as props}] (let [childs (mapv #(get objects %) (:shapes shape))] - [:& svg-raw-shape {:frame frame - :shape shape - :childs childs}])))) + (if (and (map? (:content shape)) + (or (= :svg (get-in shape [:content :tag])) + (contains? shape :svg-attrs))) + [:> shape-container {:shape shape} + [:& svg-raw-shape {:frame frame + :shape shape + :childs childs}]] + + [:& svg-raw-shape {:frame frame + :shape shape + :childs childs}]))))) (defn shape-wrapper-factory [objects] @@ -98,9 +110,8 @@ (let [shape (-> (gsh/transform-shape shape) (gsh/translate-to-frame frame)) opts #js {:shape shape} - svg-element? (and (= :svg-raw (:type shape)) - (not= :svg (get-in shape [:content :tag])))] - (if-not svg-element? + svg-raw? (= :svg-raw (:type shape))] + (if-not svg-raw? [:> shape-container {:shape shape} (case (:type shape) :text [:> text/text-shape opts] @@ -110,7 +121,6 @@ :circle [:> circle/circle-shape opts] :frame [:> frame-wrapper {:shape shape}] :group [:> group-wrapper {:shape shape :frame frame}] - :svg-raw [:> svg-raw-wrapper {:shape shape :frame frame}] nil)] ;; Don't wrap svg elements inside a otherwise some can break @@ -121,13 +131,20 @@ (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data width height thumbnails?] :as props}] + [{:keys [data width height thumbnails? embed?] :as props}] (let [objects (:objects data) root (get objects uuid/zero) - shapes (->> (:shapes root) - (map #(get objects %))) + shapes + (->> (:shapes root) + (map #(get objects %))) - vport {:width width :height height} + root-children + (->> shapes + (filter #(not= :frame (:type %))) + (mapcat #(cp/get-object-with-children (:id %) objects))) + + vport (when (and (some? width) (some? height)) + {:width width :height height}) dim (calculate-dimensions data vport) vbox (get-viewbox dim) background-color (get-in data [:options :background] default-color) @@ -140,29 +157,36 @@ (mf/use-memo (mf/deps objects) #(shape-wrapper-factory objects))] - [:svg {:view-box vbox - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& background {:vbox dim :color background-color}] - (for [item shapes] - (let [frame? (= (:type item) :frame)] - (cond - (and frame? thumbnails? (some? (:thumbnail item))) - [:image {:xlinkHref (:thumbnail item) - :x (:x item) - :y (:y item) - :width (:width item) - :height (:height item) - ;; DEBUG - ;; :style {:filter "sepia(1)"} - }] - frame? - [:& frame-wrapper {:shape item - :key (:id item)}] - :else - [:& shape-wrapper {:shape item - :key (:id item)}])))])) + [:& (mf/provider embed/context) {:value embed?} + [:svg {:view-box vbox + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg" + :xmlns:penpot "https://penpot.app/xmlns" + :style {:width "100%" + :height "100%" + :background background-color}} + + [:& use/export-page {:options (:options data)}] + [:& ff/fontfaces-style {:shapes root-children}] + (for [item shapes] + (let [frame? (= (:type item) :frame)] + (cond + (and frame? thumbnails? (some? (:thumbnail item))) + [:image {:xlinkHref (:thumbnail item) + :x (:x item) + :y (:y item) + :width (:width item) + :height (:height item) + ;; DEBUG + ;; :style {:filter "sepia(1)"} + }] + frame? + [:& frame-wrapper {:shape item + :key (:id item)}] + :else + [:& shape-wrapper {:shape item + :key (:id item)}])))]])) (mf/defc frame-svg {::mf/wrap [mf/memo]} @@ -191,7 +215,8 @@ :height height :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} + :xmlns "http://www.w3.org/2000/svg" + :xmlns:penpot "https://penpot.app/xmlns"} [:& wrapper {:shape frame :view-box vbox}]])) (mf/defc component-svg @@ -222,6 +247,60 @@ :height height :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} + :xmlns "http://www.w3.org/2000/svg" + :xmlns:penpot "https://penpot.app/xmlns"} [:& wrapper {:shape group :view-box vbox}]])) +(mf/defc component-symbol + [{:keys [id data] :as props}] + + (let [{:keys [name path objects]} data + root (get objects id) + + {:keys [width height]} (:selrect root) + vbox (str "0 0 " width " " height) + + modifier (-> (gpt/point (:x root) (:y root)) + (gpt/negate) + (gmt/translate-matrix)) + + modifier-ids (concat [id] (cp/get-children id objects)) + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) + objects (reduce update-fn objects modifier-ids) + root (assoc-in root [:modifiers :displacement] modifier) + + group-wrapper + (mf/use-memo + (mf/deps objects) + #(group-wrapper-factory objects))] + + [:> "symbol" #js {:id (str id) + :viewBox vbox + "penpot:path" path} + [:title name] + [:> shape-container {:shape root} + [:& group-wrapper {:shape root :view-box vbox}]]])) + +(mf/defc components-sprite-svg + {::mf/wrap-props false} + [props] + + (let [data (obj/get props "data") + children (obj/get props "children") + embed? (obj/get props "embed?")] + [:& (mf/provider embed/context) {:value embed?} + [:svg {:version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot "https://penpot.app/xmlns" + :style {:width "100vw" + :height "100vh" + :display (when-not (some? children) "none")}} + + [:defs + (for [[component-id component-data] (:components data)] + [:& component-symbol {:id component-id + :key (str component-id) + :data component-data}])] + + children]])) diff --git a/frontend/src/app/main/fonts.clj b/frontend/src/app/main/fonts.clj index d36810d457..c947cdabb7 100644 --- a/frontend/src/app/main/fonts.clj +++ b/frontend/src/app/main/fonts.clj @@ -7,9 +7,9 @@ (ns app.main.fonts "A fonts loading macros." (:require - [cuerdas.core :as str] + [clojure.data.json :as json] [clojure.java.io :as io] - [clojure.data.json :as json])) + [cuerdas.core :as str])) (defn- parse-gfont-variant [variant] diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index e9030abb3b..dbd50cde36 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -8,17 +8,18 @@ "Fonts management and loading logic." (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require - [app.config :as cf] [app.common.data :as d] + [app.common.text :as txt] + [app.config :as cf] [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.timers :as ts] + [app.util.http :as http] [app.util.logging :as log] - [lambdaisland.uri :as u] - [goog.events :as gev] + [app.util.object :as obj] [beicon.core :as rx] [clojure.set :as set] [cuerdas.core :as str] + [goog.events :as gev] + [lambdaisland.uri :as u] [okulary.core :as l] [promesa.core :as p])) @@ -102,7 +103,7 @@ [url on-loaded] (let [node (create-link-element url) head (.-head ^js js/document)] - (gev/listenOnce node "load" (fn [event] + (gev/listenOnce node "load" (fn [_] (when (fn? on-loaded) (on-loaded)))) (dom/append-child! head node))) @@ -138,7 +139,7 @@ (str base ":" variants "&display=block"))) (defmethod load-font :google - [{:keys [id family variants ::on-loaded] :as font}] + [{:keys [id ::on-loaded] :as font}] (when (exists? js/window) (log/debug :action "load-font" :font-id id :backend "google") (let [url (generate-gfonts-url font)] @@ -181,11 +182,13 @@ (str/join "\n"))) (defmethod load-font :custom - [{:keys [id family variants ::on-loaded] :as font}] + [{:keys [id ::on-loaded] :as font}] (when (exists? js/window) (js/console.log "[debug:fonts]: loading custom font" id) (let [css (generate-custom-font-css font)] - (add-font-css! css)))) + (add-font-css! css) + (when (fn? on-loaded) + (on-loaded))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; LOAD API @@ -211,3 +214,62 @@ (or (d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants) (first variants))) + +;; Font embedding functions + +;; Template for a CSS font face + +(def font-face-template " +/* latin */ +@font-face { + font-family: '%(family)s'; + font-style: %(style)s; + font-weight: %(weight)s; + font-display: block; + src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff'); +} +") + +(defn get-content-fonts + "Extracts the fonts used by the content of a text shape" + [{font-id :font-id children :children :as content}] + (let [current-font + (if (some? font-id) + #{(select-keys content [:font-id :font-variant-id])} + #{(select-keys txt/default-text-attrs [:font-id :font-variant-id])}) + children-font (->> children (mapv get-content-fonts))] + (reduce set/union (conj children-font current-font)))) + + +(defn fetch-font-css + "Given a font and the variant-id, retrieves the fontface CSS" + [{:keys [font-id font-variant-id] + :or {font-variant-id "regular"}}] + + (let [{:keys [backend family variants]} (get @fontsdb font-id)] + (cond + (= :google backend) + (-> (generate-gfonts-url + {:family family + :variants [{:id font-variant-id}]}) + (http/fetch-text)) + + (= :custom backend) + (let [variant (d/seek #(= (:id %) font-variant-id) variants) + result (generate-custom-font-variant-css family variant)] + (p/resolved result)) + + :else + (let [{:keys [weight style suffix] :as variant} + (d/seek #(= (:id %) font-variant-id) variants) + font-data {:family family + :style style + :suffix (or suffix font-variant-id) + :weight weight}] + (rx/of (str/fmt font-face-template font-data)))))) + +(defn extract-fontface-urls + "Parses the CSS and retrieves the font urls" + [^string css] + (->> (re-seq #"url\(([^)]+)\)" css) + (mapv second))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index db33ad5aa2..236f4a0af2 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -9,12 +9,10 @@ "A collection of derived refs." (:require [app.common.data :as d] + [app.common.geom.shapes :as gsh] [app.common.pages :as cp] - [app.common.uuid :as uuid] - [app.main.constants :as c] [app.main.data.workspace.state-helpers :as wsh] [app.main.store :as st] - [beicon.core :as rx] [okulary.core :as l])) ;; ---- Global refs @@ -80,7 +78,7 @@ (def dashboard-selected-files (l/derived (fn [state] (let [get-file #(get-in state [:dashboard-files %]) - sim-file #(select-keys % [:id :name :project-id]) + sim-file #(select-keys % [:id :name :project-id :is-shared]) selected (get-in state [:dashboard-local :selected-files]) xform (comp (map get-file) (map sim-file))] @@ -242,7 +240,7 @@ modifiers (:workspace-modifiers state) objects (cond-> objects with-modifiers? - (cp/merge-modifiers modifiers)) + (gsh/merge-modifiers modifiers)) xform (comp (map #(get objects %)) (remove nil?))] (into [] xform ids))) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs new file mode 100644 index 0000000000..e3eb8858e1 --- /dev/null +++ b/frontend/src/app/main/render.cljs @@ -0,0 +1,86 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.render + (:require + ["react-dom/server" :as rds] + [app.config :as cfg] + [app.main.exports :as exports] + [app.main.fonts :as fonts] + [app.util.http :as http] + [beicon.core :as rx] + [clojure.set :as set] + [rumext.alpha :as mf])) + +(defn- text? [{type :type}] + (= type :text)) + +(defn- get-image-data [shape] + (cond + (= :image (:type shape)) + [(:metadata shape)] + + (some? (:fill-image shape)) + [(:fill-image shape)] + + :else + [])) + +(defn populate-images-cache + [objects] + (let [images (->> objects + (vals) + (mapcat get-image-data))] + (->> (rx/from images) + (rx/map #(cfg/resolve-file-media %)) + (rx/flat-map http/fetch-data-uri)))) + +(defn populate-fonts-cache [objects] + (let [texts (->> objects + (vals) + (filterv text?) + (mapv :content)) ] + + (->> (rx/from texts) + (rx/map fonts/get-content-fonts) + (rx/reduce set/union #{}) + (rx/flat-map identity) + (rx/flat-map fonts/fetch-font-css) + (rx/flat-map fonts/extract-fontface-urls) + (rx/flat-map http/fetch-data-uri)))) + +(defn render-page + [data] + (rx/concat + (->> (rx/merge + (populate-images-cache (:objects data)) + (populate-fonts-cache (:objects data))) + (rx/ignore)) + + (->> (rx/of data) + (rx/map + (fn [data] + (let [elem (mf/element exports/page-svg #js {:data data :embed? true})] + (rds/renderToStaticMarkup elem))))))) + +(defn render-components + [data] + (let [;; Join all components objects into a single map + objects (->> (:components data) + (vals) + (map :objects) + (reduce conj))] + (rx/concat + (->> (rx/merge + (populate-images-cache objects) + (populate-fonts-cache objects)) + (rx/ignore)) + + (->> (rx/of data) + (rx/map + (fn [data] + (let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true})] + (rds/renderToStaticMarkup elem)))))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index e2405a8d7f..e9df95ee65 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -10,10 +10,7 @@ [app.common.uri :as u] [app.config :as cfg] [app.util.http :as http] - [app.util.time :as dt] - [app.util.transit :as t] - [beicon.core :as rx] - [cuerdas.core :as str])) + [beicon.core :as rx])) (defn handle-response [{:keys [status body] :as response}] @@ -87,7 +84,7 @@ ([id params] (mutation id params))) (defmethod mutation :login-with-oauth - [id {:keys [provider] :as params}] + [_ {:keys [provider] :as params}] (let [uri (u/join base-uri "api/auth/oauth/" (d/name provider)) params (dissoc params :provider)] (->> (http/send! {:method :post :uri uri :query params}) @@ -95,7 +92,7 @@ (rx/mapcat handle-response)))) (defmethod mutation :send-feedback - [id params] + [_ params] (->> (http/send! {:method :post :uri (u/join base-uri "api/feedback") :body (http/transit-data params)}) @@ -103,7 +100,7 @@ (rx/mapcat handle-response))) (defmethod query :export - [id params] + [_ params] (->> (http/send! {:method :post :uri (u/join base-uri "export") :body (http/transit-data params) diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index b8e700b6bf..6021730ef5 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -19,24 +19,24 @@ [beicon.core :as rx] [clojure.set :as set])) -(defonce ^:private snap-accuracy 5) -(defonce ^:private snap-path-accuracy 10) -(defonce ^:private snap-distance-accuracy 10) +(def ^:const snap-accuracy 5) +(def ^:const snap-path-accuracy 10) +(def ^:const snap-distance-accuracy 10) (defn- remove-from-snap-points [remove-id?] (fn [query-result] (->> query-result (map (fn [[value data]] [value (remove (comp remove-id? second) data)])) - (filter (fn [[_ data]] (not (empty? data))))))) + (filter (fn [[_ data]] (seq data)))))) (defn- flatten-to-points [query-result] - (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) query-result)) + (mapcat (fn [[_ data]] (map (fn [[point _]] point) data)) query-result)) (defn- calculate-distance [query-result point coord] (->> query-result - (map (fn [[value data]] [(mth/abs (- value (coord point))) [(coord point) value]])))) + (map (fn [[value _]] [(mth/abs (- value (coord point))) [(coord point) value]])))) (defn- get-min-distance-snap [points coord] (fn [query-result] @@ -45,7 +45,7 @@ (apply min-key first) second))) -(defn- snap-frame-id [shapes] +(defn snap-frame-id [shapes] (let [frames (into #{} (map :frame-id shapes))] (cond ;; Only shapes from one frame. The common is the only one @@ -286,8 +286,9 @@ (fn [matches other] (let [matches (into {} matches) - other (into {} other) - keys (set/union (keys matches) (keys other))] + other (into {} other) + keys (set/union (set (keys matches)) + (set (keys other)))] (into {} (map (fn [key] [key @@ -308,7 +309,7 @@ min-match-coord (fn [matches] - (if (and (seq matches) (not (empty? matches))) + (if (seq matches) (->> matches (reduce get-min)) default))] diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index f4f15b9416..4fd743ecd3 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -75,12 +75,12 @@ (logjs "state" (get-in @state [:workspace-data :pages-index page-id :objects])))) (defn ^:export dump-object [name] - (let [page-id (get @state :current-page-id)] - (let [objects (get-in @state [:workspace-data :pages-index page-id :objects]) - target (or (d/seek (fn [[id shape]] (= name (:name shape))) objects) - (get objects (uuid name)))] - (->> target - (logjs "state"))))) + (let [page-id (get @state :current-page-id) + objects (get-in @state [:workspace-data :pages-index page-id :objects]) + target (or (d/seek (fn [[_ shape]] (= name (:name shape))) objects) + (get objects (uuid name)))] + (->> target + (logjs "state")))) (defn ^:export dump-tree ([] (dump-tree false false)) @@ -89,7 +89,7 @@ (let [page-id (get @state :current-page-id) objects (get-in @state [:workspace-data :pages-index page-id :objects]) components (get-in @state [:workspace-data :components]) - libraries (get-in @state [:workspace-libraries]) + libraries (get @state :workspace-libraries) root (d/seek #(nil? (:parent-id %)) (vals objects))] (letfn [(show-shape [shape-id level objects] diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index e71667fd6a..15a05e01ae 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -7,12 +7,10 @@ (ns app.main.streams "User interaction events and streams." (:require - [beicon.core :as rx] [app.main.store :as st] - [app.main.refs :as refs] - [app.common.geom.point :as gpt] [app.util.globals :as globals] - [app.util.keyboard :as kbd])) + [app.util.keyboard :as kbd] + [beicon.core :as rx])) ;; --- User Events diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 601b89ecc1..aeafb377f8 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -6,15 +6,12 @@ (ns app.main.ui (:require - [app.config :as cf] - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cfg] - [app.main.data.users :as du] - [app.main.data.messages :as dm] + [app.config :as cf] [app.main.data.events :as ev] + [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.auth :refer [auth]] @@ -32,8 +29,6 @@ [app.main.ui.static :as static] [app.main.ui.viewer :refer [viewer-page]] [app.main.ui.workspace :as workspace] - [app.util.i18n :as i18n :refer [tr t]] - [app.util.router :as rt] [app.util.timers :as ts] [cljs.pprint :refer [pprint]] [cljs.spec.alpha :as s] @@ -60,9 +55,11 @@ (def routes [["/auth" ["/login" :auth-login] - (when cfg/registration-enabled + (when cf/registration-enabled ["/register" :auth-register]) - (when cfg/registration-enabled + (when cf/registration-enabled + ["/register/validate" :auth-register-validate]) + (when cf/registration-enabled ["/register/success" :auth-register-success]) ["/recovery/request" :auth-recovery-request] ["/recovery" :auth-recovery] @@ -85,6 +82,7 @@ ;; Used for export ["/render-object/:file-id/:page-id/:object-id" :render-object] + ["/render-sprite/:file-id" :render-sprite] ["/dashboard/team/:team-id" ["/members" :dashboard-team-members] @@ -100,9 +98,8 @@ (mf/defc on-main-error [{:keys [error] :as props}] - (let [data (ex-data error)] - (mf/use-effect #(ptk/handle-error error)) - [:span "Internal application errror"])) + (mf/use-effect #(ptk/handle-error error)) + [:span "Internal application errror"]) (mf/defc main-page {::mf/wrap [#(mf/catch % {:fallback on-main-error})]} @@ -112,6 +109,7 @@ (case (:name data) (:auth-login :auth-register + :auth-register-validate :auth-register-success :auth-recovery-request :auth-recovery) @@ -145,7 +143,7 @@ :dashboard-team-settings) [:* #_[:div.modal-wrapper - [:& app.main.ui.onboarding/release-notes-modal {:version "1.6"}]] + [:& app.main.ui.onboarding/release-notes-modal {:version "1.7"}]] [:& dashboard {:route route}]] :viewer @@ -175,6 +173,14 @@ :page-id page-id :object-id object-id}])) + :render-sprite + (do + (let [file-id (uuid (get-in route [:path-params :file-id])) + component-id (get-in route [:query-params :component-id]) + component-id (when (some? component-id) (uuid component-id))] + [:& render/render-sprite {:file-id file-id + :component-id component-id}])) + :workspace (let [project-id (some-> params :path :project-id uuid) file-id (some-> params :path :file-id uuid) @@ -208,14 +214,14 @@ (derive :service-unavailable ::exceptional-state) (defmethod ptk/handle-error ::exceptional-state - [{:keys [status] :as error}] + [error] (ts/schedule (st/emitf (dm/assign-exception error)))) ;; We receive a explicit authentication error; this explicitly clears ;; all profile data and redirect the user to the login page. (defmethod ptk/handle-error :authentication - [error] + [_] (ts/schedule (st/emitf (du/logout)))) ;; Error that happens on an active bussines model validation does not @@ -242,7 +248,7 @@ ;; Error on parsing an SVG (defmethod ptk/handle-error :svg-parser - [error] + [_] (ts/schedule (st/emitf (dm/show {:content "SVG is invalid or malformed" @@ -258,7 +264,7 @@ context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'" (:ns context) (:name context) - (str cfg/public-uri "js/cljs-runtime/" (:file context)) + (str cf/public-uri "js/cljs-runtime/" (:file context)) (:line context))] (ts/schedule (st/emitf diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 06b56dca4d..5cf1a715bd 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -6,43 +6,35 @@ (ns app.main.ui.auth (:require - [app.common.uuid :as uuid] - [app.main.data.messages :as dm] - [app.main.data.users :as du] - [app.main.repo :as rp] - [app.main.store :as st] [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-page register-success-page]] + [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page]] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :as i18n :refer [t]] - [app.util.router :as rt] - [app.util.timers :as ts] - [beicon.core :as rx] - [cljs.spec.alpha :as s] + [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (mf/defc auth [{:keys [route] :as props}] (let [section (get-in route [:data :name]) - locale (mf/deref i18n/locale) params (:query-params route)] (mf/use-effect - #(dom/set-html-title (t locale "title.default"))) + #(dom/set-html-title (tr "title.default"))) [:div.auth [:section.auth-sidebar [:a.logo {:href "https://penpot.app"} i/logo] - [:span.tagline (t locale "auth.sidebar-tagline")]] + [:span.tagline (tr "auth.sidebar-tagline")]] [:section.auth-content (case section :auth-register - [:& register-page {:locale locale :params params}] + [:& register-page {:params params}] + + :auth-register-validate + [:& register-validate-page {:params params}] :auth-register-success [:& register-success-page {:params params}] @@ -51,10 +43,11 @@ [:& login-page {:params params}] :auth-recovery-request - [:& recovery-request-page {:locale locale}] + [:& recovery-request-page] :auth-recovery - [:& recovery-page {:locale locale :params params}]) + [:& recovery-page {:params params}]) + [:div.terms-login [:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"] [:span "and"] diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 3b809e986d..3f8e89e662 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -16,13 +16,18 @@ [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] [app.util.dom :as dom] - [app.util.i18n :refer [tr t]] - [app.util.object :as obj] + [app.util.i18n :refer [tr]] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) +(def show-alt-login-buttons? + (or cfg/google-client-id + cfg/gitlab-client-id + cfg/github-client-id + cfg/oidc-client-id)) + (s/def ::email ::us/email) (s/def ::password ::us/not-empty-string) @@ -68,7 +73,7 @@ on-submit (mf/use-callback (mf/deps form) - (fn [event] + (fn [_] (reset! error nil) (let [params (with-meta (:clean-data @form) {:on-error on-error})] @@ -103,13 +108,15 @@ :tab-index "3" :help-icon i/eye :label (tr "auth.password")}]] - [:& fm/submit-button - {:label (tr "auth.login-submit")}] - (when cfg/login-with-ldap - [:& fm/submit-button - {:label (tr "auth.login-with-ldap-submit") - :on-click on-submit-ldap}])]])) + [:div.buttons-stack + [:& fm/submit-button + {:label (tr "auth.login-submit")}] + + (when cfg/login-with-ldap + [:& fm/submit-button + {:label (tr "auth.login-with-ldap-submit") + :on-click on-submit-ldap}])]]])) (mf/defc login-buttons [{:keys [params] :as props}] @@ -147,6 +154,13 @@ [:& login-form {:params params}] + (when show-alt-login-buttons? + [:* + [:span.separator (tr "labels.or")] + + [:div.buttons + [:& login-buttons {:params params}]]]) + [:div.links [:div.link-entry [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))} @@ -158,7 +172,6 @@ [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))} (tr "auth.register-submit")]])] - [:& login-buttons {:params params}] (when cfg/allow-demo-users [:div.links.demo diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index 7040059867..e18471d2b3 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -11,12 +11,9 @@ [app.main.data.users :as du] [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 [t tr]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (s/def ::password-1 ::us/not-empty-string) @@ -40,7 +37,7 @@ (assoc :password-1 {:message "errors.password-too-short"})))) (defn- on-error - [form error] + [_form _error] (st/emit! (dm/error (tr "auth.notifications.invalid-token-error")))) (defn- on-success @@ -49,7 +46,7 @@ (rt/nav :auth-login))) (defn- on-submit - [form event] + [form _event] (let [mdata {:on-error on-error :on-success on-success} params {:token (get-in @form [:clean-data :token]) @@ -57,7 +54,7 @@ (st/emit! (du/recover-profile (with-meta params mdata))))) (mf/defc recovery-form - [{:keys [locale params] :as props}] + [{:keys [params] :as props}] (let [form (fm/use-form :spec ::recovery-form :validators [password-equality] :initial params)] @@ -66,28 +63,28 @@ [:div.fields-row [:& fm/input {:type "password" :name :password-1 - :label (t locale "auth.new-password")}]] + :label (tr "auth.new-password")}]] [:div.fields-row [:& fm/input {:type "password" :name :password-2 - :label (t locale "auth.confirm-password")}]] + :label (tr "auth.confirm-password")}]] [:& fm/submit-button - {:label (t locale "auth.recovery-submit")}]])) + {:label (tr "auth.recovery-submit")}]])) ;; --- Recovery Request Page (mf/defc recovery-page - [{:keys [locale params] :as props}] + [{:keys [params] :as props}] [:section.generic-form [:div.form-container [:h1 "Forgot your password?"] [:div.subtitle "Please enter your new password"] - [:& recovery-form {:locale locale :params params}] + [:& recovery-form {:params params}] [:div.links [:div.link-entry [:a {:on-click #(st/emit! (rt/nav :auth-login))} - (t locale "profile.recovery.go-to-login")]]]]]) + (tr "profile.recovery.go-to-login")]]]]]) diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 637bbc852f..e5a16089f2 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -12,12 +12,10 @@ [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 t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (s/def ::email ::us/email) @@ -30,7 +28,7 @@ on-success (mf/use-callback - (fn [data] + (fn [_ _] (reset! submitted false) (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent")) (rt/nav :auth-login)))) @@ -86,4 +84,4 @@ [:div.links [:div.link-entry [:a {:on-click #(st/emit! (rt/nav :auth-login))} - (tr "auth.go-back-to-login")]]]]]) + (tr "labels.go-back")]]]]]) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 97c1aec2eb..5ee5ebd1f9 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -7,21 +7,19 @@ (ns app.main.ui.auth.register (:require [app.common.spec :as us] - [app.config :as cfg] - [app.main.data.users :as du] + [app.config :as cf] [app.main.data.messages :as dm] + [app.main.data.users :as du] + [app.main.repo :as rp] [app.main.store :as st] + [app.main.ui.auth.login :as login] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] - [app.main.ui.auth.login :as login] - [app.util.dom :as dom] - [app.util.i18n :refer [tr t]] + [app.util.i18n :refer [tr]] [app.util.router :as rt] - [app.util.timers :as tm] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (mf/defc demo-warning @@ -30,6 +28,8 @@ {:type :warning :content (tr "auth.demo-warning")}]) +;; --- PAGE: Register + (defn- validate [data] (let [password (:password data) @@ -48,9 +48,29 @@ (s/def ::terms-privacy ::us/boolean) (s/def ::register-form - (s/keys :req-un [::password ::fullname ::email ::terms-privacy] + (s/keys :req-un [::password ::email] :opt-un [::invitation-token])) +(defn- handle-prepare-register-error + [form error] + (case (:code error) + :registration-disabled + (st/emit! (dm/error (tr "errors.registration-disabled"))) + + :email-has-permanent-bounces + (let [email (get @form [:data :email])] + (st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email)))) + + :email-already-exists + (swap! form assoc-in [:errors :email] + {:message "errors.email-already-exists"}) + + (st/emit! (dm/error (tr "errors.generic"))))) + +(defn- handle-prepare-register-success + [_form {:keys [token] :as result}] + (st/emit! (rt/nav :auth-register-validate {} {:token token}))) + (mf/defc register-form [{:keys [params] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) @@ -59,49 +79,20 @@ :initial initial) submitted? (mf/use-state false) - on-error - (mf/use-callback - (fn [form error] - (reset! submitted? false) - (case (:code error) - :registration-disabled - (rx/of (dm/error (tr "errors.registration-disabled"))) - - :email-has-permanent-bounces - (let [email (get @form [:data :email])] - (rx/of (dm/error (tr "errors.email-has-permanent-bounces" email)))) - - :email-already-exists - (swap! form assoc-in [:errors :email] - {:message "errors.email-already-exists"}) - - (rx/throw error)))) - - on-success - (mf/use-callback - (fn [form data] - (reset! submitted? false) - (if-let [token (:invitation-token data)] - (st/emit! (rt/nav :auth-verify-token {} {:token token})) - (st/emit! (rt/nav :auth-register-success {} {:email (:email data)}))))) - on-submit (mf/use-callback - (fn [form event] + (fn [form _event] (reset! submitted? true) - (let [data (with-meta (:clean-data @form) - {:on-error (partial on-error form) - :on-success (partial on-success form)})] - (st/emit! (du/register data)))))] + (let [params (:clean-data @form)] + (->> (rp/mutation :prepare-register-profile params) + (rx/finalize #(reset! submitted? false)) + (rx/subs (partial handle-prepare-register-success form) + (partial handle-prepare-register-error form)))))) + ] [:& fm/form {:on-submit on-submit :form form} - [:div.fields-row - [:& fm/input {:name :fullname - :tab-index "1" - :label (tr "auth.fullname") - :type "text"}]] [:div.fields-row [:& fm/input {:type "email" :name :email @@ -115,18 +106,141 @@ :label (tr "auth.password") :type "password"}]] + [:& fm/submit-button + {:label (tr "auth.register-submit") + :disabled @submitted?}]])) + +(mf/defc register-page + [{:keys [params] :as props}] + [:div.form-container + [:h1 (tr "auth.register-title")] + [:div.subtitle (tr "auth.register-subtitle")] + + (when cf/demo-warning + [:& demo-warning]) + + [:& register-form {:params params}] + + (when login/show-alt-login-buttons? + [:* + [:span.separator (tr "labels.or")] + + [:div.buttons + [:& login/login-buttons {:params params}]]]) + + [:div.links + [:div.link-entry + [:span (tr "auth.already-have-account") " "] + [:a {:on-click #(st/emit! (rt/nav :auth-login {} params)) + :tab-index "4"} + (tr "auth.login-here")]] + + (when cf/allow-demo-users + [:div.link-entry + [:span (tr "auth.create-demo-profile") " "] + [:a {:on-click #(st/emit! (du/create-demo-profile)) + :tab-index "5"} + (tr "auth.create-demo-account")]])]]) + +;; --- PAGE: register validation + +(defn- handle-register-error + [form error] + (case (:code error) + :registration-disabled + (st/emit! (dm/error (tr "errors.registration-disabled"))) + + :email-has-permanent-bounces + (let [email (get @form [:data :email])] + (st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email)))) + + :email-already-exists + (swap! form assoc-in [:errors :email] + {:message "errors.email-already-exists"}) + + (do + (println (:explain error)) + (st/emit! (dm/error (tr "errors.generic")))))) + +(defn- handle-register-success + [_form data] + (cond + (some? (:invitation-token data)) + (let [token (:invitation-token data)] + (st/emit! (rt/nav :auth-verify-token {} {:token token}))) + + (not= "penpot" (:auth-backend data)) + (st/emit! (du/login-from-register)) + + :else + (st/emit! (rt/nav :auth-register-success {} {:email (:email data)})))) + +(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) +(s/def ::accept-newsletter-subscription ::us/boolean) + +(s/def ::register-validate-form + (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] + :opt-un [::accept-newsletter-subscription])) + +(mf/defc register-validate-form + [{:keys [params] :as props}] + (let [initial (mf/use-memo + (mf/deps params) + (fn [] + (assoc params :accept-newsletter-subscription false))) + form (fm/use-form :spec ::register-validate-form + :initial initial) + submitted? (mf/use-state false) + + on-submit + (mf/use-callback + (fn [form _event] + (reset! submitted? true) + (let [params (:clean-data @form)] + (->> (rp/mutation :register-profile params) + (rx/finalize #(reset! submitted? false)) + (rx/subs (partial handle-register-success form) + (partial handle-register-error form)))))) + ] + + [:& fm/form {:on-submit on-submit + :form form} [:div.fields-row - [:& fm/input {:name :terms-privacy + [:& fm/input {:name :fullname + :tab-index "1" + :label (tr "auth.fullname") + :type "text"}]] + [:div.fields-row + [:& fm/input {:name :accept-terms-and-privacy :class "check-primary" - :tab-index "4" :label (tr "auth.terms-privacy-agreement") :type "checkbox"}]] + (when (contains? @cf/flags :show-newsletter-check-on-register-validation) + [:div.fields-row + [:& fm/input {:name :accept-newsletter-subscription + :class "check-primary" + :label (tr "auth.newsletter-subscription") + :type "checkbox"}]]) + [:& fm/submit-button {:label (tr "auth.register-submit") :disabled @submitted?}]])) -;; --- Register Page + +(mf/defc register-validate-page + [{:keys [params] :as props}] + [:div.form-container + [:h1 (tr "auth.register-title")] + [:div.subtitle (tr "auth.register-subtitle")] + + [:& register-validate-form {:params params}] + + [:div.links + [:div.link-entry + [:a {:on-click #(st/emit! (rt/nav :auth-register {} {})) + :tab-index "4"} + (tr "labels.go-back")]]]]) (mf/defc register-success-page [{:keys [params] :as props}] @@ -136,32 +250,3 @@ [:div.notification-text-email (:email params "")] [:div.notification-text (tr "auth.check-your-email")]]) -(mf/defc register-page - [{:keys [params] :as props}] - [:div.form-container - [:h1 (tr "auth.register-title")] - [:div.subtitle (tr "auth.register-subtitle")] - - (when cfg/demo-warning - [:& demo-warning]) - - [:& register-form {:params params}] - - [:div.links - [:div.link-entry - [:span (tr "auth.already-have-account") " "] - [:a {:on-click #(st/emit! (rt/nav :auth-login {} params)) - :tab-index "4"} - (tr "auth.login-here")]] - - (when cfg/allow-demo-users - [:div.link-entry - [:span (tr "auth.create-demo-profile") " "] - [:a {:on-click #(st/emit! (du/create-demo-profile)) - :tab-index "5"} - (tr "auth.create-demo-account")]]) - - [:& login/login-buttons {:params params}]]]) - - - diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 3e0bf5323d..119a4cd404 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -6,23 +6,16 @@ (ns app.main.ui.auth.verify-token (:require - [app.common.uuid :as uuid] [app.main.data.messages :as dm] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.auth.login :refer [login-page]] - [app.main.ui.auth.recovery :refer [recovery-page]] - [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-page]] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] [beicon.core :as rx] - [cljs.spec.alpha :as s] [rumext.alpha :as mf])) (defmulti handle-token (fn [token] (:iss token))) @@ -34,7 +27,7 @@ (st/emit! (du/login-from-token data)))) (defmethod handle-token :change-email - [data] + [_data] (let [msg (tr "dashboard.notifications.email-changed-successfully")] (ts/schedule 100 #(st/emit! (dm/success msg))) (st/emit! (rt/nav :settings-profile) @@ -57,7 +50,7 @@ (st/emit! (rt/nav :auth-register {} {:invitation-token token}))))) (defmethod handle-token :default - [tdata] + [_tdata] (st/emit! (rt/nav :auth-login) (dm/warn (tr "errors.unexpected-token")))) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index a4a98f35e6..3ae6832382 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -11,16 +11,13 @@ [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.time :as dt] - [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -105,7 +102,7 @@ :on-focus on-focus :on-change on-change}] (when (or @show-buttons? - (not (empty? @content))) + (seq @content)) [:div.buttons [:input.btn-primary {:type "button" :value "Post" :on-click on-submit}] [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])])) @@ -323,7 +320,7 @@ (mf/defc thread-bubble {::mf/wrap [mf/memo]} - [{:keys [thread zoom open? on-click] :as params}] + [{:keys [thread zoom on-click] :as params}] (let [pos (:position thread) pos-x (* (:x pos) zoom) pos-y (* (:y pos) zoom) diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index 54872e605d..89f22128bb 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -6,9 +6,9 @@ (ns app.main.ui.components.color-bullet (:require - [rumext.alpha :as mf] + [app.util.color :as uc] [app.util.i18n :as i18n :refer [tr]] - [app.util.color :as uc])) + [rumext.alpha :as mf])) (mf/defc color-bullet [{:keys [color on-click]}] (if (uc/multiple? color) @@ -31,7 +31,7 @@ (mf/defc color-name [{:keys [color size on-click on-double-click]}] (let [color (if (string? color) {:color color :opacity 1} color) - {:keys [name color opacity gradient]} color + {:keys [name color gradient]} color color-str (or name color (gradient-type->string (:type gradient)))] (when (or (not size) (= size :big)) [:span.color-text {:on-click #(when on-click (on-click %)) diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index e6019d5b36..254a6c25f2 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -6,62 +6,57 @@ (ns app.main.ui.components.color-input (:require - [app.common.data :as d] - [app.common.math :as math] - [app.common.spec :as us] - [app.common.uuid :as uuid] [app.util.color :as uc] [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.simple-math :as sm] - [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (mf/defc color-input {::mf/wrap-props false ::mf/forward-ref true} [props external-ref] - (let [value (obj/get props "value") + (let [value (obj/get props "value") on-change (obj/get props "onChange") ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). ;; So we use the external ref if provided, and the local one if not. local-ref (mf/use-ref) - ref (or external-ref local-ref) + ref (or external-ref local-ref) parse-value (mf/use-callback - (mf/deps ref) - (fn [] - (let [input-node (mf/ref-val ref)] - (try - (let [new-value (-> (dom/get-value input-node) - (uc/expand-hex) - (uc/parse-color) - (uc/prepend-hash))] - (dom/set-validity! input-node "") - new-value) - (catch :default _ - (dom/set-validity! input-node (tr "errors.invalid-color")) - nil))))) + (mf/deps ref) + (fn [] + (let [input-node (mf/ref-val ref)] + (try + (let [new-value (-> (dom/get-value input-node) + (uc/expand-hex) + (uc/parse-color) + (uc/prepend-hash))] + (dom/set-validity! input-node "") + new-value) + (catch :default _e + (dom/set-validity! input-node (tr "errors.invalid-color")) + nil))))) update-input (mf/use-callback - (mf/deps ref) - (fn [new-value] - (let [input-node (mf/ref-val ref)] - (dom/set-value! input-node (uc/remove-hash new-value))))) + (mf/deps ref) + (fn [new-value] + (let [input-node (mf/ref-val ref)] + (dom/set-value! input-node (uc/remove-hash new-value))))) apply-value (mf/use-callback - (mf/deps on-change update-input) - (fn [new-value] - (when new-value - (when on-change - (on-change new-value)) - (update-input new-value)))) + (mf/deps on-change update-input) + (fn [new-value] + (when new-value + (when on-change + (on-change new-value)) + (update-input new-value)))) handle-key-down (mf/use-callback @@ -79,12 +74,12 @@ handle-blur (mf/use-callback - (mf/deps parse-value apply-value update-input) - (fn [event] - (let [new-value (parse-value)] - (if new-value - (apply-value new-value) - (update-input value))))) + (mf/deps parse-value apply-value update-input) + (fn [_] + (let [new-value (parse-value)] + (if new-value + (apply-value new-value) + (update-input value))))) ;; list-id (str "colors-" (uuid/next)) @@ -97,6 +92,12 @@ (obj/set! "onKeyDown" handle-key-down) (obj/set! "onBlur" handle-blur))] + (mf/use-effect + (mf/deps value) + (fn [] + (when-let [node (mf/ref-val ref)] + (dom/set-value! node value)))) + [:* [:> :input props] ;; FIXME: this causes some weird interactions because of using apply-value diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index 075d99544b..e8198377dc 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -6,13 +6,12 @@ (ns app.main.ui.components.context-menu (:require - [rumext.alpha :as mf] - [goog.object :as gobj] [app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.icons :as i] - [app.common.uuid :as uuid] [app.util.dom :as dom] - [app.util.object :as obj])) + [app.util.object :as obj] + [goog.object :as gobj] + [rumext.alpha :as mf])) (mf/defc context-menu {::mf/wrap-props false} @@ -52,7 +51,7 @@ (- node-height) 0)] - (if (not= target-offset (:offset @local)) + (when (not= target-offset (:offset @local)) (swap! local assoc :offset target-offset)))))) enter-submenu @@ -105,7 +104,9 @@ {:class (dom/classnames :is-selected (and selected (= option-name selected))) :key option-name} (if-not sub-options - [:a.context-menu-action {:on-click option-handler} + [:a.context-menu-action {:on-click #(do (dom/stop-propagation %) + (on-close) + (option-handler %))} option-name] [:a.context-menu-action.submenu {:data-no-close true diff --git a/frontend/src/app/main/ui/components/copy_button.cljs b/frontend/src/app/main/ui/components/copy_button.cljs index 8436484410..6911ba5ae4 100644 --- a/frontend/src/app/main/ui/components/copy_button.cljs +++ b/frontend/src/app/main/ui/components/copy_button.cljs @@ -6,11 +6,11 @@ (ns app.main.ui.components.copy-button (:require - [beicon.core :as rx] - [rumext.alpha :as mf] - [app.util.webapi :as wapi] + [app.main.ui.icons :as i] [app.util.timers :as timers] - [app.main.ui.icons :as i])) + [app.util.webapi :as wapi] + [beicon.core :as rx] + [rumext.alpha :as mf])) (mf/defc copy-button [{:keys [data]}] (let [just-copied (mf/use-state false)] @@ -24,9 +24,8 @@ [:button.copy-button {:on-click #(when-not @just-copied - (do - (reset! just-copied true) - (wapi/write-to-clipboard data)))} + (reset! just-copied true) + (wapi/write-to-clipboard data))} (if @just-copied i/tick i/copy)])) diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index 54e33925fc..114617fa5b 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -1,11 +1,16 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + (ns app.main.ui.components.dropdown (:require - [rumext.alpha :as mf] - [app.common.uuid :as uuid] [app.util.dom :as dom] [app.util.keyboard :as kbd] [goog.events :as events] - [goog.object :as gobj]) + [goog.object :as gobj] + [rumext.alpha :as mf]) (:import goog.events.EventType)) (mf/defc dropdown' diff --git a/frontend/src/app/main/ui/components/editable_label.cljs b/frontend/src/app/main/ui/components/editable_label.cljs index 60e5feea4d..02907d5abd 100644 --- a/frontend/src/app/main/ui/components/editable_label.cljs +++ b/frontend/src/app/main/ui/components/editable_label.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.components.editable-label (:require [app.main.ui.icons :as i] - [app.util.data :refer [classnames]] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.timers :as timers] @@ -32,7 +31,7 @@ cancel-editing (fn [] (stop-editing) (when on-cancel (on-cancel))) - on-dbl-click (fn [e] (when (not disable-dbl-click?) (start-editing))) + on-dbl-click (fn [_] (when (not disable-dbl-click?) (start-editing))) on-key-up (fn [e] (cond (kbd/esc? e) diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs index 27fe4d897e..40c13e7702 100644 --- a/frontend/src/app/main/ui/components/editable_select.cljs +++ b/frontend/src/app/main/ui/components/editable_select.cljs @@ -6,13 +6,13 @@ (ns app.main.ui.components.editable-select (:require - [rumext.alpha :as mf] - [app.common.uuid :as uuid] [app.common.data :as d] + [app.common.uuid :as uuid] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.timers :as timers] - [app.main.ui.icons :as i] - [app.main.ui.components.dropdown :refer [dropdown]])) + [rumext.alpha :as mf])) (mf/defc editable-select [{:keys [value type options class on-change placeholder]}] (let [state (mf/use-state {:id (uuid/next) @@ -24,7 +24,7 @@ open-dropdown #(swap! state assoc :is-open? true) close-dropdown #(swap! state assoc :is-open? false) select-item (fn [value] - (fn [event] + (fn [_] (swap! state assoc :current-value value) (when on-change (on-change value)))) diff --git a/frontend/src/app/main/ui/components/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs index 54898407a7..8aa5c55b53 100644 --- a/frontend/src/app/main/ui/components/file_uploader.cljs +++ b/frontend/src/app/main/ui/components/file_uploader.cljs @@ -6,23 +6,26 @@ (ns app.main.ui.components.file-uploader (:require - [rumext.alpha :as mf] - [app.main.data.workspace :as dw] [app.main.store :as st] - [app.util.dom :as dom])) + [app.util.dom :as dom] + [rumext.alpha :as mf])) (mf/defc file-uploader - [{:keys [accept multi label-text label-class input-id input-ref on-selected] :as props}] + {::mf/forward-ref true} + [{:keys [accept multi label-text label-class input-id on-selected] :as props} input-ref] (let [opt-pick-one #(if multi % (first %)) - on-files-selected (fn [event] - (let [target (dom/get-target event)] - (st/emit! - (some-> target - (dom/get-files) - (opt-pick-one) - (on-selected))) - (dom/clean-value! target)))] + on-files-selected + (mf/use-callback + (mf/deps opt-pick-one) + (fn [event] + (let [target (dom/get-target event)] + (st/emit! + (some-> target + (dom/get-files) + (opt-pick-one) + (on-selected))) + (dom/clean-value! target))))] [:* (when label-text [:label {:for input-id :class-name label-class} label-text]) diff --git a/frontend/src/app/main/ui/components/fullscreen.cljs b/frontend/src/app/main/ui/components/fullscreen.cljs index baf9ceca2f..21ee1f29b7 100644 --- a/frontend/src/app/main/ui/components/fullscreen.cljs +++ b/frontend/src/app/main/ui/components/fullscreen.cljs @@ -8,7 +8,6 @@ (:require [app.util.dom :as dom] [app.util.webapi :as wapi] - [beicon.core :as rx] [rumext.alpha :as mf])) (def fullscreen-context @@ -21,7 +20,7 @@ change (mf/use-callback - (fn [event] + (fn [_] (let [val (dom/fullscreen?)] (reset! state val)))) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index b460d7b6c4..a6bbb02e3c 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -70,11 +70,11 @@ apply-value (mf/use-callback - (mf/deps on-change update-input) + (mf/deps on-change update-input value) (fn [new-value] - (when new-value - (when on-change - (on-change new-value)) + (when (and (some? new-value) (not= new-value value) (some? on-change)) + (on-change new-value)) + (when (some? new-value) (update-input new-value)))) set-delta @@ -132,7 +132,7 @@ handle-blur (mf/use-callback (mf/deps parse-value apply-value update-input) - (fn [event] + (fn [_] (let [new-value (parse-value)] (if new-value (apply-value new-value) diff --git a/frontend/src/app/main/ui/components/select.cljs b/frontend/src/app/main/ui/components/select.cljs index c88138c8f1..d651313177 100644 --- a/frontend/src/app/main/ui/components/select.cljs +++ b/frontend/src/app/main/ui/components/select.cljs @@ -6,10 +6,10 @@ (ns app.main.ui.components.select (:require - [rumext.alpha :as mf] [app.common.uuid :as uuid] + [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.main.ui.components.dropdown :refer [dropdown]])) + [rumext.alpha :as mf])) (mf/defc select [{:keys [default-value options class on-change]}] (let [state (mf/use-state {:id (uuid/next) @@ -17,9 +17,10 @@ :current-value default-value}) open-dropdown #(swap! state assoc :is-open? true) close-dropdown #(swap! state assoc :is-open? false) - select-item (fn [value] (fn [event] - (swap! state assoc :current-value value) - (when on-change (on-change value)))) + select-item (fn [value] + (fn [_] + (swap! state assoc :current-value value) + (when on-change (on-change value)))) as-key-value (fn [item] (if (map? item) [(:value item) (:label item)] [item item])) value->label (into {} (->> options (map as-key-value))) ] diff --git a/frontend/src/app/main/ui/components/tab_container.cljs b/frontend/src/app/main/ui/components/tab_container.cljs index 25a375fc43..126acff2dc 100644 --- a/frontend/src/app/main/ui/components/tab_container.cljs +++ b/frontend/src/app/main/ui/components/tab_container.cljs @@ -8,7 +8,7 @@ (:require [rumext.alpha :as mf])) (mf/defc tab-element - [{:keys [children id title]}] + [{:keys [children]}] [:div.tab-element [:div.tab-element-content children]]) diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index a622236018..a39f47b1dd 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -9,7 +9,6 @@ [app.main.data.modal :as modal] [app.main.store :as st] [app.main.ui.icons :as i] - [app.util.data :refer [classnames]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr t]] [app.util.keyboard :as k] @@ -57,10 +56,10 @@ (let [on-keydown (fn [event] (when (k/enter? event) - (do (dom/prevent-default event) - (dom/stop-propagation event) - (st/emit! (modal/hide)) - (on-accept props)))) + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (modal/hide)) + (on-accept props))) key (events/listen js/document EventType.KEYDOWN on-keydown)] #(events/unlistenByKey key)))) @@ -86,9 +85,9 @@ :on-click cancel-fn}]) [:input.accept-button - {:class (classnames :danger (= accept-style :danger) - :primary (= accept-style :primary)) + {:class (dom/classnames + :danger (= accept-style :danger) + :primary (= accept-style :primary)) :type "button" :value accept-label :on-click accept-fn}]]]]])) - diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index fe18ce753d..631b4b6f58 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -8,7 +8,6 @@ (:require [rumext.alpha :as mf])) -(def embed-ctx (mf/create-context false)) (def render-ctx (mf/create-context nil)) (def def-ctx (mf/create-context false)) diff --git a/frontend/src/app/main/ui/cursors.clj b/frontend/src/app/main/ui/cursors.clj index 5d0bedbe1b..0b63e5f579 100644 --- a/frontend/src/app/main/ui/cursors.clj +++ b/frontend/src/app/main/ui/cursors.clj @@ -6,9 +6,9 @@ (ns app.main.ui.cursors (:require + [app.common.uri :as u] [clojure.java.io :as io] - [cuerdas.core :as str] - [app.common.uri :as u])) + [cuerdas.core :as str])) (def cursor-folder "images/cursors") diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 5748bb54fe..c52be68919 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -6,9 +6,10 @@ (ns app.main.ui.cursors (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn]]) - (:require [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.util.timers :as ts])) + (:require + [app.util.timers :as ts] + [cuerdas.core :as str] + [rumext.alpha :as mf])) ;; Static cursors (def comments (cursor-ref :comments 0 2 20)) @@ -40,7 +41,7 @@ (mf/defc debug-preview {::mf/wrap-props false} - [props] + [] (let [rotation (mf/use-state 0)] (mf/use-effect (fn [] (ts/interval 100 #(reset! rotation inc)))) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index dc14df9cd6..9d2f00178f 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -6,29 +6,23 @@ (ns app.main.ui.dashboard (:require - [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] + [app.main.ui.dashboard.export] [app.main.ui.dashboard.files :refer [files-section]] + [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]] + [app.main.ui.dashboard.import] [app.main.ui.dashboard.libraries :refer [libraries-page]] [app.main.ui.dashboard.projects :refer [projects-section]] - [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]] [app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page]] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] - [app.util.router :as rt] [app.util.timers :as tm] - [beicon.core :as rx] - [cuerdas.core :as str] - [okulary.core :as l] [rumext.alpha :as mf])) (defn ^boolean uuid-str? @@ -37,9 +31,8 @@ (boolean (re-seq us/uuid-rx s)))) (defn- parse-params - [route profile] - (let [route-name (get-in route [:data :name]) - search-term (get-in route [:params :query :search-term]) + [route] + (let [search-term (get-in route [:params :query :search-term]) team-id (get-in route [:params :path :team-id]) project-id (get-in route [:params :path :project-id])] (cond-> @@ -87,7 +80,7 @@ [{:keys [route] :as props}] (let [profile (mf/deref refs/profile) section (get-in route [:data :name]) - params (parse-params route profile) + params (parse-params route) project-id (:project-id params) team-id (:team-id params) @@ -140,4 +133,3 @@ :section section :search-term search-term :team team}])])]])) - diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index 48703fdf25..ce2f8b4f53 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -6,29 +6,15 @@ (ns app.main.ui.dashboard.comments (:require - [okulary.core :as l] - [app.common.data :as d] - [app.common.spec :as us] - [app.config :as cfg] - [app.main.data.dashboard :as dd] - [app.main.data.workspace :as dw] - [app.main.data.workspace.comments :as dwcm] [app.main.data.comments :as dcm] + [app.main.data.workspace.comments :as dwcm] [app.main.refs :as refs] - [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.comments :as cmt] + [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.object :as obj] - [app.util.router :as rt] - [app.util.time :as dt] - [app.util.timers :as tm] - [beicon.core :as rx] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] + [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (mf/defc comments-section diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs new file mode 100644 index 0000000000..e89405d3ee --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/export.cljs @@ -0,0 +1,154 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.dashboard.export + (:require + [app.common.data :as d] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.worker :as uw] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(def ^:const options [:all :merge :detach]) + +(mf/defc export-entry + [{:keys [file]}] + + [:div.file-entry + {:class (dom/classnames + :loading (:loading? file) + :success (:export-success? file) + :error (:export-error? file))} + [:div.file-name + [:div.file-icon + (cond (:export-success? file) i/tick + (:export-error? file) i/close + (:loading? file) i/loader-pencil)] + + [:div.file-name-label (:name file)]]]) + +(defn mark-file-error [files file-id] + (->> files + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :export-error? true + :loading? false))))) + +(defn mark-file-success [files file-id] + (->> files + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :export-success? true + :loading? false))))) + +(mf/defc export-dialog + {::mf/register modal/components + ::mf/register-as :export} + [{:keys [team-id files has-libraries?]}] + (let [state (mf/use-state {:status :prepare + :files (->> files (mapv #(assoc % :loading? true)))}) + selected-option (mf/use-state :all) + + start-export + (fn [] + (swap! state assoc :status :exporting) + (->> (uw/ask-many! + {:cmd :export-file + :team-id team-id + :export-type @selected-option + :files (->> files (mapv :id))}) + (rx/delay-emit 1000) + (rx/subs + (fn [msg] + (when (= :error (:type msg)) + (swap! state update :files mark-file-error (:file-id msg))) + + (when (= :finish (:type msg)) + (swap! state update :files mark-file-success (:file-id msg)) + (dom/trigger-download-uri (:filename msg) (:mtype msg) (:uri msg))))))) + cancel-fn + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)))) + + accept-fn + (mf/use-callback + (mf/deps @selected-option) + (fn [event] + (dom/prevent-default event) + (start-export))) + + on-change-handler + (mf/use-callback + (fn [_ type] + (reset! selected-option type)))] + + (mf/use-effect + (fn [] + (when-not has-libraries? + ;; Start download automaticaly + (start-export)))) + + [:div.modal-overlay + [:div.modal-container.export-dialog + [:div.modal-header + [:div.modal-header-title + [:h2 (tr "dashboard.export.title")]] + + [:div.modal-close-button + {:on-click cancel-fn} i/close]] + + (cond + (= (:status @state) :prepare) + [:* + [:div.modal-content + [:p.explain (tr "dashboard.export.explain")] + [:p.detail (tr "dashboard.export.detail")] + + (for [type [:all :merge :detach]] + (let [selected? (= @selected-option type)] + [:div.export-option {:class (when selected? "selected")} + [:label.option-container + [:h3 (tr (str "dashboard.export.options." (d/name type) ".title"))] + [:p (tr (str "dashboard.export.options." (d/name type) ".message"))] + [:input {:type "radio" + :checked selected? + :on-change #(on-change-handler % type) + :name "export-option"}] + [:span {:class "option-radio-check"}]]]))] + + [:div.modal-footer + [:div.action-buttons + [:input.cancel-button + {:type "button" + :value (tr "labels.cancel") + :on-click cancel-fn}] + + [:input.accept-button + {:class "primary" + :type "button" + :value (tr "labels.continue") + :on-click accept-fn}]]]] + + (= (:status @state) :exporting) + [:* + [:div.modal-content + (for [file (:files @state)] + [:& export-entry {:file file}])] + + [:div.modal-footer + [:div.action-buttons + [:input.accept-button + {:class "primary" + :type "button" + :value (tr "labels.close") + :disabled (->> @state :files (some :loading?)) + :on-click cancel-fn}]]]])]])) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index f24a8d412f..31207a0b93 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -6,13 +6,14 @@ (ns app.main.ui.dashboard.file-menu (:require + [app.common.data :as d] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.context :as ctx] [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.context :as ctx] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] @@ -70,19 +71,19 @@ (:projects current-team)) on-new-tab - (fn [event] + (fn [_] (let [pparams {:project-id (:project-id file) :file-id (:id file)} qparams {:page-id (first (get-in file [:data :pages]))}] (st/emit! (rt/nav-new-window :workspace pparams qparams)))) on-duplicate - (fn [event] + (fn [_] (apply st/emit! (map dd/duplicate-file files)) (st/emit! (dm/success (tr "dashboard.success-duplicate-file")))) delete-fn - (fn [event] + (fn [_] (apply st/emit! (map dd/delete-file files)) (st/emit! (dm/success (tr "dashboard.success-delete-file")))) @@ -95,7 +96,7 @@ :title (tr "modals.delete-file-multi-confirm.title" file-count) :message (tr "modals.delete-file-multi-confirm.message" file-count) :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count) - :on-accept delete-fn})) + :on-accept delete-fn})) (st/emit! (modal/show {:type :confirm :title (tr "modals.delete-file-confirm.title") @@ -109,16 +110,18 @@ (st/emit! (dm/success (tr "dashboard.success-move-files"))) (st/emit! (dm/success (tr "dashboard.success-move-file")))) (if (or navigate? (not= team-id current-team-id)) - (st/emit! (dd/go-to-files project-id)) + (st/emit! (dd/go-to-files team-id project-id)) (st/emit! (dd/fetch-recent-files) (dd/clear-selected-files)))) on-move (fn [team-id project-id] - (let [data {:ids (set (map :id files)) - :project-id project-id} - mdata {:on-success #(on-move-success team-id project-id)}] - (st/emitf (dd/move-files (with-meta data mdata))))) + (let [params {:ids (set (map :id files)) + :project-id project-id}] + (fn [] + (st/emit! (dd/move-files + (with-meta params + {:on-success #(on-move-success team-id project-id)})))))) add-shared (st/emitf (dd/set-file-shared (assoc file :is-shared true))) @@ -150,7 +153,26 @@ :hint (tr "modals.remove-shared-confirm.hint") :cancel-label :omit :accept-label (tr "modals.remove-shared-confirm.accept") - :on-accept del-shared})))] + :on-accept del-shared}))) + + on-export-files + (mf/use-callback + (mf/deps files current-team-id) + (fn [_] + (->> (rx/from files) + (rx/flat-map + (fn [file] + (->> (rp/query :file-libraries {:file-id (:id file)}) + (rx/map #(assoc file :has-libraries? (d/not-empty? %)))))) + (rx/reduce conj []) + (rx/subs + (fn [files] + (st/emit! + (modal/show + {:type :export + :team-id current-team-id + :has-libraries? (->> files (some :has-libraries?)) + :files files})))))))] (mf/use-effect (fn [] @@ -176,6 +198,7 @@ [[(tr "dashboard.duplicate-multi" file-count) on-duplicate] (when (or (seq current-projects) (seq other-teams)) [(tr "dashboard.move-to-multi" file-count) nil sub-options]) + [(tr "dashboard.export-multi" file-count) on-export-files] [:separator] [(tr "labels.delete-multi-files" file-count) on-delete]] @@ -187,6 +210,7 @@ (if (:is-shared file) [(tr "dashboard.remove-shared") on-del-shared] [(tr "dashboard.add-shared") on-add-shared]) + [(tr "dashboard.export-single") on-export-files] [:separator] [(tr "labels.delete") on-delete]])] diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index a430269645..b982d8ceda 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.dashboard.files (:require [app.main.data.dashboard :as dd] - [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] @@ -16,20 +15,18 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] - [app.util.router :as rt] - [okulary.core :as l] [rumext.alpha :as mf])) (mf/defc header - [{:keys [team project] :as props}] + [{:keys [project] :as props}] (let [local (mf/use-state {:menu-open false :edition false}) - project-id (:id project) - team-id (:id team) - on-menu-click - (mf/use-callback #(swap! local assoc :menu-open true)) + (mf/use-callback + (fn [event] + (let [position (dom/get-client-position event)] + (dom/prevent-default event) + (swap! local assoc :menu-open true :menu-pos position)))) on-menu-close (mf/use-callback #(swap! local assoc :menu-open false)) @@ -47,7 +44,14 @@ (mf/deps project) (fn [event] (dom/prevent-default event) - (st/emit! (dd/create-file {:project-id (:id project)}))))] + (st/emit! (dd/create-file {:project-id (:id project)})))) + + on-import + (mf/use-callback + (mf/deps (:id project)) + (fn [] + (st/emit! (dd/fetch-files {:project-id (:id project)}) + (dd/clear-selected-files))))] [:header.dashboard-header @@ -63,20 +67,27 @@ [:div.dashboard-title [:h1 {:on-double-click on-edit} (:name project)] - [:div.icon {:on-click on-menu-click} - i/actions] [:& project-menu {:project project :show? (:menu-open @local) + :left (- (:x (:menu-pos @local)) 180) + :top (:y (:menu-pos @local)) :on-edit on-edit - :on-menu-close on-menu-close}] - [:div.icon.pin-icon - {:class (when (:is-pinned project) "active") - :on-click toggle-pin} - (if (:is-pinned project) - i/pin-fill - i/pin)]])) - [:a.btn-secondary.btn-small {:on-click on-create-clicked} - (tr "dashboard.new-file")]])) + :on-menu-close on-menu-close + :on-import on-import}]])) + [:div.dashboard-header-actions + [:a.btn-secondary.btn-small {:on-click on-create-clicked} + (tr "dashboard.new-file")] + + [:div.icon.pin-icon.tooltip.tooltip-bottom + {:class (when (:is-pinned project) "active") + :on-click toggle-pin :alt (tr "dashboard.pin-unpin")} + (if (:is-pinned project) + i/pin-fill + i/pin)] + + [:div.icon.tooltip.tooltip-bottom + {:on-click on-menu-click :alt (tr "dashboard.options")} + i/actions]]])) (mf/defc files-section [{:keys [project team] :as props}] @@ -99,6 +110,6 @@ [:* [:& header {:team team :project project}] [:section.dashboard-container - [:& grid {:id (:id project) + [:& grid {:project-id (:id project) :files files}]]])) diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index bda48caadf..72d65eb27f 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -6,27 +6,22 @@ (ns app.main.ui.dashboard.fonts (:require - [app.common.data :as d] [app.common.media :as cm] - [app.common.uuid :as uuid] - [app.main.data.dashboard :as dd] [app.main.data.fonts :as df] [app.main.data.modal :as modal] - [app.main.ui.components.file-uploader :refer [file-uploader]] - [app.main.ui.components.context-menu :refer [context-menu]] - [app.main.store :as st] - [app.main.repo :as rp] [app.main.refs :as refs] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.logging :as log] [app.util.keyboard :as kbd] - [app.util.router :as rt] - [app.util.webapi :as wa] - [cuerdas.core :as str] + [app.util.logging :as log] + ;; [app.util.router :as rt] [beicon.core :as rx] - [okulary.core :as l] + [cuerdas.core :as str] [rumext.alpha :as mf])) (log/set-level! :trace) @@ -47,29 +42,29 @@ (mf/defc header {::mf/wrap [mf/memo]} [{:keys [section team] :as props}] - (let [go-fonts - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)}))) + ;; (let [go-fonts + ;; (mf/use-callback + ;; (mf/deps team) + ;; (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)}))) - go-providers - (mf/use-callback - (mf/deps team) - (st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))] + ;; go-providers + ;; (mf/use-callback + ;; (mf/deps team) + ;; (st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))] - (use-set-page-title team section) + (use-set-page-title team section) - [:header.dashboard-header - [:div.dashboard-title - [:h1 (tr "labels.fonts")]] - [:nav - #_[:ul - [:li {:class (when (= section :fonts) "active")} - [:a {:on-click go-fonts} (tr "labels.custom-fonts")]] - [:li {:class (when (= section :providers) "active")} - [:a {:on-click go-providers} (tr "labels.font-providers")]]]] + [:header.dashboard-header + [:div.dashboard-title + [:h1 (tr "labels.fonts")]] + [:nav + #_[:ul + [:li {:class (when (= section :fonts) "active")} + [:a {:on-click go-fonts} (tr "labels.custom-fonts")]] + [:li {:class (when (= section :providers) "active")} + [:a {:on-click go-providers} (tr "labels.font-providers")]]]] - [:div]])) + [:div]]) (mf/defc font-variant-display-name [{:keys [variant]}] @@ -88,9 +83,6 @@ on-click (mf/use-callback #(dom/click (mf/ref-val input-ref))) - font-key-fn - (mf/use-callback (juxt :font-family :font-weight :font-style)) - on-selected (mf/use-callback (mf/deps team installed-fonts) @@ -144,7 +136,7 @@ [:& file-uploader {:input-id "font-upload" :accept cm/str-font-types :multi true - :input-ref input-ref + :ref input-ref :on-selected on-selected}]]] [:* @@ -190,7 +182,7 @@ (reset! state (dom/get-target-val event))) on-save - (fn [event] + (fn [_] (let [font-family @state] (when-not (str/blank? font-family) (st/emit! (df/update-font @@ -204,7 +196,7 @@ (on-save event))) on-cancel - (fn [event] + (fn [_] (reset! edit? false) (reset! state (:font-family font))) @@ -221,8 +213,7 @@ :title (tr "modals.delete-font.title") :message (tr "modals.delete-font.message") :accept-label (tr "labels.delete") - :on-accept (fn [props] - (delete-font-fn))}))) + :on-accept (fn [_props] (delete-font-fn))}))) on-delete-variant (fn [id] @@ -231,7 +222,7 @@ :title (tr "modals.delete-font-variant.title") :message (tr "modals.delete-font-variant.message") :accept-label (tr "labels.delete") - :on-accept (fn [props] + :on-accept (fn [_props] (delete-variant-fn id))})))] [:div.font-item.table-row @@ -276,7 +267,7 @@ (mf/defc installed-fonts - [{:keys [team fonts] :as props}] + [{:keys [fonts] :as props}] (let [sterm (mf/use-state "") matches? diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 3afa25470d..3bd48a1ceb 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -7,15 +7,13 @@ (ns app.main.ui.dashboard.grid (:require [app.common.math :as mth] - [app.common.uuid :as uuid] - [app.config :as cfg] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] - [app.main.data.modal :as modal] [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.file-menu :refer [file-menu]] + [app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.icons :as i] [app.main.worker :as wrk] @@ -23,12 +21,10 @@ [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.router :as rt] [app.util.time :as dt] [app.util.timers :as ts] [app.util.webapi :as wapi] [beicon.core :as rx] - [cuerdas.core :as str] [rumext.alpha :as mf])) ;; --- Grid Item Thumbnail @@ -59,7 +55,7 @@ (str (tr "ds.updated-at" time)))) (defn create-counter-element - [element file-count] + [_element file-count] (let [counter-el (dom/create-element "div")] (dom/set-property! counter-el "class" "drag-counter") (dom/set-text! counter-el (str file-count)) @@ -215,25 +211,71 @@ [:div.text (tr "dashboard.loading-files")]]) (mf/defc grid - [{:keys [id opts files] :as props}] - [:section.dashboard-grid - (cond - (nil? files) - [:& loading-placeholder] + [{:keys [files project-id] :as props}] + (let [dragging? (mf/use-state false) - (seq files) - [:div.grid-row - (for [item files] - [:& grid-item - {:file item - :key (:id item) - :navigate? true}])] + on-finish-import + (mf/use-callback + (fn [] + (st/emit! (dd/fetch-files {:project-id project-id}) + (dd/clear-selected-files)))) - :else - [:& empty-placeholder])]) + import-files (use-import-file project-id on-finish-import) + + on-drag-enter + (mf/use-callback + (fn [e] + (when (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e) + (reset! dragging? true)))) + + on-drag-over + (mf/use-callback + (fn [e] + (when (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e)))) + + on-drag-leave + (mf/use-callback + (fn [e] + (when-not (dnd/from-child? e) + (reset! dragging? false)))) + + + on-drop + (mf/use-callback + (fn [e] + (when (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e) + (reset! dragging? false) + (import-files (.-files (.-dataTransfer e))))))] + + [:section.dashboard-grid {:on-drag-enter on-drag-enter + :on-drag-over on-drag-over + :on-drag-leave on-drag-leave + :on-drop on-drop} + (cond + (nil? files) + [:& loading-placeholder] + + (seq files) + [:div.grid-row + (when @dragging? + [:div.grid-item]) + (for [item files] + [:& grid-item + {:file item + :key (:id item) + :navigate? true}])] + + :else + [:& empty-placeholder])])) (mf/defc line-grid-row - [{:keys [files team-id selected-files on-load-more dragging?] :as props}] + [{:keys [files selected-files on-load-more dragging?] :as props}] (let [rowref (mf/use-ref) width (mf/use-state nil) @@ -288,12 +330,19 @@ (tr "dashboard.show-all-files")]])])) (mf/defc line-grid - [{:keys [project-id team-id opts files on-load-more] :as props}] + [{:keys [project-id team-id files on-load-more] :as props}] (let [dragging? (mf/use-state false) - selected-files (mf/deref refs/dashboard-selected-files) selected-project (mf/deref refs/dashboard-selected-project) + on-finish-import + (mf/use-callback + (fn [] + (st/emit! (dd/fetch-recent-files) + (dd/clear-selected-files)))) + + import-files (use-import-file project-id on-finish-import) + on-drag-enter (mf/use-callback (mf/deps selected-project) @@ -303,13 +352,20 @@ (when-not (or (dnd/from-child? e) (dnd/broken-event? e)) (when (not= selected-project project-id) - (reset! dragging? true)))))) + (reset! dragging? true)))) + + (when (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e) + (reset! dragging? true)))) on-drag-over (mf/use-callback - (fn [e] - (when (dnd/has-type? e "penpot/files") - (dom/prevent-default e)))) + (fn [e] + (when (or (dnd/has-type? e "penpot/files") + (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e)))) on-drag-leave (mf/use-callback @@ -327,12 +383,19 @@ (mf/use-callback (mf/deps files selected-files) (fn [e] - (reset! dragging? false) - (when (not= selected-project project-id) - (let [data {:ids (into #{} (keys selected-files)) - :project-id project-id} - mdata {:on-success on-drop-success}] - (st/emit! (dd/move-files (with-meta data mdata)))))))] + (when (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (dom/prevent-default e) + (reset! dragging? false) + (import-files (.-files (.-dataTransfer e)))) + + (when (dnd/has-type? e "penpot/files") + (reset! dragging? false) + (when (not= selected-project project-id) + (let [data {:ids (into #{} (keys selected-files)) + :project-id project-id} + mdata {:on-success on-drop-success}] + (st/emit! (dd/move-files (with-meta data mdata))))))))] [:section.dashboard-grid {:on-drag-enter on-drag-enter :on-drag-over on-drag-over diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs new file mode 100644 index 0000000000..b8b083e04b --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -0,0 +1,315 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.dashboard.import + (:require + [app.common.data :as d] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.icons :as i] + [app.main.worker :as uw] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.logging :as log] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(log/set-level! :debug) + +(def ^:const emit-delay 1000) + +(defn use-import-file + [project-id on-finish-import] + (mf/use-callback + (mf/deps project-id on-finish-import) + (fn [files] + (when files + (let [files (->> files + (mapv + (fn [file] + {:name (.-name file) + :uri (dom/create-uri file)})))] + (st/emit! (modal/show + {:type :import + :project-id project-id + :files files + :on-finish-import on-finish-import}))))))) + +(mf/defc import-form + {::mf/forward-ref true} + [{:keys [project-id on-finish-import]} external-ref] + + (let [on-file-selected (use-import-file project-id on-finish-import)] + [:form.import-file + [:& file-uploader {:accept ".penpot" + :multi true + :ref external-ref + :on-selected on-file-selected}]])) + +(defn update-file [files file-id new-name] + (->> files + (mapv + (fn [file] + (cond-> file + (= (:file-id file) file-id) + (assoc :name new-name)))))) + +(defn remove-file [files file-id] + (->> files + (mapv + (fn [file] + (cond-> file + (= (:file-id file) file-id) + (assoc :deleted? true)))))) + +(defn set-analyze-error + [files uri] + (->> files + (mapv (fn [file] + (cond-> file + (= uri (:uri file)) + (assoc :status :analyze-error)))))) + +(defn set-analyze-result [files uri data] + (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) + replace-file + (fn [file] + (if (and (= uri (:uri file) ) + (= (:status file) :analyzing)) + (->> (:files data) + (remove (comp existing-files? first) ) + (mapv (fn [[file-id file-data]] + (-> file-data + (assoc :file-id file-id + :status :ready + :uri uri))))) + [file]))] + (into [] (mapcat replace-file) files))) + +(defn mark-files-importing [files] + (->> files + (filter #(= :ready (:status %))) + (mapv #(assoc % :status :importing)))) + +(defn update-status [files file-id status] + (->> files + (mapv (fn [file] + (cond-> file + (= file-id (:file-id file)) + (assoc :status status)))))) + +(mf/defc import-entry + [{:keys [state file editing?]}] + + (let [loading? (or (= :analyzing (:status file)) + (= :importing (:status file))) + load-success? (= :import-success (:status file)) + analyze-error? (= :analyze-error (:status file)) + import-error? (= :import-error (:status file)) + ready? (= :ready (:status file)) + is-shared? (:shared file) + + handle-edit-key-press + (mf/use-callback + (fn [e] + (when (or (kbd/enter? e) (kbd/esc? e)) + (dom/prevent-default e) + (dom/stop-propagation e) + (dom/blur! (dom/get-target e))))) + + handle-edit-blur + (mf/use-callback + (mf/deps file) + (fn [e] + (let [value (dom/get-target-val e)] + (swap! state #(-> (assoc % :editing nil) + (update :files update-file (:file-id file) value)))))) + + handle-edit-entry + (mf/use-callback + (mf/deps file) + (fn [] + (swap! state assoc :editing (:file-id file)))) + + handle-remove-entry + (mf/use-callback + (mf/deps file) + (fn [] + (swap! state update :files remove-file (:file-id file))))] + + [:div.file-entry + {:class (dom/classnames + :loading loading? + :success load-success? + :error (or import-error? analyze-error?) + :editable (and ready? (not editing?)))} + + [:div.file-name + [:div.file-icon + (cond loading? i/loader-pencil + ready? i/logo-icon + load-success? i/tick + import-error? i/close + analyze-error? i/close)] + + (if editing? + [:div.file-name-edit + [:input {:type "text" + :auto-focus true + :default-value (:name file) + :on-key-press handle-edit-key-press + :on-blur handle-edit-blur}]] + + [:div.file-name-label (:name file) (when is-shared? i/library)]) + + [:div.edit-entry-buttons + [:button {:on-click handle-edit-entry} i/pencil] + [:button {:on-click handle-remove-entry} i/trash]]] + + (when analyze-error? + [:div.error-message + (tr "dashboard.import.analyze-error")]) + + (when import-error? + [:div.error-message + (tr "dashboard.import.import-error")]) + + [:div.linked-libraries + (for [library-id (:libraries file)] + (let [library-data (->> @state :files (d/seek #(= library-id (:file-id %)))) + error? (or (:deleted? library-data) (:import-error library-data))] + (when (some? library-data) + [:div.linked-library-tag {:class (when error? "error")} + (if error? i/unchain i/chain) (:name library-data)])))]])) + +(mf/defc import-dialog + {::mf/register modal/components + ::mf/register-as :import} + [{:keys [project-id files on-finish-import]}] + (let [state (mf/use-state + {:status :analyzing + :editing nil + :files (->> files + (mapv #(assoc % :status :analyzing)))}) + + analyze-import + (mf/use-callback + (fn [files] + (->> (uw/ask-many! + {:cmd :analyze-import + :files (->> files (mapv :uri))}) + (rx/delay-emit emit-delay) + (rx/subs + (fn [{:keys [uri data error] :as msg}] + (log/debug :msg msg) + (if (some? error) + (swap! state update :files set-analyze-error uri) + (swap! state update :files set-analyze-result uri data))))))) + + import-files + (mf/use-callback + (fn [project-id files] + (->> (uw/ask-many! + {:cmd :import-files + :project-id project-id + :files files}) + (rx/delay-emit emit-delay) + (rx/subs + (fn [{:keys [file-id status] :as msg}] + (log/debug :msg msg) + (swap! state update :files update-status file-id status)))))) + + handle-cancel + (mf/use-callback + (mf/deps (:editing @state)) + (fn [event] + (when (nil? (:editing @state)) + (dom/prevent-default event) + (st/emit! (modal/hide))))) + + handle-continue + (mf/use-callback + (mf/deps project-id (:files @state)) + (fn [event] + (dom/prevent-default event) + (let [files (->> @state :files (filterv #(= :ready (:status %))))] + (import-files project-id files)) + + (swap! state + (fn [state] + (-> state + (assoc :status :importing) + (update :files mark-files-importing)))))) + + handle-accept + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (when on-finish-import (on-finish-import)))) + + success-files (->> @state :files (filter #(= (:status %) :import-success)) count) + pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0) + pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)] + + (mf/use-effect + (fn [] + (let [sub (analyze-import files)] + #(rx/dispose! sub)))) + + (mf/use-effect + (fn [] + ;; dispose uris when the component is umount + #(doseq [file files] + (dom/revoke-uri (:uri file))))) + + [:div.modal-overlay + [:div.modal-container.import-dialog + [:div.modal-header + [:div.modal-header-title + [:h2 (tr "dashboard.import")]] + + [:div.modal-close-button + {:on-click handle-cancel} i/close]] + + [:div.modal-content + (when (and (= :importing (:status @state)) + (not pending-import?)) + [:div.feedback-banner + [:div.icon i/checkbox-checked] + [:div.message (tr "dashboard.import.import-message" success-files)]]) + + (for [file (->> (:files @state) (filterv (comp not :deleted?)))] + (let [editing? (and (some? (:file-id file)) + (= (:file-id file) (:editing @state)))] + [:& import-entry {:state state + :file file + :editing? editing?}]))] + + [:div.modal-footer + [:div.action-buttons + [:input.cancel-button + {:type "button" + :value (tr "labels.cancel") + :on-click handle-cancel}] + + (when (= :analyzing (:status @state)) + [:input.accept-button + {:class "primary" + :type "button" + :value (tr "labels.continue") + :disabled pending-analysis? + :on-click handle-continue}]) + + (when (= :importing (:status @state)) + [:input.accept-button + {:class "primary" + :type "button" + :value (tr "labels.accept") + :disabled pending-import? + :on-click handle-accept}])]]]])) diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index e61d4bcd4d..7130c97216 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -7,14 +7,11 @@ (ns app.main.ui.dashboard.libraries (:require [app.main.data.dashboard :as dd] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid]] - [app.main.ui.icons :as i] - [app.main.refs :as refs] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] - [okulary.core :as l] [rumext.alpha :as mf])) (mf/defc libraries-page diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index 8ea8b4132b..f6923cc783 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -10,17 +10,17 @@ [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.refs :as refs] - [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.context :as ctx] + [app.main.ui.dashboard.import :as udi] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [beicon.core :as rx] [rumext.alpha :as mf])) (mf/defc project-menu - [{:keys [project show? on-edit on-menu-close top left] :as props}] + [{:keys [project show? on-edit on-menu-close top left on-import] :as props}] (assert (some? project) "missing `project` prop") (assert (boolean? show?) "missing `show?` prop") (assert (fn? on-edit) "missing `on-edit` prop") @@ -59,7 +59,7 @@ (dd/move-project (with-meta data mdata))))) delete-fn - (fn [event] + (fn [_] (st/emit! (dm/success (tr "dashboard.success-delete-project")) (dd/delete-project project) (dd/go-to-projects (:team-id project)))) @@ -71,22 +71,42 @@ :title (tr "modals.delete-project-confirm.title") :message (tr "modals.delete-project-confirm.message") :accept-label (tr "modals.delete-project-confirm.accept") - :on-accept delete-fn}))] + :on-accept delete-fn})) - [:& context-menu {:on-close on-menu-close - :show show? - :fixed? (or (not= top 0) (not= left 0)) - :min-width? true - :top top - :left left - :options [(when-not (:is-default project) - [(tr "labels.rename") on-edit]) - [(tr "dashboard.duplicate") on-duplicate] - [(tr "dashboard.pin-unpin") toggle-pin] - (when (seq teams) - [(tr "dashboard.move-to") nil - (for [team teams] - [(:name team) (on-move (:id team))])]) - [:separator] - [(tr "labels.delete") on-delete]]}])) + + file-input (mf/use-ref nil) + + on-import-files + (mf/use-callback + (fn [] + (dom/click (mf/ref-val file-input)))) + + on-finish-import + (mf/use-callback + (fn [] + (when (some? on-import) (on-import))))] + + [:* + [:& udi/import-form {:ref file-input + :project-id (:id project) + :on-finish-import on-finish-import}] + [:& context-menu + {:on-close on-menu-close + :show show? + :fixed? (or (not= top 0) (not= left 0)) + :min-width? true + :top top + :left left + :options [(when-not (:is-default project) + [(tr "labels.rename") on-edit]) + [(tr "dashboard.duplicate") on-duplicate] + [(tr "dashboard.pin-unpin") toggle-pin] + (when (seq teams) + [(tr "dashboard.move-to") nil + (for [team teams] + [(:name team) (on-move (:id team))])]) + (when (some? on-import) + [(tr "dashboard.import") on-import-files]) + [:separator] + [(tr "labels.delete") on-delete]]}]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 342d377f00..768599da77 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -6,8 +6,6 @@ (ns app.main.ui.dashboard.projects (:require - [app.common.exceptions :as ex] - [app.main.constants :as c] [app.main.data.dashboard :as dd] [app.main.refs :as refs] [app.main.store :as st] @@ -16,8 +14,7 @@ [app.main.ui.dashboard.project-menu :refer [project-menu]] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.keyboard :as kbd] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.time :as dt] [okulary.core :as l] @@ -30,6 +27,7 @@ [:header.dashboard-header [:div.dashboard-title [:h1 (tr "dashboard.projects-title")]] + [:a.btn-secondary.btn-small {:on-click create} (tr "dashboard.new-project")]])) @@ -37,7 +35,6 @@ [{:keys [project first? files] :as props}] (let [locale (mf/deref i18n/locale) - project-id (:id project) team-id (:team-id project) file-count (or (:count project) 0) @@ -96,17 +93,16 @@ (fn [] (let [mdata {:on-success on-file-created} params {:project-id (:id project)}] - (st/emit! (dd/create-file (with-meta params mdata))))))] + (st/emit! (dd/create-file (with-meta params mdata)))))) + + on-import + (mf/use-callback + (fn [] + (st/emit! (dd/fetch-recent-files) + (dd/clear-selected-files))))] [:div.dashboard-project-row {:class (when first? "first")} [:div.project - (when-not (:is-default project) - [:span.pin-icon - {:class (when (:is-pinned project) "active") - :on-click toggle-pin} - (if (:is-pinned project) - i/pin-fill - i/pin)]) (if (:edition? @local) [:& inline-edition {:content (:name project) :on-end on-edit}] @@ -116,13 +112,13 @@ (tr "labels.drafts") (:name project))]) - (when (:menu-open @local) - [:& project-menu {:project project - :show? (:menu-open @local) - :left (:x (:menu-pos @local)) - :top (:y (:menu-pos @local)) - :on-edit on-edit-open - :on-menu-close on-menu-close}]) + [:& project-menu {:project project + :show? (:menu-open @local) + :left (:x (:menu-pos @local)) + :top (:y (:menu-pos @local)) + :on-edit on-edit-open + :on-menu-close on-menu-close + :on-import on-import}] [:span.info (str file-count " files")] (when (> file-count 0) @@ -130,9 +126,21 @@ (dt/timeago {:locale locale}))] [:span.recent-files-row-title-info (str ", " time)])) - [:a.btn-secondary.btn-small - {:on-click create-file} - (tr "dashboard.new-file")]] + (when-not (:is-default project) + [:span.pin-icon.tooltip.tooltip-bottom + {:class (when (:is-pinned project) "active") + :on-click toggle-pin :alt (tr "dashboard.pin-unpin")} + (if (:is-pinned project) + i/pin-fill + i/pin)]) + + [:a.btn-secondary.btn-small.tooltip.tooltip-bottom + {:on-click create-file :alt (tr "dashboard.new-file")} + i/close] + + [:a.btn-secondary.btn-small.tooltip.tooltip-bottom + {:on-click on-menu-click :alt (tr "dashboard.options")} + i/actions]] [:& line-grid {:project-id (:id project) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index d3c6ed9ed6..56bc7439f0 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -13,7 +13,6 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [okulary.core :as l] [rumext.alpha :as mf])) (mf/defc search-page diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index b35edc7e0d..07416cb045 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -9,13 +9,11 @@ [app.common.data :as d] [app.common.spec :as us] [app.config :as cfg] - [app.main.data.comments :as dcm] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.refs :as refs] - [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.forms :as fm] @@ -24,23 +22,17 @@ [app.main.ui.dashboard.project-menu :refer [project-menu]] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] - [app.util.avatars :as avatars] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] [app.util.object :as obj] [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-project - [{:keys [item team-id selected?] :as props}] + [{:keys [item selected?] :as props}] (let [dstate (mf/deref refs/dashboard-local) selected-files (:selected-files dstate) selected-project (:selected-project dstate) @@ -111,7 +103,7 @@ on-drop (mf/use-callback (mf/deps item selected-files) - (fn [e] + (fn [_] (swap! local assoc :dragging? false) (when (not= selected-project (:id item)) (let [data {:ids selected-files @@ -157,7 +149,7 @@ on-search-blur (mf/use-callback - (fn [event] + (fn [_] (reset! focused? false))) on-search-change @@ -170,7 +162,7 @@ on-clear-click (mf/use-callback (mf/deps team-id) - (fn [event] + (fn [_] (let [search-input (dom/get-element "search-input")] (dom/clean-value! search-input) (dom/focus! search-input) @@ -189,7 +181,7 @@ :on-change on-search-change :ref #(when % (set! (.-value %) search-term))}] - (if (or @focused? (not (empty? search-term))) + (if (or @focused? (seq search-term)) [:div.clear-search {:on-click on-clear-click} i/close] @@ -199,9 +191,8 @@ i/search])])) (mf/defc teams-selector-dropdown - [{:keys [team profile] :as props}] - (let [show-dropdown? (mf/use-state false) - teams (mf/deref refs/teams) + [{:keys [profile] :as props}] + (let [teams (mf/deref refs/teams) on-create-clicked (mf/use-callback @@ -246,7 +237,7 @@ on-cancel (st/emitf (modal/hide)) on-accept - (fn [event] + (fn [_] (let [member-id (get-in @form [:clean-data :member-id])] (accept member-id)))] @@ -291,9 +282,6 @@ members-map (mf/deref refs/dashboard-team-members) members (vals members-map) - on-create-clicked - (st/emitf (modal/show :team-form {})) - on-rename-clicked (st/emitf (modal/show :team-form {:team team})) @@ -358,9 +346,7 @@ (mf/defc sidebar-team-switch [{:keys [team profile] :as props}] - (let [show-dropdown? (mf/use-state false) - - show-team-opts-ddwn? (mf/use-state false) + (let [show-team-opts-ddwn? (mf/use-state false) show-teams-ddwn? (mf/use-state false)] [:div.sidebar-team-switch diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 168a944c25..4c87aa4d12 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -7,10 +7,8 @@ (ns app.main.ui.dashboard.team (:require [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.spec :as us] [app.config :as cfg] - [app.main.constants :as c] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] @@ -23,15 +21,12 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] - [app.util.time :as dt] [cljs.spec.alpha :as s] - [okulary.core :as l] [rumext.alpha :as mf])) (mf/defc header {::mf/wrap [mf/memo]} - [{:keys [section team] :as props}] + [{:keys [section] :as props}] (let [go-members (st/emitf (dd/go-to-team-members)) go-settings (st/emitf (dd/go-to-team-settings)) invite-member (st/emitf (modal/show {:type ::invite-member})) @@ -43,7 +38,7 @@ [:h1 (cond members-section? (tr "labels.members") settings-section? (tr "labels.settings") - nil)]] + :else nil)]] [:nav [:ul [:li {:class (when members-section? "active")} @@ -96,7 +91,7 @@ (dm/error (tr "errors.member-is-muted")) (and (= :validation type) - (= :email-has-permanent-bounces)) + (= :email-has-permanent-bounces code)) (dm/error (tr "errors.email-has-permanent-bounces" email)) :else @@ -136,7 +131,7 @@ set-owner-fn (partial set-role :owner) set-admin (partial set-role :admin) set-editor (partial set-role :editor) - set-viewer (partial set-role :viewer) + ;; set-viewer (partial set-role :viewer) set-owner (st/emitf (modal/show @@ -242,7 +237,7 @@ :members-map members-map}]]])) (mf/defc team-settings-page - [{:keys [team profile] :as props}] + [{:keys [team] :as props}] (let [finput (mf/use-ref) members-map (mf/deref refs/dashboard-team-members) @@ -285,7 +280,7 @@ [:img {:src (cfg/resolve-team-photo-url team)}] [:& file-uploader {:accept "image/jpeg,image/png" :multi false - :input-ref finput + :ref finput :on-selected on-file-selected}]]] [:div.block.owner-block diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index e4426f0d85..1a936b26fb 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.dashboard.team-form (:require - [app.common.data :as d] [app.common.spec :as us] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] @@ -14,13 +13,10 @@ [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]] - [app.util.object :as obj] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (s/def ::name ::us/not-empty-string) @@ -28,20 +24,20 @@ (s/keys :req-un [::name])) (defn- on-create-success - [form response] + [_form response] (let [msg "Team created successfuly"] (st/emit! (dm/success msg) (modal/hide) (rt/nav :dashboard-projects {:team-id (:id response)})))) (defn- on-update-success - [form response] + [_form _response] (let [msg "Team created successfuly"] (st/emit! (dm/success msg) (modal/hide)))) (defn- on-error - [form response] + [form _response] (let [id (get-in @form [:clean-data :id])] (if id (rx/of (dm/error "Error on updating team.")) @@ -62,20 +58,19 @@ (st/emit! (dd/update-team (with-meta team mdata)) (modal/hide)))) -(mf/defc team-form-modal - {::mf/register modal/components +(defn- on-submit + [form _] + (let [data (:clean-data @form)] + (if (:id data) + (on-update-submit form) + (on-create-submit form)))) + +(mf/defc team-form-modal {::mf/register modal/components ::mf/register-as :team-form} [{:keys [team] :as props}] - (let [form (fm/use-form :spec ::team-form - :initial (or team {})) - - on-submit - (mf/use-callback - (mf/deps team) - (if team - (partial on-update-submit form) - (partial on-create-submit form)))] - + (let [initial (mf/use-memo (fn [] (or team {}))) + form (fm/use-form :spec ::team-form + :initial initial)] [:div.modal-overlay [:div.modal-container.team-form-modal [:& fm/form {:form form :on-submit on-submit} @@ -91,7 +86,7 @@ [:div.modal-content.generic-form [:& fm/input {:type "text" - :auto-focus true + :auto-focus? true :form form :name :name :label (tr "labels.create-team.placeholder")}]] diff --git a/frontend/src/app/main/ui/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs index ee82788f08..bee408f97b 100644 --- a/frontend/src/app/main/ui/handoff.cljs +++ b/frontend/src/app/main/ui/handoff.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.handoff (:require - [app.common.exceptions :as ex] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] [app.main.refs :as refs] @@ -15,15 +14,12 @@ [app.main.ui.handoff.render :refer [render-frame-svg]] [app.main.ui.handoff.right-sidebar :refer [right-sidebar]] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as i] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t tr]] [app.util.keyboard :as kbd] - [beicon.core :as rx] [goog.events :as events] - [okulary.core :as l] [rumext.alpha :as mf]) (:import goog.events.EventType)) diff --git a/frontend/src/app/main/ui/handoff/attributes.cljs b/frontend/src/app/main/ui/handoff/attributes.cljs index 51a59746fa..cdce9e76d1 100644 --- a/frontend/src/app/main/ui/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/handoff/attributes.cljs @@ -6,18 +6,18 @@ (ns app.main.ui.handoff.attributes (:require - [rumext.alpha :as mf] - [app.util.i18n :as i18n] [app.common.geom.shapes :as gsh] - [app.main.ui.handoff.exports :refer [exports]] - [app.main.ui.handoff.attributes.layout :refer [layout-panel]] - [app.main.ui.handoff.attributes.fill :refer [fill-panel]] - [app.main.ui.handoff.attributes.stroke :refer [stroke-panel]] - [app.main.ui.handoff.attributes.shadow :refer [shadow-panel]] [app.main.ui.handoff.attributes.blur :refer [blur-panel]] + [app.main.ui.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.handoff.attributes.image :refer [image-panel]] + [app.main.ui.handoff.attributes.layout :refer [layout-panel]] + [app.main.ui.handoff.attributes.shadow :refer [shadow-panel]] + [app.main.ui.handoff.attributes.stroke :refer [stroke-panel]] + [app.main.ui.handoff.attributes.svg :refer [svg-panel]] [app.main.ui.handoff.attributes.text :refer [text-panel]] - [app.main.ui.handoff.attributes.svg :refer [svg-panel]])) + [app.main.ui.handoff.exports :refer [exports]] + [app.util.i18n :as i18n] + [rumext.alpha :as mf])) (def type->options {:multiple [:fill :stroke :image :text :shadow :blur] diff --git a/frontend/src/app/main/ui/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/handoff/attributes/blur.cljs index 9d3ca9d969..67f3b9194b 100644 --- a/frontend/src/app/main/ui/handoff/attributes/blur.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/blur.cljs @@ -6,12 +6,11 @@ (ns app.main.ui.handoff.attributes.blur (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.util.i18n :refer [t]] - [app.main.ui.icons :as i] + [app.main.ui.components.copy-button :refer [copy-button]] [app.util.code-gen :as cg] - [app.main.ui.components.copy-button :refer [copy-button]])) + [app.util.i18n :refer [t]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn has-blur? [shape] (:blur shape)) diff --git a/frontend/src/app/main/ui/handoff/attributes/common.cljs b/frontend/src/app/main/ui/handoff/attributes/common.cljs index 03c3410740..2171979a50 100644 --- a/frontend/src/app/main/ui/handoff/attributes/common.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/common.cljs @@ -6,19 +6,16 @@ (ns app.main.ui.handoff.attributes.common (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [okulary.core :as l] [app.common.math :as mth] + [app.main.store :as st] + [app.main.ui.components.color-bullet :refer [color-bullet color-name]] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :refer [t] :as i18n] - [app.util.color :as uc] - [app.util.code-gen :as cg] - [app.util.webapi :as wapi] - [app.main.ui.icons :as i] - [app.main.store :as st] - [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.components.color-bullet :refer [color-bullet color-name]])) + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) (def file-colors-ref diff --git a/frontend/src/app/main/ui/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/handoff/attributes/fill.cljs index fdb15cf5c9..a2b6301dc0 100644 --- a/frontend/src/app/main/ui/handoff/attributes/fill.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/fill.cljs @@ -6,13 +6,12 @@ (ns app.main.ui.handoff.attributes.fill (:require - [rumext.alpha :as mf] - [app.util.i18n :refer [t]] - [app.util.color :as uc] - [app.main.ui.icons :as i] - [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.util.code-gen :as cg] + [app.util.color :as uc] + [app.util.i18n :refer [tr]] + [rumext.alpha :as mf])) (def fill-attributes [:fill-color :fill-color-gradient]) @@ -36,7 +35,7 @@ {:to-prop "background" :format #(uc/color->background (shape->color shape))})) -(mf/defc fill-block [{:keys [shape locale]}] +(mf/defc fill-block [{:keys [shape]}] (let [color-format (mf/use-state :hex) color (shape->color shape)] @@ -46,16 +45,15 @@ :copy-data (copy-data shape)}])) (mf/defc fill-panel - [{:keys [shapes locale]}] + [{:keys [shapes]}] (let [shapes (->> shapes (filter has-color?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.fill")] + [:div.attributes-block-title-text (tr "handoff.attributes.fill")] (when (= (count shapes) 1) [:& copy-button {:data (copy-data (first shapes))}])] (for [shape shapes] [:& fill-block {:key (str "fill-block-" (:id shape)) - :shape shape - :locale locale}])]))) + :shape shape}])]))) diff --git a/frontend/src/app/main/ui/handoff/attributes/image.cljs b/frontend/src/app/main/ui/handoff/attributes/image.cljs index 238b7dea89..e798188682 100644 --- a/frontend/src/app/main/ui/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/image.cljs @@ -6,19 +6,17 @@ (ns app.main.ui.handoff.attributes.image (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] [app.config :as cfg] - [app.util.i18n :refer [t]] - [app.util.dom :as dom] - [app.main.ui.icons :as i] + [app.main.ui.components.copy-button :refer [copy-button]] [app.util.code-gen :as cg] - [app.main.ui.components.copy-button :refer [copy-button]])) + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [rumext.alpha :as mf])) (defn has-image? [shape] - (and (= (:type shape) :image))) + (= (:type shape) :image)) -(mf/defc image-panel [{:keys [shapes locale]}] +(mf/defc image-panel [{:keys [shapes]}] (let [shapes (->> shapes (filter has-image?))] (for [shape shapes] [:div.attributes-block {:key (str "image-" (:id shape))} @@ -27,12 +25,12 @@ [:img {:src (cfg/resolve-file-media (-> shape :metadata))}]]] [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.image.width")] + [:div.attributes-label (tr "handoff.attributes.image.width")] [:div.attributes-value (-> shape :metadata :width) "px"] [:& copy-button {:data (cg/generate-css-props shape :width)}]] [:div.attributes-unit-row - [:div.attributes-label (t locale "handoff.attributes.image.height")] + [:div.attributes-label (tr "handoff.attributes.image.height")] [:div.attributes-value (-> shape :metadata :height) "px"] [:& copy-button {:data (cg/generate-css-props shape :height)}]] @@ -44,4 +42,4 @@ (str name "." extension) name) :href (cfg/resolve-file-media (-> shape :metadata))} - (t locale "handoff.attributes.image.download")])]))) + (tr "handoff.attributes.image.download")])]))) diff --git a/frontend/src/app/main/ui/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/handoff/attributes/layout.cljs index 2a55eff534..4704bbb0a9 100644 --- a/frontend/src/app/main/ui/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/layout.cljs @@ -6,13 +6,12 @@ (ns app.main.ui.handoff.attributes.layout (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.util.i18n :refer [t]] [app.common.math :as mth] - [app.main.ui.icons :as i] + [app.main.ui.components.copy-button :refer [copy-button]] [app.util.code-gen :as cg] - [app.main.ui.components.copy-button :refer [copy-button]])) + [app.util.i18n :refer [t]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (def properties [:width :height :x :y :radius :rx :r1]) diff --git a/frontend/src/app/main/ui/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/handoff/attributes/shadow.cljs index 9d757d7880..892f466794 100644 --- a/frontend/src/app/main/ui/handoff/attributes/shadow.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/shadow.cljs @@ -6,15 +6,13 @@ (ns app.main.ui.handoff.attributes.shadow (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] [app.common.data :as d] - [app.util.i18n :refer [t]] - [app.util.code-gen :as cg] - [app.main.ui.icons :as i] - [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.util.code-gen :as cg] + [app.util.i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn has-shadow? [shape] (:shadow shape)) @@ -33,26 +31,25 @@ {:to-prop "box-shadow" :format #(cg/shadow->css shadow)})) -(mf/defc shadow-block [{:keys [shape locale shadow]}] - (let [color-format (mf/use-state :hex) - copy-data (shadow-copy-data shadow)] +(mf/defc shadow-block [{:keys [shadow]}] + (let [color-format (mf/use-state :hex)] [:div.attributes-shadow-block [:div.attributes-shadow-row - [:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (t locale))] + [:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (tr))] [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")] + [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-x")] [:div.attributes-value (str (:offset-x shadow))]] [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-y")] + [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-y")] [:div.attributes-value (str (:offset-y shadow))]] [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.blur")] + [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.blur")] [:div.attributes-value (str (:blur shadow))]] [:div.attributes-shadow - [:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.spread")] + [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.spread")] [:div.attributes-value (str (:spread shadow))]] [:& copy-button {:data (shadow-copy-data shadow)}]] @@ -61,12 +58,12 @@ :format @color-format :on-change-format #(reset! color-format %)}]])) -(mf/defc shadow-panel [{:keys [shapes locale]}] +(mf/defc shadow-panel [{:keys [shapes]}] (let [shapes (->> shapes (filter has-shadow?))] (when (seq shapes) [:div.attributes-block [:div.attributes-block-title - [:div.attributes-block-title-text (t locale "handoff.attributes.shadow")] + [:div.attributes-block-title-text (tr "handoff.attributes.shadow")] (when (= (count shapes) 1) [:& copy-button {:data (shape-copy-data (first shapes))}])] @@ -74,5 +71,4 @@ (for [shape shapes] (for [shadow (:shadow shape)] [:& shadow-block {:shape shape - :locale locale :shadow shadow}]))]]))) diff --git a/frontend/src/app/main/ui/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/handoff/attributes/stroke.cljs index 075daae30d..ddd5a3263c 100644 --- a/frontend/src/app/main/ui/handoff/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/stroke.cljs @@ -6,16 +6,15 @@ (ns app.main.ui.handoff.attributes.stroke (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] [app.common.data :as d] [app.common.math :as mth] - [app.util.i18n :refer [t]] - [app.util.color :as uc] - [app.main.ui.icons :as i] - [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]] + [app.util.code-gen :as cg] + [app.util.color :as uc] + [app.util.i18n :refer [t]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn shape->color [shape] {:color (:stroke-color shape) diff --git a/frontend/src/app/main/ui/handoff/attributes/svg.cljs b/frontend/src/app/main/ui/handoff/attributes/svg.cljs index 033ad073fe..a2308c5986 100644 --- a/frontend/src/app/main/ui/handoff/attributes/svg.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/svg.cljs @@ -6,14 +6,14 @@ (ns app.main.ui.handoff.attributes.svg (:require - [rumext.alpha :as mf] - [app.common.data :as d] - [cuerdas.core :as str] - [app.util.i18n :refer [tr]] #_[app.common.math :as mth] #_[app.main.ui.icons :as i] #_[app.util.code-gen :as cg] - [app.main.ui.components.copy-button :refer [copy-button]])) + [app.common.data :as d] + [app.main.ui.components.copy-button :refer [copy-button]] + [app.util.i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn map->css [attr] @@ -47,7 +47,7 @@ [{:keys [shapes]}] (let [shape (first shapes)] - (when (and (:svg-attrs shape) (not (empty? (:svg-attrs shape)))) + (when (seq (:svg-attrs shape)) [:div.attributes-block [:div.attributes-block-title [:div.attributes-block-title-text (tr "workspace.sidebar.options.svg-attrs.title")]] diff --git a/frontend/src/app/main/ui/handoff/attributes/text.cljs b/frontend/src/app/main/ui/handoff/attributes/text.cljs index f49276d6f4..694c3525b8 100644 --- a/frontend/src/app/main/ui/handoff/attributes/text.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/text.cljs @@ -11,11 +11,9 @@ [app.main.store :as st] [app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.handoff.attributes.common :refer [color-row]] - [app.main.ui.icons :as i] - [app.util.i18n :refer [tr]] [app.util.code-gen :as cg] [app.util.color :as uc] - [app.util.webapi :as wapi] + [app.util.i18n :refer [tr]] [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -68,7 +66,7 @@ ([style & properties] (cg/generate-css-props style properties params))) -(mf/defc typography-block [{:keys [shape text style full-style]}] +(mf/defc typography-block [{:keys [text style full-style]}] (let [typography-library-ref (mf/use-memo (mf/deps (:typography-ref-file style)) (make-typographies-library-ref (:typography-ref-file style))) @@ -77,7 +75,6 @@ file-typographies (mf/deref file-typographies-ref) color-format (mf/use-state :hex) - color (shape->color style) typography (get (or typography-library file-typographies) (:typography-ref-id style))] @@ -163,14 +160,11 @@ m1)) (mf/defc text-block [{:keys [shape]}] - (let [font (cg/search-text-attrs (:content shape) - (keys txt/default-text-attrs)) - style-text-blocks (->> (keys txt/default-text-attrs) + (let [style-text-blocks (->> (keys txt/default-text-attrs) (cg/parse-style-text-blocks (:content shape)) - (remove (fn [[style text]] (str/empty? (str/trim text)))) - (mapv (fn [[style text]] (vector (merge txt/default-text-attrs style) text)))) + (remove (fn [[_ text]] (str/empty? (str/trim text)))) + (mapv (fn [[style text]] (vector (merge txt/default-text-attrs style) text))))] - font (merge txt/default-text-attrs font)] (for [[idx [full-style text]] (map-indexed vector style-text-blocks)] (let [previus-style (first (nth style-text-blocks (dec idx) nil)) style (remove-equal-values full-style previus-style) diff --git a/frontend/src/app/main/ui/handoff/code.cljs b/frontend/src/app/main/ui/handoff/code.cljs index 2bf92cc3d4..b6eab07445 100644 --- a/frontend/src/app/main/ui/handoff/code.cljs +++ b/frontend/src/app/main/ui/handoff/code.cljs @@ -7,17 +7,16 @@ (ns app.main.ui.handoff.code (:require ["js-beautify" :as beautify] - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.util.i18n :as i18n] - [app.util.dom :as dom] - [app.util.code-gen :as cg] - [app.main.ui.icons :as i] [app.common.geom.shapes :as gsh] + [app.main.ui.components.code-block :refer [code-block]] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.components.code-block :refer [code-block]])) + [app.main.ui.icons :as i] + [app.util.code-gen :as cg] + [app.util.dom :as dom] + [cuerdas.core :as str] + [rumext.alpha :as mf])) -(defn generate-markup-code [type shapes] +(defn generate-markup-code [_type shapes] (let [frame (dom/query js/document "#svg-frame") markup-shape (fn [shape] @@ -42,8 +41,6 @@ [{:keys [shapes frame on-expand]}] (let [style-type (mf/use-state "css") markup-type (mf/use-state "svg") - - locale (mf/deref i18n/locale) shapes (->> shapes (map #(gsh/translate-to-frame % frame))) diff --git a/frontend/src/app/main/ui/handoff/exports.cljs b/frontend/src/app/main/ui/handoff/exports.cljs index 6462a81014..670a8f312f 100644 --- a/frontend/src/app/main/ui/handoff/exports.cljs +++ b/frontend/src/app/main/ui/handoff/exports.cljs @@ -6,21 +6,19 @@ (ns app.main.ui.handoff.exports (:require - [rumext.alpha :as mf] - [beicon.core :as rx] - [app.util.i18n :refer [t] :as i18n] - [app.common.geom.shapes :as gsh] - [app.main.ui.icons :as i] [app.common.data :as d] - [app.util.dom :as dom] - [app.main.store :as st] [app.main.data.messages :as dm] - [app.main.ui.workspace.sidebar.options.menus.exports :as we])) + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.menus.exports :as we] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [beicon.core :as rx] + [rumext.alpha :as mf])) (mf/defc exports [{:keys [shape page-id file-id] :as props}] - (let [locale (mf/deref i18n/locale) - exports (mf/use-state (:exports shape [])) + (let [exports (mf/use-state (:exports shape [])) loading? (mf/use-state false) on-download @@ -34,8 +32,8 @@ (fn [{:keys [status body] :as response}] (js/console.log status body) (if (= status 200) - (we/trigger-download (:name shape) body) - (st/emit! (dm/error (t locale "errors.unexpected-error"))))) + (dom/trigger-download (:name shape) body) + (st/emit! (dm/error (tr "errors.unexpected-error"))))) (constantly nil) (fn [] (swap! loading? not)))))) @@ -90,7 +88,7 @@ [:div.element-set.exports-options [:div.element-set-title - [:span (t locale "workspace.options.export")] + [:span (tr "workspace.options.export")] [:div.add-page {:on-click add-export} i/close]] (when (seq @exports) @@ -124,6 +122,6 @@ :class (dom/classnames :btn-disabled @loading?) :disabled @loading?} (if @loading? - (t locale "workspace.options.exporting-object") - (t locale "workspace.options.export-object"))]])])) + (tr "workspace.options.exporting-object") + (tr "workspace.options.export-object"))]])])) diff --git a/frontend/src/app/main/ui/handoff/left_sidebar.cljs b/frontend/src/app/main/ui/handoff/left_sidebar.cljs index e378a4d5b1..12d4ea0318 100644 --- a/frontend/src/app/main/ui/handoff/left_sidebar.cljs +++ b/frontend/src/app/main/ui/handoff/left_sidebar.cljs @@ -7,11 +7,10 @@ (ns app.main.ui.handoff.left-sidebar (:require [app.common.data :as d] - [app.common.uuid :as uuid] [app.main.data.viewer :as dv] [app.main.store :as st] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]] + [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name]] [app.util.dom :as dom] [app.util.keyboard :as kbd] [okulary.core :as l] @@ -29,7 +28,7 @@ (l/derived st/state))) (mf/defc layer-item - [{:keys [index item selected objects disable-collapse?] :as props}] + [{:keys [item selected objects disable-collapse?] :as props}] (let [id (:id item) selected? (contains? selected id) item-ref (mf/use-ref nil) diff --git a/frontend/src/app/main/ui/handoff/render.cljs b/frontend/src/app/main/ui/handoff/render.cljs index 6d28af9066..d4d8a89e4e 100644 --- a/frontend/src/app/main/ui/handoff/render.cljs +++ b/frontend/src/app/main/ui/handoff/render.cljs @@ -7,50 +7,46 @@ (ns app.main.ui.handoff.render "The main container for a frame in handoff mode" (:require - [rumext.alpha :as mf] - [app.util.object :as obj] - [app.util.dom :as dom] [app.common.data :as d] - [app.common.pages :as cp] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] - [app.main.refs :as refs] - [app.main.store :as st] + [app.common.pages :as cp] [app.main.data.viewer :as dv] - [app.main.ui.shapes.filters :as filters] + [app.main.store :as st] + [app.main.ui.handoff.selection-feedback :refer [selection-feedback]] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] - [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] + [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.text :as text] - [app.main.ui.handoff.selection-feedback :refer [selection-feedback]] - [app.main.ui.shapes.shape :refer [shape-container]])) + [app.util.dom :as dom] + [app.util.object :as obj] + [rumext.alpha :as mf])) (declare shape-container-factory) (defn handle-hover-shape [{:keys [type id]} hover?] #(when-not (#{:group :frame} type) - (do - (dom/prevent-default %) - (dom/stop-propagation %) - (st/emit! (dv/hover-shape id hover?))))) + (dom/prevent-default %) + (dom/stop-propagation %) + (st/emit! (dv/hover-shape id hover?)))) (defn select-shape [{:keys [type id]}] (fn [event] (when-not (#{:group :frame} type) - (do - (dom/stop-propagation event) - (dom/prevent-default event) - (cond - (.-shiftKey event) - (st/emit! (dv/toggle-selection id)) + (dom/stop-propagation event) + (dom/prevent-default event) + (cond + (.-shiftKey event) + (st/emit! (dv/toggle-selection id)) - :else - (st/emit! (dv/select-shape id))))))) + :else + (st/emit! (dv/select-shape id)))))) (defn shape-wrapper-factory [component] @@ -114,8 +110,8 @@ (defn svg-raw-container-factory [objects] (let [shape-container (shape-container-factory objects) - svg-raw-shape (svg-raw/svg-raw-shape shape-container) - svg-raw-wrapper (shape-wrapper-factory svg-raw-shape)] + svg-raw-shape (svg-raw/svg-raw-shape shape-container) + svg-raw-wrapper (shape-wrapper-factory svg-raw-shape)] (mf/fnc group-container {::mf/wrap-props false} [props] @@ -127,7 +123,7 @@ [:> svg-raw-wrapper props])))) (defn shape-container-factory - [objects show-interactions?] + [objects] (let [path-wrapper (shape-wrapper-factory path/path-shape) text-wrapper (shape-wrapper-factory text/text-shape) rect-wrapper (shape-wrapper-factory rect/rect-shape) diff --git a/frontend/src/app/main/ui/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/handoff/right_sidebar.cljs index 5773417523..390aeaf421 100644 --- a/frontend/src/app/main/ui/handoff/right_sidebar.cljs +++ b/frontend/src/app/main/ui/handoff/right_sidebar.cljs @@ -6,16 +6,16 @@ (ns app.main.ui.handoff.right-sidebar (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [app.util.i18n :refer [t] :as i18n] [app.common.data :as d] [app.main.store :as st] - [app.main.ui.icons :as i] [app.main.ui.components.tab-container :refer [tab-container tab-element]] - [app.main.ui.workspace.sidebar.layers :refer [element-icon]] [app.main.ui.handoff.attributes :refer [attributes]] - [app.main.ui.handoff.code :refer [code]])) + [app.main.ui.handoff.code :refer [code]] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.layers :refer [element-icon]] + [app.util.i18n :refer [t] :as i18n] + [okulary.core :as l] + [rumext.alpha :as mf])) (defn make-selected-shapes-iref [] diff --git a/frontend/src/app/main/ui/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/handoff/selection_feedback.cljs index 890accbe28..70df269238 100644 --- a/frontend/src/app/main/ui/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/handoff/selection_feedback.cljs @@ -6,15 +6,11 @@ (ns app.main.ui.handoff.selection-feedback (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [okulary.core :as l] - [app.common.data :as d] - [app.common.math :as mth] [app.common.geom.shapes :as gsh] - [app.common.geom.point :as gpt] [app.main.store :as st] - [app.main.ui.measurements :refer [selection-guides size-display measurement]])) + [app.main.ui.measurements :refer [selection-guides size-display measurement]] + [okulary.core :as l] + [rumext.alpha :as mf])) ;; ------------------------------------------------ ;; CONSTANTS @@ -67,7 +63,7 @@ ;; COMPONENTS ;; ------------------------------------------------ -(mf/defc selection-rect [{:keys [frame selrect zoom]}] +(mf/defc selection-rect [{:keys [selrect zoom]}] (let [{:keys [x y width height]} selrect selection-rect-width (/ selection-rect-width zoom)] [:g.selection-rect @@ -75,7 +71,7 @@ :y y :width width :height height - :style {:fill "transparent" + :style {:fill "none" :stroke select-color :stroke-width selection-rect-width}}]])) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 5b0b0645e8..257b3b2393 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,21 +7,14 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require - [app.common.spec :as us] [app.main.data.shortcuts :as dsc] [app.main.store :as st] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.logging :as log] - [app.util.object :as obj] [app.util.timers :as ts] - [app.util.transit :as t] - [app.util.webapi :as wapi] [beicon.core :as rx] - [cljs.spec.alpha :as s] - [goog.events :as events] - [rumext.alpha :as mf]) - (:import goog.events.EventType)) + [rumext.alpha :as mf])) (log/set-level! :warn) @@ -99,10 +92,7 @@ cleanup (fn [] - ;; (js/console.log "cleanup" (:name data)) - (when-let [subscr (:subscr @state)] - ;; (js/console.log "unsubscribing" (:name data)) - (rx/unsub! (:subscr @state))) + (some-> (:subscr @state) rx/unsub!) (swap! state (fn [state] (-> state (cancel-timer) @@ -119,9 +109,8 @@ (fn [event] (if disabled (dom/prevent-default event) - (let [target (dom/get-target event)] + (do (dom/stop-propagation event) - ;; (dnd/trace event data "drag-start") (dnd/set-data! event data-type data) (dnd/set-drag-image! event (invisible-image)) (dnd/set-allowed-effect! event "move") @@ -225,3 +214,28 @@ (fn [] (mf/set-ref-val! ref value))) (mf/ref-val ref))) + +(defn use-equal-memo + [val] + (let [ref (mf/use-ref nil)] + (when-not (= (mf/ref-val ref) val) + (mf/set-ref-val! ref val)) + (mf/ref-val ref))) + +(defn- ssr? + "Checks if the current environment is run under a SSR context" + [] + (try + (not js/window) + (catch :default _e + ;; When exception accessing window we're in ssr + true))) + +(defn use-effect-ssr + "Use effect that handles SSR" + [deps effect-fn] + + (if (ssr?) + (let [ret (effect-fn)] + (when (fn? ret) (ret))) + (mf/use-effect deps effect-fn))) diff --git a/frontend/src/app/main/ui/icons.clj b/frontend/src/app/main/ui/icons.clj index c44da08e5f..a31a1fa92d 100644 --- a/frontend/src/app/main/ui/icons.clj +++ b/frontend/src/app/main/ui/icons.clj @@ -9,8 +9,9 @@ (defmacro icon-xref [id] - (let [href (str "#icon-" (name id))] + (let [href (str "#icon-" (name id)) + class (str "icon-" (name id))] `(rumext.alpha/html - [:svg {:width 500 :height 500} + [:svg {:width 500 :height 500 :class ~class} [:use {:xlinkHref ~href}]]))) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 598ba3b480..b4b4cd7126 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) UXBOX Labs SL (ns app.main.ui.icons + (:refer-clojure :exclude [import mask]) (:require-macros [app.main.ui.icons :refer [icon-xref]]) (:require [rumext.alpha :as mf])) @@ -53,6 +54,7 @@ (def icon-set (icon-xref :icon-set)) (def icon-verify (icon-xref :icon-verify)) (def image (icon-xref :image)) +(def import (icon-xref :import)) (def infocard (icon-xref :infocard)) (def interaction (icon-xref :interaction)) (def layers (icon-xref :layers)) @@ -60,9 +62,9 @@ (def libraries (icon-xref :libraries)) (def library (icon-xref :library)) (def line (icon-xref :line)) +(def line-height (icon-xref :line-height)) (def listing-enum (icon-xref :listing-enum)) (def listing-thumbs (icon-xref :listing-thumbs)) -(def line-height (icon-xref :line-height)) (def loader (icon-xref :loader)) (def lock (icon-xref :lock)) (def logo (icon-xref :uxbox-logo)) @@ -161,7 +163,7 @@ (mf/defc debug-icons-preview {::mf/wrap-props false} - [props] + [] [:section.debug-icons-preview (for [[key val] (sort-by first (ns-publics 'app.main.ui.icons))] (when (not= key 'debug-icons-preview) diff --git a/frontend/src/app/main/ui/loader.cljs b/frontend/src/app/main/ui/loader.cljs index 503d59be1e..424e071dd2 100644 --- a/frontend/src/app/main/ui/loader.cljs +++ b/frontend/src/app/main/ui/loader.cljs @@ -6,13 +6,13 @@ (ns app.main.ui.loader (:require - [rumext.alpha :as mf] + [app.main.store :as st] [app.main.ui.icons :as i] - [app.main.store :as st])) + [rumext.alpha :as mf])) ;; --- Component (mf/defc loader [] (when (mf/deref st/loader) - [:div.loader-content i/loader])) + [:div.loader-content i/loader-pencil])) diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 75dce1ac17..4fc319c1f1 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -11,9 +11,7 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.uuid :as uuid] - [app.main.store :as st] [cuerdas.core :as str] - [okulary.core :as l] [rumext.alpha :as mf])) ;; ------------------------------------------------ @@ -82,10 +80,10 @@ (and (neg? ss) (> ss se))) (conj [ from-s (+ from-s ss) ]) - (or (and (neg? se) (<= ss se))) + (and (neg? se) (<= ss se)) (conj [ from-s (+ from-s se) ]) - (or (and (pos? es) (<= es ee))) + (and (pos? es) (<= es ee)) (conj [ from-e (+ from-e es) ]) (or (and (pos? ee) (neg? es)) @@ -97,7 +95,7 @@ ;; COMPONENTS ;; ------------------------------------------------ -(mf/defc size-display [{:keys [type selrect zoom]}] +(mf/defc size-display [{:keys [selrect zoom]}] (let [{:keys [x y width height]} selrect size-label (str/fmt "%s x %s" (mth/round width) (mth/round height)) @@ -127,7 +125,6 @@ (mf/defc distance-display-pill [{:keys [x y zoom distance bounds]}] (let [distance-pill-width (/ distance-pill-width zoom) distance-pill-height (/ distance-pill-height zoom) - distance-line-stroke (/ distance-line-stroke zoom) font-size (/ font-size zoom) text-padding (/ 3 zoom) distance-border-radius (/ distance-border-radius zoom) @@ -169,7 +166,7 @@ :font-size font-size}} distance]])) -(mf/defc selection-rect [{:keys [frame selrect zoom]}] +(mf/defc selection-rect [{:keys [selrect zoom]}] (let [{:keys [x y width height]} selrect selection-rect-width (/ selection-rect-width zoom)] [:g.selection-rect @@ -177,11 +174,11 @@ :y y :width width :height height - :style {:fill "transparent" + :style {:fill "none" :stroke hover-color :stroke-width selection-rect-width}}]])) -(mf/defc distance-display [{:keys [type from to zoom frame bounds]}] +(mf/defc distance-display [{:keys [from to zoom bounds]}] (let [fixed-x (if (gsh/fully-contained? from to) (+ (:x to) (/ (:width to) 2)) (+ (:x from) (/ (:width from) 2))) diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs index 70aecb9b2b..aff6c6f81e 100644 --- a/frontend/src/app/main/ui/messages.cljs +++ b/frontend/src/app/main/ui/messages.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.messages (:require - [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.data.messages :as dm] [app.main.refs :as refs] diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 0e462130e1..b07e3dd57f 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.modal (:require [app.main.data.modal :as dm] - [app.main.refs :as refs] [app.main.store :as st] [app.util.dom :as dom] [app.util.keyboard :as k] @@ -19,8 +18,8 @@ (defn- on-esc-clicked [event allow-click-outside] (when (and (k/esc? event) (not allow-click-outside)) - (do (dom/stop-propagation event) - (st/emit! (dm/hide))))) + (dom/stop-propagation event) + (st/emit! (dm/hide)))) (defn- on-pop-state [event] @@ -29,16 +28,6 @@ (st/emit! (dm/hide)) (.forward js/history)) -(defn- on-parent-clicked - [event parent-ref] - (let [parent (mf/ref-val parent-ref) - current (dom/get-target event)] - (when (and (dom/equals? (.-firstElementChild ^js parent) current) - (= (.-className ^js current) "modal-overlay")) - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dm/hide))))) - (defn- on-click-outside [event wrapper-ref type allow-click-outside] (let [wrapper (mf/ref-val wrapper-ref) diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index 3079151450..92c970bd64 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -6,18 +6,18 @@ (ns app.main.ui.onboarding (:require - [app.config :as cf] [app.common.spec :as us] + [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] - [app.main.ui.components.forms :as fm :refer [input submit-button form]] + [app.main.ui.components.forms :as fm] [app.util.dom :as dom] + [app.util.object :as obj] [app.util.router :as rt] [app.util.timers :as tm] - [app.util.object :as obj] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -186,24 +186,24 @@ (mf/defc onboarding-team-modal {::mf/register modal/components ::mf/register-as :onboarding-team} - [props] + [] (let [close (mf/use-fn (st/emitf (modal/hide))) form (fm/use-form :spec ::team-form :initial {}) on-success (mf/use-callback - (fn [form response] + (fn [_form response] (st/emit! (modal/hide) (rt/nav :dashboard-projects {:team-id (:id response)})))) on-error (mf/use-callback - (fn [form response] + (fn [_form _response] (st/emit! (dm/error "Error on creating team.")))) on-submit (mf/use-callback - (fn [form event] + (fn [form _event] (let [mdata {:on-success (partial on-success form) :on-error (partial on-error form)} params {:name (get-in @form [:clean-data :name])}] @@ -292,7 +292,8 @@ (let [versions (methods render-release-notes) version (obj/get props "version")] (when (contains? versions version) - [:> release-notes props]))) + [:div.relnotes + [:> release-notes props]]))) (defmethod render-release-notes "0.0" [params] @@ -572,3 +573,108 @@ {:slide @slide :navigate navigate :total 4}]]]]]]))) + + +(defmethod render-release-notes "1.7" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case @slide + :start + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.7"}]] + [:div.modal-right + [:div.modal-title + [:h2 "What's new?"]] + [:span.release "Alpha version " version] + [:div.modal-content + [:p "Penpot continues growing with new features that improve performance, user experience and visual design."] + [:p "We are happy to show you a sneak peak of the most important stuff that the Alpha 1.7 version brings."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]] + + 0 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/export.gif" :border "0" :alt "Export & Import"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Export and import Penpot files"]] + [:div.modal-content + [:p [:strong "Export files from the dashboard to your computer and import them from your computer to your projects."] + " This means that Penpot users can freely save and share Penpot files."] + [:p "Exported files linked to shared libraries provide + different ways to export their assets. Choose the one that + suits you better!"]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 1 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/constraints.gif" :border "0" :alt "Resizing constraints"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Resizing constraints"]] + [:div.modal-content + [:p "Constraints allow you to " [:strong "decide how layers will behave when resizing its container"] " being a group or an artboard."] + [:p "You can manually set horizontal and vertical + constraints for every layer. This is especially useful to + control how your designs look when working with responsive + components."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 2 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/group-components.gif" :border "0" :alt "Library assets management improvements"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Library assets management"]] + [:div.modal-content + [:p [:strong "Collapse/expand groups"] " at any nesting level, so you don’t have to manage their visibility individually."] + [:p "Penpot " [:strong "remembers the last library state"] ", so you don’t have to collapse a group you want hidden every time."] + [:p "Easily " [:strong "rename and ungroup"] " asset groups."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 3 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/copy-paste.gif" :border "0" :alt "Paste components from file to file"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Paste components from file to file"]] + [:div.modal-content + [:p "Do you sometimes copy and paste component copies that belong to a library already shared by the original and destination files? From now on, those component copies are aware of this and will retain their linkage to the library."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click finish} "Start!"] + [:& navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index c56e85a983..1a7ca192fb 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -12,16 +12,15 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.uuid :as uuid] - [app.main.store :as st] [app.main.data.fonts :as df] [app.main.exports :as exports] [app.main.repo :as repo] - [app.main.ui.context :as muc] + [app.main.store :as st] + [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.embed :refer [embed-fontfaces-style]] + [app.util.dom :as dom] [beicon.core :as rx] - [cljs.spec.alpha :as s] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -45,12 +44,6 @@ objects (reduce updt-fn objects mod-ids) object (get objects object-id) - txt-xfm (comp (map #(get objects %)) - (filter #(= :text (:type %)))) - txt-objs (into [] txt-xfm mod-ids) - - {:keys [width height]} (gsh/points->selrect (:points object)) - ;; We need to get the shadows/blurs paddings to create the viewbox properly {:keys [x y width height]} (filters/get-filters-bounds object) @@ -77,17 +70,20 @@ #(exports/shape-wrapper-factory objects)) ] - [:& (mf/provider muc/embed-ctx) {:value true} - (when (seq txt-objs) - [:& embed-fontfaces-style {:shapes txt-objs}]) + (mf/use-effect + (mf/deps width height) + #(dom/set-page-style {:size (str (mth/round width) "px " + (mth/round height) "px")})) + [:& (mf/provider embed/context) {:value true} [:svg {:id "screenshot" :view-box vbox :width width :height height :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} + :xmlns "http://www.w3.org/2000/svg" + :xmlns:penpot "https://penpot.app/xmlns"} (case (:type object) :frame [:& frame-wrapper {:shape object :view-box vbox}] @@ -107,6 +103,7 @@ (assoc objects (:id object) object)) objects)) + ;; NOTE: for now, it is ok download the entire file for render only ;; single page but in a future we need consider to add a specific ;; backend entry point for download only the data of single page. @@ -121,7 +118,7 @@ (repo/query! :font-variants {:file-id file-id}) (repo/query! :file {:id file-id})) (rx/subs - (fn [[fonts {:keys [data]} :as kaka]] + (fn [[fonts {:keys [data]}]] (when (seq fonts) (st/emit! (df/fonts-fetched fonts))) (let [objs (get-in data [:pages-index page-id :objects]) @@ -133,3 +130,30 @@ [:& object-svg {:objects @objects :object-id object-id :zoom 1}]))) + +(mf/defc render-sprite + [{:keys [file-id component-id] :as props}] + (let [file (mf/use-state nil)] + (mf/use-effect + (mf/deps file-id) + (fn [] + (->> (repo/query! :file {:id file-id}) + (rx/subs + (fn [result] + (reset! file result)))) + (constantly nil))) + + (when @file + [:* + [:& exports/components-sprite-svg {:data (:data @file) :embed true} + + (when (some? component-id) + [:use {:x 0 :y 0 + :xlinkHref (str "#" component-id)}])] + + (when-not (some? component-id) + [:ul + (for [[id data] (get-in @file [:data :components])] + (let [url (str "#/render-sprite/" (:id @file) "?component-id=" id)] + [:li [:a {:href url} (:name data)]]))])]))) + diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 7ca893dfef..7c9cd788ff 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -7,14 +7,13 @@ (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.change-email] + [app.main.ui.settings.delete-account] [app.main.ui.settings.feedback :refer [feedback-page]] + [app.main.ui.settings.options :refer [options-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 [tr]] [rumext.alpha :as mf])) diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index 46726c87fb..5ea32034f3 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -15,10 +15,9 @@ [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [beicon.core :as rx] [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (s/def ::email-1 ::us/email) @@ -63,7 +62,7 @@ (modal/hide))))) (defn- on-submit - [form event] + [form _event] (let [params {:email (get-in @form [:clean-data :email-1])} mdata {:on-error (partial on-error form) :on-success (partial on-success form)}] @@ -73,8 +72,7 @@ {::mf/register modal/components ::mf/register-as :change-email} [] - (let [locale (mf/deref i18n/locale) - profile (mf/deref refs/profile) + (let [profile (mf/deref refs/profile) form (fm/use-form :spec ::email-change-form :validators [email-equality] :initial profile) @@ -88,30 +86,30 @@ [:div.modal-header [:div.modal-header-title - [:h2 (t locale "modals.change-email.title")]] + [:h2 (tr "modals.change-email.title")]] [:div.modal-close-button {:on-click on-close} i/close]] [:div.modal-content [:& msgs/inline-banner {:type :info - :content (t locale "modals.change-email.info" (:email profile))}] + :content (tr "modals.change-email.info" (:email profile))}] [:div.fields-row [:& fm/input {:type "text" :name :email-1 - :label (t locale "modals.change-email.new-email") + :label (tr "modals.change-email.new-email") :trim true}]] [:div.fields-row [:& fm/input {:type "text" :name :email-2 - :label (t locale "modals.change-email.confirm-email") + :label (tr "modals.change-email.confirm-email") :trim true}]]] [:div.modal-footer [:div.action-buttons [:& fm/submit-button - {:label (t locale "modals.change-email.submit")}]]]]]])) + {:label (tr "modals.change-email.submit")}]]]]]])) diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs index f26881ddb5..f4e7262192 100644 --- a/frontend/src/app/main/ui/settings/delete_account.cljs +++ b/frontend/src/app/main/ui/settings/delete_account.cljs @@ -13,9 +13,7 @@ [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] [app.util.i18n :as i18n :refer [tr]] - [app.util.router :as rt] [beicon.core :as rx] - [cljs.spec.alpha :as s] [rumext.alpha :as mf])) (defn on-error @@ -28,7 +26,7 @@ (mf/defc delete-account-modal {::mf/register modal/components ::mf/register-as :delete-account} - [props] + [] (let [on-close (mf/use-callback (st/emitf (modal/hide))) diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index f8f8b63f10..a8ea4bd220 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -9,15 +9,13 @@ (: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.repo :as rp] [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])) @@ -37,7 +35,7 @@ on-succes (mf/use-callback (mf/deps profile) - (fn [event] + (fn [_] (reset! loading false) (st/emit! (dm/success (tr "labels.feedback-sent"))) (swap! form assoc :data {} :touched {} :errors {}))) @@ -54,7 +52,7 @@ on-submit (mf/use-callback (mf/deps profile) - (fn [form event] + (fn [form _] (reset! loading true) (let [data (:clean-data @form)] (->> (rp/mutation! :send-feedback data) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index 0b7bfb597f..e3b3aed6b4 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -13,7 +13,6 @@ [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 [t tr]] [cljs.spec.alpha :as s] @@ -26,11 +25,11 @@ (s/keys :opt-un [::lang ::theme])) (defn- on-success - [form] + [_] (st/emit! (dm/success (tr "notifications.profile-saved")))) (defn- on-submit - [form event] + [form _event] (let [data (:clean-data @form) data (cond-> data (empty? (:lang data)) diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 6b63fc4d83..f0b6a32230 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -11,7 +11,6 @@ [app.main.data.users :as udu] [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 [t tr]] [cljs.spec.alpha :as s] @@ -29,7 +28,7 @@ (st/emit! (dm/error msg))))) (defn- on-success - [form] + [_] (let [msg (tr "dashboard.notifications.password-saved")] (st/emit! (dm/success msg)))) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index 2ed0cb1829..eded1e265e 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -6,8 +6,8 @@ (ns app.main.ui.settings.profile (:require - [app.config :as cfg] [app.common.spec :as us] + [app.config :as cfg] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] @@ -16,13 +16,10 @@ [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr t]] [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.config :as cfg])) + [rumext.alpha :as mf])) (s/def ::fullname ::us/not-empty-string) (s/def ::email ::us/email) @@ -31,11 +28,11 @@ (s/keys :req-un [::fullname ::email])) (defn- on-success - [form] + [_] (st/emit! (dm/success (tr "notifications.profile-saved")))) (defn- on-submit - [form event] + [form _event] (let [data (:clean-data @form) mdata {:on-success (partial on-success form)}] (st/emit! (du/update-profile (with-meta data mdata))))) @@ -96,7 +93,7 @@ [:img {:src photo}] [:& file-uploader {:accept "image/jpeg,image/png" :multi false - :input-ref file-input + :ref file-input :on-selected on-file-selected}]]])) ;; --- Profile Page diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index bbd09a2621..dcb26a3695 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -6,12 +6,11 @@ (ns app.main.ui.shapes.attrs (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.common.data :as d] - [app.util.object :as obj] [app.main.ui.context :as muc] - [app.util.svg :as usvg])) + [app.util.object :as obj] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn- stroke-type->dasharray [style] @@ -89,7 +88,7 @@ ;; we setup the default fill as transparent (instead of black) (and (not (contains? shape :svg-attrs)) (not (#{ :svg-raw :group } (:type shape)))) - {:fill "transparent"} + {:fill "none"} :else {}) @@ -141,23 +140,28 @@ styles (-> svg-attrs (:style {}) (clj->js))] [attrs styles])) +(defn add-style-attrs + [props shape] + (let [render-id (mf/use-ctx muc/render-ctx) + svg-defs (:svg-defs shape {}) + svg-attrs (:svg-attrs shape {}) + + [svg-attrs svg-styles] (mf/use-memo + (mf/deps render-id svg-defs svg-attrs) + #(extract-svg-attrs render-id svg-defs svg-attrs)) + + styles (-> (obj/get props "style" (obj/new)) + (obj/merge! svg-styles) + (add-fill shape render-id) + (add-stroke shape render-id) + (add-layer-props shape))] + + (-> props + (obj/merge! svg-attrs) + (add-border-radius shape) + (obj/set! "style" styles)))) + (defn extract-style-attrs - ([shape] - (let [render-id (mf/use-ctx muc/render-ctx) - svg-defs (:svg-defs shape {}) - svg-attrs (:svg-attrs shape {}) - - [svg-attrs svg-styles] (mf/use-memo - (mf/deps render-id svg-defs svg-attrs) - #(extract-svg-attrs render-id svg-defs svg-attrs)) - - styles (-> (obj/new) - (obj/merge! svg-styles) - (add-fill shape render-id) - (add-stroke shape render-id) - (add-layer-props shape))] - - (-> (obj/new) - (obj/merge! svg-attrs) - (add-border-radius shape) - (obj/set! "style" styles))))) + [shape] + (-> (obj/new) + (add-style-attrs shape))) diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index b3cdecbf0c..74a4084fbd 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -6,17 +6,17 @@ (ns app.main.ui.shapes.circle (:require - [rumext.alpha :as mf] + [app.common.geom.shapes :as geom] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] - [app.common.geom.shapes :as geom] - [app.util.object :as obj])) + [app.util.object :as obj] + [rumext.alpha :as mf])) (mf/defc circle-shape {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [id x y width height]} shape + {:keys [x y width height]} shape transform (geom/transform-matrix shape) cx (+ x (/ width 2)) @@ -32,6 +32,5 @@ :ry ry :transform transform}))] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "ellipse"}])) + [:& shape-custom-stroke {:shape shape} + [:> :ellipse props]])) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 643f074853..741494946c 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -6,10 +6,130 @@ (ns app.main.ui.shapes.custom-stroke (:require - [rumext.alpha :as mf] - [app.common.uuid :as uuid] - [app.common.geom.shapes :as geom] - [app.util.object :as obj])) + [app.common.data :as d] + [app.main.ui.context :as muc] + [app.util.object :as obj] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(defn add-props + [props new-props] + (-> props + (obj/merge (clj->js new-props)))) + +(defn add-style + [props new-style] + (let [old-style (obj/get props "style") + style (obj/merge old-style (clj->js new-style))] + (-> props (obj/merge #js {:style style})))) + +(mf/defc inner-stroke-clip-path + [{:keys [render-id]}] + (let [clip-id (str "inner-stroke-" render-id) + shape-id (str "stroke-shape-" render-id)] + [:> "clipPath" #js {:id clip-id} + [:use {:xlinkHref (str "#" shape-id)}]])) + +(mf/defc outer-stroke-mask + [{:keys [shape render-id]}] + (let [stroke-mask-id (str "outer-stroke-" render-id) + shape-id (str "stroke-shape-" render-id) + stroke-width (:stroke-width shape 0)] + [:mask {:id stroke-mask-id} + [:use {:xlinkHref (str "#" shape-id) + :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] + + [:use {:xlinkHref (str "#" shape-id) + :style #js {:fill "black"}}]])) + +(mf/defc stroke-defs + [{:keys [shape render-id]}] + (cond + (and (= :inner (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& inner-stroke-clip-path {:shape shape + :render-id render-id}] + + (and (= :outer (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& outer-stroke-mask {:shape shape + :render-id render-id}])) + +;; Outer alingmnent: display the shape in two layers. One +;; without stroke (only fill), and another one only with stroke +;; at double width (transparent fill) and passed through a mask +;; that shows the whole shape, but hides the original shape +;; without stroke +(mf/defc outer-stroke + {::mf/wrap-props false} + [props] + + (let [render-id (mf/use-ctx muc/render-ctx) + child (obj/get props "children") + base-props (obj/get child "props") + elem-name (obj/get child "type") + stroke-mask-id (str "outer-stroke-" render-id) + shape-id (str "stroke-shape-" render-id) + + style-str (->> (obj/get base-props "style") + (js->clj) + (mapv (fn [[k v]] + (-> (d/name k) + (str/kebab) + (str ":" v)))) + (str/join ";"))] + + [:g.outer-stroke-shape + [:defs + [:> elem-name (-> (obj/clone base-props) + (obj/set! "id" shape-id) + (obj/set! "data-style" style-str) + (obj/without ["style"]))]] + + [:use {:xlinkHref (str "#" shape-id) + :mask (str "url(#" stroke-mask-id ")") + :style (-> (obj/get base-props "style") + (obj/clone) + (obj/update! "strokeWidth" * 2) + (obj/without ["fill" "fillOpacity"]) + (obj/set! "fill" "none"))}] + + [:use {:xlinkHref (str "#" shape-id) + :style (-> (obj/get base-props "style") + (obj/clone) + (obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]])) + + +;; Inner alignment: display the shape with double width stroke, +;; and clip the result with the original shape without stroke. +(mf/defc inner-stroke + {::mf/wrap-props false} + [props] + (let [render-id (mf/use-ctx muc/render-ctx) + child (obj/get props "children") + base-props (obj/get child "props") + elem-name (obj/get child "type") + shape (obj/get props "shape") + transform (obj/get base-props "transform") + + stroke-width (:stroke-width shape 0) + + clip-id (str "inner-stroke-" render-id) + shape-id (str "stroke-shape-" render-id) + + clip-path (str "url('#" clip-id "')") + shape-props (-> base-props + (add-props {:id shape-id + :transform nil}) + (add-style {:strokeWidth (* stroke-width 2)}))] + + [:g.inner-stroke-shape {:transform transform} + [:defs + [:> elem-name shape-props]] + + [:use {:xlinkHref (str "#" shape-id) + :clipPath clip-path}]])) + ; The SVG standard does not implement yet the 'stroke-alignment' ; attribute, to define the position of the stroke relative to the @@ -19,100 +139,25 @@ (mf/defc shape-custom-stroke {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - base-props (unchecked-get props "base-props") - elem-name (unchecked-get props "elem-name") - base-style (obj/get base-props "style") - {:keys [x y width height]} (:selrect shape) - stroke-id (mf/use-var (uuid/next)) + (let [child (obj/get props "children") + shape (obj/get props "shape") + stroke-width (:stroke-width shape 0) stroke-style (:stroke-style shape :none) - stroke-position (:stroke-alignment shape :center)] + stroke-position (:stroke-alignment shape :center) + has-stroke? (and (> stroke-width 0) + (not= stroke-style :none)) + inner? (= :inner stroke-position) + outer? (= :outer stroke-position)] + (cond - ;; Center alignment (or no stroke): the default in SVG - (or (= stroke-style :none) (= stroke-position :center)) - [:> elem-name (obj/merge! #js {} base-props)] + (and has-stroke? inner?) + [:& inner-stroke {:shape shape} + child] - ;; Inner alignment: display the shape with double width stroke, - ;; and clip the result with the original shape without stroke. - (= stroke-position :inner) - (let [clip-id (str "clip-" @stroke-id) + (and has-stroke? outer?) + [:& outer-stroke {:shape shape} + child] - clip-props (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "white" - :fillOpacity 1})}) - - stroke-width (obj/get base-style "strokeWidth" 0) - shape-props (obj/merge - base-props - #js {:clipPath (str "url('#" clip-id "')") - :style (obj/merge - base-style - #js {:strokeWidth (* stroke-width 2)})})] - [:* - [:> "clipPath" #js {:id clip-id} - [:> elem-name clip-props]] - [:> elem-name shape-props]]) - - ;; Outer alingmnent: display the shape in two layers. One - ;; without stroke (only fill), and another one only with stroke - ;; at double width (transparent fill) and passed through a mask - ;; that shows the whole shape, but hides the original shape - ;; without stroke - - (= stroke-position :outer) - (let [stroke-mask-id (str "mask-" @stroke-id) - stroke-width (obj/get base-style "strokeWidth" 0) - mask-props1 (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke "white" - :strokeWidth (* stroke-width 2) - :strokeOpacity 1 - :strokeDasharray nil - :fill "white" - :fillOpacity 1})}) - mask-props2 (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "black" - :fillOpacity 1})}) - - shape-props1 (obj/merge - base-props - #js {:style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil})}) - shape-props2 (obj/merge - base-props - #js {:mask (str "url('#" stroke-mask-id "')") - :style (obj/merge - base-style - #js {:strokeWidth (* stroke-width 2) - :fill "none" - :fillOpacity 0})})] - [:* - [:mask {:id stroke-mask-id} - [:> elem-name mask-props1] - [:> elem-name mask-props2]] - [:> elem-name shape-props1] - [:> elem-name shape-props2]])))) + :else + child))) diff --git a/frontend/src/app/main/ui/shapes/embed.cljs b/frontend/src/app/main/ui/shapes/embed.cljs new file mode 100644 index 0000000000..25d00243d9 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/embed.cljs @@ -0,0 +1,43 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.shapes.embed + (:require + [app.main.ui.hooks :as hooks] + [app.util.http :as http] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(def context (mf/create-context false)) + +(defn use-data-uris [urls] + (let [embed? (mf/use-ctx context) + urls (hooks/use-equal-memo urls) + uri-data (mf/use-ref {}) + state (mf/use-state 0)] + + (hooks/use-effect-ssr + (mf/deps embed? urls) + (fn [] + (let [;; When not active the embedding we return the URI + url-mapping (fn [obs] + (if embed? + (rx/merge-map http/fetch-data-uri obs) + (rx/map identity obs))) + + sub (->> (rx/from urls) + (url-mapping) + (rx/reduce conj {}) + (rx/subs (fn [data] + (when-not (= data (mf/ref-val uri-data)) + (mf/set-ref-val! uri-data data) + (reset! state inc)))))] + #(when sub + (rx/dispose! sub))))) + + ;; Use ref so if the urls are cached will return inmediately instead of the + ;; next render + (mf/ref-val uri-data))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs new file mode 100644 index 0000000000..b8fa606f32 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -0,0 +1,228 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.shapes.export + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.util.json :as json] + [app.util.object :as obj] + [app.util.svg :as usvg] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(mf/defc render-xml + [{{:keys [tag attrs content] :as node} :xml}] + + (cond + (map? node) + [:> (d/name tag) (clj->js (usvg/clean-attrs attrs)) + (for [child content] + [:& render-xml {:xml child}])] + + (string? node) + node + + :else + nil)) + +(defn uuid->string [m] + (->> m + (d/deep-mapm + (fn [[k v]] + (if (uuid? v) + [k (str v)] + [k v]))))) + +(defn bool->str [val] + (when (some? val) (str val))) + +(defn add-factory [shape] + (fn add! + ([props attr] + (add! props attr str)) + + ([props attr trfn] + (let [val (get shape attr) + val (if (keyword? val) (d/name val) val) + ns-attr (str "penpot:" (-> attr d/name))] + (cond-> props + (some? val) + (obj/set! ns-attr (trfn val))))))) + +(defn add-data + "Adds as metadata properties that we cannot deduce from the exported SVG" + [props shape] + (let [add! (add-factory shape) + group? (= :group (:type shape)) + rect? (= :rect (:type shape)) + text? (= :text (:type shape)) + mask? (and group? (:masked-group? shape)) + center (gsh/center-shape shape)] + (-> props + (add! :name) + (add! :blocked) + (add! :hidden) + (add! :type) + (add! :stroke-style) + (add! :stroke-alignment) + (add! :transform) + (add! :transform-inverse) + (add! :flip-x) + (add! :flip-y) + (add! :proportion) + (add! :proportion-lock) + (add! :rotation) + (obj/set! "penpot:center-x" (-> center :x str)) + (obj/set! "penpot:center-y" (-> center :y str)) + + ;; Constraints + (add! :constraints-h) + (add! :constraints-v) + (add! :fixed-scroll) + + (cond-> (and rect? (some? (:r1 shape))) + (-> (add! :r1) + (add! :r2) + (add! :r3) + (add! :r4))) + + (cond-> text? + (-> (add! :grow-type) + (add! :content (comp json/encode uuid->string)))) + + (cond-> mask? + (obj/set! "penpot:masked-group" "true"))))) + + +(defn add-library-refs [props shape] + (let [add! (add-factory shape)] + (-> props + (add! :fill-color-ref-id) + (add! :fill-color-ref-file) + (add! :stroke-color-ref-id) + (add! :stroke-color-ref-file) + (add! :typography-ref-id) + (add! :typography-ref-file) + (add! :component-file) + (add! :component-id) + (add! :component-root) + (add! :shape-ref)))) + +(defn prefix-keys [m] + (letfn [(prefix-entry [[k v]] + [(str "penpot:" (d/name k)) v])] + (into {} (map prefix-entry) m))) + + +(mf/defc export-grid-data + [{:keys [grids]}] + (when-not (empty? grids) + [:> "penpot:grids" #js {} + (for [{:keys [type display params]} grids] + (let [props (->> (d/without-keys params [:color]) + (prefix-keys) + (clj->js))] + [:> "penpot:grid" + (-> props + (obj/set! "penpot:color" (get-in params [:color :color])) + (obj/set! "penpot:opacity" (get-in params [:color :opacity])) + (obj/set! "penpot:type" (d/name type)) + (cond-> (some? display) + (obj/set! "penpot:display" (str display))))]))])) + +(mf/defc export-page + [{:keys [options]}] + (let [saved-grids (get options :saved-grids)] + (when-not (empty? saved-grids) + (let [parse-grid + (fn [[type params]] + {:type type :params params}) + grids (->> saved-grids (mapv parse-grid))] + [:> "penpot:page" #js {} + [:& export-grid-data {:grids grids}]])))) + +(mf/defc export-shadow-data + [{:keys [shadow]}] + (for [{:keys [style hidden color offset-x offset-y blur spread]} shadow] + [:> "penpot:shadow" + #js {:penpot:shadow-type (d/name style) + :penpot:hidden (str hidden) + :penpot:color (str (:color color)) + :penpot:opacity (str (:opacity color)) + :penpot:offset-x (str offset-x) + :penpot:offset-y (str offset-y) + :penpot:blur (str blur) + :penpot:spread (str spread)}])) + +(mf/defc export-blur-data [{:keys [blur]}] + (when (some? blur) + (let [{:keys [type hidden value]} blur] + [:> "penpot:blur" + #js {:penpot:blur-type (d/name type) + :penpot:hidden (str hidden) + :penpot:value (str value)}]))) + +(mf/defc export-exports-data [{:keys [exports]}] + (for [{:keys [scale suffix type]} exports] + [:> "penpot:export" + #js {:penpot:type (d/name type) + :penpot:suffix suffix + :penpot:scale (str scale)}])) + +(mf/defc export-svg-data [shape] + [:* + (when (contains? shape :svg-attrs) + (let [svg-transform (get shape :svg-transform) + svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") ) + svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))] + [:> "penpot:svg-import" + #js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs) + :penpot:svg-defs (when-not (empty? svg-defs) svg-defs) + :penpot:svg-transform (when svg-transform (str svg-transform)) + :penpot:svg-viewbox-x (get-in shape [:svg-viewbox :x]) + :penpot:svg-viewbox-y (get-in shape [:svg-viewbox :y]) + :penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width]) + :penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])} + (for [[def-id def-xml] (:svg-defs shape)] + [:> "penpot:svg-def" #js {:def-id def-id} + [:& render-xml {:xml def-xml}]])])) + + (when (= (:type shape) :svg-raw) + (let [props + (-> (obj/new) + (obj/set! "penpot:x" (:x shape)) + (obj/set! "penpot:y" (:y shape)) + (obj/set! "penpot:width" (:width shape)) + (obj/set! "penpot:height" (:height shape)) + (obj/set! "penpot:tag" (-> (get-in shape [:content :tag]) d/name)) + (obj/merge! (-> (get-in shape [:content :attrs]) + (clj->js))))] + [:> "penpot:svg-content" props + (for [leaf (->> shape :content :content (filter string?))] + [:> "penpot:svg-child" {} leaf])]))]) + +(mf/defc export-interactions-data + [{:keys [interactions]}] + (when-not (empty? interactions) + [:> "penpot:interactions" #js {} + (for [{:keys [action-type destination event-type]} interactions] + [:> "penpot:interaction" + #js {:penpot:action-type (d/name action-type) + :penpot:destination (str destination) + :penpot:event-type (d/name event-type)}])])) + +(mf/defc export-data + [{:keys [shape]}] + (let [props (-> (obj/new) (add-data shape) (add-library-refs shape))] + [:> "penpot:shape" props + [:& export-shadow-data shape] + [:& export-blur-data shape] + [:& export-exports-data shape] + [:& export-svg-data shape] + [:& export-interactions-data shape] + [:& export-grid-data shape]])) + diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs index 76acb5b070..986dbc37e3 100644 --- a/frontend/src/app/main/ui/shapes/fill_image.cljs +++ b/frontend/src/app/main/ui/shapes/fill_image.cljs @@ -8,10 +8,9 @@ (:require [app.common.geom.shapes :as gsh] [app.config :as cfg] + [app.main.ui.shapes.embed :as embed] [app.util.object :as obj] - [rumext.alpha :as mf] - [app.common.geom.point :as gpt] - [app.main.ui.shapes.image :as image])) + [rumext.alpha :as mf])) (mf/defc fill-image-pattern {::mf/wrap-props false} @@ -22,8 +21,8 @@ (when (contains? shape :fill-image) (let [{:keys [x y width height]} (:selrect shape) fill-image-id (str "fill-image-" render-id) - media (:fill-image shape) - {:keys [uri loading]} (image/use-image-uri media) + uri (cfg/resolve-file-media (:fill-image shape)) + embed (embed/use-data-uris [uri]) transform (gsh/transform-matrix shape)] [:pattern {:id fill-image-id @@ -33,7 +32,7 @@ :height height :width width :patternTransform transform - :data-loading (str loading)} - [:image {:xlinkHref uri + :data-loading (str (not (contains? embed uri)))} + [:image {:xlinkHref (get embed uri uri) :width width :height height}]])))) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 838d7e421b..c2664d695c 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -84,7 +84,7 @@ :result filter-id}]])) (mf/defc background-blur-filter - [{:keys [filter-id filter-in params]}] + [{:keys [filter-id params]}] [:* [:feGaussianBlur {:in "BackgroundImage" :stdDeviation (/ (:value params) 2)}] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 966619c67c..d9e95c1351 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -6,14 +6,10 @@ (ns app.main.ui.shapes.frame (:require - [app.common.data :as d] - [app.common.geom.shapes :as geom] [app.main.ui.shapes.attrs :as attrs] [app.util.object :as obj] [rumext.alpha :as mf])) -(def frame-default-props {:fill-color "#ffffff"}) - (defn frame-shape [shape-wrapper] (mf/fnc frame-shape @@ -21,10 +17,13 @@ [props] (let [childs (unchecked-get props "childs") shape (unchecked-get props "shape") - {:keys [id width height]} shape + {:keys [width height]} shape - props (-> (merge frame-default-props shape) - (attrs/extract-style-attrs) + has-background? (or (some? (:fill-color shape)) + (some? (:fill-color-gradient shape))) + has-stroke? (not= :none (:stroke-style shape)) + + props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x 0 :y 0 @@ -32,8 +31,9 @@ :height height :className "frame-background"}))] [:* - [:> :rect props] - (for [[i item] (d/enumerate childs)] + (when (or has-background? has-stroke?) + [:> :rect props]) + (for [item childs] [:& shape-wrapper {:frame shape :shape item :key (:id item)}])]))) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 9475ec8d36..51a34a84fe 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -6,71 +6,80 @@ (ns app.main.ui.shapes.gradients (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.util.object :as obj] - [app.common.uuid :as uuid] - [app.main.ui.context :as muc] - [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] - [app.common.geom.shapes :as gsh])) + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.main.ui.context :as muc] + [app.util.object :as obj] + [rumext.alpha :as mf])) (mf/defc linear-gradient [{:keys [id gradient shape]}] - (let [{:keys [x y width height]} (:selrect shape) - 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) - :gradientTransform transform} + (let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5)))] + [:> :linearGradient #js {:id id + :x1 (:start-x gradient) + :y1 (:start-y gradient) + :x2 (:end-x gradient) + :y2 (:end-y gradient) + :gradientTransform transform + :penpot:gradient "true"} (for [{:keys [offset color opacity]} (:stops gradient)] [:stop {:key (str id "-stop-" offset) :offset (or offset 0) :stop-color color :stop-opacity opacity}])])) +(defn add-metadata [props gradient] + (-> props + (obj/set! "penpot:gradient" "true") + (obj/set! "penpot:start-x" (:start-x gradient)) + (obj/set! "penpot:start-x" (:start-x gradient)) + (obj/set! "penpot:start-y" (:start-y gradient)) + (obj/set! "penpot:end-x" (:end-x gradient)) + (obj/set! "penpot:end-y" (:end-y gradient)) + (obj/set! "penpot:width" (:width gradient)))) + (mf/defc radial-gradient [{:keys [id gradient shape]}] (let [{:keys [x y width height]} (:selrect shape) - center (gsh/center-shape shape) transform (if (= :path (:type shape)) (gsh/transform-matrix shape) - (gmt/matrix))] - (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)))) + (gmt/matrix)) + [x y] (if (= (:type shape) :frame) [0 0] [x y]) + translate-vec (gpt/point (+ x (* width (:start-x gradient))) + (+ y (* height (:start-y gradient)))) - gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient)) - (* height (:start-y gradient))) - (gpt/point (* width (:end-x gradient)) - (* height (:end-y gradient)))) + gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient)) + (* height (:start-y gradient))) + (gpt/point (* width (:end-x gradient)) + (* height (:end-y gradient)))) - angle (gpt/angle gradient-vec - (gpt/point 1 0)) + angle (gpt/angle gradient-vec + (gpt/point 1 0)) - shape-height-vec (gpt/point 0 (/ height 2)) + scale-factor-y (/ (gpt/length gradient-vec) (/ height 2)) + scale-factor-x (* scale-factor-y (:width gradient)) - scale-factor-y (/ (gpt/length gradient-vec) (/ height 2)) - scale-factor-x (* scale-factor-y (:width gradient)) + scale-vec (gpt/point (* scale-factor-y (/ height 2)) + (* scale-factor-x (/ width 2))) - scale-vec (gpt/point (* scale-factor-y (/ height 2)) - (* scale-factor-x (/ width 2))) + transform (gmt/multiply transform + (gmt/translate-matrix translate-vec) + (gmt/rotate-matrix angle) + (gmt/scale-matrix scale-vec)) - transform (gmt/multiply transform - (gmt/translate-matrix translate-vec) - (gmt/rotate-matrix angle) - (gmt/scale-matrix scale-vec))] - [:radialGradient {:id id + base-props #js {:id id :cx 0 :cy 0 :r 1 :gradientUnits "userSpaceOnUse" :gradientTransform transform} - (for [{:keys [offset color opacity]} (:stops gradient)] - [:stop {:key (str id "-stop-" offset) - :offset (or offset 0) - :stop-color color - :stop-opacity opacity}])]))) + + props (-> base-props (add-metadata gradient))] + [:> :radialGradient props + (for [{:keys [offset color opacity]} (:stops gradient)] + [:stop {:key (str id "-stop-" offset) + :offset (or offset 0) + :stop-color color + :stop-opacity opacity}])])) (mf/defc gradient {::mf/wrap-props false} diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index fd7d8552ff..84d8d54a64 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -6,10 +6,9 @@ (ns app.main.ui.shapes.group (:require + [app.main.ui.shapes.mask :refer [mask-str clip-str mask-factory]] [app.util.object :as obj] - [rumext.alpha :as mf] - [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.mask :refer [mask-str clip-str mask-factory]])) + [rumext.alpha :as mf])) (defn group-shape [shape-wrapper] @@ -20,33 +19,28 @@ (let [frame (unchecked-get props "frame") shape (unchecked-get props "shape") childs (unchecked-get props "childs") - expand-mask (unchecked-get props "expand-mask") - pointer-events (unchecked-get props "pointer-events") - {:keys [id x y width height]} shape + masked-group? (:masked-group? shape) - show-mask? (and (:masked-group? shape) (not expand-mask)) - mask (when show-mask? (first childs)) - childs (if show-mask? (rest childs) childs) + [mask childs] (if masked-group? + [(first childs) (rest childs)] + [nil childs]) - mask-props (when (and mask (not expand-mask)) - #js {:clipPath (clip-str mask) - :mask (mask-str mask)}) - mask-wrapper (if (and mask (not expand-mask)) - "g" - mf/Fragment) + [mask-wrapper mask-props] + (if masked-group? + ["g" (-> (obj/new) + (obj/set! "clipPath" (clip-str mask)) + (obj/set! "mask" (mask-str mask)))] + [mf/Fragment nil])] - props (-> (attrs/extract-style-attrs shape))] + [:> mask-wrapper mask-props + (when masked-group? + [:> render-mask #js {:frame frame :mask mask}]) - [:> :g (attrs/extract-style-attrs shape) - [:> mask-wrapper mask-props - (when mask - [:> render-mask #js {:frame frame :mask mask}]) - - (for [item childs] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])]])))) + (for [item childs] + [:& shape-wrapper {:frame frame + :shape item + :key (:id item)}])])))) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 124ce7052d..5fa4eec47a 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -8,61 +8,37 @@ (:require [app.common.geom.shapes :as geom] [app.config :as cfg] - [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.embed :as embed] [app.util.dom :as dom] - [app.util.http :as http] [app.util.object :as obj] - [app.util.webapi :as wapi] - [beicon.core :as rx] [rumext.alpha :as mf])) -(defn use-image-uri - [media] - (let [uri (mf/use-memo (mf/deps (:id media)) - #(cfg/resolve-file-media media)) - embed-resources? (mf/use-ctx muc/embed-ctx) - data-uri (mf/use-state (when (not embed-resources?) uri))] - - (mf/use-effect - (mf/deps uri) - (fn [] - (if embed-resources? - (->> (http/send! {:method :get - :uri uri - :response-type :blob}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/subs #(reset! data-uri %)))))) - - {:uri (or @data-uri uri) - :loading (not (some? @data-uri))})) - (mf/defc image-shape {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [id x y width height rotation metadata]} shape - {:keys [uri loading]} (use-image-uri metadata)] + {:keys [x y width height metadata]} shape + uri (cfg/resolve-file-media metadata) + embed (embed/use-data-uris [uri]) - (let [transform (geom/transform-matrix shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :preserveAspectRatio "none"}) - (cond-> loading - (obj/set! "data-loading" "true"))) + transform (geom/transform-matrix shape) + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x x + :y y + :transform transform + :width width + :height height + :preserveAspectRatio "none" + :data-loading (str (not (contains? embed uri)))})) - on-drag-start (fn [event] - ;; Prevent browser dragging of the image - (dom/prevent-default event))] + on-drag-start (fn [event] + ;; Prevent browser dragging of the image + (dom/prevent-default event))] - [:> "image" (obj/merge! - props - #js {:xlinkHref uri - :onDragStart on-drag-start})]))) + [:> "image" (obj/merge! + props + #js {:xlinkHref (get embed uri uri) + :onDragStart on-drag-start})])) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index c48f9c9c43..093ec72016 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -6,9 +6,9 @@ (ns app.main.ui.shapes.mask (:require - [rumext.alpha :as mf] + [app.common.geom.shapes :as gsh] [cuerdas.core :as str] - [app.common.geom.shapes :as gsh])) + [rumext.alpha :as mf])) (defn mask-str [mask] (str/fmt "url(#%s)" (str (:id mask) "-mask"))) diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index 08b716e152..8c2decdc6f 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -6,36 +6,23 @@ (ns app.main.ui.shapes.path (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] [app.util.object :as obj] - [app.util.path.format :as upf])) + [app.util.path.format :as upf] + [rumext.alpha :as mf])) ;; --- Path Shape (mf/defc path-shape {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - background? (unchecked-get props "background?") - {:keys [id x y width height]} (:selrect shape) + (let [shape (unchecked-get props "shape") content (:content shape) - pdata (mf/use-memo (mf/deps content) #(upf/format-path content)) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:d pdata}))] - (if background? - [:g - [:path {:stroke "transparent" - :fill "transparent" - :stroke-width "20px" - :d pdata}] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "path"}]] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "path"}]))) + pdata (mf/use-memo (mf/deps content) #(upf/format-path content)) + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:d pdata}))] + [:& shape-custom-stroke {:shape shape} + [:> :path props]])) diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index aeb61b17a8..a995f51ccb 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -6,23 +6,18 @@ (ns app.main.ui.shapes.rect (:require - [rumext.alpha :as mf] + [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] - [app.common.geom.shapes :as geom] [app.util.object :as obj] - [app.main.ui.shapes.gradients :refer [gradient]] - - [cuerdas.core :as str] - [app.common.uuid :as uuid] - [app.common.geom.point :as gpt])) + [rumext.alpha :as mf])) (mf/defc rect-shape {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - {:keys [id x y width height]} shape - transform (geom/transform-matrix shape) + {:keys [x y width height]} shape + transform (gsh/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! @@ -30,11 +25,11 @@ :y y :transform transform :width width - :height height}))] + :height height})) - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name - (if (.-d props) - "path" - "rect")}])) + path? (some? (.-d props))] + + [:& shape-custom-stroke {:shape shape} + (if path? + [:> :path props] + [:> :rect props])])) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 82dddd2386..0b8d294774 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -9,6 +9,9 @@ [app.common.data :as d] [app.common.uuid :as uuid] [app.main.ui.context :as muc] + [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.custom-stroke :as cs] + [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.gradients :as grad] @@ -33,28 +36,42 @@ {:keys [x y width height type]} shape frame? (= :frame type) - group-props (-> (obj/clone props) - (obj/without ["shape" "children"]) - (obj/set! "ref" ref) - (obj/set! "id" (str "shape-" (:id shape))) - (obj/set! "filter" (filters/filter-str filter-id shape)) - (obj/set! "style" styles) + group? (= :group type) - (cond-> frame? - (-> (obj/set! "x" x) - (obj/set! "y" y) - (obj/set! "width" width) - (obj/set! "height" height) - (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink") - (obj/set! "xmlns" "http://www.w3.org/2000/svg")))) + wrapper-props + (-> (obj/clone props) + (obj/without ["shape" "children"]) + (obj/set! "ref" ref) + (obj/set! "id" (str "shape-" (:id shape))) + (obj/set! "filter" (filters/filter-str filter-id shape)) + (obj/set! "style" styles)) + + wrapper-props + (cond-> wrapper-props + frame? + (-> (obj/set! "x" x) + (obj/set! "y" y) + (obj/set! "width" width) + (obj/set! "height" height) + (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink") + (obj/set! "xmlns" "http://www.w3.org/2000/svg") + (obj/set! "xmlns:penpot" "https://penpot.app/xmlns"))) + + wrapper-props + (cond-> wrapper-props + group? + (attrs/add-style-attrs shape)) wrapper-tag (if frame? "svg" "g")] + [:& (mf/provider muc/render-ctx) {:value render-id} - [:> wrapper-tag group-props + [:> wrapper-tag wrapper-props + [:& ed/export-data {:shape shape}] [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] [:& grad/gradient {:shape shape :attr :fill-color-gradient}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] - [:& fim/fill-image-pattern {:shape shape :render-id render-id}]] + [:& fim/fill-image-pattern {:shape shape :render-id render-id}] + [:& cs/stroke-defs {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index 4002be4568..ac04f57f70 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -100,7 +100,8 @@ (cond->> id (contains? svg-defs id) (str render-id "-")))] - (when (and svg-defs (not (empty? svg-defs))) + ;; TODO: no key? + (when (seq svg-defs) (for [svg-def (vals svg-defs)] [:& svg-node {:node svg-def :prefix-id prefix-id diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 7038c91280..407f5096d0 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -6,15 +6,10 @@ (ns app.main.ui.shapes.svg-raw (:require - [app.common.data :as cd] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as usa] - [app.util.data :as ud] [app.util.object :as obj] [app.util.svg :as usvg] - [cuerdas.core :as str] [rumext.alpha :as mf])) ;; Graphic tags @@ -27,6 +22,8 @@ (defn set-styles [attrs shape] (let [custom-attrs (-> (usa/extract-style-attrs shape) (obj/without ["transform"])) + + attrs (or attrs {}) attrs (cond-> attrs (string? (:style attrs)) usvg/clean-attrs) style (obj/merge! (clj->js (:style attrs {})) @@ -51,7 +48,7 @@ children (unchecked-get props "children") {:keys [x y width height]} shape - {:keys [tag attrs] :as content} (:content shape) + {:keys [attrs] :as content} (:content shape) ids-mapping (mf/use-memo #(usvg/generate-id-mapping content)) @@ -83,9 +80,7 @@ element-id (get-in content [:attrs :id]) attrs (cond-> (set-styles attrs shape) (and element-id (contains? ids-mapping element-id)) - (obj/set! "id" (get ids-mapping element-id))) - - {:keys [x y width height]} (:selrect shape)] + (obj/set! "id" (get ids-mapping element-id)))] [:> (name tag) attrs children])) (defn svg-raw-shape [shape-wrapper] diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 48a5162400..b6e037dd01 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -8,12 +8,9 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as geom] - [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] - [app.util.color :as uc] [app.util.object :as obj] - [cuerdas.core :as str] [rumext.alpha :as mf])) (mf/defc render-text @@ -40,8 +37,7 @@ (mf/defc render-paragraph-set {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - children (obj/get props "children") + (let [children (obj/get props "children") shape (obj/get props "shape") style (sts/generate-paragraph-set-styles shape)] [:div.paragraph-set {:style style} children])) diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs deleted file mode 100644 index 22999cd32a..0000000000 --- a/frontend/src/app/main/ui/shapes/text/embed.cljs +++ /dev/null @@ -1,147 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.ui.shapes.text.embed - (:refer-clojure :exclude [memoize]) - (:require - [app.common.data :as d] - [app.common.text :as txt] - [app.main.fonts :as fonts] - [app.util.http :as http] - [app.util.time :as dt] - [app.util.webapi :as wapi] - [app.util.object :as obj] - [clojure.set :as set] - [cuerdas.core :as str] - [promesa.core :as p] - [beicon.core :as rx] - [rumext.alpha :as mf])) - - -(defonce cache (atom {})) - -(defn with-cache - [{:keys [key max-age]} observable] - (let [entry (get @cache key) - age (when entry - (dt/diff (dt/now) - (:created-at entry)))] - (if (and (some? entry) - (< age max-age)) - (rx/of (:data entry)) - (->> observable - (rx/tap (fn [data] - (let [entry {:created-at (dt/now) :data data}] - (swap! cache assoc key entry)))))))) - -(def font-face-template " -/* latin */ -@font-face { - font-family: '%(family)s'; - font-style: %(style)s; - font-weight: %(weight)s; - font-display: block; - src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff'); -} -") - -;; -- Embed fonts into styles - -(defn get-node-fonts - [node] - (let [current-font (if (not (nil? (:font-id node))) - #{(select-keys node [:font-id :font-variant-id])} - #{(select-keys txt/default-text-attrs [:font-id :font-variant-id])}) - children-font (map get-node-fonts (:children node))] - (reduce set/union (conj children-font current-font)))) - -(defn get-font-css - "Given a font and the variant-id, retrieves the style CSS for it." - [{:keys [id backend family variants] :as font} font-variant-id] - (cond - (= :google backend) - (let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})] - (->> (http/send! {:method :get - :mode :cors - :omit-default-headers true - :uri uri - :response-type :text}) - (rx/map :body) - (http/as-promise))) - - - (= :custom backend) - (let [variant (d/seek #(= (:id %) font-variant-id) variants) - result (fonts/generate-custom-font-variant-css family variant)] - (p/resolved result)) - - :else - (let [{:keys [name weight style suffix] :as variant} (d/seek #(= (:id %) font-variant-id) variants) - result (str/fmt font-face-template {:family family - :style style - :suffix (or suffix font-variant-id) - :weight weight})] - (p/resolved result)))) - -(defn fetch-font-data - "Parses the CSS and retrieves the font data as DataURI." - [^string css] - (let [uris (->> (re-seq #"url\(([^)]+)\)" css) - (mapv second))] - (with-cache {:key uris :max-age (dt/duration {:hours 4})} - (->> (rx/from (seq uris)) - (rx/mapcat (fn [uri] - (->> (http/send! {:method :get :uri uri :response-type :blob :omit-default-headers true}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(vector uri %))))) - (rx/reduce conj []))))) - -(defn get-font-data - "Parses the CSS and retrieves the font data as DataURI." - [^string css] - (->> (fetch-font-data css) - (http/as-promise))) - -(defn embed-font - "Given a font-id and font-variant-id, retrieves the CSS for it and - convert all external urls to embedded data URI's." - [{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}] - (let [{:keys [backend family] :as font} (get @fonts/fontsdb font-id)] - (p/let [css (get-font-css font font-variant-id) - url-to-data (get-font-data css) - replace-text (fn [text [url data]] (str/replace text url data))] - (reduce replace-text css url-to-data)))) - -;; NOTE: we can't move this to generic hooks namespace because that -;; namespace imports some code incompatible with webworkers and this -;; font embbeding should be able run on browser and webworker -;; contexts. -(defn- memoize - [val] - (let [ref (mf/use-ref #js {})] - (when-not (= (mf/ref-val ref) val) - (mf/set-ref-val! ref val)) - (mf/ref-val ref))) - -(mf/defc embed-fontfaces-style - {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} - [props] - (let [shapes (obj/get props "shapes") - node {:children (->> shapes (map :content))} - fonts (-> node get-node-fonts memoize) - style (mf/use-state nil)] - - (mf/use-effect - (mf/deps fonts) - (fn [] - (-> (p/all (map embed-font fonts)) - (p/then (fn [result] - (reset! style (str/join "\n" result))))))) - - (when (some? @style) - [:style @style]))) diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs new file mode 100644 index 0000000000..5f2ae54fc0 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -0,0 +1,91 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.shapes.text.fontfaces + (:require + [app.common.data :as d] + [app.main.fonts :as fonts] + [app.main.ui.hooks :as hooks] + [app.main.ui.shapes.embed :as embed] + [app.util.object :as obj] + [beicon.core :as rx] + [clojure.set :as set] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(defn replace-embeds + "Replace into the font-faces of a CSS the URL's that are present in `embed-data` by its + data-uri" + [css urls embed-data] + (letfn [(replace-url [css url] + (str/replace css url (get embed-data url url)))] + (->> urls + (reduce replace-url css)))) + +(defn use-fonts-css + "Hook that retrieves the CSS of the fonts passed as parameter" + [fonts] + (let [fonts-css-ref (mf/use-ref "") + redraw (mf/use-state 0)] + + (hooks/use-effect-ssr + (mf/deps fonts) + (fn [] + (let [sub + (->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/reduce conj []) + (rx/subs + (fn [result] + (let [css (str/join "\n" result)] + (when-not (= (mf/ref-val fonts-css-ref) css) + (mf/set-ref-val! fonts-css-ref css) + (reset! redraw inc))))))] + #(rx/dispose! sub)))) + + (mf/ref-val fonts-css-ref))) + +(mf/defc fontfaces-style-render + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} + [props] + + (let [fonts (obj/get props "fonts") + + ;; Fetch its CSS fontfaces + fonts-css (use-fonts-css fonts) + + ;; Extract from the CSS the URL's to embed + fonts-urls (mf/use-memo + (mf/deps fonts-css) + #(fonts/extract-fontface-urls fonts-css)) + + ;; Calculate the data-uris for these fonts + fonts-embed (embed/use-data-uris fonts-urls) + + ;; Creates a style tag by replacing the urls with the data uri + style (replace-embeds fonts-css fonts-urls fonts-embed)] + + (when (d/not-empty? style) + [:style style]))) + +(mf/defc fontfaces-style + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} + [props] + (let [shapes (->> (obj/get props "shapes") + (filterv #(= :text (:type %)))) + + content (->> shapes (mapv :content)) + + ;; Retrieve the fonts ids used by the text shapes + fonts (->> content + (mapv fonts/get-content-fonts) + (reduce set/union #{}) + (hooks/use-equal-memo))] + + (when (d/not-empty? fonts) + [:> fontfaces-style-render {:fonts fonts}]))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 8c15ccb43c..9d247e13fd 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -15,7 +15,7 @@ (defn generate-root-styles [shape node] - (let [valign (or (:vertical-align node "top")) + (let [valign (:vertical-align node "top") base #js {:height (or (:height shape) "100%") :width (or (:width shape) "100%")}] (cond-> base @@ -104,18 +104,18 @@ (when (and (string? font-id) (pos? (alength font-id))) (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id)] - (let [font-family (str/quote - (or (:family font) - (:font-family data))) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (:font-style data)) - font-weight (or (:weight font-variant) - (:font-weight data))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight)))) + (let [font (get fontsdb font-id) + font-family (str/quote + (or (:family font) + (:font-family data))) + font-variant (d/seek #(= font-variant-id (:id %)) + (:variants font)) + font-style (or (:style font-variant) + (:font-style data)) + font-weight (or (:weight font-variant) + (:font-weight data))] + (obj/set! base "fontFamily" font-family) + (obj/set! base "fontStyle" font-style) + (obj/set! base "fontWeight" font-weight))) base)) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 0cd9c0b5a8..aec0d11bc1 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -10,12 +10,9 @@ [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.i18n :refer [tr]] [app.util.router :as rt] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] [rumext.alpha :as mf])) (defn- go-to-dashboard @@ -24,7 +21,7 @@ (st/emit! (rt/nav :dashboard-projects {:team-id team-id})))) (mf/defc not-found - [{:keys [error] :as props}] + [] (let [profile (mf/deref refs/profile)] [:section.exception-layout [:div.exception-header @@ -42,7 +39,7 @@ (tr "labels.sign-out")]]]]])) (mf/defc bad-gateway - [{:keys [error] :as props}] + [] (let [profile (mf/deref refs/profile)] [:section.exception-layout [:div.exception-header @@ -59,7 +56,7 @@ (tr "labels.retry")]]]]])) (mf/defc service-unavailable - [{:keys [error] :as props}] + [] (let [profile (mf/deref refs/profile)] [:section.exception-layout [:div.exception-header @@ -76,7 +73,7 @@ (tr "labels.retry")]]]]])) (mf/defc internal-error - [props] + [] (let [profile (mf/deref refs/profile)] [:section.exception-layout [:div.exception-header diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index f45d405ca5..212301613e 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.viewer (:require [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] @@ -19,9 +18,8 @@ [app.main.store :as st] [app.main.ui.comments :as cmt] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as i] [app.main.ui.viewer.header :refer [header]] - [app.main.ui.viewer.shapes :as shapes :refer [frame-svg]] + [app.main.ui.viewer.shapes :as shapes] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t tr]] @@ -44,7 +42,7 @@ (l/derived :comments-local st/state)) (mf/defc comments-layer - [{:keys [width height zoom frame data] :as props}] + [{:keys [zoom frame data] :as props}] (let [profile (mf/deref refs/profile) modifier1 (-> (gpt/point (:x frame) (:y frame)) @@ -62,7 +60,7 @@ mframe (geom/transform-shape frame) threads (->> (vals threads-map) (dcm/apply-filters cstate profile) - (filter (fn [{:keys [seqn position]}] + (filter (fn [{:keys [position]}] (frame-contains? mframe position)))) on-bubble-click @@ -127,7 +125,7 @@ (mf/defc viewport {::mf/wrap [mf/memo]} - [{:keys [state data index section] :or {zoom 1} :as props}] + [{:keys [state data index section] :as props}] (let [zoom (:zoom state) objects (:objects data) @@ -273,7 +271,7 @@ (mf/use-effect (mf/deps (:file data)) #(when-let [name (get-in data [:file :name])] - (dom/set-html-title (tr "title.viewer" name)))) + (dom/set-html-title (str "\u25b6 " (tr "title.viewer" name))))) (when (and data state) [:& viewer-content diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 46a29b984a..061afd0f17 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -10,7 +10,6 @@ [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.comments :as dcm] - [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] @@ -20,11 +19,9 @@ [app.main.ui.components.fullscreen :as fs] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.webapi :as wapi] - [cuerdas.core :as str] - [potok.core :as ptk] [rumext.alpha :as mf])) (mf/defc zoom-widget @@ -34,7 +31,8 @@ on-decrease on-zoom-to-50 on-zoom-to-100 - on-zoom-to-200] + on-zoom-to-200 + on-fullscreen] :as props}] (let [show-dropdown? (mf/use-state false)] [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} @@ -52,14 +50,15 @@ [:li {:on-click on-zoom-to-100} "Zoom to 100%" [:span (sc/get-tooltip :reset-zoom)]] [:li {:on-click on-zoom-to-200} - "Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]]]]])) + "Zoom to 200%" [:span (sc/get-tooltip :zoom-200)]] + [:li {:on-click on-fullscreen} + "Full screen"]]]])) + ;; "Full screen" [:span (sc/get-tooltip :full-screen)]]]]])) (mf/defc share-link - [{:keys [page token] :as props}] + [{:keys [token] :as props}] (let [show-dropdown? (mf/use-state false) dropdown-ref (mf/use-ref) - locale (mf/deref i18n/locale) - create (st/emitf (dv/create-share-link)) delete (st/emitf (dv/delete-share-link)) @@ -72,45 +71,45 @@ link (assoc cfg/public-uri :fragment link) copy-link - (fn [event] + (fn [_] (wapi/write-to-clipboard (str link)) (st/emit! (dm/show {:type :info :content "Link copied successfuly!" :timeout 3000})))] [:* [:span.btn-primary.btn-small - {:alt (t locale "viewer.header.share.title") + {:alt (tr "viewer.header.share.title") :on-click #(swap! show-dropdown? not)} - (t locale "viewer.header.share.title")] + (tr "viewer.header.share.title")] [:& dropdown {:show @show-dropdown? :on-close #(swap! show-dropdown? not) :container dropdown-ref} [:div.dropdown.share-link-dropdown {:ref dropdown-ref} - [:span.share-link-title (t locale "viewer.header.share.title")] + [:span.share-link-title (tr "viewer.header.share.title")] [:div.share-link-input (if (string? token) [:* [:span.link (str link)] [:span.link-button {:on-click copy-link} - (t locale "viewer.header.share.copy-link")]] - [:span.link-placeholder (t locale "viewer.header.share.placeholder")])] + (tr "viewer.header.share.copy-link")]] + [:span.link-placeholder (tr "viewer.header.share.placeholder")])] - [:span.share-link-subtitle (t locale "viewer.header.share.subtitle")] + [:span.share-link-subtitle (tr "viewer.header.share.subtitle")] [:div.share-link-buttons (if (string? token) [:button.btn-warning {:on-click delete} - (t locale "viewer.header.share.remove-link")] + (tr "viewer.header.share.remove-link")] [:button.btn-primary {:on-click create} - (t locale "viewer.header.share.create-link")])]]]])) + (tr "viewer.header.share.create-link")])]]]])) (mf/defc interactions-menu - [{:keys [state locale] :as props}] + [{:keys [state] :as props}] (let [imode (:interactions-mode state) - show-dropdown? (mf/use-state false) - show-dropdown (mf/use-fn #(reset! show-dropdown? true)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) select-mode (mf/use-callback @@ -118,33 +117,34 @@ (st/emit! (dv/set-interactions-mode mode))))] [:div.view-options - [:div.icon {:on-click #(swap! show-dropdown? not)} i/eye] + [:div.view-options-dropdown {:on-click toggle-dropdown} + [:span (tr "viewer.header.interactions")] + i/arrow-down] [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} [:ul.dropdown.with-check [:li {:class (dom/classnames :selected (= imode :hide)) :on-click #(select-mode :hide)} [:span.icon i/tick] - [:span.label (t locale "viewer.header.dont-show-interactions")]] + [:span.label (tr "viewer.header.dont-show-interactions")]] [:li {:class (dom/classnames :selected (= imode :show)) :on-click #(select-mode :show)} [:span.icon i/tick] - [:span.label (t locale "viewer.header.show-interactions")]] + [:span.label (tr "viewer.header.show-interactions")]] [:li {:class (dom/classnames :selected (= imode :show-on-click)) :on-click #(select-mode :show-on-click)} [:span.icon i/tick] - [:span.label (t locale "viewer.header.show-interactions-on-click")]]]]])) - + [:span.label (tr "viewer.header.show-interactions-on-click")]]]]])) (mf/defc comments-menu - [{:keys [locale] :as props}] + [] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) - show-dropdown? (mf/use-state false) - show-dropdown (mf/use-fn #(reset! show-dropdown? true)) - hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) update-mode (mf/use-callback @@ -157,26 +157,48 @@ (st/emit! (dcm/update-filters {:show mode}))))] [:div.view-options - [:div.icon {:on-click #(swap! show-dropdown? not)} i/eye] + [:div.icon {:on-click toggle-dropdown} i/eye] [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} [:ul.dropdown.with-check [:li {:class (dom/classnames :selected (= :all cmode)) :on-click #(update-mode :all)} [:span.icon i/tick] - [:span.label (t locale "labels.show-all-comments")]] + [:span.label (tr "labels.show-all-comments")]] [:li {:class (dom/classnames :selected (= :yours cmode)) :on-click #(update-mode :yours)} [:span.icon i/tick] - [:span.label (t locale "labels.show-your-comments")]] + [:span.label (tr "labels.show-your-comments")]] [:hr] [:li {:class (dom/classnames :selected (= :pending cshow)) :on-click #(update-show (if (= :pending cshow) :all :pending))} [:span.icon i/tick] - [:span.label (t locale "labels.hide-resolved-comments")]]]]])) + [:span.label (tr "labels.hide-resolved-comments")]]]]])) + +(mf/defc file-menu + [{:keys [project-id file-id page-id] :as props}] + (let [show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + on-edit + (mf/use-callback + (mf/deps project-id file-id page-id) + (st/emitf (rt/nav :workspace + {:project-id project-id + :file-id file-id} + {:page-id page-id})))] + [:div.file-menu + [:span.btn-icon-dark.btn-small {:on-click toggle-dropdown} + i/actions + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown + [:li {:on-click on-edit} + [:span.label (tr "viewer.header.edit-file")]]]]]])) (mf/defc header [{:keys [data index section state] :as props}] @@ -185,7 +207,6 @@ fullscreen (mf/use-ctx fs/fullscreen-context) total (count frames) - locale (mf/deref i18n/locale) profile (mf/deref refs/profile) teams (mf/deref refs/teams) @@ -207,18 +228,17 @@ (mf/deps project) (st/emitf (dv/go-to-dashboard project))) - on-edit - (mf/use-callback - (mf/deps project-id file-id page-id) - (st/emitf (rt/nav :workspace - {:project-id project-id - :file-id file-id} - {:page-id page-id}))) navigate (mf/use-callback (mf/deps file-id page-id) (fn [section] - (st/emit! (dv/go-to-section section))))] + (st/emit! (dv/go-to-section section)))) + + toggle-fullscreen + (mf/use-callback + (mf/deps fullscreen) + (fn [] + (if @fullscreen (fullscreen false) (fullscreen true))))] [:header.viewer-header [:div.main-icon @@ -226,7 +246,7 @@ ;; If the user doesn't have permission we disable the link :style {:pointer-events (when-not has-permission? "none")}} i/logo-icon]] - [:div.sitemap-zone {:alt (t locale "viewer.header.sitemap") + [:div.sitemap-zone {:alt (tr "viewer.header.sitemap") :on-click on-click} [:span.project-name (:name project)] [:span "/"] @@ -258,31 +278,32 @@ [:div.options-zone (case section - :interactions [:& interactions-menu {:state state :locale locale}] - :comments [:& comments-menu {:locale locale}] + :interactions [:& interactions-menu {:state state}] + :comments [:& comments-menu] nil) (when has-permission? [:& share-link {:token (:token data) :page (:page data)}]) - (when has-permission? - [:a.btn-text-basic.btn-small {:on-click on-edit} - (t locale "viewer.header.edit-page")]) - [:& zoom-widget {:zoom (:zoom state) :on-increase (st/emitf dv/increase-zoom) :on-decrease (st/emitf dv/decrease-zoom) :on-zoom-to-50 (st/emitf dv/zoom-to-50) :on-zoom-to-100 (st/emitf dv/reset-zoom) - :on-zoom-to-200 (st/emitf dv/zoom-to-200)}] + :on-zoom-to-200 (st/emitf dv/zoom-to-200) + :on-fullscreen toggle-fullscreen}] - [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left - {:alt (t locale "viewer.header.fullscreen") - :on-click #(if @fullscreen (fullscreen false) (fullscreen true))} + [:span.btn-icon-basic.btn-small.tooltip.tooltip-bottom-left + {:alt (tr "viewer.header.fullscreen") + :on-click toggle-fullscreen} (if @fullscreen i/full-screen-off i/full-screen)] - ]])) + + (when has-permission? + [:& file-menu {:project-id project-id + :file-id file-id + :page-id page-id}])]])) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 54dde52d74..39d90d175c 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -13,10 +13,8 @@ [app.common.geom.shapes :as geom] [app.common.pages :as cp] [app.main.data.viewer :as dv] - [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.shapes.circle :as circle] - [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.image :as image] @@ -62,13 +60,10 @@ (mf/fnc generic-wrapper {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") + (let [shape (unchecked-get props "shape") objects (unchecked-get props "objects") - {:keys [x y width height]} (:selrect shape) - frame? (= :frame (:type shape)) - - childs (unchecked-get props "childs") - frame (unchecked-get props "frame") + childs (unchecked-get props "childs") + frame (unchecked-get props "frame") interactions (->> (:interactions shape) (filter #(contains? objects (:destination %)))) diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 66bf403b0b..26397302e3 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -6,25 +6,16 @@ (ns app.main.ui.viewer.thumbnails (:require - [goog.events :as events] - [goog.object :as gobj] - [rumext.alpha :as mf] - [app.main.ui.icons :as i] [app.common.data :as d] - [app.main.store :as st] [app.main.data.viewer :as dv] - [app.main.ui.components.dropdown :refer [dropdown']] - [app.main.ui.shapes.frame :as frame] [app.main.exports :as exports] - [app.util.data :refer [classnames]] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown']] + [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.util.i18n :as i18n :refer [t tr]] - [app.common.math :as mth] - [app.util.router :as rt] - [app.main.data.viewer :as vd]) - (:import goog.events.EventType)) + [app.util.i18n :as i18n :refer [tr]] + [goog.object :as gobj] + [rumext.alpha :as mf])) (mf/defc thumbnails-content [{:keys [children expanded? total] :as props}] @@ -35,14 +26,14 @@ offset (mf/use-state 0) on-left-arrow-click - (fn [event] + (fn [_] (swap! offset (fn [v] (if (pos? v) (dec v) v)))) on-right-arrow-click - (fn [event] + (fn [_] (let [visible (/ @width @element-width) max-val (- total visible)] (swap! offset (fn [v] @@ -84,37 +75,36 @@ [{:keys [selected? frame on-click index objects] :as props}] [:div.thumbnail-item {:on-click #(on-click % index)} [:div.thumbnail-preview - {:class (classnames :selected selected?)} + {:class (dom/classnames :selected selected?)} [:& exports/frame-svg {:frame frame :objects objects}]] [:div.thumbnail-info [:span.name {:title (:name frame)} (:name frame)]]]) (mf/defc thumbnails-panel - [{:keys [data index screen] :as props}] + [{:keys [data index] :as props}] (let [expanded? (mf/use-state false) container (mf/use-ref) - page-id (get-in data [:page :id]) - file-id (get-in data [:file :id]) on-close #(st/emit! dv/toggle-thumbnails-panel) selected (mf/use-var false) on-mouse-leave - (fn [event] + (fn [_] (when @selected (on-close))) on-item-click - (fn [event index] + (fn [_ index] (compare-and-set! selected false true) (st/emit! (dv/go-to-frame-by-index index)) (when @expanded? (on-close)))] + [:& dropdown' {:on-close on-close :container container :show true} [:section.viewer-thumbnails - {:class (classnames :expanded @expanded?) + {:class (dom/classnames :expanded @expanded?) :ref container :on-mouse-leave on-mouse-leave} diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 6fc58ebd2c..2086412dfb 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -6,16 +6,11 @@ (ns app.main.ui.workspace (:require - [app.common.geom.point :as gpt] - [app.main.constants :as c] - [app.main.data.history :as udh] [app.main.data.messages :as dm] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.context :as ctx] - [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.workspace.colorpalette :refer [colorpalette]] [app.main.ui.workspace.colorpicker] @@ -29,10 +24,7 @@ [app.main.ui.workspace.viewport :refer [viewport]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [app.util.keyboard :as kbd] [app.util.object :as obj] - [beicon.core :as rx] - [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -67,10 +59,8 @@ file (obj/get props "file") layout (obj/get props "layout")] [:* - ;; TODO: left-sidebar option is obsolete because left-sidebar now - ;; is always visible. (when (:colorpalette layout) - [:& colorpalette {:left-sidebar? true}]) + [:& colorpalette]) [:section.workspace-content [:section.workspace-viewport @@ -96,22 +86,21 @@ (mf/defc workspace-page [{:keys [file layout page-id] :as props}] - (let [page (mf/deref trimmed-page-ref)] - (mf/use-layout-effect - (mf/deps page-id) + (mf/use-layout-effect + (mf/deps page-id) + (fn [] + (if (nil? page-id) + (st/emit! (dw/go-to-page)) + (st/emit! (dw/initialize-page page-id))) + (fn [] - (if (nil? page-id) - (st/emit! (dw/go-to-page)) - (st/emit! (dw/initialize-page page-id))) + (when page-id + (st/emit! (dw/finalize-page page-id)))))) - (fn [] - (when page-id - (st/emit! (dw/finalize-page page-id)))))) - - (when page - [:& workspace-content {:key page-id - :file file - :layout layout}]))) + (when (mf/deref trimmed-page-ref) + [:& workspace-content {:key page-id + :file file + :layout layout}])) (mf/defc workspace-loader [] diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index d9d4df2304..e4bbf6d37a 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -7,14 +7,12 @@ (ns app.main.ui.workspace.colorpalette (:require [app.common.math :as mth] - [app.main.data.workspace :as udw] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.state-helpers :as wsh] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.color :as uc] [app.util.i18n :refer [tr]] @@ -39,14 +37,9 @@ (-> (l/in [:workspace-local :selected-palette-size]) (l/derived st/state))) -(defn- make-selected-palette-item-ref - [lib-id] - (-> (l/in [:library-items :palettes lib-id]) - (l/derived st/state))) - ;; --- Components (mf/defc palette-item - [{:keys [color size local?]}] + [{:keys [color size]}] (let [select-color (fn [event] (let [ids (wsh/lookup-selected @st/state)] @@ -60,7 +53,7 @@ [:& cb/color-name {:color color :size size}]])) (mf/defc palette - [{:keys [left-sidebar? current-colors recent-colors file-colors shared-libs selected size]}] + [{:keys [current-colors recent-colors file-colors shared-libs selected size]}] (let [state (mf/use-state {:show-menu false }) width (:width @state 0) @@ -70,13 +63,12 @@ max-offset (- (count current-colors) visible) - close-fn #(st/emit! (udw/toggle-layout-flags :colorpalette)) container (mf/use-ref nil) on-left-arrow-click (mf/use-callback (mf/deps max-offset visible) - (fn [event] + (fn [_] (swap! state update :offset (fn [offset] (if (pos? offset) @@ -86,7 +78,7 @@ on-right-arrow-click (mf/use-callback (mf/deps max-offset visible) - (fn [event] + (fn [_] (swap! state update :offset (fn [offset] (if (< offset max-offset) @@ -104,7 +96,7 @@ on-resize (mf/use-callback - (fn [event] + (fn [_] (let [dom (mf/ref-val container) width (obj/get dom "clientWidth")] (swap! state assoc :width width))))] @@ -120,7 +112,7 @@ (fn [] (events/unlistenByKey key1)))) - [:div.color-palette {:class (when left-sidebar? "left-sidebar-open")} + [:div.color-palette.left-sidebar-open [:& dropdown {:show (:show-menu @state) :on-close #(swap! state assoc :show-menu false)} [:ul.workspace-context-menu.palette-menu @@ -132,7 +124,7 @@ (when (= selected (:id cur-library)) i/tick) [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count colors)))] [:div.color-sample - (for [[idx {:keys [id color]}] (map-indexed vector (take 7 colors))] + (for [[idx {:keys [color]}] (map-indexed vector (take 7 colors))] [:& cb/color-bullet {:key (str "color-" idx) :color color}])]])) @@ -192,9 +184,8 @@ (vals)))) (mf/defc colorpalette - [{:keys [left-sidebar?]}] - (let [team-id (mf/use-ctx ctx/current-team-id) - recent-colors (mf/deref refs/workspace-recent-colors) + [] + (let [recent-colors (mf/deref refs/workspace-recent-colors) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) selected (or (mf/deref selected-palette-ref) :recent) @@ -210,7 +201,7 @@ (cond (= selected :recent) (reverse recent-colors) (= selected :file) (->> (vals file-colors) (sort-by :name)) - :else (library->colors shared-libs selected)))))) + :else (->> (library->colors shared-libs selected) (sort-by :name))))))) (mf/use-effect (mf/deps recent-colors) @@ -225,8 +216,7 @@ (reset! current-library-colors (into [] (->> (vals file-colors) (sort-by :name))))))) - [:& palette {:left-sidebar? left-sidebar? - :current-colors @current-library-colors + [:& palette {:current-colors @current-library-colors :recent-colors recent-colors :file-colors file-colors :shared-libs shared-libs diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 993c1cdc83..96f338b59d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -6,28 +6,24 @@ (ns app.main.ui.workspace.colorpicker (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] - [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] - [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] [app.main.data.modal :as modal] + [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.libraries :as dwl] + [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] + [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] [app.main.ui.workspace.colorpicker.gradients :refer [gradients]] [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]] [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] + [app.main.ui.workspace.colorpicker.libraries :refer [libraries]] [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] - [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] - [app.main.ui.workspace.colorpicker.libraries :refer [libraries]])) + [app.util.color :as uc] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t]] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) ;; --- Refs @@ -127,7 +123,6 @@ picking-color? (mf/deref picking-color?) picked-color (mf/deref picked-color) picked-color-select (mf/deref picked-color-select) - picked-shift? (mf/deref picked-shift?) editing-spot-state (mf/deref editing-spot-state-ref) current-gradient (mf/deref current-gradient-ref) @@ -156,11 +151,11 @@ handle-change-stop (fn [offset] (when-let [offset-color (get-in @state [:stops offset])] - (do (swap! state assoc - :current-color offset-color - :editing-stop offset) + (swap! state assoc + :current-color offset-color + :editing-stop offset) - (st/emit! (dc/select-gradient-stop offset))))) + (st/emit! (dc/select-gradient-stop offset)))) on-select-library-color (fn [color] @@ -172,7 +167,7 @@ (on-change color))))) on-add-library-color - (fn [color] + (fn [_] (st/emit! (dwl/add-color (state->data @state)))) on-activate-gradient @@ -197,7 +192,7 @@ (mf/use-effect (mf/deps current-color) (fn [] (let [node (mf/ref-val ref-picker) - {:keys [r g b h s v]} current-color + {:keys [r g b h v]} current-color rgb [r g b] hue-rgb (uc/hsv->rgb [h 1.0 255]) hsl-from (uc/hsv->hsl [h 0.0 v]) @@ -248,9 +243,8 @@ :end-x :end-y :width])] (when (not= (:gradient-data @state) gradient-data) - (do - (reset! dirty? true) - (swap! state assoc :gradient-data gradient-data))))))) + (reset! dirty? true) + (swap! state assoc :gradient-data gradient-data)))))) ;; Check if we've opened a color with gradient (mf/use-effect @@ -265,7 +259,7 @@ (mf/use-effect (mf/deps @state) (fn [] - (if @dirty? + (when @dirty? (let [color (state->data @state)] (reset! dirty? false) (reset! last-color color) @@ -357,7 +351,7 @@ (mf/defc colorpicker-modal {::mf/register modal/components ::mf/register-as :colorpicker} - [{:keys [x y default data page position + [{:keys [x y data position disable-gradient disable-opacity on-change on-close on-accept] :as props}] @@ -367,7 +361,7 @@ position (or position :left) style (calculate-position vport position x y) - handle-change (fn [new-data shift-clicked?] + handle-change (fn [new-data _shift-clicked?] (reset! dirty? (not= data new-data)) (reset! last-change new-data) (when on-change diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index 08a0ddd2db..ed86777bdf 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -6,22 +6,10 @@ (ns app.main.ui.workspace.colorpicker.color-inputs (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]])) + [app.util.dom :as dom] + [rumext.alpha :as mf])) (mf/defc color-inputs [{:keys [type color disable-opacity on-change]}] (let [{red :r green :g blue :b diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 447bfbbe79..56c3ef0509 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -6,22 +6,8 @@ (ns app.main.ui.workspace.colorpicker.gradients (:require - [rumext.alpha :as mf] - [okulary.core :as l] [cuerdas.core :as str] - [app.common.geom.point :as gpt] - [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] - [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]])) + [rumext.alpha :as mf])) (defn gradient->string [stops] (let [format-stop diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs index d9b9f57a23..d4a2e17726 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -6,24 +6,14 @@ (ns app.main.ui.workspace.colorpicker.harmony (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] [app.common.geom.point :as gpt] [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] [app.util.color :as uc] + [app.util.dom :as dom] [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] - [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) - + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn create-color-wheel [canvas-node] @@ -100,7 +90,7 @@ (on-change {:hex hex :r r :g g :b b :v new-value}))) - on-complement-click (fn [ev] + on-complement-click (fn [_] (let [new-hue (mod (+ hue 180) 360) hex (uc/hsv->hex [new-hue saturation value]) [r g b] (uc/hex->rgb hex)] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs index 0e5e9b8bb6..e9523cf09c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs @@ -6,23 +6,9 @@ (ns app.main.ui.workspace.colorpicker.hsva (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] - [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] - [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) + [rumext.alpha :as mf])) (mf/defc hsva-selector [{:keys [color disable-opacity on-change]}] (let [{hue :h saturation :s value :v alpha :alpha} color diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index c866a08e1c..a305e8fed4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -6,42 +6,29 @@ (ns app.main.ui.workspace.colorpicker.libraries (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] - [app.common.math :as math] [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] - [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] + [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.components.color-bullet :refer [color-bullet]] - [app.main.ui.workspace.colorpicker.gradients :refer [gradients]] - [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector]] - [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]] - [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]] - [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]])) + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [okulary.core :as l] + [rumext.alpha :as mf])) (def selected-palette-ref (-> (l/in [:workspace-local :selected-palette-colorpicker]) (l/derived st/state))) -(mf/defc libraries [{:keys [current-color on-select-color on-add-library-color - disable-gradient disable-opacity]}] +(mf/defc libraries + [{:keys [on-select-color on-add-library-color disable-gradient disable-opacity]}] (let [selected-library (or (mf/deref selected-palette-ref) :recent) current-library-colors (mf/use-state []) shared-libs (mf/deref refs/workspace-libraries) file-colors (mf/deref refs/workspace-file-colors) recent-colors (mf/deref refs/workspace-recent-colors) - locale (mf/deref i18n/locale) parse-selected (fn [selected-str] @@ -85,8 +72,8 @@ (when-let [val (parse-selected (dom/get-target-val e))] (st/emit! (dc/change-palette-selected-colorpicker val)))) :value (name selected-library)} - [:option {:value "recent"} (t locale "workspace.libraries.colors.recent-colors")] - [:option {:value "file"} (t locale "workspace.libraries.colors.file-library")] + [:option {:value "recent"} (tr "workspace.libraries.colors.recent-colors")] + [:option {:value "file"} (tr "workspace.libraries.colors.file-library")] (for [[_ {:keys [name id]}] shared-libs] [:option {:key id diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index 23bb4f4133..ea79bbd3cf 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -6,26 +6,14 @@ (ns app.main.ui.workspace.colorpicker.ramp (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] [app.common.math :as math] - [app.common.uuid :refer [uuid]] - [app.util.dom :as dom] - [app.util.color :as uc] - [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]] [app.main.ui.components.color-bullet :refer [color-bullet]] - [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]])) + [app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]] + [app.util.color :as uc] + [app.util.dom :as dom] + [rumext.alpha :as mf])) -(mf/defc value-saturation-selector [{:keys [hue saturation value on-change]}] +(mf/defc value-saturation-selector [{:keys [saturation value on-change]}] (let [dragging? (mf/use-state false) calculate-pos (fn [ev] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index 0c3a8e1da1..e0e630e437 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -6,22 +6,10 @@ (ns app.main.ui.workspace.colorpicker.slider-selector (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [cuerdas.core :as str] - [app.common.geom.point :as gpt] [app.common.math :as math] - [app.common.uuid :refer [uuid]] [app.util.dom :as dom] - [app.util.color :as uc] [app.util.object :as obj] - [app.main.store :as st] - [app.main.refs :as refs] - [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.colors :as dc] - [app.main.data.modal :as modal] - [app.main.ui.icons :as i] - [app.util.i18n :as i18n :refer [t]])) + [rumext.alpha :as mf])) (mf/defc slider-selector [{:keys [value class min-value max-value vertical? reverse? on-change]}] diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index da126962e0..7084739318 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -16,7 +16,7 @@ [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] + [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as tm] [rumext.alpha :as mf])) @@ -25,10 +25,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc sidebar-options - [{:keys [local] :as props}] + [] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) - locale (mf/deref i18n/locale) - update-mode (mf/use-callback (fn [mode] @@ -43,19 +41,19 @@ [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) :on-click #(update-mode :all)} [:span.icon i/tick] - [:span.label (t locale "labels.show-all-comments")]] + [:span.label (tr "labels.show-all-comments")]] [:li {:class (dom/classnames :selected (= :yours cmode)) :on-click #(update-mode :yours)} [:span.icon i/tick] - [:span.label (t locale "labels.show-your-comments")]] + [:span.label (tr "labels.show-your-comments")]] [:hr] [:li {:class (dom/classnames :selected (= :pending cshow)) :on-click #(update-show (if (= :pending cshow) :all :pending))} [:span.icon i/tick] - [:span.label (t locale "labels.hide-resolved-comments")]]])) + [:span.label (tr "labels.hide-resolved-comments")]]])) (mf/defc comments-sidebar [] @@ -109,7 +107,7 @@ :on-thread-click on-thread-click :users users :key (:page-id tgroup)}]])] - + [:div.thread-groups-placeholder i/chat (tr "labels.no-comments-available")])])) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 5720cd50b9..e493bb5fc5 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -9,23 +9,17 @@ (:require [app.main.data.modal :as modal] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shortcuts :as sc] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.context :as ctx] - [app.main.ui.hooks :refer [use-rxsub]] - [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :refer [t] :as i18n] + [app.util.i18n :refer [tr] :as i18n] [app.util.timers :as timers] - [beicon.core :as rx] [okulary.core :as l] - [potok.core :as ptk] [rumext.alpha :as mf])) (def menu-ref @@ -43,13 +37,12 @@ [:span.shortcut (or shortcut "")]]) (mf/defc menu-separator - [props] + [] [:li.separator]) (mf/defc shape-context-menu [{:keys [mdata] :as props}] - (let [locale (mf/deref i18n/locale) - {:keys [id] :as shape} (:shape mdata) + (let [{:keys [id] :as shape} (:shape mdata) selected (:selected mdata) single? (= (count selected) 1) @@ -97,91 +90,91 @@ do-update-remote-component (st/emitf (modal/show {:type :confirm :message "" - :title (t locale "modals.update-remote-component.message") - :hint (t locale "modals.update-remote-component.hint") - :cancel-label (t locale "modals.update-remote-component.cancel") - :accept-label (t locale "modals.update-remote-component.accept") + :title (tr "modals.update-remote-component.message") + :hint (tr "modals.update-remote-component.hint") + :cancel-label (tr "modals.update-remote-component.cancel") + :accept-label (tr "modals.update-remote-component.accept") :accept-style :primary :on-accept confirm-update-remote-component})) do-show-component (st/emitf (dw/go-to-layout :assets)) do-navigate-component-file (st/emitf (dwl/nav-to-component-file (:component-file shape)))] [:* - [:& menu-entry {:title (t locale "workspace.shape.menu.copy") + [:& menu-entry {:title (tr "workspace.shape.menu.copy") :shortcut (sc/get-tooltip :copy) :on-click do-copy}] - [:& menu-entry {:title (t locale "workspace.shape.menu.cut") + [:& menu-entry {:title (tr "workspace.shape.menu.cut") :shortcut (sc/get-tooltip :cut) :on-click do-cut}] - [:& menu-entry {:title (t locale "workspace.shape.menu.paste") + [:& menu-entry {:title (tr "workspace.shape.menu.paste") :shortcut (sc/get-tooltip :paste) :on-click do-paste}] - [:& menu-entry {:title (t locale "workspace.shape.menu.duplicate") + [:& menu-entry {:title (tr "workspace.shape.menu.duplicate") :shortcut (sc/get-tooltip :duplicate) :on-click do-duplicate}] [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.forward") + [:& menu-entry {:title (tr "workspace.shape.menu.forward") :shortcut (sc/get-tooltip :bring-forward) :on-click do-bring-forward}] - [:& menu-entry {:title (t locale "workspace.shape.menu.front") + [:& menu-entry {:title (tr "workspace.shape.menu.front") :shortcut (sc/get-tooltip :bring-front) :on-click do-bring-to-front}] - [:& menu-entry {:title (t locale "workspace.shape.menu.backward") + [:& menu-entry {:title (tr "workspace.shape.menu.backward") :shortcut (sc/get-tooltip :bring-backward) :on-click do-send-backward}] - [:& menu-entry {:title (t locale "workspace.shape.menu.back") + [:& menu-entry {:title (tr "workspace.shape.menu.back") :shortcut (sc/get-tooltip :bring-back) :on-click do-send-to-back}] [:& menu-separator] (when multiple? [:* - [:& menu-entry {:title (t locale "workspace.shape.menu.group") + [:& menu-entry {:title (tr "workspace.shape.menu.group") :shortcut (sc/get-tooltip :group) :on-click do-create-group}] - [:& menu-entry {:title (t locale "workspace.shape.menu.mask") + [:& menu-entry {:title (tr "workspace.shape.menu.mask") :shortcut (sc/get-tooltip :mask) :on-click do-mask-group}] [:& menu-separator]]) (when (or single? multiple?) [:* - [:& menu-entry {:title (t locale "workspace.shape.menu.flip-vertical") + [:& menu-entry {:title (tr "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") + [:& menu-entry {:title (tr "workspace.shape.menu.flip-horizontal") :shortcut (sc/get-tooltip :flip-horizontal) :on-click do-flip-horizontal}] [:& menu-separator]]) (when (and single? (= (:type shape) :group)) [:* - [:& menu-entry {:title (t locale "workspace.shape.menu.ungroup") + [:& menu-entry {:title (tr "workspace.shape.menu.ungroup") :shortcut (sc/get-tooltip :ungroup) :on-click do-remove-group}] (if (:masked-group? shape) - [:& menu-entry {:title (t locale "workspace.shape.menu.unmask") + [:& menu-entry {:title (tr "workspace.shape.menu.unmask") :shortcut (sc/get-tooltip :unmask) :on-click do-unmask-group}] - [:& menu-entry {:title (t locale "workspace.shape.menu.mask") + [:& menu-entry {:title (tr "workspace.shape.menu.mask") :shortcut (sc/get-tooltip :group) :on-click do-mask-group}])]) (when (and single? editable-shape?) - [:& menu-entry {:title (t locale "workspace.shape.menu.edit") + [:& menu-entry {:title (tr "workspace.shape.menu.edit") :shortcut (sc/get-tooltip :start-editing) :on-click do-start-editing}]) (if (:hidden shape) - [:& menu-entry {:title (t locale "workspace.shape.menu.show") + [:& menu-entry {:title (tr "workspace.shape.menu.show") :on-click do-show-shape}] - [:& menu-entry {:title (t locale "workspace.shape.menu.hide") + [:& menu-entry {:title (tr "workspace.shape.menu.hide") :on-click do-hide-shape}]) (if (:blocked shape) - [:& menu-entry {:title (t locale "workspace.shape.menu.unlock") + [:& menu-entry {:title (tr "workspace.shape.menu.unlock") :on-click do-unlock-shape}] - [:& menu-entry {:title (t locale "workspace.shape.menu.lock") + [:& menu-entry {:title (tr "workspace.shape.menu.lock") :on-click do-lock-shape}]) (when (and (or (nil? (:shape-ref shape)) @@ -189,7 +182,7 @@ (not= (:type shape) :frame)) [:* [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.create-component") + [:& menu-entry {:title (tr "workspace.shape.menu.create-component") :shortcut (sc/get-tooltip :create-component) :on-click do-add-component}]]) @@ -201,41 +194,39 @@ (if (= (:component-file shape) current-file-id) [:* [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") + [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :on-click do-detach-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") + [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.update-main") + [:& menu-entry {:title (tr "workspace.shape.menu.update-main") :on-click do-update-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.show-main") + [:& menu-entry {:title (tr "workspace.shape.menu.show-main") :on-click do-show-component}]] [:* [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.detach-instance") + [:& menu-entry {:title (tr "workspace.shape.menu.detach-instance") :on-click do-detach-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.reset-overrides") + [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}] - [:& menu-entry {:title (t locale "workspace.shape.menu.go-main") + [:& menu-entry {:title (tr "workspace.shape.menu.go-main") :on-click do-navigate-component-file}] - [:& menu-entry {:title (t locale "workspace.shape.menu.update-main") + [:& menu-entry {:title (tr "workspace.shape.menu.update-main") :on-click do-update-remote-component}]])) [:& menu-separator] - [:& menu-entry {:title (t locale "workspace.shape.menu.delete") + [:& menu-entry {:title (tr "workspace.shape.menu.delete") :shortcut (sc/get-tooltip :delete) :on-click do-delete}]])) (mf/defc viewport-context-menu - [{:keys [mdata] :as props}] - (let [locale (mf/deref i18n/locale) - do-paste (st/emitf dw/paste)] - [:* - [:& menu-entry {:title (t locale "workspace.shape.menu.paste") - :shortcut (sc/get-tooltip :paste) - :on-click do-paste}]])) + [] + (let [do-paste (st/emitf dw/paste)] + [:& menu-entry {:title (tr "workspace.shape.menu.paste") + :shortcut (sc/get-tooltip :paste) + :on-click do-paste}])) (mf/defc context-menu - [props] + [] (let [mdata (mf/deref menu-ref) top (- (get-in mdata [:position :y]) 20) left (get-in mdata [:position :x]) diff --git a/frontend/src/app/main/ui/workspace/coordinates.cljs b/frontend/src/app/main/ui/workspace/coordinates.cljs index bb8ad3e7f1..a261e9ae92 100644 --- a/frontend/src/app/main/ui/workspace/coordinates.cljs +++ b/frontend/src/app/main/ui/workspace/coordinates.cljs @@ -6,8 +6,8 @@ (ns app.main.ui.workspace.coordinates (:require - [app.main.ui.hooks :as hooks] [app.main.streams :as ms] + [app.main.ui.hooks :as hooks] [rumext.alpha :as mf])) (mf/defc coordinates diff --git a/frontend/src/app/main/ui/workspace/effects.cljs b/frontend/src/app/main/ui/workspace/effects.cljs index b42e1f92ba..12515dc0da 100644 --- a/frontend/src/app/main/ui/workspace/effects.cljs +++ b/frontend/src/app/main/ui/workspace/effects.cljs @@ -68,7 +68,7 @@ (dom/stop-propagation event) (let [toggle-selected? (and selected? shift?) - deselect? (and (not selected?) (not (empty? selected)) (not shift?))] + deselect? (and (not selected?) (seq selected) (not shift?))] (apply st/emit! (cond-> [] diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 309803e9ee..e958221158 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -6,13 +6,14 @@ (ns app.main.ui.workspace.header (:require + [app.common.data :as d] [app.common.math :as mth] [app.config :as cfg] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.shortcuts :as sc] - [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] + [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] @@ -21,6 +22,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] + [beicon.core :as rx] [okulary.core :as l] [rumext.alpha :as mf])) @@ -89,8 +91,6 @@ (let [show-menu? (mf/use-state false) editing? (mf/use-state false) - locale (mf/deref i18n/locale) - edit-input-ref (mf/use-ref nil) add-shared-fn @@ -125,7 +125,7 @@ :on-accept del-shared-fn}))) - handle-blur (fn [event] + handle-blur (fn [_] (let [value (-> edit-input-ref mf/ref-val dom/get-value)] (st/emit! (dw/rename-file (:id file) value))) (reset! editing? false)) @@ -135,7 +135,27 @@ (handle-blur event))) start-editing-name (fn [event] (dom/prevent-default event) - (reset! editing? true))] + (reset! editing? true)) + + on-export-files + (mf/use-callback + (mf/deps file team-id) + (fn [_] + (->> (rx/of file) + (rx/flat-map + (fn [file] + (->> (rp/query :file-libraries {:file-id (:id file)}) + (rx/map #(assoc file :has-libraries? (d/not-empty? %)))))) + (rx/reduce conj []) + (rx/subs + (fn [files] + (st/emit! + (modal/show + {:type :export + :team-id team-id + :has-libraries? (->> files (some :has-libraries?)) + :files files})))))))] + (mf/use-effect (mf/deps @editing?) #(when @editing? @@ -231,6 +251,9 @@ [:li {:on-click on-add-shared} [:span (tr "dashboard.add-shared")]]) + [:li.export-file {:on-click on-export-files} + [:span (tr "dashboard.export-single")]] + (when cfg/feedback-enabled [:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))} [:span (tr "labels.give-feedback")] @@ -243,9 +266,7 @@ [{:keys [file layout project page-id] :as props}] (let [team-id (:team-id project) zoom (mf/deref refs/selected-zoom) - router (mf/deref refs/router) params {:page-id page-id :file-id (:id file)} - view-url (rt/resolve router :viewer params {:index 0}) go-back (mf/use-callback @@ -282,7 +303,6 @@ [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) - :href (str "#" view-url) :on-click go-viewer} i/play]]])) diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index 1692a392f6..f7ed31957b 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -52,7 +52,7 @@ [:& file-uploader {:input-id "image-upload" :accept cm/str-image-types :multi true - :input-ref ref + :ref ref :on-selected on-files-selected}]]])) (mf/defc left-toolbar diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index f78a3aef11..f48d73b454 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -7,18 +7,18 @@ (ns app.main.ui.workspace.libraries (:require [app.common.data :as d] - [rumext.alpha :as mf] - [cuerdas.core :as str] - [okulary.core :as l] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] - [app.util.data :refer [classnames matches-search]] - [app.main.store :as st] - [app.main.refs :as refs] + [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.icons :as i] - [app.main.data.modal :as modal])) + [app.util.data :refer [matches-search]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) (def workspace-file (l/derived :workspace-file st/state)) @@ -68,7 +68,7 @@ on-search-clear (mf/use-callback - (fn [event] + (fn [_] (reset! search-term ""))) link-library @@ -152,8 +152,7 @@ project (mf/deref refs/workspace-project) file (mf/deref workspace-file) libraries (->> (mf/deref refs/workspace-libraries) - (d/removem (fn [[key val]] - (:is-indirect val)))) + (d/removem (fn [[_ val]] (:is-indirect val)))) shared-files (mf/deref refs/workspace-shared-files) change-tab #(reset! selected-tab %) @@ -171,11 +170,11 @@ [:div.modal-content [:div.libraries-header [:div.header-item - {:class (classnames :active (= @selected-tab :libraries)) + {:class (dom/classnames :active (= @selected-tab :libraries)) :on-click #(change-tab :libraries)} (t locale "workspace.libraries.libraries")] [:div.header-item - {:class (classnames :active (= @selected-tab :updates)) + {:class (dom/classnames :active (= @selected-tab :updates)) :on-click #(change-tab :updates)} (t locale "workspace.libraries.updates")]] [:div.libraries-content diff --git a/frontend/src/app/main/ui/workspace/presence.cljs b/frontend/src/app/main/ui/workspace/presence.cljs index e5a7f0e2e7..22138de9d6 100644 --- a/frontend/src/app/main/ui/workspace/presence.cljs +++ b/frontend/src/app/main/ui/workspace/presence.cljs @@ -8,32 +8,27 @@ (:require [app.config :as cfg] [app.main.refs :as refs] - [app.main.store :as st] - [app.util.router :as rt] [rumext.alpha :as mf])) ;; --- SESSION WIDGET (mf/defc session-widget - [{:keys [session self? profile] :as props}] + [{:keys [session profile] :as props}] [:li.tooltip.tooltip-bottom - {:alt (:fullname profile) - :on-click (when self? (st/emitf (rt/navigate :settings/profile)))} + {:alt (:fullname profile)} [:img {:style {:border-color (:color session)} :src (cfg/resolve-profile-photo-url profile)}]]) (mf/defc active-sessions {::mf/wrap [mf/memo]} [] - (let [profile (mf/deref refs/profile) - users (mf/deref refs/users) + (let [users (mf/deref refs/users) presence (mf/deref refs/workspace-presence)] [:ul.active-users (for [session (vals presence)] [:& session-widget {:session session :profile (get users (:profile-id session)) - :self? (= (:profile-id session) (:id profile)) :key (:id session)}])])) diff --git a/frontend/src/app/main/ui/workspace/rules.cljs b/frontend/src/app/main/ui/workspace/rules.cljs index d599a6bacf..d455cf9705 100644 --- a/frontend/src/app/main/ui/workspace/rules.cljs +++ b/frontend/src/app/main/ui/workspace/rules.cljs @@ -6,10 +6,9 @@ (ns app.main.ui.workspace.rules (:require - [rumext.alpha :as mf] [app.common.math :as mth] [app.util.object :as obj] - [app.util.timers :as timers])) + [rumext.alpha :as mf])) (defn- calculate-step-size [zoom] @@ -30,7 +29,7 @@ :else 1)) (defn draw-rule! - [dctx {:keys [zoom size start count type] :or {count 200}}] + [dctx {:keys [zoom size start type]}] (when start (let [txfm (- (* (- 0 start) zoom) 20) step (calculate-step-size zoom) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 48b6f00e86..b310d21180 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -13,11 +13,13 @@ common." (:require [app.common.geom.shapes :as geom] + [app.common.pages :as cp] [app.common.uuid :as uuid] [app.main.refs :as refs] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.rect :as rect] + [app.main.ui.shapes.text.fontfaces :as ff] [app.main.ui.workspace.shapes.bounding-box :refer [bounding-box]] [app.main.ui.workspace.shapes.common :as common] [app.main.ui.workspace.shapes.frame :as frame] @@ -53,18 +55,24 @@ [props] (let [objects (obj/get props "objects") active-frames (obj/get props "active-frames") - root-shapes (get-in objects [uuid/zero :shapes]) - shapes (->> root-shapes (mapv #(get objects %)))] + root-shapes (get-in objects [uuid/zero :shapes]) + shapes (->> root-shapes (mapv #(get objects %))) - (for [item shapes] - (if (= (:type item) :frame) - [:& frame-wrapper {:shape item - :key (:id item) - :objects objects - :thumbnail? (not (get active-frames (:id item) false))}] + root-children (->> shapes + (filter #(not= :frame (:type %))) + (mapcat #(cp/get-object-with-children (:id %) objects)))] - [:& shape-wrapper {:shape item - :key (:id item)}])))) + [:* + [:& ff/fontfaces-style {:shapes root-children}] + (for [item shapes] + (if (= (:type item) :frame) + [:& frame-wrapper {:shape item + :key (:id item) + :objects objects + :thumbnail? (not (get active-frames (:id item) false))}] + + [:& shape-wrapper {:shape item + :key (:id item)}]))])) (mf/defc shape-wrapper {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] @@ -83,20 +91,19 @@ (when (and shape (not (:hidden shape))) [:* (if-not svg-element? - [:g.shape-wrapper - (case (:type shape) - :path [:> path/path-wrapper opts] - :text [:> text/text-wrapper opts] - :group [:> group-wrapper opts] - :rect [:> rect-wrapper opts] - :image [:> image-wrapper opts] - :circle [:> circle-wrapper opts] - :svg-raw [:> svg-raw-wrapper opts] + (case (:type shape) + :path [:> path/path-wrapper opts] + :text [:> text/text-wrapper opts] + :group [:> group-wrapper opts] + :rect [:> rect-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + :svg-raw [:> svg-raw-wrapper opts] - ;; Only used when drawing a new frame. - :frame [:> frame-wrapper {:shape shape}] + ;; Only used when drawing a new frame. + :frame [:> frame-wrapper {:shape shape}] - nil)] + nil) ;; Don't wrap svg elements inside a otherwise some can break [:> svg-raw-wrapper opts]) diff --git a/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs b/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs index 62746cd99b..c7d599a2bb 100644 --- a/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/bounding_box.cljs @@ -6,15 +6,11 @@ (ns app.main.ui.workspace.shapes.bounding-box (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [app.util.debug :as debug] + ["randomcolor" :as rdcolor] [app.common.geom.shapes :as gsh] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.util.debug :refer [debug?]] [app.main.refs :as refs] - ["randomcolor" :as rdcolor])) + [cuerdas.core :as str] + [rumext.alpha :as mf])) (defn fixed [num] @@ -42,7 +38,7 @@ :height height :transform (or transform "none") :style {:stroke color - :fill "transparent" + :fill "none" :stroke-width "1px" :pointer-events "none"}}]) @@ -58,15 +54,11 @@ (mf/defc bounding-box {::mf/wrap-props false} [props] - (let [shape (-> (unchecked-get props "shape")) - frame (unchecked-get props "frame") + (let [shape (unchecked-get props "shape") bounding-box (gsh/points->selrect (-> shape :points)) shape-center (gsh/center-shape shape) - line-color (rdcolor #js {:seed (str (:id shape))}) - zoom (mf/deref refs/selected-zoom) - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) - childs (->> (mf/deref childs-ref) - (map gsh/transform-shape))] + line-color (rdcolor #js {:seed (str (:id shape))}) + zoom (mf/deref refs/selected-zoom)] [:g.bounding-box [:text {:x (:x bounding-box) @@ -81,7 +73,7 @@ [:& cross-point {:point shape-center :zoom zoom :color line-color}]] - + [:g.points (for [point (:points shape)] [:& cross-point {:point point diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index b6744c3469..6f7504222a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,45 +6,16 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.main.data.workspace :as dw] - [app.main.data.workspace.changes :as dch] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.context :as muc] + [app.common.pages :as cp] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.embed :as ste] - [app.util.dom :as dom] - [app.util.keyboard :as kbd] + [app.main.ui.shapes.text.fontfaces :as ff] [app.util.object :as obj] [app.util.timers :as ts] [beicon.core :as rx] - [okulary.core :as l] [rumext.alpha :as mf])) -(def obs-config - #js {:attributes true - :childList true - :subtree true - :characterData true}) - -(defn make-is-moving-ref - [id] - (let [check-moving (fn [local] - (and (= :move (:transform local)) - (contains? (:selected local) id)))] - (l/derived check-moving refs/workspace-local))) - -(defn check-props - ([props] (check-props props =)) - ([props eqfn?] - (fn [np op] - (every? #(eqfn? (unchecked-get np %) - (unchecked-get op %)) - props)))) - (defn check-frame-props "Checks for changes in the props of a frame" [new-props old-props] @@ -102,20 +73,16 @@ {::mf/wrap [#(mf/memo' % check-frame-props) custom-deferred] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - objects (unchecked-get props "objects") - thumbnail? (unchecked-get props "thumbnail?") - - edition (mf/deref refs/selected-edition) - embed-fonts? (mf/use-ctx muc/embed-ctx) + (let [shape (unchecked-get props "shape") + objects (unchecked-get props "objects") + thumbnail? (unchecked-get props "thumbnail?") shape (gsh/transform-shape shape) children (mapv #(get objects %) (:shapes shape)) - text-childs (->> (vals objects) - (filterv #(and (= :text (:type %)) - (= (:id shape) (:frame-id %))))) - rendered? (mf/use-state false) + all-children (cp/get-children-objects (:id shape) objects) + + rendered? (mf/use-state false) show-thumbnail? (and thumbnail? (some? (:thumbnail shape))) @@ -124,16 +91,12 @@ (fn [node] (ts/schedule-on-idle #(reset! rendered? (some? node)))))] - (when (and shape (not (:hidden shape))) + (when (some? shape) [:g.frame-wrapper {:display (when (:hidden shape) "none")} (when-not show-thumbnail? - [:> shape-container {:shape shape - :ref on-dom} - - (when embed-fonts? - [:& ste/embed-fontfaces-style {:shapes text-childs}]) - + [:> shape-container {:shape shape :ref on-dom} + [:& ff/fontfaces-style {:shapes all-children}] [:& frame-shape {:shape shape :childs children}]]) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index db7ece44ae..745523a1bf 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -6,15 +6,12 @@ (ns app.main.ui.workspace.shapes.group (:require - [app.common.geom.shapes :as gsh] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] - [app.main.ui.hooks :as hooks] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.shape :refer [shape-container]] - [app.util.debug :refer [debug?]] [app.util.dom :as dom] [rumext.alpha :as mf])) @@ -33,18 +30,15 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - - {:keys [id x y width height]} shape + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true})) childs (mf/deref childs-ref)] [:> shape-container {:shape shape} - [:g.group-shape - [:& group-shape - {:frame frame - :shape shape - :childs childs}]]])))) + [:& group-shape + {:frame frame + :shape shape + :childs childs}]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 22232ca011..bae6a5d990 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -6,13 +6,10 @@ (ns app.main.ui.workspace.shapes.path (:require - [app.main.data.workspace :as dw] [app.main.refs :as refs] - [app.main.store :as st] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.workspace.shapes.path.common :as pc] - [app.util.dom :as dom] [app.util.path.commands :as upc] [rumext.alpha :as mf])) @@ -28,5 +25,4 @@ [:> shape-container {:shape shape :pointer-events (when editing? "none")} - [:& path/path-shape {:shape shape - :background? true}]])) + [:& path/path-shape {:shape shape}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index 88cc101144..aa683f708e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -17,10 +17,11 @@ [app.main.ui.hooks :as hooks] [app.main.ui.workspace.shapes.path.common :as pc] [app.util.dom :as dom] - [app.util.path.geom :as upg] + [app.util.keyboard :as kbd] [app.util.path.commands :as upc] [app.util.path.format :as upf] - [app.util.keyboard :as kbd] + [app.util.path.geom :as upg] + [app.util.path.shapes-to-path :as ups] [clojure.set :refer [map-invert]] [goog.events :as events] [rumext.alpha :as mf]) @@ -31,12 +32,12 @@ on-enter (mf/use-callback - (fn [event] + (fn [_] (st/emit! (drp/path-pointer-enter position)))) - + on-leave (mf/use-callback - (fn [event] + (fn [_] (st/emit! (drp/path-pointer-leave position)))) on-mouse-down @@ -86,20 +87,21 @@ :on-mouse-down on-mouse-down :on-mouse-enter on-enter :on-mouse-leave on-leave + :pointer-events (when-not preview? "visible") :style {:cursor (cond (= edit-mode :draw) cur/pen-node (= edit-mode :move) cur/pointer-node) - :fill "transparent"}}]])) + :fill "none"}}]])) (mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode snap-angle?]}] (when (and point handler) (let [{:keys [x y]} handler on-enter - (fn [event] + (fn [_] (st/emit! (drp/path-handler-enter index prefix))) on-leave - (fn [event] + (fn [_] (st/emit! (drp/path-handler-leave index prefix))) on-mouse-down @@ -111,7 +113,7 @@ (= edit-mode :move) (st/emit! (drp/start-move-handler index prefix))))] - [:g.handler {:pointer-events (when (= edit-mode :draw))} + [:g.handler {:pointer-events (if (= edit-mode :draw) "none" "visible")} [:line {:x1 (:x point) :y1 (:y point) @@ -134,7 +136,7 @@ :y (- y (/ 3 zoom)) :width (/ 6 zoom) :height (/ 6 zoom) - + :style {:stroke-width (/ 1 zoom) :stroke (cond (or selected? hover?) pc/black-color :else pc/primary-color) @@ -147,12 +149,12 @@ :on-mouse-enter on-enter :on-mouse-leave on-leave :style {:cursor (when (= edit-mode :move) cur/pointer-move) - :fill "transparent"}}]]))) + :fill "none"}}]]))) (mf/defc path-preview [{:keys [zoom command from]}] [:g.preview {:style {:pointer-events "none"}} (when (not= :move-to (:command command)) - [:path {:style {:fill "transparent" + [:path {:style {:fill "none" :stroke pc/black-color :stroke-width (/ 1 zoom) :stroke-dasharray (/ 4 zoom)} @@ -213,6 +215,13 @@ selected-points (or selected-points #{}) + shape (cond-> shape + (not= :path (:type shape)) + ups/convert-to-path + + :always + hooks/use-equal-memo) + base-content (:content shape) base-points (mf/use-memo (mf/deps base-content) #(->> base-content upg/content->points)) @@ -224,7 +233,6 @@ points (into #{} content-points) - last-command (last content) last-p (->> content last upc/command->point) handlers (upc/content->handlers content) @@ -246,7 +254,7 @@ moving-nodes)) handle-double-click-outside - (fn [event] + (fn [_] (when (= edit-mode :move) (st/emit! :interrupt)))] @@ -274,6 +282,7 @@ [:g.drag-handler {:pointer-events "none"} [:& path-handler {:point last-p :handler drag-handler + :edit-mode edit-mode :zoom zoom}]]) (when @hover-point @@ -296,7 +305,7 @@ last-p? (= last-point (get point->base position)) pos-handlers (->> pos-handlers (filter show-handler?)) - curve? (not (empty? pos-handlers))] + curve? (boolean (seq pos-handlers))] [:g.path-node [:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")} @@ -325,6 +334,7 @@ (when prev-handler [:g.prev-handler {:pointer-events "none"} [:& path-handler {:point last-p + :edit-mode edit-mode :handler prev-handler :zoom zoom}]]) diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index a07307048d..dda2e33b3f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -7,11 +7,9 @@ (ns app.main.ui.workspace.shapes.svg-raw (:require [app.main.refs :as refs] - [app.main.ui.shapes.svg-raw :as svg-raw] [app.main.ui.shapes.shape :refer [shape-container]] - [rumext.alpha :as mf] - [app.common.geom.shapes :as gsh] - [app.main.ui.context :as muc])) + [app.main.ui.shapes.svg-raw :as svg-raw] + [rumext.alpha :as mf])) (defn svg-raw-wrapper-factory [shape-wrapper] @@ -20,38 +18,20 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - frame (unchecked-get props "frame") - - {:keys [id x y width height]} shape + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) - childs (mf/deref childs-ref) + childs (mf/deref childs-ref)] - {:keys [id x y width height]} shape - transform (gsh/transform-matrix shape) - tag (get-in shape [:content :tag]) - - def-ctx? (mf/use-ctx muc/def-ctx)] - - (cond - (and (svg-raw/graphic-element? tag) (not def-ctx?)) - [:> shape-container { :shape shape } - [:& svg-raw-shape - {:frame frame - :shape shape - :childs childs}]] - - ;; We cannot wrap inside groups the shapes that go inside the defs tag - ;; we use the context so we know when we should not render the container - (= tag :defs) - [:& (mf/provider muc/def-ctx) {:value true} + (if (or (= (get-in shape [:content :tag]) :svg) + (and (contains? shape :svg-attrs) (map? (:content shape)))) + [:> shape-container {:shape shape} [:& svg-raw-shape {:frame frame :shape shape :childs childs}]] - :else [:& svg-raw-shape {:frame frame :shape shape :childs childs}]))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 9e4a3efbec..6f3b652ce7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,25 +6,20 @@ (ns app.main.ui.workspace.shapes.text (:require - [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.context :as muc] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text :as text] - [app.main.ui.workspace.shapes.common :as common] [app.util.dom :as dom] [app.util.logging :as log] [app.util.object :as obj] + [app.util.text-editor :as ted] [app.util.timers :as timers] [app.util.webapi :as wapi] - [app.util.text-editor :as ted] - [okulary.core :as l] [beicon.core :as rx] + [okulary.core :as l] [rumext.alpha :as mf])) ;; Change this to :info :debug or :trace to debug this module @@ -50,7 +45,7 @@ (mf/defc text-resize-content {::mf/wrap-props false} [props] - (let [{:keys [id name x y grow-type] :as shape} (obj/get props "shape") + (let [{:keys [id name grow-type] :as shape} (obj/get props "shape") ;; NOTE: this breaks the hooks rule of "no hooks inside ;; conditional code"; but we ensure that this component will @@ -77,8 +72,8 @@ #(let [width (obj/get-in entries [0 "contentRect" "width"]) height (obj/get-in entries [0 "contentRect" "height"])] (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) - (do (log/debug :msg "Resize detected" :shape-id id :width width :height height) - (st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height)))))))))) + (log/debug :msg "Resize detected" :shape-id id :width width :height height) + (st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height))))))))) text-ref-cb (mf/use-callback @@ -109,7 +104,7 @@ (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [{:keys [id x y width height] :as shape} (unchecked-get props "shape") + (let [{:keys [id] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref)] @@ -124,4 +119,4 @@ ;; the component if the edition flag changes. [:& text-resize-content {:shape shape :edition? edition? - :key (str (:id shape) edition?)}]]])) + :key (str id edition?)}]]])) 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 8486228935..2859344226 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,12 +7,9 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] - [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.text :as txt] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.selection :as dws] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] @@ -22,9 +19,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.text-editor :as ted] - [cuerdas.core :as str] [goog.events :as events] - [okulary.core :as l] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -34,12 +29,11 @@ (mf/defc block-component {::mf/wrap-props false} [props] - (let [children (obj/get props "children") - bprops (obj/get props "blockProps") - data (obj/get bprops "data") - style (sts/generate-paragraph-styles (obj/get bprops "shape") + (let [bprops (obj/get props "blockProps") + data (obj/get bprops "data") + style (sts/generate-paragraph-styles (obj/get bprops "shape") (obj/get bprops "data")) - dir (:text-direction data "auto")] + dir (:text-direction data "auto")] [:div {:style style :dir dir} @@ -72,10 +66,9 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false ::mf/forward-ref true} - [props ref] - (let [{:keys [id x y width height grow-type content] :as shape} (obj/get props "shape") + [props _] + (let [{:keys [id content] :as shape} (obj/get props "shape") - zoom (mf/deref refs/selected-zoom) state-map (mf/deref refs/workspace-editor-state) state (get state-map id empty-editor-state) self-ref (mf/use-ref) @@ -86,9 +79,8 @@ (fn [event] (dom/stop-propagation event) (when (kbd/esc? event) - (do - (st/emit! :interrupt) - (st/emit! dw/clear-edition-mode)))) + (st/emit! :interrupt) + (st/emit! dw/clear-edition-mode))) on-mount (fn [] @@ -111,7 +103,7 @@ on-focus (mf/use-callback (mf/deps shape state) - (fn [event] + (fn [_] (reset! blured false))) on-change @@ -131,7 +123,7 @@ handle-return (mf/use-callback - (fn [event state] + (fn [_ state] (st/emit! (dwt/update-editor-state shape (ted/editor-split-block state))) "handled")) ] @@ -165,7 +157,7 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false ::mf/forward-ref true} - [props ref] + [props _] (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape") clip-id (str "clip-" id)] [:g.text-editor {:clip-path (str "url(#" clip-id ")")} diff --git a/frontend/src/app/main/ui/workspace/sidebar/align.cljs b/frontend/src/app/main/ui/workspace/sidebar/align.cljs index 7bf85b1a65..acb3c6a426 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/align.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/align.cljs @@ -6,13 +6,13 @@ (ns app.main.ui.workspace.sidebar.align (:require - [rumext.alpha :as mf] - [app.main.ui.icons :as i] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.main.data.workspace :as dw] - [app.util.i18n :as i18n :refer [t]] - [app.common.uuid :as uuid])) + [app.main.ui.icons :as i] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (mf/defc align-options [] @@ -25,14 +25,12 @@ (empty? selected) true (> (count selected) 1) false :else - (= uuid/zero (:frame-id (get objects (first selected))))) + (= uuid/zero (:frame-id (get objects (first selected))))) disabled-distribute (cond - (empty? selected) true - (< (count selected) 2) true - :else false) - - locale (i18n/use-locale) + (empty? selected) true + (< (count selected) 2) true + :else false) on-align-button-clicked (fn [axis] (when-not disabled (st/emit! (dw/align-objects axis)))) @@ -42,52 +40,52 @@ [:div.align-options [:div.align-group - [:div.align-button.tooltip.tooltip-bottom-right - {:alt (t locale "workspace.align.hleft") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :hleft)} - i/shape-halign-left] + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.align.hleft") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :hleft)} + i/shape-halign-left] - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.hcenter") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :hcenter)} - i/shape-halign-center] + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.align.hcenter") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :hcenter)} + i/shape-halign-center] - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.hright") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :hright)} - i/shape-halign-right] + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.align.hright") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :hright)} + i/shape-halign-right] - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.hdistribute") - :class (when disabled-distribute "disabled") - :on-click #(on-distribute-button-clicked :horizontal)} - i/shape-hdistribute]] + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.align.hdistribute") + :class (when disabled-distribute "disabled") + :on-click #(on-distribute-button-clicked :horizontal)} + i/shape-hdistribute]] [:div.align-group - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.vtop") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :vtop)} - i/shape-valign-top] + [:div.align-button.tooltip.tooltip-bottom + {:alt (tr "workspace.align.vtop") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :vtop)} + i/shape-valign-top] - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.vcenter") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :vcenter)} - i/shape-valign-center] + [:div.align-button.tooltip.tooltip-bottom-left + {:alt (tr "workspace.align.vcenter") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :vcenter)} + i/shape-valign-center] - [:div.align-button.tooltip.tooltip-bottom - {:alt (t locale "workspace.align.vbottom") - :class (when disabled "disabled") - :on-click #(on-align-button-clicked :vbottom)} - i/shape-valign-bottom] + [:div.align-button.tooltip.tooltip-bottom-left + {:alt (tr "workspace.align.vbottom") + :class (when disabled "disabled") + :on-click #(on-align-button-clicked :vbottom)} + i/shape-valign-bottom] - [:div.align-button.tooltip.tooltip-bottom-left - {:alt (t locale "workspace.align.vdistribute") - :class (when disabled-distribute "disabled") - :on-click #(on-distribute-button-clicked :vertical)} - i/shape-vdistribute]]])) + [:div.align-button.tooltip.tooltip-bottom-left + {:alt (tr "workspace.align.vdistribute") + :class (when disabled-distribute "disabled") + :on-click #(on-distribute-button-clicked :vertical)} + i/shape-vdistribute]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index de210b4224..8149b52d61 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -7,22 +7,18 @@ (ns app.main.ui.workspace.sidebar.assets (:require [app.common.data :as d] - [app.common.spec :as us] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] [app.common.media :as cm] [app.common.pages :as cp] + [app.common.spec :as us] [app.common.text :as txt] - [app.common.uuid :as uuid] [app.config :as cfg] - [app.main.data.workspace.colors :as dc] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.undo :as dwu] [app.main.exports :as exports] [app.main.refs :as refs] [app.main.store :as st] @@ -31,68 +27,122 @@ [app.main.ui.components.editable-label :refer [editable-label]] [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.components.forms :as fm] - [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]] [app.util.data :refer [matches-search]] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] - [app.util.timers :as timers] [cljs.spec.alpha :as s] [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) +; TODO: refactor to remove duplicate code and less parameter passing. +; - Move all state to [:workspace-local :assets-bar file-id :open-boxes {} +; :open-groups {} +; :reverse-sort? +; :listing-thumbs? +; :selected-assets {}] +; - Move selection code to independent functions that receive the state as a parameter. +; +; TODO: change update operations to admit multiple ids, thus avoiding the need of +; emitting many events and opening an undo transaction. Also move the logic +; of grouping, deleting, etc. to events in the data module, since now the +; selection info is in the global state. + ;; ---- Group assets management ---- +(defn group-assets + "Convert a list of assets in a nested structure like this: + + {'': [{assetA} {assetB}] + 'group1': {'': [{asset1A} {asset1B}] + 'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]} + 'subgroup12': {'': [{asset12A}]}} + 'group2': {'subgroup21': {'': [{asset21A}}}} + " + [assets] + (when-not (empty? assets) + (reduce (fn [groups asset] + (let [path-vector (cp/split-path (or (:path asset) ""))] + (update-in groups (conj path-vector "") + (fn [group] + (if-not group + [asset] + (conj group asset)))))) + {} + assets))) + +(defn add-group + [asset group-name] + (-> (:path asset) + (cp/merge-path-item group-name) + (cp/merge-path-item (:name asset)))) + +(defn rename-group + [asset path last-path] + (-> (:path asset) + (str/slice 0 (count path)) + (cp/split-path) + butlast + (vec) + (conj last-path) + (cp/join-path) + (str (str/slice (:path asset) (count path))) + (cp/merge-path-item (:name asset)))) + +(defn ungroup + [asset path] + (-> (:path asset) + (str/slice 0 (count path)) + (cp/split-path) + butlast + (cp/join-path) + (str (str/slice (:path asset) (count path))) + (cp/merge-path-item (:name asset)))) + (s/def ::asset-name ::us/not-empty-string) -(s/def ::create-group-form +(s/def ::name-group-form (s/keys :req-un [::asset-name])) -(defn group-assets - [assets] - (reduce (fn [groups asset] - (update groups (or (:path asset) "") - #(conj (or % []) asset))) - (sorted-map) - assets)) - -(def empty-folded-groups #{}) - -(defn toggle-folded-group - [folded-groups path] - (if (contains? folded-groups path) - (disj folded-groups path) - (conj folded-groups path))) - -(mf/defc create-group-dialog +(mf/defc name-group-dialog {::mf/register modal/components - ::mf/register-as :create-group-dialog} - [{:keys [create] :as ctx}] - (let [form (fm/use-form :spec ::create-group-form - :initial {}) + ::mf/register-as :name-group-dialog} + [{:keys [path last-path accept] :as ctx + :or {path "" last-path ""}}] + (let [initial (mf/use-memo + (mf/deps last-path) + (constantly {:asset-name last-path})) + form (fm/use-form :spec ::name-group-form + :initial initial) - close #(modal/hide!) + create? (empty? path) + + on-close #(modal/hide!) on-accept (mf/use-callback (mf/deps form) - (fn [event] + (fn [_] (let [asset-name (get-in @form [:clean-data :asset-name])] - (create asset-name) + (if create? + (accept asset-name) + (accept path asset-name)) (modal/hide!))))] [:div.modal-overlay [:div.modal-container.confirm-dialog [:div.modal-header [:div.modal-header-title - [:h2 (tr "workspace.assets.create-group")]] + [:h2 (if create? + (tr "workspace.assets.create-group") + (tr "workspace.assets.rename-group"))]] [:div.modal-close-button - {:on-click close} i/close]] + {:on-click on-close} i/close]] [:div.modal-content.generic-form [:& fm/form {:form form} @@ -106,56 +156,223 @@ [:input.cancel-button {:type "button" :value (tr "labels.cancel") - :on-click close}] + :on-click on-close}] [:input.accept-button.primary {:type "button" :class (when-not (:valid @form) "btn-disabled") :disabled (not (:valid @form)) - :value (tr "labels.create") + :value (if create? (tr "labels.create") (tr "labels.rename")) :on-click on-accept}]]]]])) +;; ---- Common blocks ---- + +(def auto-pos-menu-state {:open? false + :top nil + :left nil}) + +(defn- open-auto-pos-menu + [state event] + (let [pos (dom/get-client-position event) + top (:y pos) + left (+ (:x pos) 10)] + (dom/prevent-default event) + (assoc state + :open? true + :top top + :left left))) + +(defn- close-auto-pos-menu + [state] + (assoc state :open? false)) + +(mf/defc auto-pos-menu + [{:keys [options state on-close]}] + [:& context-menu + {:selectable false + :show (:open? state) + :on-close on-close + :top (:top state) + :left (:left state) + :options options}]) + +(mf/defc asset-section + [{:keys [children file-id title box assets-count open?]}] + (let [children (->> (if (array? children) children [children]) + (filter some?)) + get-role #(.. % -props -role) + title-buttons (filter #(= (get-role %) :title-button) children) + content (filter #(= (get-role %) :content) children)] + [:div.asset-section + [:div.asset-title {:class (when (not open?) "closed")} + [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id box (not open?)))} + i/arrow-slide title] + [:span.num-assets (str "\u00A0(") assets-count ")"] ;; Unicode 00A0 is non-breaking space + title-buttons] + (when open? + content)])) + +(mf/defc asset-section-block + [{:keys [children]}] + [:* children]) + +(mf/defc asset-group-title + [{:keys [file-id box path group-open? on-rename on-ungroup]}] + (when-not (empty? path) + (let [[other-path last-path truncated] (cp/compact-path path 35) + menu-state (mf/use-state auto-pos-menu-state) + + on-fold-group + (mf/use-callback + (mf/deps group-open?) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dwl/set-assets-group-open file-id + box + path + (not group-open?))))) + on-context-menu + (mf/use-callback + (fn [event] + (swap! menu-state #(open-auto-pos-menu % event)))) + + on-close-menu + (mf/use-callback + (fn [] + (swap! menu-state close-auto-pos-menu)))] + + [:div.group-title {:class (when-not group-open? "closed") + :on-click on-fold-group + :on-context-menu on-context-menu} + [:span i/arrow-slide] + (when-not (empty? other-path) + [:span.dim {:title (when truncated path)} + other-path "\u00A0/\u00A0"]) + [:span {:title (when truncated path)} + last-path] + [:& auto-pos-menu + {:on-close on-close-menu + :state @menu-state + :options [[(tr "workspace.assets.rename") #(on-rename % path last-path)] + [(tr "workspace.assets.ungroup") #(on-ungroup path)]]}]]))) + ;; ---- Components box ---- +(mf/defc components-item + [{:keys [component renaming listing-thumbs? selected-components + on-asset-click on-context-menu on-drag-start do-rename cancel-rename]}] + [:div {:key (:id component) + :class-name (dom/classnames + :selected (contains? selected-components (:id component)) + :grid-cell @listing-thumbs? + :enum-item (not @listing-thumbs?)) + :draggable true + :on-click #(on-asset-click % (:id component) nil) + :on-context-menu (on-context-menu (:id component)) + :on-drag-start (partial on-drag-start component)} + [:& exports/component-svg {:group (get-in component [:objects (:id component)]) + :objects (:objects component)}] + (let [renaming? (= renaming (:id component))] + [:& editable-label + {:class-name (dom/classnames + :cell-name @listing-thumbs? + :item-name (not @listing-thumbs?) + :editing renaming?) + :value (cp/merge-path-item (:path component) (:name component)) + :tooltip (cp/merge-path-item (:path component) (:name component)) + :display-value (if @listing-thumbs? + (:name component) + (cp/compact-name (:path component) + (:name component))) + :editing? renaming? + :disable-dbl-click? true + :on-change do-rename + :on-cancel cancel-rename}])]) + +(mf/defc components-group + [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected-components on-asset-click + on-drag-start do-rename cancel-rename on-rename-group on-ungroup on-context-menu]}] + (let [group-open? (get open-groups prefix true)] + + [:* + [:& asset-group-title {:file-id file-id + :box :components + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [components (get groups "" [])] + [:div {:class-name (dom/classnames + :asset-grid @listing-thumbs? + :big @listing-thumbs? + :asset-enum (not @listing-thumbs?))} + (for [component components] + [:& components-item {:component component + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-components selected-components + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename}])]) + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& components-group {:file-id file-id + :prefix (cp/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-components selected-components + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}]))])])) + (mf/defc components-box - [{:keys [file-id local? components listing-thumbs? open? selected-assets + [{:keys [file-id local? components listing-thumbs? open? open-groups selected-assets on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [state (mf/use-state {:menu-open false - :renaming nil - :top nil - :left nil - :component-id nil - :folded-groups empty-folded-groups}) + (let [state (mf/use-state {:renaming nil + :component-id nil}) + + menu-state (mf/use-state auto-pos-menu-state) selected-components (:components selected-assets) multi-components? (> (count selected-components) 1) - multi-assets? (or (not (empty? (:graphics selected-assets))) - (not (empty? (:colors selected-assets))) - (not (empty? (:typographies selected-assets)))) + multi-assets? (or (seq (:graphics selected-assets)) + (seq (:colors selected-assets)) + (seq (:typographies selected-assets))) - groups (group-assets components) - folded-groups (:folded-groups @state) + groups (group-assets components) on-duplicate (mf/use-callback - (mf/deps @state) - (fn [] - (if (empty? selected-components) - (st/emit! (dwl/duplicate-component {:id (:component-id @state)})) - (do - (st/emit! (dwu/start-undo-transaction)) - (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected-components)) - (st/emit! (dwu/commit-undo-transaction)))))) + (mf/deps @state) + (fn [] + (if (empty? selected-components) + (st/emit! (dwl/duplicate-component {:id (:component-id @state)})) + (do + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected-components)) + (st/emit! (dwu/commit-undo-transaction)))))) on-delete (mf/use-callback - (mf/deps @state file-id multi-components? multi-assets?) - (fn [] - (if (or multi-components? multi-assets?) - (on-assets-delete) - (st/emit! (dwl/delete-component {:id (:component-id @state)}))) - (st/emit! (dwl/sync-file file-id file-id)))) + (mf/deps @state file-id multi-components? multi-assets?) + (fn [] + (if (or multi-components? multi-assets?) + (on-assets-delete) + (st/emit! (dwu/start-undo-transaction) + (dwl/delete-component {:id (:component-id @state)}) + (dwl/sync-file file-id file-id) + (dwu/commit-undo-transaction))))) on-rename (mf/use-callback @@ -181,47 +398,75 @@ (fn [component-id] (fn [event] (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (when-not (contains? selected-components component-id) - (on-clear-selection)) - (swap! state assoc :menu-open true - :top top - :left left - :component-id component-id)))))) + (when-not (contains? selected-components component-id) + (on-clear-selection)) + (swap! state assoc :component-id component-id) + (swap! menu-state #(open-auto-pos-menu % event)))))) + + on-close-menu + (mf/use-callback + (fn [] + (swap! menu-state close-auto-pos-menu))) create-group (mf/use-callback (mf/deps components selected-components on-clear-selection) - (fn [name] + (fn [group-name] (on-clear-selection) (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> components - (filter #(contains? selected-components (:id %))) + (filter #(if multi-components? + (contains? selected-components (:id %)) + (= (:component-id @state) (:id %)))) (map #(dwl/rename-component (:id %) - (str name " / " - (cp/merge-path-item (:path %) (:name %))))))) + (add-group % group-name))))) (st/emit! (dwu/commit-undo-transaction)))) - on-fold-group + rename-group (mf/use-callback - (mf/deps groups folded-groups) - (fn [path] - (fn [event] - (dom/stop-propagation event) - (swap! state update :folded-groups - toggle-folded-group path)))) + (mf/deps components) + (fn [path last-path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component + (:id %) + (rename-group % path last-path))))) + (st/emit! (dwu/commit-undo-transaction)))) on-group (mf/use-callback (mf/deps components selected-components) (fn [event] (dom/stop-propagation event) - (modal/show! :create-group-dialog {:create create-group}))) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-callback + (mf/deps components) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + + on-ungroup + (mf/use-callback + (mf/deps components) + (fn [path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> components + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-component + (:id %) + (ungroup % path))))) + (st/emit! (dwu/commit-undo-transaction)))) on-drag-start (mf/use-callback @@ -230,98 +475,135 @@ :component component}) (dnd/set-allowed-effect! event "move")))] - [:div.asset-section - [:div.asset-title {:class (when (not open?) "closed")} - [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :components (not open?)))} - i/arrow-slide (tr "workspace.assets.components")] - [:span (str "\u00A0(") (count components) ")"]] ;; Unicode 00A0 is non-breaking space - (when open? - (for [group groups] - (let [path (first group) - components (second group) - group-open? (not (contains? folded-groups path))] - [:* - (when-not (empty? path) - (let [[other-path last-path truncated] (cp/compact-path path 35)] - [:div.group-title {:class (when-not group-open? "closed") - :on-click (on-fold-group path)} - [:span i/arrow-slide] - (when-not (empty? other-path) - [:span.dim {:title (when truncated path)} - other-path "\u00A0/\u00A0"]) - [:span {:title (when truncated path)} - last-path]])) - (when group-open? - [:div {:class-name (dom/classnames - :asset-grid @listing-thumbs? - :big @listing-thumbs? - :asset-enum (not @listing-thumbs?))} - (for [component components] - (let [renaming? (= (:renaming @state)(:id component))] - [:div {:key (:id component) - :class-name (dom/classnames - :selected (contains? selected-components (:id component)) - :grid-cell @listing-thumbs? - :enum-item (not @listing-thumbs?)) - :draggable true - :on-click #(on-asset-click % (:id component) groups nil) - :on-context-menu (on-context-menu (:id component)) - :on-drag-start (partial on-drag-start component)} - [:& exports/component-svg {:group (get-in component [:objects (:id component)]) - :objects (:objects component)}] - [:& editable-label - {:class-name (dom/classnames - :cell-name @listing-thumbs? - :item-name (not @listing-thumbs?) - :editing renaming?) - :value (cp/merge-path-item (:path component) (:name component)) - :tooltip (cp/merge-path-item (:path component) (:name component)) - :display-value (if @listing-thumbs? - (:name component) - (cp/compact-name (:path component) - (:name component))) - :editing? renaming? - :disable-dbl-click? true - :on-change do-rename - :on-cancel cancel-rename}]]))])]))) - - (when local? - [:& context-menu - {:selectable false - :show (:menu-open @state) - :on-close #(swap! state assoc :menu-open false) - :top (:top @state) - :left (:left @state) - :options [(when-not (or multi-components? multi-assets?) - [(tr "workspace.assets.rename") on-rename]) - (when-not multi-assets? - [(tr "workspace.assets.duplicate") on-duplicate]) - [(tr "workspace.assets.delete") on-delete] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}])])) + [:& asset-section {:file-id file-id + :title (tr "workspace.assets.components") + :box :components + :assets-count (count components) + :open? open?} + [:& asset-section-block {:role :content} + [:& components-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :renaming (:renaming @state) + :listing-thumbs? listing-thumbs? + :selected-components selected-components + :on-asset-click (partial on-asset-click groups) + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}] + (when local? + [:& auto-pos-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-components? multi-assets?) + [(tr "workspace.assets.rename") on-rename]) + (when-not multi-assets? + [(tr "workspace.assets.duplicate") on-duplicate]) + [(tr "workspace.assets.delete") on-delete] + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])]])) ;; ---- Graphics box ---- +(mf/defc graphics-item + [{:keys [object renaming listing-thumbs? selected-objects + on-asset-click on-context-menu on-drag-start do-rename cancel-rename]}] + [:div {:key (:id object) + :class-name (dom/classnames + :selected (contains? selected-objects (:id object)) + :grid-cell @listing-thumbs? + :enum-item (not @listing-thumbs?)) + :draggable true + :on-click #(on-asset-click % (:id object) nil) + :on-context-menu (on-context-menu (:id object)) + :on-drag-start (partial on-drag-start object)} + [:img {:src (cfg/resolve-file-media object true) + :draggable false}] ;; Also need to add css pointer-events: none + + (let [renaming? (= renaming (:id object))] + [:& editable-label + {:class-name (dom/classnames + :cell-name @listing-thumbs? + :item-name (not @listing-thumbs?) + :editing renaming?) + :value (cp/merge-path-item (:path object) (:name object)) + :tooltip (cp/merge-path-item (:path object) (:name object)) + :display-value (if @listing-thumbs? + (:name object) + (cp/compact-name (:path object) + (:name object))) + :editing? renaming? + :disable-dbl-click? true + :on-change do-rename + :on-cancel cancel-rename}])]) + +(mf/defc graphics-group + [{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected-objects on-asset-click + on-drag-start do-rename cancel-rename on-rename-group on-ungroup + on-context-menu]}] + (let [group-open? (get open-groups prefix true)] + + [:* + [:& asset-group-title {:file-id file-id + :box :graphics + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [objects (get groups "" [])] + [:div {:class-name (dom/classnames + :asset-grid @listing-thumbs? + :asset-enum (not @listing-thumbs?))} + (for [object objects] + [:& graphics-item {:object object + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-objects selected-objects + :on-asset-click on-asset-click + :on-context-menu on-context-menu + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename}])]) + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& graphics-group {:file-id file-id + :prefix (cp/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :renaming renaming + :listing-thumbs? listing-thumbs? + :selected-objects selected-objects + :on-asset-click on-asset-click + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}]))])])) + (mf/defc graphics-box - [{:keys [file-id local? objects listing-thumbs? open? selected-assets + [{:keys [file-id local? objects listing-thumbs? open? open-groups selected-assets on-asset-click on-assets-delete on-clear-selection] :as props}] (let [input-ref (mf/use-ref nil) - state (mf/use-state {:menu-open false - :renaming nil - :top nil - :left nil - :object-id nil - :folded-groups empty-folded-groups}) + state (mf/use-state {:renaming nil + :object-id nil}) + + menu-state (mf/use-state auto-pos-menu-state) selected-objects (:graphics selected-assets) multi-objects? (> (count selected-objects) 1) - multi-assets? (or (not (empty? (:components selected-assets))) - (not (empty? (:colors selected-assets))) - (not (empty? (:typographies selected-assets)))) + multi-assets? (or (seq (:components selected-assets)) + (seq (:colors selected-assets)) + (seq (:typographies selected-assets))) - groups (group-assets objects) - folded-groups (:folded-groups @state) + groups (group-assets objects) add-graphic (mf/use-callback @@ -369,47 +651,74 @@ (fn [object-id] (fn [event] (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (when-not (contains? selected-objects object-id) - (on-clear-selection)) - (swap! state assoc :menu-open true - :top top - :left left - :object-id object-id)))))) + (when-not (contains? selected-objects object-id) + (on-clear-selection)) + (swap! state assoc :object-id object-id) + (swap! menu-state #(open-auto-pos-menu % event)))))) + + on-close-menu + (mf/use-callback + (fn [] + (swap! menu-state close-auto-pos-menu))) create-group (mf/use-callback (mf/deps objects selected-objects on-clear-selection) - (fn [name] + (fn [group-name] (on-clear-selection) (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> objects - (filter #(contains? selected-objects (:id %))) + (filter #(if multi-objects? + (contains? selected-objects (:id %)) + (= (:object-id @state) (:id %)))) (map #(dwl/rename-media (:id %) - (str name " / " - (cp/merge-path-item (:path %) (:name %))))))) + (add-group % group-name))))) (st/emit! (dwu/commit-undo-transaction)))) - on-fold-group + rename-group (mf/use-callback - (mf/deps groups folded-groups) - (fn [path] - (fn [event] - (dom/stop-propagation event) - (swap! state update :folded-groups - toggle-folded-group path)))) + (mf/deps objects) + (fn [path last-path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media + (:id %) + (rename-group % path last-path))))) + (st/emit! (dwu/commit-undo-transaction)))) on-group (mf/use-callback (mf/deps objects selected-objects) (fn [event] (dom/stop-propagation event) - (modal/show! :create-group-dialog {:create create-group}))) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-callback + (mf/deps objects) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + on-ungroup + (mf/use-callback + (mf/deps objects) + (fn [path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> objects + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/rename-media + (:id %) + (ungroup % path))))) + (st/emit! (dwu/commit-undo-transaction)))) on-drag-start (mf/use-callback @@ -419,103 +728,65 @@ (dnd/set-data! event "text/asset-type" mtype) (dnd/set-allowed-effect! event "move")))] - [:div.asset-section - [:div.asset-title {:class (when (not open?) "closed")} - [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :graphics (not open?)))} - i/arrow-slide (tr "workspace.assets.graphics")] - [:span.num-assets (str "\u00A0(") (count objects) ")"] ;; Unicode 00A0 is non-breaking space + [:& asset-section {:file-id file-id + :title (tr "workspace.assets.graphics") + :box :graphics + :assets-count (count objects) + :open? open?} (when local? - [:div.assets-button {:on-click add-graphic} - i/plus - [:& file-uploader {:accept cm/str-image-types - :multi true - :input-ref input-ref - :on-selected on-file-selected}]])] - (when open? - (for [group groups] - (let [path (first group) - objects (second group) - group-open? (not (contains? folded-groups path))] - [:* - (when-not (empty? path) - (let [[other-path last-path truncated] (cp/compact-path path 35)] - [:div.group-title {:class (when-not group-open? "closed") - :on-click (on-fold-group path)} - [:span i/arrow-slide] - (when-not (empty? other-path) - [:span.dim {:title (when truncated path)} - other-path "\u00A0/\u00A0"]) - [:span {:title (when truncated path)} - last-path]])) - (when group-open? - [:div {:class-name (dom/classnames - :asset-grid @listing-thumbs? - :asset-enum (not @listing-thumbs?))} - (for [object objects] - [:div {:key (:id object) - :class-name (dom/classnames - :selected (contains? selected-objects (:id object)) - :grid-cell @listing-thumbs? - :enum-item (not @listing-thumbs?)) - :draggable true - :on-click #(on-asset-click % (:id object) groups nil) - :on-context-menu (on-context-menu (:id object)) - :on-drag-start (partial on-drag-start object)} - [:img {:src (cfg/resolve-file-media object true) - :draggable false}] ;; Also need to add css pointer-events: none + [:& asset-section-block {:role :title-button} + [:div.assets-button {:on-click add-graphic} + i/plus + [:& file-uploader {:accept cm/str-image-types + :multi true + :ref input-ref + :on-selected on-file-selected}]]]) - (let [renaming? (= (:renaming @state) (:id object))] - [:& editable-label - {:class-name (dom/classnames - :cell-name @listing-thumbs? - :item-name (not @listing-thumbs?) - :editing renaming?) - :value (cp/merge-path-item (:path object) (:name object)) - :tooltip (cp/merge-path-item (:path object) (:name object)) - :display-value (if @listing-thumbs? - (:name object) - (cp/compact-name (:path object) - (:name object))) - :editing? renaming? - :disable-dbl-click? true - :on-change do-rename - :on-cancel cancel-rename}])])])]))) - - (when local? - [:& context-menu - {:selectable false - :show (:menu-open @state) - :on-close #(swap! state assoc :menu-open false) - :top (:top @state) - :left (:left @state) - :options [(when-not (or multi-objects? multi-assets?) - [(tr "workspace.assets.rename") on-rename]) - [(tr "workspace.assets.delete") on-delete] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}])])) + [:& asset-section-block {:role :content} + [:& graphics-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :renaming (:renaming @state) + :listing-thumbs? listing-thumbs? + :selected-objects selected-objects + :on-asset-click (partial on-asset-click groups) + :on-drag-start on-drag-start + :do-rename do-rename + :cancel-rename cancel-rename + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}] + (when local? + [:& auto-pos-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-objects? multi-assets?) + [(tr "workspace.assets.rename") on-rename]) + [(tr "workspace.assets.delete") on-delete] + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])]])) ;; ---- Colors box ---- (mf/defc color-item [{:keys [color local? file-id selected-colors multi-colors? multi-assets? - on-asset-click on-assets-delete on-clear-selection on-group - colors locale] :as props}] + on-asset-click on-assets-delete on-clear-selection on-group] :as props}] (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color)) - id (:id color) input-ref (mf/use-ref) - state (mf/use-state {:menu-open false - :top nil - :left nil - :editing rename?}) + state (mf/use-state {:editing rename?}) + + menu-state (mf/use-state auto-pos-menu-state) default-name (cond (:gradient color) (bc/gradient-type->string (get-in color [:gradient :type])) (:color color) (:color color) :else (:value color)) + ;; TODO: looks like the first argument is not necessary apply-color - (fn [color-id event] + (fn [_ event] (let [ids (wsh/lookup-selected @st/state)] (if (kbd/shift? event) (st/emit! (dc/change-stroke ids color)) @@ -527,16 +798,21 @@ edit-color (fn [new-color] - (let [updated-color (merge new-color (select-keys color [:id :file-id :name]))] + (let [old-data (-> (select-keys color [:id :file-id]) + (assoc :name (cp/merge-path-item (:path color) (:name color)))) + updated-color (merge new-color old-data)] (st/emit! (dwl/update-color updated-color file-id)))) delete-color (mf/use-callback - (mf/deps @state multi-colors? multi-assets?) + (mf/deps @state multi-colors? multi-assets? file-id) (fn [] (if (or multi-colors? multi-assets?) (on-assets-delete) - (st/emit! (dwl/delete-color color))))) + (st/emit! (dwu/start-undo-transaction) + (dwl/delete-color color) + (dwl/sync-file file-id file-id) + (dwu/commit-undo-transaction))))) rename-color-clicked (fn [event] @@ -574,16 +850,14 @@ (mf/deps color selected-colors on-clear-selection) (fn [event] (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (+ 10 (:x pos))] - (dom/prevent-default event) - (when-not (contains? selected-colors (:id color)) - (on-clear-selection)) - (swap! state assoc - :menu-open true - :top top - :left left)))))] + (when-not (contains? selected-colors (:id color)) + (on-clear-selection)) + (swap! menu-state #(open-auto-pos-menu % event))))) + + on-close-menu + (mf/use-callback + (fn [] + (swap! menu-state close-auto-pos-menu)))] (mf/use-effect (mf/deps (:editing @state)) @@ -596,7 +870,7 @@ :selected (contains? selected-colors (:id color))) :on-context-menu on-context-menu :on-click (when-not (:editing @state) - #(on-asset-click % (:id color) {"" colors} + #(on-asset-click % (:id color) (partial apply-color (:id color))))} [:& bc/color-bullet {:color color}] @@ -607,45 +881,91 @@ :on-blur input-blur :on-key-down input-key-down :auto-focus true - :default-value (:name color "")}] + :default-value (cp/merge-path-item (:path color) (:name color))}] [:div.name-block {:on-double-click rename-color-clicked} (:name color) (when-not (= (:name color) default-name) [:span default-name])]) (when local? - [:& context-menu - {:selectable false - :show (:menu-open @state) - :on-close #(swap! state assoc :menu-open false) - :top (:top @state) - :left (:left @state) + [:& auto-pos-menu + {:on-close on-close-menu + :state @menu-state :options [(when-not (or multi-colors? multi-assets?) - [(t locale "workspace.assets.rename") rename-color-clicked]) + [(tr "workspace.assets.rename") rename-color-clicked]) (when-not (or multi-colors? multi-assets?) - [(t locale "workspace.assets.edit") edit-color-clicked]) - [(t locale "workspace.assets.delete") delete-color] + [(tr "workspace.assets.edit") edit-color-clicked]) + [(tr "workspace.assets.delete") delete-color] (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}])])) + [(tr "workspace.assets.group") (on-group (:id color))])]}])])) + +(mf/defc colors-group + [{:keys [file-id prefix groups open-groups local? selected-colors + multi-colors? multi-assets? on-asset-click on-assets-delete + on-clear-selection on-group on-rename-group on-ungroup colors]}] + (let [group-open? (get open-groups prefix true)] + + [:* + [:& asset-group-title {:file-id file-id + :box :colors + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [colors (get groups "" [])] + [:div.asset-list + (for [color colors] + (let [color (cond-> color + (:value color) (assoc :color (:value color) :opacity 1) + (:value color) (dissoc :value) + true (assoc :file-id file-id))] + [:& color-item {:key (:id color) + :color color + :file-id file-id + :local? local? + :selected-colors selected-colors + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :colors colors}]))]) + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& colors-group {:file-id file-id + :prefix (cp/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :local? local? + :selected-colors selected-colors + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :colors colors}]))])])) (mf/defc colors-box - [{:keys [file-id local? colors locale open? selected-assets + [{:keys [file-id local? colors open? open-groups selected-assets on-asset-click on-assets-delete on-clear-selection] :as props}] - (let [state (mf/use-state {:folded-groups empty-folded-groups}) - - selected-colors (:colors selected-assets) + (let [selected-colors (:colors selected-assets) multi-colors? (> (count selected-colors) 1) - multi-assets? (or (not (empty? (:components selected-assets))) - (not (empty? (:graphics selected-assets))) - (not (empty? (:typographies selected-assets)))) + multi-assets? (or (seq (:components selected-assets)) + (seq (:graphics selected-assets)) + (seq (:typographies selected-assets))) - groups (group-assets colors) - folded-groups (:folded-groups @state) + groups (group-assets colors) add-color (mf/use-callback (mf/deps file-id) - (fn [value opacity] + (fn [value _opacity] (st/emit! (dwl/add-color value)))) add-color-clicked @@ -664,107 +984,167 @@ create-group (mf/use-callback (mf/deps colors selected-colors on-clear-selection file-id) - (fn [name] + (fn [color-id] + (fn [group-name] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> colors + (filter #(if multi-colors? + (contains? selected-colors (:id %)) + (= color-id (:id %)))) + (map #(dwl/update-color + (assoc % :name + (add-group % group-name)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction))))) + + rename-group + (mf/use-callback + (mf/deps colors) + (fn [path last-path] (on-clear-selection) (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> colors - (filter #(contains? selected-colors (:id %))) + (filter #(str/starts-with? (:path %) path)) (map #(dwl/update-color (assoc % :name - (str name " / " - (cp/merge-path-item (:path %) (:name %)))) + (rename-group % path last-path)) file-id)))) (st/emit! (dwu/commit-undo-transaction)))) - on-fold-group - (mf/use-callback - (mf/deps groups folded-groups) - (fn [path] - (fn [event] - (dom/stop-propagation event) - (swap! state update :folded-groups - toggle-folded-group path)))) - on-group (mf/use-callback (mf/deps colors selected-colors) - (fn [event] + (fn [color-id] + (fn [event] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:accept (create-group color-id)})))) + + on-rename-group + (mf/use-callback + (mf/deps colors) + (fn [event path last-path] (dom/stop-propagation event) - (modal/show! :create-group-dialog {:create create-group})))] + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + on-ungroup + (mf/use-callback + (mf/deps colors) + (fn [path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> colors + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-color + (assoc % :name + (ungroup % path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction))))] - [:div.asset-section - [:div.asset-title {:class (when (not open?) "closed")} - [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :colors (not open?)))} - i/arrow-slide (t locale "workspace.assets.colors")] - [:span.num-assets (str "\u00A0(") (count colors) ")"] ;; Unicode 00A0 is non-breaking space + [:& asset-section {:file-id file-id + :title (tr "workspace.assets.colors") + :box :colors + :assets-count (count colors) + :open? open?} (when local? - [:div.assets-button {:on-click add-color-clicked} i/plus])] - (when open? - (for [group groups] - (let [path (first group) - colors (second group) - group-open? (not (contains? folded-groups path))] - [:* - (when-not (empty? path) - (let [[other-path last-path truncated] (cp/compact-path path 35)] - [:div.group-title {:class (when-not group-open? "closed") - :on-click (on-fold-group path)} - [:span i/arrow-slide] - (when-not (empty? other-path) - [:span.dim {:title (when truncated path)} - other-path "\u00A0/\u00A0"]) - [:span {:title (when truncated path)} - last-path]])) - (when group-open? - [:div.asset-list - (for [color colors] - (let [color (cond-> color - (:value color) (assoc :color (:value color) :opacity 1) - (:value color) (dissoc :value) - true (assoc :file-id file-id))] - [:& color-item {:key (:id color) - :color color - :file-id file-id - :local? local? - :selected-colors selected-colors - :multi-colors? multi-colors? - :multi-assets? multi-assets? - :on-asset-click on-asset-click - :on-assets-delete on-assets-delete - :on-clear-selection on-clear-selection - :on-group on-group - :colors colors - :locale locale}]))])])))])) + [:& asset-section-block {:role :title-button} + [:div.assets-button {:on-click add-color-clicked} + i/plus]]) + [:& asset-section-block {:role :content} + [:& colors-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :local? local? + :selected-colors selected-colors + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click (partial on-asset-click groups) + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :on-group on-group + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :colors colors}]]])) ;; ---- Typography box ---- -(mf/defc typography-box - [{:keys [file file-id local? typographies locale open? selected-assets +(mf/defc typographies-group + [{:keys [file-id prefix groups open-groups file local? selected-typographies local + editting-id on-asset-click handle-change apply-typography + on-rename-group on-ungroup on-context-menu]}] + (let [group-open? (get open-groups prefix true)] + + [:* + [:& asset-group-title {:file-id file-id + :box :typographies + :path prefix + :group-open? group-open? + :on-rename on-rename-group + :on-ungroup on-ungroup}] + (when group-open? + [:* + (let [typographies (get groups "" [])] + [:div.asset-list + (for [typography typographies] + [:& typography-entry + {:key (:id typography) + :typography typography + :file file + :read-only? (not local?) + :on-context-menu #(on-context-menu (:id typography) %) + :on-change #(handle-change typography %) + :selected? (contains? selected-typographies (:id typography)) + :on-click #(on-asset-click % (:id typography) + (partial apply-typography typography)) + :editting? (= editting-id (:id typography)) + :focus-name? (= (:rename-typography local) (:id typography))}])]) + + (for [[path-item content] groups] + (when-not (empty? path-item) + [:& typographies-group {:file-id file-id + :prefix (cp/merge-path-item prefix path-item) + :groups content + :open-groups open-groups + :file file + :local? local? + :selected-typographies selected-typographies + :editting-id editting-id + :local local + :on-asset-click on-asset-click + :handle-change handle-change + :apply-typography apply-typography + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}]))])])) + +(mf/defc typographies-box + [{:keys [file file-id local? typographies open? open-groups selected-assets on-asset-click on-assets-delete on-clear-selection] :as props}] (let [state (mf/use-state {:detail-open? false - :menu-open? false - :top nil - :left nil - :id nil - :folded-groups empty-folded-groups}) + :id nil}) + + menu-state (mf/use-state auto-pos-menu-state) local (deref refs/workspace-local) groups (group-assets typographies) - folded-groups (:folded-groups @state) selected-typographies (:typographies selected-assets) multi-typographies? (> (count selected-typographies) 1) - multi-assets? (or (not (empty? (:components selected-assets))) - (not (empty? (:graphics selected-assets))) - (not (empty? (:colors selected-assets)))) + multi-assets? (or (seq (:components selected-assets)) + (seq (:graphics selected-assets)) + (seq (:colors selected-assets))) add-typography (mf/use-callback (mf/deps file-id) - (fn [value opacity] + (fn [_] (st/emit! (dwl/add-typography txt/default-typography)))) handle-change @@ -774,7 +1154,7 @@ (st/emit! (dwl/update-typography (merge typography changes) file-id)))) apply-typography - (fn [typography event] + (fn [typography _event] (let [ids (wsh/lookup-selected @st/state) attrs (merge {:typography-ref-file file-id @@ -786,56 +1166,79 @@ create-group (mf/use-callback (mf/deps typographies selected-typographies on-clear-selection file-id) - (fn [name] + (fn [group-name] (on-clear-selection) (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> typographies - (filter #(contains? selected-typographies (:id %))) + (filter #(if multi-typographies? + (contains? selected-typographies (:id %)) + (= (:id @state) (:id %)))) (map #(dwl/update-typography (assoc % :name - (str name " / " - (cp/merge-path-item (:path %) (:name %)))) + (add-group % group-name)) file-id)))) (st/emit! (dwu/commit-undo-transaction)))) - on-fold-group + rename-group (mf/use-callback - (mf/deps groups folded-groups) - (fn [path] - (fn [event] - (dom/stop-propagation event) - (swap! state update :folded-groups - toggle-folded-group path)))) + (mf/deps typographies) + (fn [path last-path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> typographies + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-typography + (assoc % :name + (rename-group % path last-path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction)))) on-group (mf/use-callback (mf/deps typographies selected-typographies) (fn [event] (dom/stop-propagation event) - (modal/show! :create-group-dialog {:create create-group}))) + (modal/show! :name-group-dialog {:accept create-group}))) + + on-rename-group + (mf/use-callback + (mf/deps typographies) + (fn [event path last-path] + (dom/stop-propagation event) + (modal/show! :name-group-dialog {:path path + :last-path last-path + :accept rename-group}))) + on-ungroup + (mf/use-callback + (mf/deps typographies) + (fn [path] + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! + (->> typographies + (filter #(str/starts-with? (:path %) path)) + (map #(dwl/update-typography + (assoc % :name + (ungroup % path)) + file-id)))) + (st/emit! (dwu/commit-undo-transaction)))) on-context-menu (mf/use-callback (mf/deps selected-typographies on-clear-selection) (fn [id event] (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (when-not (contains? selected-typographies id) - (on-clear-selection)) - (swap! state assoc - :menu-open? true - :top top - :left left - :id id))))) + (when-not (contains? selected-typographies id) + (on-clear-selection)) + (swap! state assoc :id id) + (swap! menu-state #(open-auto-pos-menu % event))))) - closed-typography-edit + on-close-menu (mf/use-callback - (mf/deps file-id) - (fn [event] )) + (fn [] + (swap! menu-state close-auto-pos-menu))) handle-rename-typography-clicked (fn [] @@ -851,7 +1254,10 @@ (fn [] (if (or multi-typographies? multi-assets?) (on-assets-delete) - (st/emit! (dwl/delete-typography (:id @state)))))) + (st/emit! (dwu/start-undo-transaction) + (dwl/delete-typography (:id @state)) + (dwl/sync-file file-id file-id) + (dwu/commit-undo-transaction))))) editting-id (or (:rename-typography local) (:edit-typography local))] @@ -863,58 +1269,45 @@ (when (:edit-typography local) (st/emit! #(update % :workspace-local dissoc :edit-typography))))) - [:div.asset-section - [:div.asset-title {:class (when (not open?) "closed")} - [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :typographies (not open?)))} - i/arrow-slide (t locale "workspace.assets.typography")] - [:span.num-assets (str "\u00A0(") (count typographies) ")"] ;; Unicode 00A0 is non-breaking space + [:& asset-section {:file-id file-id + :title (tr "workspace.assets.typography") + :box :typographies + :assets-count (count typographies) + :open? open?} (when local? - [:div.assets-button {:on-click add-typography} i/plus])] + [:& asset-section-block {:role :title-button} + [:div.assets-button {:on-click add-typography} + i/plus]]) - [:& context-menu - {:selectable false - :show (:menu-open? @state) - :on-close #(swap! state assoc :menu-open? false) - :top (:top @state) - :left (:left @state) - :options [(when-not (or multi-typographies? multi-assets?) - [(t locale "workspace.assets.rename") handle-rename-typography-clicked]) - (when-not (or multi-typographies? multi-assets?) - [(t locale "workspace.assets.edit") handle-edit-typography-clicked]) - [(t locale "workspace.assets.delete") handle-delete-typography] - (when-not multi-assets? - [(tr "workspace.assets.group") on-group])]}] - (when open? - (for [group groups] - (let [path (first group) - typographies (second group) - group-open? (not (contains? folded-groups path))] - [:* - (when-not (empty? path) - (let [[other-path last-path truncated] (cp/compact-path path 35)] - [:div.group-title {:class (when-not group-open? "closed") - :on-click (on-fold-group path)} - [:span i/arrow-slide] - (when-not (empty? other-path) - [:span.dim {:title (when truncated path)} - other-path "\u00A0/\u00A0"]) - [:span {:title (when truncated path)} - last-path]])) - (when group-open? - [:div.asset-list - (for [typography typographies] - [:& typography-entry - {:key (:id typography) - :typography typography - :file file - :read-only? (not local?) - :on-context-menu #(on-context-menu (:id typography) %) - :on-change #(handle-change typography %) - :selected? (contains? selected-typographies (:id typography)) - :on-click #(on-asset-click % (:id typography) {"" typographies} - (partial apply-typography typography)) - :editting? (= editting-id (:id typography)) - :focus-name? (= (:rename-typography local) (:id typography))}])])])))])) + [:& asset-section-block {:role :content} + [:& typographies-group {:file-id file-id + :prefix "" + :groups groups + :open-groups open-groups + :state state + :file file + :local? local? + :selected-typographies selected-typographies + :editting-id editting-id + :local local + :on-asset-click (partial on-asset-click groups) + :handle-change handle-change + :apply-typography apply-typography + :on-rename-group on-rename-group + :on-ungroup on-ungroup + :on-context-menu on-context-menu}] + + (when local? + [:& auto-pos-menu + {:on-close on-close-menu + :state @menu-state + :options [(when-not (or multi-typographies? multi-assets?) + [(tr "workspace.assets.rename") handle-rename-typography-clicked]) + (when-not (or multi-typographies? multi-assets?) + [(tr "workspace.assets.edit") handle-edit-typography-clicked]) + [(tr "workspace.assets.delete") handle-delete-typography] + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])]])) ;; --- Assets toolbox ---- @@ -924,7 +1317,7 @@ (l/derived (fn [state] (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:colors])) + (vals (get wfile :colors)) (vals (get-in state [:workspace-libraries id :data :colors]))))) st/state =)) @@ -933,7 +1326,7 @@ (l/derived (fn [state] (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:media])) + (vals (get wfile :media)) (vals (get-in state [:workspace-libraries id :data :media]))))) st/state =)) @@ -942,7 +1335,7 @@ (l/derived (fn [state] (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:components])) + (vals (get wfile :components)) (vals (get-in state [:workspace-libraries id :data :components]))))) st/state =)) @@ -951,7 +1344,7 @@ (l/derived (fn [state] (let [wfile (:workspace-data state)] (if (= (:id wfile) id) - (vals (get-in wfile [:typographies])) + (vals (get wfile :typographies)) (vals (get-in state [:workspace-libraries id :data :typographies]))))) st/state =)) @@ -967,10 +1360,16 @@ (filter (fn [item] (or (matches-search (:name item "!$!") (:term filters)) (matches-search (:value item "!$!") (:term filters))))) - (sort-by #(str/lower (:name %)) comp-fn)))) + ; Sort by folder order, but putting all "root" items always first, + ; independently of sort order. + (sort-by #(str/lower (cp/merge-path-item (if (empty? (:path %)) + (if reverse-sort? "z" "a") + (:path %)) + (:name %))) + comp-fn)))) (mf/defc file-library - [{:keys [file local? default-open? filters locale] :as props}] + [{:keys [file local? default-open? filters] :as props}] (let [open-file (mf/deref (open-file-ref (:id file))) open? (-> open-file :library @@ -979,6 +1378,11 @@ (-> open-file box (d/nilv true))) + open-groups (fn [box] + (-> open-file + :groups + box + (d/nilv {}))) shared? (:is-shared file) router (mf/deref refs/router) @@ -1016,44 +1420,53 @@ toggle-sort (mf/use-callback - (fn [event] - (swap! reverse-sort? not))) + (fn [_] + (swap! reverse-sort? not))) toggle-listing (mf/use-callback - (fn [event] - (swap! listing-thumbs? not))) + (fn [_] + (swap! listing-thumbs? not))) toggle-selected-asset (mf/use-callback - (mf/deps @selected-assets) - (fn [asset-type asset-id] - (swap! selected-assets update asset-type - (fn [selected] - (if (contains? selected asset-id) - (disj selected asset-id) - (conj selected asset-id)))))) + (mf/deps @selected-assets) + (fn [asset-type asset-id] + (swap! selected-assets update asset-type + (fn [selected] + (if (contains? selected asset-id) + (disj selected asset-id) + (conj selected asset-id)))))) extend-selected-assets (mf/use-callback (mf/deps @selected-assets) - (fn [asset-type asset-id asset-groups] - (swap! selected-assets update asset-type - (fn [selected] - (let [all-assets (-> asset-groups vals flatten) - clicked-idx (d/index-of-pred all-assets #(= (:id %) asset-id)) - selected-idx (->> selected - (map (fn [id] - (d/index-of-pred all-assets - #(= (:id %) id))))) - min-idx (apply min (conj selected-idx clicked-idx)) - max-idx (apply max (conj selected-idx clicked-idx))] + (fn [asset-type asset-groups asset-id] + (letfn [(flatten-groups + [groups] + (concat + (get groups "" []) + (reduce concat + [] + (->> (filter #(seq (first %)) groups) + (map second) + (map flatten-groups)))))] + (swap! selected-assets update asset-type + (fn [selected] + (let [all-assets (flatten-groups asset-groups) + clicked-idx (d/index-of-pred all-assets #(= (:id %) asset-id)) + selected-idx (->> selected + (map (fn [id] + (d/index-of-pred all-assets + #(= (:id %) id))))) + min-idx (apply min (conj selected-idx clicked-idx)) + max-idx (apply max (conj selected-idx clicked-idx))] - (->> all-assets - d/enumerate - (filter #(<= min-idx (first %) max-idx)) - (map #(-> % second :id)) - set)))))) + (->> all-assets + d/enumerate + (filter #(<= min-idx (first %) max-idx)) + (map #(-> % second :id)) + set))))))) unselect-all (mf/use-callback @@ -1066,7 +1479,7 @@ on-asset-click (mf/use-callback (mf/deps toggle-selected-asset extend-selected-assets) - (fn [asset-type event asset-id all-assets default-click] + (fn [asset-type asset-groups event asset-id default-click] (cond (kbd/ctrl? event) (do @@ -1076,7 +1489,7 @@ (kbd/shift? event) (do (dom/stop-propagation event) - (extend-selected-assets asset-type asset-id all-assets)) + (extend-selected-assets asset-type asset-groups asset-id)) :else (when default-click @@ -1086,16 +1499,20 @@ (mf/use-callback (mf/deps @selected-assets) (fn [] - (do + (let [selected-assets @selected-assets] (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (map #(dwl/delete-component {:id %}) - (:components @selected-assets))) + (:components selected-assets))) (apply st/emit! (map #(dwl/delete-media {:id %}) - (:graphics @selected-assets))) + (:graphics selected-assets))) (apply st/emit! (map #(dwl/delete-color {:id %}) - (:colors @selected-assets))) + (:colors selected-assets))) (apply st/emit! (map #(dwl/delete-typography %) - (:typographies @selected-assets))) + (:typographies selected-assets))) + (when (or (d/not-empty? (:components selected-assets)) + (d/not-empty? (:colors selected-assets)) + (d/not-empty? (:typographies selected-assets))) + (st/emit! (dwl/sync-file (:id file) (:id file)))) (st/emit! (dwu/commit-undo-transaction)))))] [:div.tool-window {:on-context-menu #(dom/prevent-default %) @@ -1108,9 +1525,9 @@ (if local? [:* - [:span (t locale "workspace.assets.file-library")] + [:span (tr "workspace.assets.file-library")] (when shared? - [:span.tool-badge (t locale "workspace.assets.shared")])] + [:span.tool-badge (tr "workspace.assets.shared")])] [:* [:span (:name file)] [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"} @@ -1156,6 +1573,7 @@ :components components :listing-thumbs? listing-thumbs? :open? (open-box? :components) + :open-groups (open-groups :components) :selected-assets @selected-assets :on-asset-click (partial on-asset-click :components) :on-assets-delete on-assets-delete @@ -1167,6 +1585,7 @@ :objects media :listing-thumbs? listing-thumbs? :open? (open-box? :graphics) + :open-groups (open-groups :graphics) :selected-assets @selected-assets :on-asset-click (partial on-asset-click :graphics) :on-assets-delete on-assets-delete @@ -1174,29 +1593,29 @@ (when show-colors? [:& colors-box {:file-id (:id file) :local? local? - :locale locale :colors colors :open? (open-box? :colors) + :open-groups (open-groups :colors) :selected-assets @selected-assets :on-asset-click (partial on-asset-click :colors) :on-assets-delete on-assets-delete :on-clear-selection unselect-all}]) (when show-typography? - [:& typography-box {:file file - :file-id (:id file) - :local? local? - :locale locale - :typographies typographies - :open? (open-box? :typographies) - :selected-assets @selected-assets - :on-asset-click (partial on-asset-click :typographies) - :on-assets-delete on-assets-delete - :on-clear-selection unselect-all}]) + [:& typographies-box {:file file + :file-id (:id file) + :local? local? + :typographies typographies + :open? (open-box? :typographies) + :open-groups (open-groups :typographies) + :selected-assets @selected-assets + :on-asset-click (partial on-asset-click :typographies) + :on-assets-delete on-assets-delete + :on-clear-selection unselect-all}]) (when (and (not show-components?) (not show-graphics?) (not show-colors?)) [:div.asset-section - [:div.asset-title (t locale "workspace.assets.not-found")]])]))])) + [:div.asset-title (tr "workspace.assets.not-found")]])]))])) (mf/defc assets-toolbox @@ -1205,7 +1624,6 @@ (vals) (remove :is-indirect)) file (mf/deref refs/workspace-file) - locale (mf/deref i18n/locale) team-id (mf/use-ctx ctx/current-team-id) filters (mf/use-state {:term "" :box :all}) @@ -1219,7 +1637,7 @@ on-search-clear-click (mf/use-callback (mf/deps team-id) - (fn [event] + (fn [_] (swap! filters assoc :term ""))) on-box-filter-change @@ -1235,10 +1653,10 @@ [:div.tool-window [:div.tool-window-content [:div.assets-bar-title - (t locale "workspace.assets.assets") + (tr "workspace.assets.assets") [:div.libraries-button {:on-click #(modal/show! :libraries-dialog {})} i/text-align-justify - (t locale "workspace.assets.libraries")]] + (tr "workspace.assets.libraries")]] [:div.search-block [:input.search-input @@ -1255,16 +1673,15 @@ [:select.input-select {:value (:box @filters) :on-change on-box-filter-change} - [:option {:value ":all"} (t locale "workspace.assets.box-filter-all")] - [:option {:value ":components"} (t locale "workspace.assets.components")] - [:option {:value ":graphics"} (t locale "workspace.assets.graphics")] - [:option {:value ":colors"} (t locale "workspace.assets.colors")] - [:option {:value ":typographies"} (t locale "workspace.assets.typography")]]]] + [:option {:value ":all"} (tr "workspace.assets.box-filter-all")] + [:option {:value ":components"} (tr "workspace.assets.components")] + [:option {:value ":graphics"} (tr "workspace.assets.graphics")] + [:option {:value ":colors"} (tr "workspace.assets.colors")] + [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]] [:div.libraries-wrapper [:& file-library {:file file - :locale locale :local? true :default-open? true :filters @filters}] @@ -1275,7 +1692,6 @@ {:key (:id file) :file file :local? false - :locale locale :default-open? false :filters @filters}])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index f188172a23..86f294d5b2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -6,21 +6,15 @@ (ns app.main.ui.workspace.sidebar.history (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] [app.common.data :as d] - [app.main.ui.icons :as i] - [app.main.data.history :as udh] - [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] - [app.util.data :refer [read-string]] + [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :refer [t] :as i18n] - [app.util.router :as r] - [app.util.time :as dt] + [cuerdas.core :as str] [okulary.core :as l] - [app.main.store :as st])) + [rumext.alpha :as mf])) (def workspace-undo (l/derived :workspace-undo st/state)) @@ -137,7 +131,7 @@ i/layers)) (defn is-shape? [type] - #{:shape :rect :circle :text :path :frame :group}) + (contains? #{:shape :rect :circle :text :path :frame :group} type)) (defn parse-entry [{:keys [redo-changes]}] (->> redo-changes @@ -211,7 +205,7 @@ :modify (->> candidates (filter #(= :modify (:operation %))) (group-by :id) - (d/mapm (fn [k v] (->> v + (d/mapm (fn [_ v] (->> v (mapcat :detail) (map (comp safe-name :attr)) (remove nil?) @@ -280,7 +274,7 @@ (mf/defc history-toolbox [] (let [locale (mf/deref i18n/locale) objects (mf/deref refs/workspace-page-objects) - {:keys [items index transaction]} (mf/deref workspace-undo) + {:keys [items index]} (mf/deref workspace-undo) entries (parse-entries items objects)] [:div.history-toolbox [:div.history-toolbox-title (t locale "workspace.undo.title")] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 310ece2765..0f147b1f26 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -16,12 +16,9 @@ [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.perf :as perf] [app.util.timers :as ts] - [beicon.core :as rx] [okulary.core :as l] [rumext.alpha :as mf])) @@ -175,7 +172,7 @@ (st/emit! (dw/select-shape id)))) on-drop - (fn [side {:keys [id] :as data}] + (fn [side _data] (if (= side :center) (st/emit! (dw/relocate-selected-shapes (:id item) 0)) (let [to-index (if (= side :top) (inc index) index) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index ec9424f50b..89248b58b0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -6,13 +6,11 @@ (ns app.main.ui.workspace.sidebar.options (:require - [app.common.spec :as us] [app.main.data.workspace :as udw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.context :as ctx] - [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.align :refer [align-options]] [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-menu]] [app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]] @@ -26,9 +24,8 @@ [app.main.ui.workspace.sidebar.options.shapes.rect :as rect] [app.main.ui.workspace.sidebar.options.shapes.svg-raw :as svg-raw] [app.main.ui.workspace.sidebar.options.shapes.text :as text] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] - [beicon.core :as rx] [rumext.alpha :as mf])) ;; --- Options @@ -52,32 +49,30 @@ :page-id page-id :file-id file-id}]]) - (mf/defc options-content {::mf/wrap [mf/memo]} [{:keys [selected section shapes shapes-with-children page-id file-id]}] - (let [locale (mf/deref i18n/locale)] - [:div.tool-window - [:div.tool-window-content - [:& tab-container {:on-change-tab #(st/emit! (udw/set-options-mode %)) - :selected section} - [:& tab-element {:id :design - :title (t locale "workspace.options.design")} - [:div.element-options - [:& align-options] - (case (count selected) - 0 [:& page/options {:page-id page-id}] - 1 [:& shape-options {:shape (first shapes) - :page-id page-id - :file-id file-id - :shapes-with-children shapes-with-children}] - [:& multiple/options {:shapes-with-children shapes-with-children - :shapes shapes}])]] + [:div.tool-window + [:div.tool-window-content + [:& tab-container {:on-change-tab #(st/emit! (udw/set-options-mode %)) + :selected section} + [:& tab-element {:id :design + :title (tr "workspace.options.design")} + [:div.element-options + [:& align-options] + (case (count selected) + 0 [:& page/options] + 1 [:& shape-options {:shape (first shapes) + :page-id page-id + :file-id file-id + :shapes-with-children shapes-with-children}] + [:& multiple/options {:shapes-with-children shapes-with-children + :shapes shapes}])]] - [:& tab-element {:id :prototype - :title (t locale "workspace.options.prototype")} - [:div.element-options - [:& interactions-menu {:shape (first shapes)}]]]]]])) + [:& tab-element {:id :prototype + :title (tr "workspace.options.prototype")} + [:div.element-options + [:& interactions-menu {:shape (first shapes)}]]]]]]) ;; TODO: this need optimizations, selected-objects and diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs index 5fbabea126..733fd420b8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs @@ -6,14 +6,10 @@ (ns app.main.ui.workspace.sidebar.options.common (:require - [rumext.alpha :as mf] - [app.util.dom :as dom])) + [rumext.alpha :as mf])) -(mf/defc advanced-options [{:keys [visible? on-close children]}] - (let [ref (mf/use-ref nil) - handle-click (fn [event] (when on-close - (do (dom/stop-propagation event) - (on-close))))] +(mf/defc advanced-options [{:keys [visible? children]}] + (let [ref (mf/use-ref nil)] (mf/use-effect (mf/deps visible?) (fn [] @@ -22,9 +18,7 @@ (.scrollIntoViewIfNeeded ^js node))))) (when visible? - [:* - [:div.focus-overlay {:on-click handle-click}] - [:div.advanced-options-wrapper {:ref ref} - [:div.advanced-options {} - children]]]))) + [:div.advanced-options-wrapper {:ref ref} + [:div.advanced-options {} + children]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index f6c8698cad..3ff9d3f268 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -6,17 +6,13 @@ (ns app.main.ui.workspace.sidebar.options.menus.blur (:require - [rumext.alpha :as mf] - [app.common.data :as d] [app.common.uuid :as uuid] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] [app.main.store :as st] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]])) + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (def blur-attrs [:blur]) @@ -28,8 +24,7 @@ :hidden false})) (mf/defc blur-menu [{:keys [ids type values]}] - (let [locale (i18n/use-locale) - blur (:blur values) + (let [blur (:blur values) has-value? (not (nil? blur)) multiple? (= blur :multiple) @@ -60,9 +55,9 @@ [:div.element-set-title [:span (case type - :multiple (t locale "workspace.options.blur-options.title.multiple") - :group (t locale "workspace.options.blur-options.title.group") - (t locale "workspace.options.blur-options.title"))] + :multiple (tr "workspace.options.blur-options.title.multiple") + :group (tr "workspace.options.blur-options.title.group") + (tr "workspace.options.blur-options.title"))] [:div.element-set-title-actions (when (and has-value? (not multiple?)) @@ -77,7 +72,7 @@ [:div.element-set-content [:& input-row {:label "Value" :class "pixels" - :min 0 + :min "0" :value (:value blur) - :placeholder (t locale "settings.multiple") + :placeholder (tr "settings.multiple") :on-change handle-change}]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index bc7c393847..024c391d10 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -6,20 +6,19 @@ (ns app.main.ui.workspace.sidebar.options.menus.component (:require - [rumext.alpha :as mf] [app.common.pages :as cp] [app.main.data.modal :as modal] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.context-menu :refer [context-menu]] [app.main.ui.context :as ctx] [app.main.ui.icons :as i] - [app.main.ui.components.context-menu :refer [context-menu]] - [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] - [app.main.data.workspace.libraries :as dwl] + [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t]] - [app.util.dom :as dom])) + [rumext.alpha :as mf])) (def component-attrs [:component-id :component-file :shape-ref]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs new file mode 100644 index 0000000000..a0ec0508fc --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -0,0 +1,179 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.sidebar.options.menus.constraints + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.pages.spec :as spec] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(def constraint-attrs [:constraints-h + :constraints-v + :fixed-scroll + :parent-id + :frame-id]) + +(mf/defc constraints-menu + [{:keys [ids values] :as props}] + (let [old-shapes (deref (refs/objects-by-id ids)) + frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) + + shapes (as-> old-shapes $ + (map gsh/transform-shape $) + (map gsh/translate-to-frame $ frames)) + + values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)] + (cond-> values + (not= (:x values) :multiple) (assoc :x x) + (not= (:y values) :multiple) (assoc :y y))) + + values (let [{:keys [width height]} (-> shapes first :selrect)] + (cond-> values + (not= (:width values) :multiple) (assoc :width width) + (not= (:height values) :multiple) (assoc :height height))) + + in-frame? (and (some? ids) + (not= (:parent-id values) uuid/zero)) + ;; TODO: uncomment when fixed-scroll is fully implemented + ;; first-level? (and in-frame? + ;; (= (:parent-id values) (:frame-id values))) + + constraints-h (get values :constraints-h (spec/default-constraints-h values)) + constraints-v (get values :constraints-v (spec/default-constraints-v values)) + + on-constraint-button-clicked + (mf/use-callback + (mf/deps [ids values]) + (fn [button] + (fn [_] + (let [constraints-h (get values :constraints-h :scale) + constraints-v (get values :constraints-v :scale) + + [constraint new-value] + (case button + :top (case constraints-v + :top [:constraints-v :scale] + :topbottom [:constraints-v :bottom] + :bottom [:constraints-v :topbottom] + [:constraints-v :top]) + :bottom (case constraints-v + :bottom [:constraints-v :scale] + :topbottom [:constraints-v :top] + :top [:constraints-v :topbottom] + [:constraints-v :bottom]) + :left (case constraints-h + :left [:constraints-h :scale] + :leftright [:constraints-h :right] + :right [:constraints-h :leftright] + [:constraints-h :left]) + :right (case constraints-h + :right [:constraints-h :scale] + :leftright [:constraints-h :left] + :left [:constraints-h :leftright] + [:constraints-h :right]) + :centerv (case constraints-v + :center [:constraints-v :scale] + [:constraints-v :center]) + :centerh (case constraints-h + :center [:constraints-h :scale] + [:constraints-h :center]))] + (st/emit! (dch/update-shapes + ids + #(assoc % constraint new-value))))))) + + on-constraint-select-changed + (mf/use-callback + (mf/deps [ids values]) + (fn [constraint] + (fn [event] + (let [value (-> (dom/get-target-val event) (keyword))] + (when-not (str/empty? value) + (st/emit! (dch/update-shapes + ids + #(assoc % constraint value)))))))) + + ;; TODO: uncomment when fixed-scroll is fully implemented + ;; on-fixed-scroll-clicked + ;; (mf/use-callback + ;; (mf/deps [ids values]) + ;; (fn [_] + ;; (st/emit! (dch/update-shapes ids #(update % :fixed-scroll not))))) + ] + + ;; CONSTRAINTS + (when in-frame? + [:div.element-set + [:div.element-set-title + [:span (tr "workspace.options.constraints")]] + + [:div.element-set-content + [:div.row-flex.align-top + + [:div.constraints-widget + [:div.constraints-box] + [:div.constraint-button.top + {:class (dom/classnames :active (or (= constraints-v :top) + (= constraints-v :topbottom))) + :on-click (on-constraint-button-clicked :top)}] + [:div.constraint-button.bottom + {:class (dom/classnames :active (or (= constraints-v :bottom) + (= constraints-v :topbottom))) + :on-click (on-constraint-button-clicked :bottom)}] + [:div.constraint-button.left + {:class (dom/classnames :active (or (= constraints-h :left) + (= constraints-h :leftright))) + :on-click (on-constraint-button-clicked :left)}] + [:div.constraint-button.right + {:class (dom/classnames :active (or (= constraints-h :right) + (= constraints-h :leftright))) + :on-click (on-constraint-button-clicked :right)}] + [:div.constraint-button.centerv + {:class (dom/classnames :active (= constraints-v :center)) + :on-click (on-constraint-button-clicked :centerv)}] + [:div.constraint-button.centerh + {:class (dom/classnames :active (= constraints-h :center)) + :on-click (on-constraint-button-clicked :centerh)}]] + + [:div.constraints-form + [:div.row-flex + [:span.left-right i/full-screen] + [:select.input-select {:on-change (on-constraint-select-changed :constraints-h) + :value (d/name constraints-h "scale")} + (when (= constraints-h :multiple) + [:option {:value ""} (tr "settings.multiple")]) + [:option {:value "left"} (tr "workspace.options.constraints.left")] + [:option {:value "right"} (tr "workspace.options.constraints.right")] + [:option {:value "leftright"} (tr "workspace.options.constraints.leftright")] + [:option {:value "center"} (tr "workspace.options.constraints.center")] + [:option {:value "scale"} (tr "workspace.options.constraints.scale")]]] + [:div.row-flex + [:span.top-bottom i/full-screen] + [:select.input-select {:on-change (on-constraint-select-changed :constraints-v) + :value (d/name constraints-v "scale")} + (when (= constraints-v :multiple) + [:option {:value ""} (tr "settings.multiple")]) + [:option {:value "top"} (tr "workspace.options.constraints.top")] + [:option {:value "bottom"} (tr "workspace.options.constraints.bottom")] + [:option {:value "topbottom"} (tr "workspace.options.constraints.topbottom")] + [:option {:value "center"} (tr "workspace.options.constraints.center")] + [:option {:value "scale"} (tr "workspace.options.constraints.scale")] + ;; TODO: uncomment when fixed-scroll is fully implemented + ;; (when first-level? + ;; [:div.row-flex + ;; [:div.fix-when {:class (dom/classnames :active (:fixed-scroll values)) + ;; :on-click on-fixed-scroll-clicked} + ;; i/pin + ;; [:span (tr "workspace.options.constraints.fix-when-scrolling")]]]) + ]]]]]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs index 65f05ff673..7e65db893e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.cljs @@ -6,21 +6,18 @@ (ns app.main.ui.workspace.sidebar.options.menus.exports (:require - [cuerdas.core :as str] - [beicon.core :as rx] - [rumext.alpha :as mf] [app.common.data :as d] - [app.main.repo :as rp] - [app.main.ui.icons :as i] [app.main.data.messages :as dm] [app.main.data.workspace :as udw] + [app.main.repo :as rp] [app.main.store :as st] - [app.util.object :as obj] + [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.http :as http] - [app.util.i18n :as i18n :refer [tr t]])) + [app.util.i18n :as i18n :refer [tr]] + [beicon.core :as rx] + [rumext.alpha :as mf])) -(defn- request-export +(defn request-export [shape exports] (rp/query! :export {:page-id (:page-id shape) @@ -29,25 +26,9 @@ :name (:name shape) :exports exports})) -(defn- trigger-download - [filename blob] - (let [link (dom/create-element "a") - uri (dom/create-uri blob) - extension (dom/mtype->extension (.-type ^js blob)) - filename (if extension - (str filename "." extension) - filename)] - (obj/set! link "href" uri) - (obj/set! link "download" filename) - (obj/set! (.-style ^js link) "display" "none") - (.appendChild (.-body ^js js/document) link) - (.click link) - (.remove link))) - (mf/defc exports-menu [{:keys [shape page-id file-id] :as props}] - (let [locale (mf/deref i18n/locale) - exports (:exports shape []) + (let [exports (:exports shape []) loading? (mf/use-state false) filename (cond-> (:name shape) @@ -55,6 +36,11 @@ (not (empty (:suffix (first exports))))) (str (:suffix (first exports)))) + scale-enabled? + (mf/use-callback + (fn [export] + (#{:png :jpeg} (:type export)))) + on-download (mf/use-callback (mf/deps shape) @@ -64,8 +50,8 @@ (->> (request-export (assoc shape :page-id page-id :file-id file-id) exports) (rx/subs (fn [body] - (trigger-download filename body)) - (fn [error] + (dom/trigger-download filename body)) + (fn [_error] (swap! loading? not) (st/emit! (dm/error (tr "errors.unexpected-error")))) (fn [] @@ -123,22 +109,23 @@ [:div.element-set.exports-options [:div.element-set-title - [:span (t locale "workspace.options.export")] + [:span (tr "workspace.options.export")] [:div.add-page {:on-click add-export} i/close]] (when (seq exports) [:div.element-set-content (for [[index export] (d/enumerate exports)] [:div.element-set-options-group {:key index} - [:select.input-select {:on-change (partial on-scale-change index) - :value (:scale export)} - [:option {:value "0.5"} "0.5x"] - [:option {:value "0.75"} "0.75x"] - [:option {:value "1"} "1x"] - [:option {:value "1.5"} "1.5x"] - [:option {:value "2"} "2x"] - [:option {:value "4"} "4x"] - [:option {:value "6"} "6x"]] + (when (scale-enabled? export) + [:select.input-select {:on-change (partial on-scale-change index) + :value (:scale export)} + [:option {:value "0.5"} "0.5x"] + [:option {:value "0.75"} "0.75x"] + [:option {:value "1"} "1x"] + [:option {:value "1.5"} "1.5x"] + [:option {:value "2"} "2x"] + [:option {:value "4"} "4x"] + [:option {:value "6"} "6x"]]) [:input.input-text {:value (:suffix export) :placeholder (tr "workspace.options.export.suffix") :on-change (partial on-suffix-change index)}] @@ -146,7 +133,8 @@ :on-change (partial on-type-change index)} [:option {:value "png"} "PNG"] [:option {:value "jpeg"} "JPEG"] - [:option {:value "svg"} "SVG"]] + [:option {:value "svg"} "SVG"] + [:option {:value "pdf"} "PDF"]] [:div.delete-icon {:on-click (partial delete-export index)} i/minus]]) @@ -156,6 +144,6 @@ :btn-disabled @loading?) :disabled @loading?} (if @loading? - (t locale "workspace.options.exporting-object") - (t locale "workspace.options.export-object"))]])])) + (tr "workspace.options.exporting-object") + (tr "workspace.options.export-object"))]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 47112a3f2b..c13fc2b9a7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -8,16 +8,12 @@ (:require [app.common.pages :as cp] [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.undo :as dwu] - [app.main.data.workspace.texts :as dwt] - [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.util.color :as uc] - [app.util.i18n :as i18n :refer [tr t]] - [app.util.object :as obj] + [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (def fill-attrs @@ -30,14 +26,13 @@ (mf/defc fill-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]} [{:keys [ids type values] :as props}] - (let [locale (mf/deref i18n/locale) - show? (or (not (nil? (:fill-color values))) - (not (nil? (:fill-color-gradient values)))) + (let [show? (or (not (nil? (:fill-color values))) + (not (nil? (:fill-color-gradient values)))) label (case type - :multiple (t locale "workspace.options.selection-fill") - :group (t locale "workspace.options.group-fill") - (t locale "workspace.options.fill")) + :multiple (tr "workspace.options.selection-fill") + :group (tr "workspace.options.group-fill") + (tr "workspace.options.fill")) color {:color (:fill-color values) :opacity (:fill-opacity values) @@ -48,21 +43,21 @@ on-add (mf/use-callback (mf/deps ids) - (fn [event] + (fn [_] (st/emit! (dc/change-fill ids {:color cp/default-color :opacity 1})))) on-delete (mf/use-callback (mf/deps ids) - (fn [event] + (fn [_] (st/emit! (dc/change-fill ids (into {} uc/empty-color))))) on-change (mf/use-callback (mf/deps ids) (fn [color] - (let [remove-multiple (fn [[key value]] (not= value :multiple)) + (let [remove-multiple (fn [[_ value]] (not= value :multiple)) color (into {} (filter remove-multiple) color)] (st/emit! (dc/change-fill ids color))))) @@ -70,7 +65,7 @@ (mf/use-callback (mf/deps ids) (fn [] - (let [remove-multiple (fn [[key value]] (not= value :multiple)) + (let [remove-multiple (fn [[_ value]] (not= value :multiple)) color (-> (into {} (filter remove-multiple) color) (assoc :id nil :file-id nil))] (st/emit! (dc/change-fill ids color))))) @@ -78,13 +73,13 @@ on-open-picker (mf/use-callback (mf/deps ids) - (fn [value opacity id file-id] + (fn [_value _opacity _id _file-id] (st/emit! (dwu/start-undo-transaction)))) on-close-picker (mf/use-callback (mf/deps ids) - (fn [value opacity id file-id] + (fn [_value _opacity _id _file-id] (st/emit! (dwu/commit-undo-transaction))))] (if show? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs index 40bbe0a244..99f43711ea 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs @@ -6,38 +6,34 @@ (ns app.main.ui.workspace.sidebar.options.menus.frame-grid (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [app.util.dom :as dom] - [app.util.data :as d] [app.common.math :as mth] - [app.common.data :refer [parse-integer]] - [app.main.store :as st] - [app.main.refs :as refs] [app.main.data.workspace.grid :as dw] - [app.util.geom.grid :as gg] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.editable-select :refer [editable-select]] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.select :refer [select]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] - [app.main.ui.components.numeric-input :refer [numeric-input]] - [app.main.ui.components.select :refer [select]] - [app.main.ui.components.editable-select :refer [editable-select]] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.util.i18n :as i18n :refer [tr t]])) + [app.util.data :as d] + [app.util.geom.grid :as gg] + [app.util.i18n :as i18n :refer [tr]] + [okulary.core :as l] + [rumext.alpha :as mf])) (def workspace-saved-grids (l/derived :saved-grids refs/workspace-page-options)) -(defn- get-size-options [locale] - [{:value :auto :label (t locale "workspace.options.grid.auto")} +(defn- get-size-options [] + [{:value :auto :label (tr "workspace.options.grid.auto")} :separator 18 12 10 8 6 4 3 2]) (mf/defc grid-options [{:keys [grid frame default-grid-params on-change on-remove on-save-grid]}] - (let [locale (i18n/use-locale) - size-options (get-size-options locale) + (let [size-options (get-size-options) state (mf/use-state {:show-advanced-options false}) {:keys [type display params]} grid @@ -45,12 +41,12 @@ #(swap! state update :show-advanced-options not) handle-toggle-visibility - (fn [event] + (fn [_] (when on-change (on-change (update grid :display #(if (nil? %) false (not %)))))) handle-remove-grid - (fn [event] + (fn [_] (when on-remove (on-remove))) handle-change-type @@ -82,7 +78,7 @@ handle-change-item-length (fn [item-length] - (let [{:keys [margin gutter size]} (:params grid) + (let [size (get-in grid [:params :size]) size (if (and (nil? item-length) (or (nil? size) (= :auto size))) 12 size)] (when on-change (on-change (-> grid @@ -117,18 +113,20 @@ (on-save-grid grid))) is-default (= (->> grid :params) - (->> grid :type default-grid-params))] + (->> grid :type default-grid-params)) + + open? (:show-advanced-options @state)] [:div.grid-option - [:div.grid-option-main - [:button.custom-button {:class (when (:show-advanced-options @state) "is-active") + [:div.grid-option-main {:style {:display (when open? "none")}} + [:button.custom-button {:class (when open? "is-active") :on-click toggle-advanced-options} i/actions] [:& select {:class "flex-grow" :default-value type - :options [{:value :square :label (t locale "workspace.options.grid.square")} - {:value :column :label (t locale "workspace.options.grid.column")} - {:value :row :label (t locale "workspace.options.grid.row")}] + :options [{:value :square :label (tr "workspace.options.grid.square")} + {:value :column :label (tr "workspace.options.grid.column")} + {:value :row :label (tr "workspace.options.grid.row")}] :on-change handle-change-type}] (if (= type :square) @@ -148,17 +146,18 @@ [:button.custom-button {:on-click handle-toggle-visibility} (if display i/eye i/eye-closed)] [:button.custom-button {:on-click handle-remove-grid} i/minus]]] - [:& advanced-options {:visible? (:show-advanced-options @state) + [:& advanced-options {:visible? open? :on-close toggle-advanced-options} + [:button.custom-button {:on-click toggle-advanced-options} i/actions] (when (= :square type) - [:& input-row {:label (t locale "workspace.options.grid.params.size") + [:& input-row {:label (tr "workspace.options.grid.params.size") :class "pixels" :min 1 :value (:size params) :on-change (handle-change :params :size)}]) (when (= :row type) - [:& input-row {:label (t locale "workspace.options.grid.params.rows") + [:& input-row {:label (tr "workspace.options.grid.params.rows") :type :editable-select :options size-options :value (:size params) @@ -167,7 +166,7 @@ :on-change handle-change-size}]) (when (= :column type) - [:& input-row {:label (t locale "workspace.options.grid.params.columns") + [:& input-row {:label (tr "workspace.options.grid.params.columns") :type :editable-select :options size-options :value (:size params) @@ -176,23 +175,23 @@ :on-change handle-change-size}]) (when (#{:row :column} type) - [:& input-row {:label (t locale "workspace.options.grid.params.type") + [:& input-row {:label (tr "workspace.options.grid.params.type") :type :select - :options [{:value :stretch :label (t locale "workspace.options.grid.params.type.stretch")} + :options [{:value :stretch :label (tr "workspace.options.grid.params.type.stretch")} {:value :left :label (if (= type :row) - (t locale "workspace.options.grid.params.type.top") - (t locale "workspace.options.grid.params.type.left"))} - {:value :center :label (t locale "workspace.options.grid.params.type.center")} + (tr "workspace.options.grid.params.type.top") + (tr "workspace.options.grid.params.type.left"))} + {:value :center :label (tr "workspace.options.grid.params.type.center")} {:value :right :label (if (= type :row) - (t locale "workspace.options.grid.params.type.bottom") - (t locale "workspace.options.grid.params.type.right"))}] + (tr "workspace.options.grid.params.type.bottom") + (tr "workspace.options.grid.params.type.right"))}] :value (:type params) :on-change (handle-change :params :type)}]) (when (#{:row :column} type) [:& input-row {:label (if (= :row type) - (t locale "workspace.options.grid.params.height") - (t locale "workspace.options.grid.params.width")) + (tr "workspace.options.grid.params.height") + (tr "workspace.options.grid.params.width")) :class "pixels" :placeholder "Auto" :value (or (:item-length params) "") @@ -200,13 +199,13 @@ (when (#{:row :column} type) [:* - [:& input-row {:label (t locale "workspace.options.grid.params.gutter") + [:& input-row {:label (tr "workspace.options.grid.params.gutter") :class "pixels" :value (:gutter params) :min 0 :placeholder "0" :on-change (handle-change :params :gutter)}] - [:& input-row {:label (t locale "workspace.options.grid.params.margin") + [:& input-row {:label (tr "workspace.options.grid.params.margin") :class "pixels" :min 0 :placeholder "0" @@ -219,13 +218,12 @@ :on-detach handle-detach-color}] [:div.row-flex [:button.btn-options {:disabled is-default - :on-click handle-use-default} (t locale "workspace.options.grid.params.use-default")] + :on-click handle-use-default} (tr "workspace.options.grid.params.use-default")] [:button.btn-options {:disabled is-default - :on-click handle-set-as-default} (t locale "workspace.options.grid.params.set-default")]]]])) + :on-click handle-set-as-default} (tr "workspace.options.grid.params.set-default")]]]])) (mf/defc frame-grid [{:keys [shape]}] - (let [locale (i18n/use-locale) - id (:id shape) + (let [id (:id shape) default-grid-params (merge dw/default-grid-params (mf/deref workspace-saved-grids)) handle-create-grid #(st/emit! (dw/add-frame-grid id)) handle-remove-grid (fn [index] #(st/emit! (dw/remove-frame-grid id index))) @@ -233,10 +231,10 @@ handle-save-grid (fn [grid] (st/emit! (dw/set-default-grid (:type grid) (:params grid))))] [:div.element-set [:div.element-set-title - [:span (t locale "workspace.options.grid.title")] + [:span (tr "workspace.options.grid.title")] [:div.add-page {:on-click handle-create-grid} i/close]] - (when (not (empty? (:grids shape))) + (when (seq (:grids shape)) [:div.element-set-content (for [[index grid] (map-indexed vector (:grids shape))] [:& grid-options {:key (str (:id shape) "-" index) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 10dad31477..500b5e0388 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.sidebar.options.menus.interactions (:require - [rumext.alpha :as mf] [app.common.data :as d] [app.common.pages :as cp] [app.main.data.workspace :as dw] @@ -14,13 +13,12 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]])) + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (mf/defc interactions-menu [{:keys [shape] :as props}] - (let [locale (mf/deref i18n/locale) - objects (deref refs/workspace-page-objects) + (let [objects (deref refs/workspace-page-objects) interaction (first (:interactions shape)) ; TODO: in the ; future we may ; have several @@ -48,26 +46,26 @@ (if (not shape) [:* [:div.interactions-help-icon i/interaction] - [:div.interactions-help (t locale "workspace.options.select-a-shape")] + [:div.interactions-help (tr "workspace.options.select-a-shape")] [:div.interactions-help-icon i/play] - [:div.interactions-help (t locale "workspace.options.use-play-button")]] + [:div.interactions-help (tr "workspace.options.use-play-button")]] [:div.element-set {:on-blur on-set-blur} [:div.element-set-title - [:span (t locale "workspace.options.navigate-to")]] + [:span (tr "workspace.options.navigate-to")]] [:div.element-set-content [:div.row-flex [:div.custom-select.flex-grow {:on-click #(reset! show-frames-dropdown? true)} (if destination [:span (:name destination)] - [:span (t locale "workspace.options.select-artboard")]) + [:span (tr "workspace.options.select-artboard")]) [:span.dropdown-button i/arrow-down] [:& dropdown {:show @show-frames-dropdown? :on-close #(reset! show-frames-dropdown? false)} [:ul.custom-select-dropdown [:li.dropdown-separator {:on-click #(on-select-destination nil)} - (t locale "workspace.options.none")] + (tr "workspace.options.none")] (for [frame frames] (when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index c7be781cf5..68f2e86650 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -8,7 +8,6 @@ (:require [app.common.data :as d] [app.common.math :as mth] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input]] @@ -54,25 +53,25 @@ handle-set-hidden (mf/use-callback (mf/deps change!) - (fn [event] + (fn [_] (change! :hidden true))) handle-set-visible (mf/use-callback (mf/deps change!) - (fn [event] + (fn [_] (change! :hidden false))) handle-set-blocked (mf/use-callback (mf/deps change!) - (fn [event] + (fn [_] (change! :blocked true))) handle-set-unblocked (mf/use-callback (mf/deps change!) - (fn [event] + (fn [_] (change! :blocked false)))] [:div.element-set @@ -92,7 +91,7 @@ [:option {:value "multiple"} "--"]) [:option {:value "normal"} (tr "workspace.options.layer-options.blend-mode.normal")] - + [:option {:value "darken"} (tr "workspace.options.layer-options.blend-mode.darken")] [:option {:value "multiply"} (tr "workspace.options.layer-options.blend-mode.multiply")] [:option {:value "color-burn"} (tr "workspace.options.layer-options.blend-mode.color-burn")] @@ -127,13 +126,13 @@ (cond (or (= :multiple (:hidden values)) (not (:hidden values))) [:div.element-set-actions-button {:on-click handle-set-hidden} i/eye] - + :else [:div.element-set-actions-button {:on-click handle-set-visible} i/eye-closed]) (cond (or (= :multiple (:blocked values)) (not (:blocked values))) [:div.element-set-actions-button {:on-click handle-set-blocked} i/unlock] - + :else [:div.element-set-actions-button {:on-click handle-set-unblocked} i/lock])]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 0d02a54196..9305404f56 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -6,21 +6,18 @@ (ns app.main.ui.workspace.sidebar.options.menus.measures (:require - [rumext.alpha :as mf] - [app.main.ui.icons :as i] - [app.main.store :as st] - [app.main.refs :as refs] [app.common.data :as d] - [app.util.dom :as dom] - [app.util.data :refer [classnames]] [app.common.geom.shapes :as gsh] - [app.common.geom.point :as gpt] - [app.main.data.workspace :as udw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.changes :as dch] - [app.main.ui.components.numeric-input :refer [numeric-input]] [app.common.math :as math] - [app.util.i18n :refer [t] :as i18n])) + [app.main.data.workspace :as udw] + [app.main.data.workspace.changes :as dch] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (def measure-attrs [:proportion-lock :width :height @@ -42,7 +39,6 @@ (mf/defc measures-menu [{:keys [options ids ids-with-children values] :as props}] (let [options (or options #{:size :position :rotation :radius}) - locale (i18n/use-locale) ids-with-children (or ids-with-children ids) @@ -74,7 +70,7 @@ on-proportion-lock-change (mf/use-callback (mf/deps ids) - (fn [event] + (fn [_] (let [new-lock (if (= proportion-lock :multiple) true (not proportion-lock))] (run! #(st/emit! (udw/set-shape-proportion-lock % new-lock)) ids)))) @@ -100,7 +96,7 @@ on-switch-to-radius-1 (mf/use-callback (mf/deps ids) - (fn [value] + (fn [_value] (let [radius-update (fn [shape] (cond-> shape @@ -112,7 +108,7 @@ on-switch-to-radius-4 (mf/use-callback (mf/deps ids) - (fn [value] + (fn [_value] (let [radius-update (fn [shape] (cond-> shape @@ -161,109 +157,111 @@ on-radius-r2-change #(on-radius-4-change % :r2) on-radius-r3-change #(on-radius-4-change % :r3) on-radius-r4-change #(on-radius-4-change % :r4) + select-all #(-> % (dom/get-target) (.select))] - [:div.element-set - [:div.element-set-content + [:* + [:div.element-set + [:div.element-set-content - ;; WIDTH & HEIGHT - (when (options :size) - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.size")] - [:div.input-element.width - [:> numeric-input {:min 1 - :no-validate true - :placeholder "--" - :on-click select-all - :on-change on-width-change - :value (attr->string :width values)}]] + ;; WIDTH & HEIGHT + (when (options :size) + [:div.row-flex + [:span.element-set-subtitle (tr "workspace.options.size")] + [:div.input-element.width + [:> numeric-input {:min 1 + :no-validate true + :placeholder "--" + :on-click select-all + :on-change on-width-change + :value (attr->string :width values)}]] - [:div.input-element.height - [:> numeric-input {:min 1 - :no-validate true - :placeholder "--" - :on-click select-all - :on-change on-height-change - :value (attr->string :height values)}]] + [:div.input-element.height + [:> numeric-input {:min 1 + :no-validate true + :placeholder "--" + :on-click select-all + :on-change on-height-change + :value (attr->string :height values)}]] - [:div.lock-size {:class (classnames + [:div.lock-size {:class (dom/classnames :selected (true? proportion-lock) :disabled (= proportion-lock :multiple)) - :on-click on-proportion-lock-change} - (if proportion-lock - i/lock - i/unlock)]]) + :on-click on-proportion-lock-change} + (if proportion-lock + i/lock + i/unlock)]]) - ;; POSITION - (when (options :position) - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.position")] - [:div.input-element.Xaxis - [:> numeric-input {:no-validate true - :placeholder "--" - :on-click select-all - :on-change on-pos-x-change - :value (attr->string :x values)}]] - [:div.input-element.Yaxis - [:> numeric-input {:no-validate true - :placeholder "--" - :on-click select-all - :on-change on-pos-y-change - :value (attr->string :y values)}]]]) + ;; POSITION + (when (options :position) + [:div.row-flex + [:span.element-set-subtitle (tr "workspace.options.position")] + [:div.input-element.Xaxis + [:> numeric-input {:no-validate true + :placeholder "--" + :on-click select-all + :on-change on-pos-x-change + :value (attr->string :x values)}]] + [:div.input-element.Yaxis + [:> numeric-input {:no-validate true + :placeholder "--" + :on-click select-all + :on-change on-pos-y-change + :value (attr->string :y values)}]]]) - ;; ROTATION - (when (options :rotation) - [:div.row-flex - [:span.element-set-subtitle (t locale "workspace.options.rotation")] - [:div.input-element.degrees - [:> numeric-input - {:no-validate true - :min 0 - :max 359 - :data-wrap true - :placeholder "--" - :on-click select-all - :on-change on-rotation-change - :value (attr->string :rotation values)}]] - #_[:input.slidebar - {:type "range" - :min "0" - :max "359" - :step "10" - :no-validate true - :on-change on-rotation-change - :value (attr->string :rotation values)}]]) + ;; ROTATION + (when (options :rotation) + [:div.row-flex + [:span.element-set-subtitle (tr "workspace.options.rotation")] + [:div.input-element.degrees + [:> numeric-input + {:no-validate true + :min 0 + :max 359 + :data-wrap true + :placeholder "--" + :on-click select-all + :on-change on-rotation-change + :value (attr->string :rotation values)}]] + #_[:input.slidebar + {:type "range" + :min "0" + :max "359" + :step "10" + :no-validate true + :on-change on-rotation-change + :value (attr->string :rotation values)}]]) - ;; RADIUS - (let [radius-1? (some? (:rx values)) - radius-4? (some? (:r1 values))] - (when (and (options :radius) (or radius-1? radius-4?)) - [:div.row-flex - [:div.radius-options + ;; RADIUS + (let [radius-1? (some? (:rx values)) + radius-4? (some? (:r1 values))] + (when (and (options :radius) (or radius-1? radius-4?)) + [:div.row-flex + [:div.radius-options [:div.radius-icon.tooltip.tooltip-bottom - {:class (classnames - :selected - (and radius-1? (not radius-4?))) - :alt (t locale "workspace.options.radius.all-corners") + {:class (dom/classnames + :selected + (and radius-1? (not radius-4?))) + :alt (tr "workspace.options.radius.all-corners") :on-click on-switch-to-radius-1} i/radius-1] [:div.radius-icon.tooltip.tooltip-bottom - {:class (classnames - :selected - (and radius-4? (not radius-1?))) - :alt (t locale "workspace.options.radius.single-corners") + {:class (dom/classnames + :selected + (and radius-4? (not radius-1?))) + :alt (tr "workspace.options.radius.single-corners") :on-click on-switch-to-radius-4} i/radius-4]] - (if radius-1? - [:div.input-element.mini - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-1-change - :value (attr->string :rx values)}]] + (if radius-1? + [:div.input-element.mini + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-1-change + :value (attr->string :rx values)}]] - [:* + [:* [:div.input-element.mini [:> numeric-input {:placeholder "--" @@ -291,5 +289,4 @@ :min 0 :on-click select-all :on-change on-radius-r4-change - :value (attr->string :r4 values)}]]]) - ]))]])) + :value (attr->string :r4 values)}]]])]))]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index 0f84e67c6b..ee75d4cfcc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -6,19 +6,18 @@ (ns app.main.ui.workspace.sidebar.options.menus.shadow (:require - [rumext.alpha :as mf] [app.common.data :as d] [app.common.uuid :as uuid] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.undo :as dwu] [app.main.store :as st] - [app.main.ui.icons :as i] [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]])) + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (def shadow-attrs [:shadow]) @@ -38,8 +37,7 @@ (mf/defc shadow-entry [{:keys [ids index value]}] - (let [locale (i18n/use-locale) - open-shadow (mf/use-state false) + (let [open-shadow (mf/use-state false) basic-offset-x-ref (mf/use-ref nil) basic-offset-y-ref (mf/use-ref nil) @@ -52,7 +50,7 @@ remove-shadow-by-index (fn [values index] (->> (d/enumerate values) - (filterv (fn [[idx s]] (not= idx index))) + (filterv (fn [[idx _]] (not= idx index))) (mapv second))) on-remove-shadow @@ -61,7 +59,7 @@ (st/emit! (dch/update-shapes ids #(update % :shadow remove-shadow-by-index index) )))) select-text - (fn [ref] (fn [event] (dom/select-text! (mf/ref-val ref)))) + (fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref)))) update-attr (fn update-attr @@ -72,7 +70,9 @@ (fn [value] (when (or (not valid?) (valid? value)) (do (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index attr] value))) - (when update-ref (dom/set-value! (mf/ref-val update-ref) value))))))) + (let [update-node (and update-ref (mf/ref-val update-ref))] + (when update-node + (dom/set-value! update-node value)))))))) update-color (fn [index] @@ -86,37 +86,44 @@ detach-color (fn [index] - (fn [color opacity] - (if-not (string? (:color value)) + (fn [_color _opacity] + (when-not (string? (:color value)) (st/emit! (dch/update-shapes - ids - #(assoc-in % [:shadow index :color] - (dissoc (:color value) :id :file-id))))))) + ids + #(assoc-in % [:shadow index :color] + (dissoc (:color value) :id :file-id))))))) toggle-visibility (fn [index] (fn [] (st/emit! (dch/update-shapes ids #(update-in % [:shadow index :hidden] not)))))] [:* - [:div.element-set-options-group - + [:div.element-set-options-group {:style {:display (when @open-shadow "none")}} [:div.element-set-actions-button {:on-click #(reset! open-shadow true)} i/actions] - [:> numeric-input {:ref basic-offset-x-ref - :on-change (update-attr index :offset-x valid-number?) - :on-click (select-text basic-offset-x-ref) - :value (:offset-x value)}] - [:> numeric-input {:ref basic-offset-y-ref - :on-change (update-attr index :offset-y valid-number?) - :on-click (select-text basic-offset-y-ref) - :value (:offset-y value)}] - [:> numeric-input {:ref basic-blur-ref - :on-click (select-text basic-blur-ref) - :on-change (update-attr index :blur valid-number?) - :min 0 - :value (:blur value)}] + ;; [:> numeric-input {:ref basic-offset-x-ref + ;; :on-change (update-attr index :offset-x valid-number?) + ;; :on-click (select-text basic-offset-x-ref) + ;; :value (:offset-x value)}] + ;; [:> numeric-input {:ref basic-offset-y-ref + ;; :on-change (update-attr index :offset-y valid-number?) + ;; :on-click (select-text basic-offset-y-ref) + ;; :value (:offset-y value)}] + ;; [:> numeric-input {:ref basic-blur-ref + ;; :on-click (select-text basic-blur-ref) + ;; :on-change (update-attr index :blur valid-number?) + ;; :min 0 + ;; :value (:blur value)}] + + [:select.input-select + {:default-value (str (:style value)) + :on-change (fn [event] + (let [value (-> event dom/get-target dom/get-value d/read-string)] + (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} + [:option {:value ":drop-shadow"} (tr "workspace.options.shadow-options.drop-shadow")] + [:option {:value ":inner-shadow"} (tr "workspace.options.shadow-options.inner-shadow")]] [:div.element-set-actions [:div.element-set-actions-button {:on-click (toggle-visibility index)} @@ -126,14 +133,17 @@ [:& advanced-options {:visible? @open-shadow :on-close #(reset! open-shadow false)} - [:div.row-grid-2 + [:div.color-data + [:div.element-set-actions-button + {:on-click #(reset! open-shadow false)} + i/actions] [:select.input-select {:default-value (str (:style value)) :on-change (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} - [:option {:value ":drop-shadow"} (t locale "workspace.options.shadow-options.drop-shadow")] - [:option {:value ":inner-shadow"} (t locale "workspace.options.shadow-options.inner-shadow")]]] + [:option {:value ":drop-shadow"} (tr "workspace.options.shadow-options.drop-shadow")] + [:option {:value ":inner-shadow"} (tr "workspace.options.shadow-options.inner-shadow")]]] [:div.row-grid-2 [:div.input-element @@ -143,7 +153,7 @@ :on-click (select-text adv-offset-x-ref) :on-change (update-attr index :offset-x valid-number? basic-offset-x-ref) :value (:offset-x value)}] - [:span.after (t locale "workspace.options.shadow-options.offsetx")]] + [:span.after (tr "workspace.options.shadow-options.offsetx")]] [:div.input-element [:> numeric-input {:ref adv-offset-y-ref @@ -152,7 +162,7 @@ :on-click (select-text adv-offset-y-ref) :on-change (update-attr index :offset-y valid-number? basic-offset-y-ref) :value (:offset-y value)}] - [:span.after (t locale "workspace.options.shadow-options.offsety")]]] + [:span.after (tr "workspace.options.shadow-options.offsety")]]] [:div.row-grid-2 [:div.input-element @@ -163,7 +173,7 @@ :on-change (update-attr index :blur valid-number? basic-blur-ref) :min 0 :value (:blur value)}] - [:span.after (t locale "workspace.options.shadow-options.blur")]] + [:span.after (tr "workspace.options.shadow-options.blur")]] [:div.input-element [:> numeric-input {:ref adv-spread-ref @@ -173,7 +183,7 @@ :on-change (update-attr index :spread valid-number?) :min 0 :value (:spread value)}] - [:span.after (t locale "workspace.options.shadow-options.spread")]]] + [:span.after (tr "workspace.options.shadow-options.spread")]]] [:div.color-row-wrap [:& color-row {:color (if (string? (:color value)) @@ -187,10 +197,8 @@ :on-close #(st/emit! (dwu/commit-undo-transaction))}]]]])) (mf/defc shadow-menu [{:keys [ids type values] :as props}] - (let [locale (i18n/use-locale) - on-remove-all-shadows - (fn [event] - (st/emit! (dch/update-shapes ids #(dissoc % :shadow) ))) + (let [on-remove-all-shadows + (fn [_] (st/emit! (dch/update-shapes ids #(dissoc % :shadow)))) on-add-shadow (fn [] @@ -199,9 +207,9 @@ [:div.element-set-title [:span (case type - :multiple (t locale "workspace.options.shadow-options.title.multiple") - :group (t locale "workspace.options.shadow-options.title.group") - (t locale "workspace.options.shadow-options.title"))] + :multiple (tr "workspace.options.shadow-options.title.multiple") + :group (tr "workspace.options.shadow-options.title.group") + (tr "workspace.options.shadow-options.title"))] (when-not (= :multiple (:shadow values)) [:div.add-page {:on-click on-add-shadow} i/close])] @@ -210,14 +218,14 @@ (= :multiple (:shadow values)) [:div.element-set-content [:div.element-set-options-group - [:div.element-set-label (t locale "settings.multiple")] + [:div.element-set-label (tr "settings.multiple")] [:div.element-set-actions [:div.element-set-actions-button {:on-click on-remove-all-shadows} i/minus]]]] - (not (empty? (:shadow values))) + (seq (:shadow values)) [:div.element-set-content - (for [[index {:keys [id] :as value}] (d/enumerate (:shadow values []))] + (for [[index value] (d/enumerate (:shadow values []))] [:& shadow-entry {:key (str "shadow-" index) :ids ids :value value diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 14ae0a784c..beee39a191 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -6,21 +6,18 @@ (ns app.main.ui.workspace.sidebar.options.menus.stroke (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] [app.common.data :as d] [app.common.math :as math] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] - [app.util.data :refer [classnames]] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr t]] - [app.util.object :as obj])) + [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (def stroke-attrs [:stroke-style @@ -47,11 +44,10 @@ (mf/defc stroke-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]} [{:keys [ids type values] :as props}] - (let [locale (i18n/use-locale) - label (case type - :multiple (t locale "workspace.options.selection-stroke") - :group (t locale "workspace.options.group-stroke") - (t locale "workspace.options.stroke")) + (let [label (case type + :multiple (tr "workspace.options.selection-stroke") + :group (tr "workspace.options.group-stroke") + (tr "workspace.options.stroke")) show-options (not= (:stroke-style values :none) :none) @@ -65,7 +61,7 @@ (mf/use-callback (mf/deps ids) (fn [color] - (let [remove-multiple (fn [[key value]] (not= value :multiple)) + (let [remove-multiple (fn [[_ value]] (not= value :multiple)) color (into {} (filter remove-multiple) color)] (st/emit! (dc/change-stroke ids color))))) @@ -99,7 +95,7 @@ (st/emit! (dch/update-shapes ids #(assoc % :stroke-width value)))))) on-add-stroke - (fn [event] + (fn [_] (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :solid :stroke-color "#000000" @@ -107,19 +103,19 @@ :stroke-width 1)))) on-del-stroke - (fn [event] + (fn [_] (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none)))) on-open-picker (mf/use-callback (mf/deps ids) - (fn [value opacity id file-id] + (fn [_value _opacity _id _file-id] (st/emit! (dwu/start-undo-transaction)))) on-close-picker (mf/use-callback (mf/deps ids) - (fn [value opacity id file-id] + (fn [_value _opacity _id _file-id] (st/emit! (dwu/commit-undo-transaction))))] (if show-options @@ -139,29 +135,29 @@ ;; Stroke Width, Alignment & Style [:div.row-flex [:div.input-element - {:class (classnames :pixels (not= (:stroke-width values) :multiple))} + {:class (dom/classnames :pixels (not= (:stroke-width values) :multiple))} [:input.input-text {:type "number" :min "0" :value (-> (:stroke-width values) width->string) - :placeholder (t locale "settings.multiple") + :placeholder (tr "settings.multiple") :on-change on-stroke-width-change}]] [:select#style.input-select {:value (enum->string (:stroke-alignment values)) :on-change on-stroke-alignment-change} (when (= (:stroke-alignment values) :multiple) [:option {:value ""} "--"]) - [:option {:value ":center"} (t locale "workspace.options.stroke.center")] - [:option {:value ":inner"} (t locale "workspace.options.stroke.inner")] - [:option {:value ":outer"} (t locale "workspace.options.stroke.outer")]] + [:option {:value ":center"} (tr "workspace.options.stroke.center")] + [:option {:value ":inner"} (tr "workspace.options.stroke.inner")] + [:option {:value ":outer"} (tr "workspace.options.stroke.outer")]] [:select#style.input-select {:value (enum->string (:stroke-style values)) :on-change on-stroke-style-change} (when (= (:stroke-style values) :multiple) [:option {:value ""} "--"]) - [:option {:value ":solid"} (t locale "workspace.options.stroke.solid")] - [:option {:value ":dotted"} (t locale "workspace.options.stroke.dotted")] - [:option {:value ":dashed"} (t locale "workspace.options.stroke.dashed")] - [:option {:value ":mixed"} (t locale "workspace.options.stroke.mixed")]]]]] + [:option {:value ":solid"} (tr "workspace.options.stroke.solid")] + [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")] + [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")] + [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]]]] ;; NO STROKE [:div.element-set diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs index bd6dc955ae..3d953a699b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs @@ -6,16 +6,14 @@ (ns app.main.ui.workspace.sidebar.options.menus.svg-attrs (:require - [cuerdas.core :as str] [app.common.data :as d] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] [app.main.store :as st] + [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [rumext.alpha :as mf] - [app.main.ui.icons :as i])) + [rumext.alpha :as mf])) (mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}] (let [handle-change @@ -55,7 +53,7 @@ :on-change on-change :on-delete on-delete}])])])) -(mf/defc svg-attrs-menu [{:keys [ids type values]}] +(mf/defc svg-attrs-menu [{:keys [ids values]}] (let [handle-change (mf/use-callback (mf/deps ids) @@ -86,7 +84,7 @@ [:div.element-set-title [:span (tr "workspace.sidebar.options.svg-attrs.title")]] - (for [[index [attr-key attr-value]] (d/enumerate (:svg-attrs values))] + (for [[attr-key attr-value] (:svg-attrs values)] [:& attribute-value {:key attr-key :attr [attr-key] :value attr-value diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 0d6ab25464..d9f9ec455b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -7,9 +7,8 @@ (ns app.main.ui.workspace.sidebar.options.menus.text (:require [app.common.data :as d] - [app.common.uuid :as uuid] [app.common.text :as txt] - [app.main.data.workspace.common :as dwc] + [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwt] @@ -81,10 +80,10 @@ (def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs)) (mf/defc text-align-options - [{:keys [ids values on-change] :as props}] + [{:keys [values on-change] :as props}] (let [{:keys [text-align]} values handle-change - (fn [event new-align] + (fn [_ new-align] (on-change {:text-align new-align}))] ;; --- Align @@ -111,54 +110,53 @@ i/text-align-justify]])) (mf/defc text-direction-options - [{:keys [ids values on-change] :as props}] + [{:keys [values on-change] :as props}] (let [direction (:text-direction values) - handle-change (fn [event val] + handle-change (fn [_ val] (on-change {:text-direction val}))] ;; --- Align [:div.align-icons - [:span.tooltip.tooltip-bottom + [:span.tooltip.tooltip-bottom-left {:alt (tr "workspace.options.text-options.direction-ltr") :class (dom/classnames :current (= "ltr" direction)) :on-click #(handle-change % "ltr")} i/text-direction-ltr] - [:span.tooltip.tooltip-bottom + [:span.tooltip.tooltip-bottom-left {:alt (tr "workspace.options.text-options.direction-rtl") :class (dom/classnames :current (= "rtl" direction)) :on-click #(handle-change % "rtl")} i/text-direction-rtl]])) (mf/defc vertical-align - [{:keys [shapes ids values on-change] :as props}] + [{:keys [values on-change] :as props}] (let [{:keys [vertical-align]} values vertical-align (or vertical-align "top") handle-change - (fn [event new-align] + (fn [_ new-align] (on-change {:vertical-align new-align}))] [:div.align-icons - [:span.tooltip.tooltip-bottom + [:span.tooltip.tooltip-bottom-left {:alt (tr "workspace.options.text-options.align-top") :class (dom/classnames :current (= "top" vertical-align)) :on-click #(handle-change % "top")} i/align-top] - [:span.tooltip.tooltip-bottom + [:span.tooltip.tooltip-bottom-left {:alt (tr "workspace.options.text-options.align-middle") :class (dom/classnames :current (= "center" vertical-align)) :on-click #(handle-change % "center")} i/align-middle] - [:span.tooltip.tooltip-bottom + [:span.tooltip.tooltip-bottom-left {:alt (tr "workspace.options.text-options.align-bottom") :class (dom/classnames :current (= "bottom" vertical-align)) :on-click #(handle-change % "bottom")} i/align-bottom]])) (mf/defc grow-options - [{:keys [ids values on-change] :as props}] - (let [to-single-value (fn [coll] (if (> (count coll) 1) nil (first coll))) - grow-type (->> values :grow-type) + [{:keys [ids values] :as props}] + (let [grow-type (:grow-type values) handle-change-grow - (fn [event grow-type] + (fn [_ grow-type] (st/emit! (dch/update-shapes ids #(assoc % :grow-type grow-type))))] [:div.align-icons @@ -179,13 +177,10 @@ i/auto-height]])) (mf/defc text-decoration-options - [{:keys [ids values on-change] :as props}] - (let [{:keys [text-decoration]} values - - text-decoration (or text-decoration "none") - + [{:keys [values on-change] :as props}] + (let [text-decoration (or (:text-decoration values) "none") handle-change - (fn [event type] + (fn [_ type] (on-change {:text-decoration type}))] [:div.align-icons [:span.tooltip.tooltip-bottom @@ -262,18 +257,18 @@ (get typographies (:typography-ref-id values))))) on-convert-to-typography - (fn [event] + (fn [_] (let [setted-values (-> (d/without-nils values) (select-keys (d/concat text-font-attrs text-spacing-attrs text-transform-attrs))) typography (merge txt/default-typography setted-values) - typography (generate-typography-name typography)] - (let [id (uuid/next)] - (st/emit! (dwl/add-typography (assoc typography :id id) false)) - (run! #(emit-update! % {:typography-ref-id id - :typography-ref-file file-id}) ids)))) + typography (generate-typography-name typography) + id (uuid/next)] + (st/emit! (dwl/add-typography (assoc typography :id id) false)) + (run! #(emit-update! % {:typography-ref-id id + :typography-ref-file file-id}) ids))) handle-detach-typography (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 5fa6300e64..ca5e77631a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -7,30 +7,24 @@ (ns app.main.ui.workspace.sidebar.options.menus.typography (:require ["react-virtualized" :as rvt] - [app.common.exceptions :as ex] [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.pages :as cp] [app.common.text :as txt] - [app.main.data.workspace.texts :as dwt] [app.main.data.shortcuts :as dsc] - [app.main.data.fonts :as df] - [app.main.data.workspace :as dw] [app.main.fonts :as fonts] - [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.editable-select :refer [editable-select]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.timers :as tm] - [app.util.keyboard :as kbd] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.object :as obj] [app.util.router :as rt] - [app.util.timers :as ts] - [goog.events :as events] + [app.util.timers :as tm] [cuerdas.core :as str] + [goog.events :as events] [rumext.alpha :as mf])) (defn- attr->string [value] @@ -88,11 +82,11 @@ (comp (filter #(contains? backends (:backend %)))))] (into [] xform fonts))) -(defn- toggle-backend - [backends id] - (if (contains? backends id) - (disj backends id) - (conj backends id))) +;; (defn- toggle-backend +;; [backends id] +;; (if (contains? backends id) +;; (disj backends id) +;; (conj backends id))) (mf/defc font-selector [{:keys [on-select on-close current-font] :as props}] @@ -101,7 +95,6 @@ flist (mf/use-ref) input (mf/use-ref) - ddown (mf/use-ref) fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts)) @@ -237,7 +230,7 @@ :current? (= (:id font) (:id selected))}]))) (mf/defc font-options - [{:keys [editor ids values on-change] :as props}] + [{:keys [values on-change] :as props}] (let [{:keys [font-id font-size font-variant-id]} values font-id (or font-id (:font-id txt/default-text-attrs)) @@ -261,15 +254,6 @@ :font-weight weight :font-style style})))) - on-font-family-change - (mf/use-callback - (mf/deps fonts change-font) - (fn [event] - (let [new-font-id (dom/get-target-val event)] - (when-not (str/empty? new-font-id) - (let [font (get fonts new-font-id)] - (fonts/ensure-loaded! new-font-id (partial change-font new-font-id))))))) - on-font-size-change (mf/use-callback (mf/deps on-change) @@ -345,7 +329,7 @@ (mf/defc spacing-options - [{:keys [editor ids values on-change] :as props}] + [{:keys [values on-change] :as props}] (let [{:keys [line-height letter-spacing]} values @@ -385,13 +369,10 @@ :on-change #(handle-change % :letter-spacing)}]]])) (mf/defc text-transform-options - [{:keys [editor ids values on-change] :as props}] - (let [{:keys [text-transform]} values - - text-transform (or text-transform "none") - + [{:keys [values on-change] :as props}] + (let [text-transform (or (:text-transform values) "none") handle-change - (fn [event type] + (fn [_ type] (on-change {:text-transform type}))] [:div.align-icons [:span.tooltip.tooltip-bottom @@ -424,7 +405,8 @@ [:div.element-set-content [:> font-options opts] [:div.row-flex - [:> spacing-options opts] + [:> spacing-options opts]] + [:div.row-flex [:> text-transform-options opts]]])) @@ -460,14 +442,15 @@ (mf/deps focus-name?) (fn [] (when focus-name? - (ts/schedule + (tm/schedule #(when-let [node (mf/ref-val name-input-ref)] (dom/focus! node) (dom/select-text! node)))))) [:* [:div.element-set-options-group.typography-entry - {:class (when selected? "selected")} + {:class (when selected? "selected") + :style {:display (when @open? "none")}} [:div.typography-selection-wrapper {:class (when on-click "is-selectable") :on-click on-click @@ -501,6 +484,10 @@ [:span.label (tr "workspace.assets.typography.font-id")] [:span (:font-id typography)]] + [:div.element-set-actions-button.actions-inside + {:on-click #(reset! open? false)} + i/actions] + [:div.row-flex [:span.label (tr "workspace.assets.typography.font-variant-id")] [:span (:font-variant-id typography)]] @@ -532,6 +519,11 @@ {:type "text" :ref name-input-ref :default-value (cp/merge-path-item (:path typography) (:name typography)) - :on-blur on-name-blur}]]] + :on-blur on-name-blur}] + + [:div.element-set-actions-button + {:on-click #(reset! open? false)} + i/actions]]] + [:& typography-options {:values typography :on-change on-change}]])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index ef96fe0db7..a5859e682f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -7,46 +7,39 @@ (ns app.main.ui.workspace.sidebar.options.page "Page options menu entries." (:require - [rumext.alpha :as mf] - [okulary.core :as l] + [app.main.data.workspace :as dw] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] - [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.undo :as dwu] - [app.util.i18n :as i18n :refer [t]] - [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]])) - -(defn use-change-color [page-id] - (mf/use-callback - (mf/deps page-id) - (fn [value] - (st/emit! (dw/change-canvas-color value))))) + [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (mf/defc options - [{:keys [page-id] :as props}] - (let [locale (i18n/use-locale) - options (mf/deref refs/workspace-page-options) - handle-change-color (use-change-color page-id) + {::mf/wrap [mf/memo]} + [] + (let [options (mf/deref refs/workspace-page-options) + + on-change + (fn [value] + (st/emit! (dw/change-canvas-color value))) on-open - (mf/use-callback - (mf/deps page-id) - #(st/emit! (dwu/start-undo-transaction))) + (fn [] + (st/emit! (dwu/start-undo-transaction))) on-close - (mf/use-callback - (mf/deps page-id) - #(st/emit! (dwu/commit-undo-transaction)))] + (fn [] + (st/emit! (dwu/commit-undo-transaction)))] [:div.element-set - [:div.element-set-title (t locale "workspace.options.canvas-background")] + [:div.element-set-title (tr "workspace.options.canvas-background")] [:div.element-set-content [:& color-row {:disable-gradient true :disable-opacity true :color {:color (get options :background "#E8E9EA") :opacity 1} - :on-change handle-change-color + :on-change on-change :on-open on-open :on-close on-close}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index be8a067efd..af5a3e81ad 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -6,23 +6,21 @@ (ns app.main.ui.workspace.sidebar.options.rows.color-row (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] + [app.common.data :as d] [app.common.math :as math] [app.common.pages :as cp] - [app.common.data :as d] - [app.util.dom :as dom] - [app.util.data :refer [classnames]] - [app.util.i18n :as i18n :refer [tr]] - [app.util.color :as uc] - [app.main.refs :as refs] [app.main.data.modal :as modal] - [app.main.ui.hooks :as h] - [app.main.ui.icons :as i] - [app.main.ui.context :as ctx] + [app.main.refs :as refs] [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.color-input :refer [color-input]] - [app.main.ui.components.numeric-input :refer [numeric-input]])) + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.context :as ctx] + [app.main.ui.hooks :as h] + [app.main.ui.icons :as i] + [app.util.color :as uc] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) (defn color-picker-callback [color disable-gradient disable-opacity handle-change-color handle-open handle-close] @@ -163,7 +161,7 @@ (when (and (not disable-opacity) (not (:gradient color))) [:div.input-element - {:class (classnames :percentail (not= (:opacity color) :multiple))} + {:class (dom/classnames :percentail (not= (:opacity color) :multiple))} [:> numeric-input {:value (-> color :opacity opacity->string) :placeholder (tr "settings.multiple") :on-click select-all diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs index b42996c5de..c44d93ad28 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/input_row.cljs @@ -6,12 +6,10 @@ (ns app.main.ui.workspace.sidebar.options.rows.input-row (:require - [rumext.alpha :as mf] - [app.common.data :as d] + [app.main.ui.components.editable-select :refer [editable-select]] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.components.select :refer [select]] - [app.main.ui.components.editable-select :refer [editable-select]] - [app.util.dom :as dom])) + [rumext.alpha :as mf])) (mf/defc input-row [{:keys [label options value class min max on-change type placeholder]}] [:div.row-flex.input-row diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index 8c8888aaa1..713022c439 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -6,14 +6,15 @@ (ns app.main.ui.workspace.sidebar.options.shapes.circle (:require - [rumext.alpha :as mf] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [rumext.alpha :as mf])) (mf/defc options [{:keys [shape] :as props}] @@ -21,12 +22,15 @@ type (:type shape) measure-values (select-keys shape measure-attrs) stroke-values (select-keys shape stroke-attrs) - layer-values (select-keys shape layer-attrs)] + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :options #{:size :position :rotation}}] + [:& constraints-menu {:ids ids + :values constraint-values}] [:& layer-menu {:ids ids :type type :values layer-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 3990546eef..c3f5d8a6d3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -6,23 +6,22 @@ (ns app.main.ui.workspace.sidebar.options.shapes.frame (:require - [rumext.alpha :as mf] [app.common.data :as d] - [app.util.dom :as dom] - [app.common.geom.point :as gpt] - [app.util.i18n :refer [tr]] [app.common.math :as math] - [app.main.store :as st] [app.main.data.workspace :as udw] - [app.main.ui.icons :as i] + [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.numeric-input :refer [numeric-input]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [rumext.alpha :as mf])) (declare +size-presets+) @@ -50,7 +49,7 @@ (st/emit! (udw/update-dimensions [(:id shape)] attr value))) on-proportion-lock-change - (fn [event] + (fn [_] (st/emit! (udw/set-shape-proportion-lock (:id shape) (not (:proportion-lock shape))))) on-position-change @@ -82,8 +81,8 @@ :on-click #(on-preset-selected (:width size-preset) (:height size-preset))} (:name size-preset) [:span (:width size-preset) " x " (:height size-preset)]]))]]] - [:span.orientation-icon {on-click #(on-orientation-clicked :vert)} i/size-vert] - [:span.orientation-icon {on-click #(on-orientation-clicked :horiz)} i/size-horiz]] + [:span.orientation-icon {:on-click #(on-orientation-clicked :vert)} i/size-vert] + [:span.orientation-icon {:on-click #(on-orientation-clicked :horiz)} i/size-horiz]] ;; WIDTH & HEIGHT [:div.row-flex @@ -215,34 +214,34 @@ :width 1920 :height 1080} - {:name "PRINT (72dpi)"} + {:name "PRINT (96dpi)"} {:name "A0" - :width 2384 - :height 3370} + :width 3179 + :height 4494} {:name "A1" - :width 1684 - :height 2384} + :width 2245 + :height 3179} {:name "A2" - :width 1191 - :height 1684} + :width 1587 + :height 2245} {:name "A3" - :width 842 - :height 1191} + :width 1123 + :height 1587} {:name "A4" - :width 595 - :height 842} + :width 794 + :height 1123} {:name "A5" - :width 420 - :height 595} + :width 559 + :height 794} {:name "A6" - :width 297 - :height 420} + :width 397 + :height 559} {:name "Letter" - :width 612 - :height 792} + :width 816 + :height 1054} {:name "DIN Lang" - :width 595 - :height 281} + :width 835 + :height 413} {:name "SOCIAL MEDIA"} {:name "Instagram profile" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 49d87ddebd..c7778b0b60 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -6,18 +6,19 @@ (ns app.main.ui.workspace.sidebar.options.shapes.group (:require - [rumext.alpha :as mf] [app.common.data :as d] - [app.main.ui.workspace.sidebar.options.shapes.multiple :refer [get-attrs]] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.component :refer [component-attrs component-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.text :as ot] [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [app.main.ui.workspace.sidebar.options.menus.text :as ot] + [app.main.ui.workspace.sidebar.options.shapes.multiple :refer [get-attrs]] + [rumext.alpha :as mf])) (mf/defc options {::mf/wrap [mf/memo] @@ -28,20 +29,22 @@ objects (->> shape-with-children (group-by :id) (d/mapm (fn [_ v] (first v)))) type :group - [measure-ids measure-values] (get-attrs [shape] objects :measure) - [layer-ids layer-values] (get-attrs [shape] objects :layer) - [fill-ids fill-values] (get-attrs [shape] objects :fill) - [shadow-ids shadow-values] (get-attrs [shape] objects :shadow) - [blur-ids blur-values] (get-attrs [shape] objects :blur) - [stroke-ids stroke-values] (get-attrs [shape] objects :stroke) - [text-ids text-values] (get-attrs [shape] objects :text) - [svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])] - [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]] + [measure-ids measure-values] (get-attrs [shape] objects :measure) + [layer-ids layer-values] (get-attrs [shape] objects :layer) + [constraint-ids constraint-values] (get-attrs [shape] objects :constraint) + [fill-ids fill-values] (get-attrs [shape] objects :fill) + [shadow-ids shadow-values] (get-attrs [shape] objects :shadow) + [blur-ids blur-values] (get-attrs [shape] objects :blur) + [stroke-ids stroke-values] (get-attrs [shape] objects :stroke) + [text-ids text-values] (get-attrs [shape] objects :text) + [svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])] + [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]] [:div.options [:& measures-menu {:type type :ids measure-ids :values measure-values}] - [:& layer-menu {:type type :ids layer-ids :values layer-values}] [:& component-menu {:ids comp-ids :values comp-values}] + [:& constraints-menu {:ids constraint-ids :values constraint-values}] + [:& layer-menu {:type type :ids layer-ids :values layer-values}] (when-not (empty? fill-ids) [:& fill-menu {:type type :ids fill-ids :values fill-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs index 4db2bb0290..b71574c29c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs @@ -6,25 +6,32 @@ (ns app.main.ui.workspace.sidebar.options.shapes.image (:require - [rumext.alpha :as mf] + [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] - [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [rumext.alpha :as mf])) (mf/defc options [{:keys [shape] :as props}] (let [ids [(:id shape)] type (:type shape) measure-values (select-keys shape measure-attrs) - layer-values (select-keys shape layer-attrs)] + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values}] + + [:& constraints-menu {:ids ids + :values constraint-values}] + [:& layer-menu {:ids ids :type type :values layer-values}] + [:& shadow-menu {:ids ids :values (select-keys shape [:shadow])}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 86a0490531..5f69d1ea9a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.text :as txt] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] @@ -22,85 +23,94 @@ ;; attribute and how to handle them (def type->props {:frame - {:measure :shape - :layer :shape - :fill :shape - :shadow :children - :blur :children - :stroke :children - :text :children} + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :children + :blur :children + :stroke :children + :text :children} :group - {:measure :shape - :layer :shape - :fill :children - :shadow :shape - :blur :shape - :stroke :children - :text :children} + {:measure :shape + :layer :shape + :constraint :shape + :fill :children + :shadow :shape + :blur :shape + :stroke :children + :text :children} :path - {:measure :shape - :layer :shape - :fill :shape - :shadow :shape - :blur :shape - :stroke :shape - :text :ignore} + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :shape + :blur :shape + :stroke :shape + :text :ignore} :text - {:measure :shape - :layer :shape - :fill :text - :shadow :shape - :blur :shape - :stroke :ignore - :text :text} + {:measure :shape + :layer :shape + :constraint :shape + :fill :text + :shadow :shape + :blur :shape + :stroke :ignore + :text :text} :image - {:measure :shape - :layer :shape - :fill :ignore - :shadow :shape - :blur :shape - :stroke :ignore - :text :ignore} + {:measure :shape + :layer :shape + :constraint :shape + :fill :ignore + :shadow :shape + :blur :shape + :stroke :ignore + :text :ignore} :rect - {:measure :shape - :layer :shape - :fill :shape - :shadow :shape - :blur :shape - :stroke :shape - :text :ignore} + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :shape + :blur :shape + :stroke :shape + :text :ignore} :circle - {:measure :shape - :layer :shape - :fill :shape - :shadow :shape - :blur :shape - :stroke :shape - :text :ignore} + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :shape + :blur :shape + :stroke :shape + :text :ignore} :svg-raw - {:measure :shape - :layer :shape - :fill :shape - :shadow :shape - :blur :shape - :stroke :shape - :text :ignore}}) + {:measure :shape + :layer :shape + :constraint :shape + :fill :shape + :shadow :shape + :blur :shape + :stroke :shape + :text :ignore}}) (def props->attrs - {:measure measure-attrs - :layer layer-attrs - :fill fill-attrs - :shadow shadow-attrs - :blur blur-attrs - :stroke stroke-attrs - :text ot/attrs}) + {:measure measure-attrs + :layer layer-attrs + :constraint constraint-attrs + :fill fill-attrs + :shadow shadow-attrs + :blur blur-attrs + :stroke stroke-attrs + :text ot/attrs}) (def shadow-keys [:style :color :offset-x :offset-y :blur :spread]) @@ -147,7 +157,7 @@ :else (attrs/get-attrs-multi [v1 v2] attrs))) extract-attrs - (fn [[ids values] {:keys [id type shapes content] :as shape}] + (fn [[ids values] {:keys [id type content] :as shape}] (let [props (get-in type->props [type attr-type])] (case props :ignore [ids values] @@ -176,18 +186,22 @@ objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v)))) type :multiple - [measure-ids measure-values] (get-attrs shapes objects :measure) - [layer-ids layer-values] (get-attrs shapes objects :layer) - [fill-ids fill-values] (get-attrs shapes objects :fill) - [shadow-ids shadow-values] (get-attrs shapes objects :shadow) - [blur-ids blur-values] (get-attrs shapes objects :blur) - [stroke-ids stroke-values] (get-attrs shapes objects :stroke) - [text-ids text-values] (get-attrs shapes objects :text)] + [measure-ids measure-values] (get-attrs shapes objects :measure) + [layer-ids layer-values] (get-attrs shapes objects :layer) + [constraint-ids constraint-values] (get-attrs shapes objects :constraint) + [fill-ids fill-values] (get-attrs shapes objects :fill) + [shadow-ids shadow-values] (get-attrs shapes objects :shadow) + [blur-ids blur-values] (get-attrs shapes objects :blur) + [stroke-ids stroke-values] (get-attrs shapes objects :stroke) + [text-ids text-values] (get-attrs shapes objects :text)] [:div.options (when-not (empty? measure-ids) [:& measures-menu {:type type :ids measure-ids :values measure-values}]) + (when-not (empty? constraint-ids) + [:& constraints-menu {:ids constraint-ids :values constraint-values}]) + (when-not (empty? layer-ids) [:& layer-menu {:type type :ids layer-ids :values layer-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index 00d09a17ec..9d8727a028 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -6,15 +6,15 @@ (ns app.main.ui.workspace.sidebar.options.shapes.path (:require - [rumext.alpha :as mf] - [app.common.data :as d] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [rumext.alpha :as mf])) (mf/defc options [{:keys [shape] :as props}] @@ -22,11 +22,14 @@ type (:type shape) measure-values (select-keys shape measure-attrs) stroke-values (select-keys shape stroke-attrs) - layer-values (select-keys shape layer-attrs)] + layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values}] + [:& constraints-menu {:ids ids + :values constraint-values}] [:& layer-menu {:ids ids :type type :values layer-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index bf8a4ac47e..ad537732a0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -6,14 +6,15 @@ (ns app.main.ui.workspace.sidebar.options.shapes.rect (:require - [rumext.alpha :as mf] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] - [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]])) + [rumext.alpha :as mf])) (mf/defc options {::mf/wrap [mf/memo]} @@ -22,6 +23,7 @@ type (:type shape) measure-values (select-keys shape measure-attrs) layer-values (select-keys shape layer-attrs) + constraint-values (select-keys shape constraint-attrs) fill-values (select-keys shape fill-attrs) stroke-values (select-keys shape stroke-attrs)] [:* @@ -29,6 +31,9 @@ :type type :values measure-values}] + [:& constraints-menu {:ids ids + :values constraint-values}] + [:& layer-menu {:ids ids :type type :values layer-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs index 01494340fc..5283cd8114 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs @@ -6,22 +6,24 @@ (ns app.main.ui.workspace.sidebar.options.shapes.svg-raw (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.util.data :as d] - [app.util.color :as uc] - [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] - [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] - [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] - [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] - [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]])) + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] + [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]] + [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] + [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] + [app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]] + [app.util.color :as uc] + [app.util.data :as d] + [cuerdas.core :as str] + [rumext.alpha :as mf])) ;; This is a list of svg tags that can be grouped in shape-container ;; this allows them to have gradients, shadows and masks (def svg-elements #{:svg :g :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath}) -(defn hex->number [hex] 1) +(defn hex->number [_] 1) + (defn shorthex->longhex [hex] (let [[_ r g b] hex] (str "#" r r g g b b))) @@ -45,10 +47,10 @@ (defn get-fill-values [shape] - (let [fill-values (or (select-keys shape fill-attrs)) - color (-> (or (get-in shape [:content :attrs :fill]) - (get-in shape [:content :attrs :style :fill])) - (parse-color)) + (let [fill-values (select-keys shape fill-attrs) + color (-> (or (get-in shape [:content :attrs :fill]) + (get-in shape [:content :attrs :style :fill])) + (parse-color)) fill-values (if (and (empty? fill-values) color) {:fill-color (:color color) @@ -57,10 +59,10 @@ fill-values)) (defn get-stroke-values [shape] - (let [stroke-values (or (select-keys shape stroke-attrs)) - color (-> (or (get-in shape [:content :attrs :stroke]) - (get-in shape [:content :attrs :style :stroke])) - (parse-color)) + (let [stroke-values (select-keys shape stroke-attrs) + color (-> (or (get-in shape [:content :attrs :stroke]) + (get-in shape [:content :attrs :style :stroke])) + (parse-color)) stroke-color (:color color "#000000") stroke-opacity (:opacity color 1) @@ -90,8 +92,9 @@ (let [ids [(:id shape)] type (:type shape) - {:keys [tag attrs] :as content} (:content shape) + {:keys [tag] :as content} (:content shape) measure-values (select-keys shape measure-attrs) + constraint-values (select-keys shape constraint-attrs) fill-values (get-fill-values shape) stroke-values (get-stroke-values shape)] @@ -101,12 +104,17 @@ :type type :values measure-values}] + [:& constraints-menu {:ids ids + :values constraint-values}] + [:& fill-menu {:ids ids :type type :values fill-values}] + [:& stroke-menu {:ids ids :type type :values stroke-values}] + [:& shadow-menu {:ids ids :values (select-keys shape [:shadow])}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 0274d00e92..2989834322 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -10,6 +10,7 @@ [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] + [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] @@ -60,6 +61,10 @@ :type type :values (select-keys shape measure-attrs)}] + [:& constraints-menu + {:ids ids + :values (select-keys shape constraint-attrs)}] + [:& layer-menu {:ids ids :type type :values layer-values}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index ae5ac0ed8c..65cf1c0fef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -18,8 +18,6 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.router :as rt] - [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -86,12 +84,12 @@ on-drop (mf/use-callback (mf/deps id) - (fn [side {:keys [id name] :as data}] + (fn [side {:keys [id] :as data}] (let [index (if (= :bot side) (inc index) index)] (st/emit! (dw/relocate-page id index))))) on-duplicate - (fn [event] + (fn [_] (st/emit! (dw/duplicate-page id))) [dprops dref] @@ -169,7 +167,7 @@ st/state =)) (mf/defc page-item-wrapper - [{:keys [file-id page-id index deletable? selected?] :as props}] + [{:keys [page-id index deletable? selected?] :as props}] (let [page-ref (mf/use-memo (mf/deps page-id) #(make-page-ref page-id)) page (mf/deref page-ref)] [:& page-item {:page page @@ -197,7 +195,7 @@ ;; --- Sitemap Toolbox (mf/defc sitemap - [{:keys [layout] :as props}] + [] (let [file (mf/deref refs/workspace-file) create (mf/use-callback (mf/deps file) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a210dad733..a0fa7552c2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -7,12 +7,12 @@ (ns app.main.ui.workspace.viewport (:require [app.common.data :as d] - [app.common.pages :as cp] [app.common.geom.shapes :as gsh] [app.main.refs :as refs] [app.main.ui.context :as ctx] - [app.main.ui.context :as muc] [app.main.ui.measurements :as msr] + [app.main.ui.shapes.embed :as embed] + [app.main.ui.shapes.export :as use] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.text.editor :as editor] [app.main.ui.workspace.viewport.actions :as actions] @@ -64,7 +64,8 @@ object-modifiers (mf/deref refs/workspace-modifiers) objects (mf/use-memo (mf/deps objects object-modifiers) - #(cp/merge-modifiers objects object-modifiers)) + #(gsh/merge-modifiers objects object-modifiers)) + background (get options :background "#E8E9EA") ;; STATE alt? (mf/use-state false) @@ -77,7 +78,6 @@ ;; REFS viewport-ref (mf/use-ref nil) - zoom-view-ref (mf/use-ref nil) render-ref (mf/use-ref nil) ;; VARS @@ -104,7 +104,7 @@ create-comment? (= :comments drawing-tool) drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) - path-editing? (and edition (= :path (get-in objects [edition :type]))) + node-editing? (and edition (not= :text (get-in objects [edition :type]))) text-editing? (and edition (= :text (get-in objects [edition :type]))) on-click (actions/on-click hover selected edition drawing-path? drawing-tool) @@ -113,7 +113,7 @@ on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) on-drop (actions/on-drop file viewport-ref zoom) - on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?) + on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? node-editing? drawing-path? create-comment?) on-mouse-up (actions/on-mouse-up disable-paste) on-pointer-down (actions/on-pointer-down) on-pointer-enter (actions/on-pointer-enter in-viewport?) @@ -137,26 +137,29 @@ show-presence? page-id show-prototypes? (= options-mode :prototype) show-selection-handlers? (seq selected) - show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (not (empty? selected))) + show-snap-distance? (and (contains? layout :dynamic-alignment) + (= transform :move) + (seq selected)) show-snap-points? (and (or (contains? layout :dynamic-alignment) (contains? layout :snap-grid)) (or drawing-obj transform)) show-selrect? (and selrect (empty? drawing)) - show-measures? (and (not transform) (not path-editing?) show-distances?)] + show-measures? (and (not transform) (not node-editing?) show-distances?)] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) - (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? path-editing?) + (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? node-editing?) (hooks/setup-resize layout viewport-ref) (hooks/setup-keyboard alt? ctrl?) (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom) (hooks/setup-viewport-modifiers modifiers selected objects render-ref) - (hooks/setup-shortcuts path-editing? drawing-path?) + (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames objects vbox hover active-frames) [:div.viewport [:div.viewport-overlays - [:& wtr/frame-renderer {:objects objects}] + [:& wtr/frame-renderer {:objects objects + :background background}] (when show-comments? [:& comments/comments-layer {:vbox vbox @@ -179,15 +182,18 @@ :ref render-ref :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot "https://penpot.app/xmlns" :preserveAspectRatio "xMidYMid meet" :key (str "render" page-id) :width (:width vport 0) :height (:height vport 0) :view-box (utils/format-viewbox vbox) - :style {:background-color (get options :background "#E8E9EA") + :style {:background-color background :pointer-events "none"}} - [:& (mf/provider muc/embed-ctx) {:value true} + [:& use/export-page {:options options}] + + [:& (mf/provider embed/context) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id :objects objects diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index a32ae106e3..2fbfbac085 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -12,10 +12,10 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.drawing :as dd] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.path :as dwdp] [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.workspace.viewport.utils :as utils] - [app.main.data.workspace.path :as dwdp] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.keyboard :as kbd] @@ -27,9 +27,9 @@ (:import goog.events.WheelEvent)) (defn on-mouse-down - [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?] + [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? node-editing? drawing-path? create-comment?] (mf/use-callback - (mf/deps id blocked hidden type selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?) + (mf/deps id blocked hidden type selected edition drawing-tool text-editing? node-editing? drawing-path? create-comment?) (fn [bevent] (when (or (dom/class? (dom/get-target bevent) "viewport-controls") (dom/class? (dom/get-target bevent) "viewport-selrect")) @@ -65,12 +65,12 @@ drawing-tool (st/emit! (dd/start-drawing drawing-tool)) - path-editing? + node-editing? ;; Handle path node area selection - (st/emit! (dwdp/handle-selection shift?)) + (st/emit! (dwdp/handle-area-selection shift?)) (or (not id) (and frame? (not selected?))) - (st/emit! (dw/handle-selection shift?)) + (st/emit! (dw/handle-area-selection shift?)) (not drawing-tool) (st/emit! (when (or shift? (not selected?)) @@ -158,9 +158,7 @@ {:keys [id type] :as shape} @hover frame? (= :frame type) - group? (= :group type) - text? (= :text type) - path? (= :path type)] + group? (= :group type)] (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?)) @@ -174,12 +172,8 @@ (reset! hover-ids (into [] (rest @hover-ids))) (st/emit! (dw/select-shape (:id selected)))) - (and (not= id edition) (or text? path?)) + (not= id edition) (st/emit! (dw/select-shape id) - (dw/start-editing-selected)) - - :else - (st/emit! (dw/selected-to-path) (dw/start-editing-selected)))))))) (defn on-context-menu @@ -290,8 +284,7 @@ (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta?)))))) (defn on-mouse-move [viewport-ref zoom] - (let [last-position (mf/use-var nil) - viewport (mf/ref-val viewport-ref)] + (let [last-position (mf/use-var nil)] (mf/use-callback (mf/deps zoom) (fn [event] @@ -477,7 +470,7 @@ (defn on-resize [viewport-ref] (mf/use-callback - (fn [event] + (fn [_] (let [node (mf/ref-val viewport-ref) prnt (dom/get-parent node) size (dom/get-client-size prnt)] diff --git a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs index ebf106595a..740c6956c3 100644 --- a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs @@ -7,17 +7,12 @@ (ns app.main.ui.workspace.viewport.drawarea "Drawing components." (:require - [rumext.alpha :as mf] - [app.main.data.workspace :as dw] - [app.main.data.workspace.drawing :as dd] - [app.main.store :as st] - [app.main.ui.workspace.shapes :as shapes] - [app.main.ui.shapes.path :refer [path-shape]] - [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] [app.common.geom.shapes :as gsh] - [app.common.data :as d] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t]])) + [app.common.math :as mth] + [app.main.ui.shapes.path :refer [path-shape]] + [app.main.ui.workspace.shapes :as shapes] + [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] + [rumext.alpha :as mf])) (declare generic-draw-area) (declare path-draw-area) @@ -38,13 +33,13 @@ [{:keys [shape zoom]}] (let [{:keys [x y width height]} (:selrect (gsh/transform-shape shape))] (when (and x y - (not (d/nan? x)) - (not (d/nan? y))) + (not (mth/nan? x)) + (not (mth/nan? y))) [:rect.main {:x x :y y :width width :height height :style {:stroke "#1FDEA7" - :fill "transparent" + :fill "none" :stroke-width (/ 1 zoom)}}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index e10f485a99..5cd49cc681 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -6,18 +6,17 @@ (ns app.main.ui.workspace.viewport.frame-grid (:require - [rumext.alpha :as mf] - [okulary.core :as l] - [app.main.refs :as refs] - [app.common.math :as mth] - [app.common.pages :as cp] [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.common.uuid :as uuid] + [app.main.refs :as refs] [app.util.geom.grid :as gg] - [app.common.uuid :as uuid])) + [okulary.core :as l] + [rumext.alpha :as mf])) (mf/defc square-grid [{:keys [frame zoom grid] :as props}] (let [grid-id (mf/use-memo #(uuid/next)) - {:keys [color size] :as params} (-> grid :params) + {:keys [size] :as params} (-> grid :params) {color-value :color color-opacity :opacity} (-> grid :params :color) ;; Support for old color format color-value (or color-value (:value (get-in grid [:params :color :value])))] @@ -43,7 +42,7 @@ :height (:height frame) :fill (str "url(#" grid-id ")")}]])) -(mf/defc layout-grid [{:keys [key frame zoom grid]}] +(mf/defc layout-grid [{:keys [key frame grid]}] (let [{color-value :color color-opacity :opacity} (-> grid :params :color) ;; Support for old color format color-value (or color-value (:value (get-in grid [:params :color :value]))) @@ -55,7 +54,7 @@ :opacity color-opacity} #js {:stroke color-value :strokeOpacity color-opacity - :fill "transparent"})] + :fill "none"})] [:g.grid (for [{:keys [x y width height]} (gg/grid-areas frame grid)] (do diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 380c96f777..48a8668754 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -7,21 +7,19 @@ (ns app.main.ui.workspace.viewport.gradients "Gradients handlers and renders" (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [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] - [app.main.store :as st] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.math :as mth] + [app.main.data.workspace.colors :as dc] [app.main.refs :as refs] + [app.main.store :as st] [app.main.streams :as ms] - [app.main.data.modal :as modal] - [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.colors :as dc])) + [app.util.dom :as dom] + [beicon.core :as rx] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) (def gradient-line-stroke-width 2) (def gradient-line-stroke-color "white") @@ -94,7 +92,7 @@ on-click on-mouse-down on-mouse-up]}] [:g {:filter (str/fmt "url(#%s)" filter-id) :transform (gmt/rotate-matrix angle point)} - + [:image {:href checkboard :x (- (:x point) (/ gradient-square-width 2 zoom)) :y (- (:y point) (/ gradient-square-width 2 zoom)) @@ -127,7 +125,7 @@ (mf/defc gradient-handler-transformed [{:keys [from-p to-p width-p from-color to-color zoom editing - on-change-start on-change-finish on-change-width on-change-stop-color]}] + on-change-start on-change-finish on-change-width]}] (let [moving-point (mf/use-var nil) angle (+ 90 (gpt/angle from-p to-p)) @@ -138,7 +136,7 @@ (st/emit! (dc/select-gradient-stop (case position :from-p 0 :to-p 1))))) - + on-mouse-down (fn [position event] (dom/stop-propagation event) (dom/prevent-default event) @@ -148,7 +146,7 @@ :from-p 0 :to-p 1))))) - on-mouse-up (fn [position event] + on-mouse-up (fn [_position event] (dom/stop-propagation event) (dom/prevent-default event) (reset! moving-point nil))] @@ -278,7 +276,6 @@ (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) end-y (mth/precision end-y 2)] (change! {:end-x end-x :end-y end-y}))) @@ -287,8 +284,8 @@ (let [scale-factor-y (/ gradient-length (/ height 2)) norm-dist (/ (gpt/distance point from-p) (* (/ width 2) scale-factor-y))] - - (change! {:width norm-dist})))] + (when (and norm-dist (mth/finite? norm-dist)) + (change! {:width norm-dist}))))] (when (and gradient (= id (:shape-id gradient)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index bbda478b0c..1d9c98b1a6 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -37,8 +37,6 @@ (mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-resize on-paste) (fn [] (let [node (mf/ref-val viewport-ref) - prnt (dom/get-parent node) - keys [(events/listen js/document EventType.KEYDOWN on-key-down) (events/listen js/document EventType.KEYUP on-key-up) (events/listen node EventType.MOUSEMOVE on-mouse-move) @@ -90,7 +88,8 @@ (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) (hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %))) -(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom] +;; TODO: revisit the arguments, looks like `selected` is not necessary here +(defn setup-hover-shapes [page-id move-stream _selected objects transform selected ctrl? hover hover-ids zoom] (let [query-point (mf/use-callback (mf/deps page-id) @@ -111,12 +110,7 @@ ;; When transforming shapes we stop querying the worker (rx/filter #(not (some? (mf/ref-val transform-ref)))) (rx/switch-map query-point)) - - roots (mf/use-memo - (mf/deps selected objects) - (fn [] - (let [roots-ids (cp/clean-loops objects selected)] - (->> roots-ids (mapv #(get objects %))))))] + ] (mf/use-effect (mf/deps transform) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 6c894fab61..ca0d1b0e2e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -7,15 +7,11 @@ (ns app.main.ui.workspace.viewport.interactions "Visually show shape interactions in workspace" (:require - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.workspace.viewport.outline :refer [outline]] - [app.util.data :as dt] [app.util.dom :as dom] - [app.util.keyboard :as kbd] [cuerdas.core :as str] [rumext.alpha :as mf] )) @@ -24,14 +20,11 @@ [shape] (first (filter #(= (:event-type %) :click) (:interactions shape)))) - (defn- on-mouse-down - [event {:keys [id type] :as shape} selected] - (do - (dom/stop-propagation event) - (st/emit! (dw/select-shape id)) - (st/emit! (dw/start-create-interaction)))) - + [event {:keys [id] :as shape}] + (dom/stop-propagation event) + (st/emit! (dw/select-shape id)) + (st/emit! (dw/start-create-interaction))) (defn connect-to-shape "Calculate the best position to draw an interaction line @@ -109,7 +102,7 @@ "translate(" (* zoom x) ", " (* zoom y) ")")}] (when arrow-dir [:path {:stroke "#31EFB8" - :fill "transparent" + :fill "none" :stroke-width 2 :d arrow-pdata :transform (str @@ -118,7 +111,7 @@ (mf/defc interaction-path - [{:keys [orig-shape dest-shape dest-point selected selected? zoom] :as props}] + [{:keys [orig-shape dest-shape dest-point selected? zoom] :as props}] (let [[orig-pos orig-x orig-y dest-pos dest-x dest-y] (if dest-shape (connect-to-shape orig-shape dest-shape) @@ -134,14 +127,16 @@ (if-not selected? [:path {:stroke "#B1B2B5" - :fill "transparent" + :fill "none" + :pointer-events "visible" :stroke-width (/ 2 zoom) :d pdata - :on-mouse-down #(on-mouse-down % orig-shape selected)}] + :on-mouse-down #(on-mouse-down % orig-shape)}] - [:g {:on-mouse-down #(on-mouse-down % orig-shape selected)} + [:g {:on-mouse-down #(on-mouse-down % orig-shape)} [:path {:stroke "#31EFB8" - :fill "transparent" + :fill "none" + :pointer-events "visible" :stroke-width (/ 2 zoom) :d pdata}] [:& interaction-marker {:x orig-x @@ -159,11 +154,11 @@ (mf/defc interaction-handle - [{:keys [shape selected zoom] :as props}] + [{:keys [shape zoom] :as props}] (let [shape-rect (:selrect shape) handle-x (+ (:x shape-rect) (:width shape-rect)) handle-y (+ (:y shape-rect) (/ (:height shape-rect) 2))] - [:g {:on-mouse-down #(on-mouse-down % shape selected)} + [:g {:on-mouse-down #(on-mouse-down % shape)} [:& interaction-marker {:x handle-x :y handle-y :arrow-dir :right diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index 6c87337768..44d644f6b0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -9,8 +9,8 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.refs :as refs] - [app.util.path.format :as upf] [app.util.object :as obj] + [app.util.path.format :as upf] [clojure.set :as set] [rumext.alpha :as mf] [rumext.util :refer [map->obj]])) @@ -29,14 +29,14 @@ (mf/deps shape) #(when path? (upf/format-path (:content shape)))) - {:keys [id x y width height selrect]} shape + {:keys [x y width height selrect]} shape outline-type (case (:type shape) :circle "ellipse" :path "path" "rect") - common {:fill "transparent" + common {:fill "none" :stroke color :strokeWidth (/ 1 zoom) :pointerEvents "none" diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs index d504faf56b..f6d85e32cf 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.cljs @@ -7,9 +7,7 @@ (ns app.main.ui.workspace.viewport.path-actions (:require [app.main.data.workspace.path :as drp] - [app.main.data.workspace.path.helpers :as wph] [app.main.data.workspace.path.shortcuts :as sc] - [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.shapes.path.common :as pc] @@ -20,8 +18,8 @@ (defn check-enabled [content selected-points] (let [segments (upt/get-segments content selected-points) num-points (count selected-points) - points-selected? (not (empty? selected-points)) - segments-selected? (not (empty? segments))] + points-selected? (seq selected-points) + segments-selected? (seq segments)] {:make-corner points-selected? :make-curve points-selected? :add-node segments-selected? @@ -31,8 +29,7 @@ :separate-nodes segments-selected?})) (mf/defc path-actions [{:keys [shape]}] - (let [id (mf/deref refs/selected-edition) - {:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref) + (let [{:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref) content (:content shape) enabled-buttons @@ -42,66 +39,66 @@ on-select-draw-mode (mf/use-callback - (fn [event] + (fn [_] (st/emit! (drp/change-edit-mode :draw)))) - + on-select-edit-mode (mf/use-callback - (fn [event] + (fn [_] (st/emit! (drp/change-edit-mode :move)))) - + on-add-node (mf/use-callback (mf/deps (:add-node enabled-buttons)) - (fn [event] + (fn [_] (when (:add-node enabled-buttons) (st/emit! (drp/add-node))))) - + on-remove-node (mf/use-callback (mf/deps (:remove-node enabled-buttons)) - (fn [event] + (fn [_] (when (:remove-node enabled-buttons) (st/emit! (drp/remove-node))))) - + on-merge-nodes (mf/use-callback (mf/deps (:merge-nodes enabled-buttons)) - (fn [event] + (fn [_] (when (:merge-nodes enabled-buttons) (st/emit! (drp/merge-nodes))))) - + on-join-nodes (mf/use-callback (mf/deps (:join-nodes enabled-buttons)) - (fn [event] + (fn [_] (when (:join-nodes enabled-buttons) (st/emit! (drp/join-nodes))))) - + on-separate-nodes (mf/use-callback (mf/deps (:separate-nodes enabled-buttons)) - (fn [event] + (fn [_] (when (:separate-nodes enabled-buttons) (st/emit! (drp/separate-nodes))))) on-make-corner (mf/use-callback (mf/deps (:make-corner enabled-buttons)) - (fn [event] + (fn [_] (when (:make-corner enabled-buttons) (st/emit! (drp/make-corner))))) - + on-make-curve (mf/use-callback (mf/deps (:make-curve enabled-buttons)) - (fn [event] + (fn [_] (when (:make-curve enabled-buttons) (st/emit! (drp/make-curve))))) on-toggle-snap (mf/use-callback - (fn [event] + (fn [_] (st/emit! (drp/toggle-snap)))) ] @@ -121,7 +118,7 @@ :alt (tr "workspace.path.actions.draw-nodes" (sc/get-tooltip :draw-nodes)) :on-click on-select-edit-mode} i/pointer-inner]] - + [:div.viewport-actions-group ;; Add Node [:div.viewport-actions-entry.tooltip.tooltip-bottom diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index 9601b2b982..1373a151e0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -7,21 +7,18 @@ (ns app.main.ui.workspace.viewport.pixel-overlay (:require [app.common.uuid :as uuid] - [app.main.data.workspace.colors :as dwc] [app.main.data.modal :as modal] + [app.main.data.workspace.colors :as dwc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.context :as muc] [app.main.ui.cursors :as cur] [app.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] - [app.util.timers :as timers] [beicon.core :as rx] [cuerdas.core :as str] [goog.events :as events] - [okulary.core :as l] [promesa.core :as p] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -54,11 +51,8 @@ {::mf/wrap-props false} [props] (let [vport (unchecked-get props "vport") - vbox (unchecked-get props "vbox") viewport-ref (unchecked-get props "viewport-ref") viewport-node (mf/ref-val viewport-ref) - options (unchecked-get props "options") - svg-ref (mf/use-ref nil) canvas-ref (mf/use-ref nil) img-ref (mf/use-ref nil) @@ -67,11 +61,11 @@ handle-keydown (mf/use-callback (fn [event] - (when (and (kbd/esc? event)) - (do (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (dwc/stop-picker)) - (modal/disallow-click-outside!))))) + (when (kbd/esc? event) + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (dwc/stop-picker)) + (modal/disallow-click-outside!)))) handle-mouse-move-picker (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 85b91a8ffb..b8282b5d8a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -10,23 +10,15 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] - [app.common.math :as mth] - [app.common.uuid :as uuid] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.cursors :as cur] - [app.main.ui.hooks :as hooks] [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] - [app.util.data :as d] [app.util.debug :refer [debug?]] [app.util.dom :as dom] [app.util.object :as obj] - [beicon.core :as rx] [cuerdas.core :as str] - [potok.core :as ptk] [rumext.alpha :as mf] [rumext.util :refer [map->obj]])) @@ -53,7 +45,7 @@ :on-mouse-down on-move-selected :style {:stroke color :stroke-width (/ selection-rect-width zoom) - :fill "transparent"}}]))) + :fill "none"}}]))) (defn- handlers-for-selection [{:keys [x y width height]} {:keys [type]} zoom] (let [zoom-width (* width zoom) @@ -142,7 +134,7 @@ :y y :width size :height size - :fill (if (debug? :rotation-handler) "blue" "transparent") + :fill (if (debug? :rotation-handler) "blue" "none") :transform transform :on-mouse-down on-rotate}])) @@ -174,21 +166,16 @@ :width resize-point-circle-radius :height resize-point-circle-radius :transform (when rotation (str/fmt "rotate(%s, %s, %s)" rotation cx' cy')) - :style {:fill (if (debug? :resize-handler) "red" "transparent") + :style {:fill (if (debug? :resize-handler) "red" "none") :cursor cursor} :on-mouse-down #(on-resize {:x cx' :y cy'} %)}]) - (let [rot-square (case position - :top-left 0 - :top-right 90 - :bottom-right 180 - :bottom-left 270)] - [:circle {:on-mouse-down #(on-resize {:x cx' :y cy'} %) - :r (/ resize-point-circle-radius zoom) - :cx cx' - :cy cy' - :style {:fill (if (debug? :resize-handler) "red" "transparent") - :cursor cursor}}]) + [:circle {:on-mouse-down #(on-resize {:x cx' :y cy'} %) + :r (/ resize-point-circle-radius zoom) + :cx cx' + :cy cy' + :style {:fill (if (debug? :resize-handler) "red" "none") + :cursor cursor}}] )])) (mf/defc resize-side-handler @@ -214,7 +201,7 @@ :transform (gmt/multiply transform (gmt/rotate-matrix angle (gpt/point x y))) :on-mouse-down #(on-resize res-point %) - :style {:fill (if (debug? :resize-handler) "yellow" "transparent") + :style {:fill (if (debug? :resize-handler) "yellow" "none") :cursor (if (#{:left :right} position) (cur/resize-ew rotation) (cur/resize-ns rotation)) }}])) @@ -232,7 +219,7 @@ (mf/defc controls {::mf/wrap-props false} [props] - (let [{:keys [overflow-text type] :as shape} (obj/get props "shape") + (let [{:keys [overflow-text] :as shape} (obj/get props "shape") zoom (obj/get props "zoom") color (obj/get props "color") on-move-selected (obj/get props "on-move-selected") @@ -245,7 +232,7 @@ transform (geom/transform-matrix shape {:no-flip true})] (when (not (#{:move :rotate} current-transform)) - [:g.controls {:pointer-events (when disable-handlers "none")} + [:g.controls {:pointer-events (if disable-handlers "none" "visible")} ;; Selection rect [:& selection-rect {:rect selrect @@ -275,17 +262,18 @@ ;; TODO: add specs for clarity (mf/defc text-edition-selection-handlers - [{:keys [shape zoom color] :as props}] + [{:keys [shape color] :as props}] (let [{:keys [x y width height]} shape] [:g.controls [:rect.main {:x x :y y :transform (geom/transform-matrix shape) :width width :height height + :pointer-events "visible" :style {:stroke color :stroke-width "0.5" :stroke-opacity "1" - :fill "transparent"}}]])) + :fill "none"}}]])) (mf/defc multiple-selection-handlers [{:keys [shapes selected zoom color disable-handlers on-move-selected] :as props}] @@ -298,18 +286,17 @@ shape-center (geom/center-shape shape) - hover-id (-> (mf/deref refs/current-hover) first) - hover-id (when-not (d/seek #(= hover-id (:id %)) shapes) hover-id) - hover-shape (mf/deref (refs/object-by-id hover-id)) + on-resize + (fn [current-position _initial-position event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-resize current-position selected shape)))) - vbox (mf/deref refs/vbox) - - on-resize (fn [current-position initial-position event] - (dom/stop-propagation event) - (st/emit! (dw/start-resize current-position initial-position selected shape))) - - on-rotate #(do (dom/stop-propagation %) - (st/emit! (dw/start-rotate shapes)))] + on-rotate + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-rotate shapes))))] [:* [:& controls {:shape shape @@ -328,22 +315,20 @@ (let [shape-id (:id shape) shape (geom/transform-shape shape {:round-coords? false}) - frame (mf/deref (refs/object-by-id (:frame-id shape))) - frame (when-not (= (:id frame) uuid/zero) frame) - vbox (mf/deref refs/vbox) - - hover-id (-> (mf/deref refs/current-hover) first) - hover-id (when-not (= shape-id hover-id) hover-id) - hover-shape (mf/deref (refs/object-by-id hover-id)) - shape' (if (debug? :simple-selection) (geom/setup {:type :rect} (geom/selection-rect [shape])) shape) - on-resize (fn [current-position initial-position event] - (dom/stop-propagation event) - (st/emit! (dw/start-resize current-position initial-position #{shape-id} shape'))) + + on-resize + (fn [current-position _initial-position event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-resize current-position #{shape-id} shape')))) on-rotate - #(do (dom/stop-propagation %) - (st/emit! (dw/start-rotate [shape])))] + (fn [event] + (when (dom/left-mouse? event) + (dom/stop-propagation event) + (st/emit! (dw/start-rotate [shape]))))] + [:& controls {:shape shape' :zoom zoom :color color @@ -356,7 +341,7 @@ {::mf/wrap [mf/memo]} [{:keys [shapes selected edition zoom disable-handlers on-move-selected] :as props}] (let [num (count shapes) - {:keys [id type] :as shape} (first shapes) + {:keys [type] :as shape} (first shapes) color (if (or (> num 1) (nil? (:shape-ref shape))) selection-rect-color-normal @@ -379,8 +364,7 @@ :zoom zoom :color color}] - (and (= type :path) - (= edition (:id shape))) + (= edition (:id shape)) [:& path-editor {:zoom zoom :shape shape}] diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs index ae6c809ad3..a5c85499f9 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs @@ -7,15 +7,11 @@ (ns app.main.ui.workspace.viewport.snap-distances (:require [app.common.data :as d] - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages :as cp] - [app.common.uuid :as uuid] [app.main.refs :as refs] - [app.main.snap :as snap] [app.main.worker :as uw] - [app.util.geom.snap-points :as sp] [beicon.core :as rx] [clojure.set :as set] [cuerdas.core :as str] @@ -275,21 +271,20 @@ update-shape (fn [shape] (-> shape (update :modifiers merge (:modifiers local)) - gsh/transform-shape))] - (let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect) - key (->> selected (map str) (str/join "-"))] - [:g.distance - [:& shape-distance - {:selrect selrect - :page-id page-id - :frame frame - :zoom zoom - :coord :x - :selected selected}] - [:& shape-distance - {:selrect selrect - :page-id page-id - :frame frame - :zoom zoom - :coord :y - :selected selected}]]))) + gsh/transform-shape)) + selrect (->> selected-shapes (map update-shape) gsh/selection-rect)] + [:g.distance + [:& shape-distance + {:selrect selrect + :page-id page-id + :frame frame + :zoom zoom + :coord :x + :selected selected}] + [:& shape-distance + {:selrect selrect + :page-id page-id + :frame frame + :zoom zoom + :coord :y + :selected selected}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index f9cc2707c7..5fad2a5c1d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -6,12 +6,10 @@ (ns app.main.ui.workspace.viewport.snap-points (:require - [app.common.pages :as cp] - [app.common.math :as mth] [app.common.data :as d] - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.main.refs :as refs] + [app.common.math :as mth] + [app.common.pages :as cp] [app.main.snap :as snap] [app.util.geom.snap-points :as sp] [beicon.core :as rx] @@ -106,7 +104,7 @@ (hash-map coord fixedv (flip coord) maxv)])))) (mf/defc snap-feedback - [{:keys [shapes page-id filter-shapes zoom modifiers] :as props}] + [{:keys [shapes filter-shapes zoom modifiers] :as props}] (let [state (mf/use-state []) subject (mf/use-memo #(rx/subject)) diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs index dc6d5eb694..95f27e5a93 100644 --- a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs @@ -17,7 +17,7 @@ (mf/defc frame-thumbnail "Renders the canvas and image for a frame thumbnail and stores its value into the shape" - [{:keys [shape on-thumbnail-data on-frame-not-found]}] + [{:keys [shape background on-thumbnail-data on-frame-not-found]}] (let [thumbnail-img (mf/use-ref nil) thumbnail-canvas (mf/use-ref nil) @@ -45,12 +45,21 @@ on-image-load (mf/use-callback - (mf/deps on-thumbnail-data) + (mf/deps on-thumbnail-data background) (fn [] - (let [canvas-node (mf/ref-val thumbnail-canvas) - img-node (mf/ref-val thumbnail-img) + (let [canvas-node (mf/ref-val thumbnail-canvas) + img-node (mf/ref-val thumbnail-img) + canvas-context (.getContext canvas-node "2d") + canvas-width (.-width canvas-node) + canvas-height (.-height canvas-node) + + _ (.clearRect canvas-context 0 0 canvas-width canvas-height) + _ (.rect canvas-context 0 0 canvas-width canvas-height) + _ (set! (.-fillStyle canvas-context) background) + _ (.fill canvas-context) _ (.drawImage canvas-context img-node 0 0) + data (.toDataURL canvas-node "image/jpeg" 0.8)] (on-thumbnail-data data))))] @@ -72,6 +81,7 @@ {::mf/wrap-props false} [props] (let [objects (obj/get props "objects") + background (obj/get props "background") ;; Id of the current frame being rendered shape-id (mf/use-state nil) @@ -131,5 +141,6 @@ (when (and (some? @shape-id) (contains? objects @shape-id)) [:& frame-thumbnail {:key (str "thumbnail-" @shape-id) :shape (get objects @shape-id) + :background background :on-thumbnail-data on-thumbnail-data :on-frame-not-found on-frame-not-found}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 092da6bbce..384f381177 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -6,14 +6,14 @@ (ns app.main.ui.workspace.viewport.utils (:require - [app.util.dom :as dom] - [app.common.geom.point :as gpt] - [cuerdas.core :as str] [app.common.data :as d] + [app.common.geom.point :as gpt] [app.main.ui.cursors :as cur] - )) + [app.util.dom :as dom] + [cuerdas.core :as str])) -(defn update-transform [node shapes modifiers] +;; TODO: looks like first argument is not necessary. +(defn update-transform [_node shapes modifiers] (doseq [{:keys [id type]} shapes] (let [shape-node (dom/get-element (str "shape-" id)) @@ -30,7 +30,8 @@ shape-node)] (dom/set-attribute node "transform" (str (:displacement modifiers))))))) -(defn remove-transform [node shapes] +;; TODO: looks like first argument is not necessary. +(defn remove-transform [_node shapes] (doseq [{:keys [id type]} shapes] (when-let [node (dom/get-element (str "shape-" id))] (let [node (if (= :frame type) (.-parentNode node) node)] diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 3faa465556..1fe2dce1db 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -16,7 +16,6 @@ [app.main.ui.hooks :as hooks] [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] [app.util.dom :as dom] - [app.util.object :as obj] [rumext.alpha :as mf])) (mf/defc pixel-grid @@ -48,7 +47,7 @@ shape (-> selected first)] (when (and (= (count selected) 1) (= (:id shape) edition) - (= :path (:type shape))) + (not= :text (:type shape))) [:div.viewport-actions [:& path-actions {:shape shape}]]))) @@ -110,13 +109,13 @@ on-pointer-enter (mf/use-callback (mf/deps (:id frame) on-frame-enter) - (fn [event] + (fn [_] (on-frame-enter (:id frame)))) on-pointer-leave (mf/use-callback (mf/deps (:id frame) on-frame-leave) - (fn [event] + (fn [_] (on-frame-leave (:id frame))))] [:text {:x 0 diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs index a0985d97ff..c9004a9f00 100644 --- a/frontend/src/app/main/worker.cljs +++ b/frontend/src/app/main/worker.cljs @@ -6,9 +6,7 @@ (ns app.main.worker (:require - [cljs.spec.alpha :as s] [app.config :as cfg] - [app.common.spec :as us] [app.util.worker :as uw])) (defn on-error @@ -26,3 +24,7 @@ (defn ask-buffered! [message] (uw/ask-buffered! instance message)) + +(defn ask-many! + [message] + (uw/ask-many! instance message)) diff --git a/frontend/src/app/util/avatars.cljs b/frontend/src/app/util/avatars.cljs index ff5afc9e16..acca7decaa 100644 --- a/frontend/src/app/util/avatars.cljs +++ b/frontend/src/app/util/avatars.cljs @@ -6,9 +6,8 @@ (ns app.util.avatars (:require - [cuerdas.core :as str] [app.util.object :as obj] - ["randomcolor" :as rdcolor])) + [cuerdas.core :as str])) (defn generate* [{:keys [name color size] diff --git a/frontend/src/app/util/cache.cljs b/frontend/src/app/util/cache.cljs new file mode 100644 index 0000000000..53fa610ccb --- /dev/null +++ b/frontend/src/app/util/cache.cljs @@ -0,0 +1,26 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.cache + (:require + [app.util.time :as dt] + [beicon.core :as rx])) + +(defonce cache (atom {})) + +(defn with-cache + [{:keys [key max-age]} observable] + (let [entry (get @cache key) + age (when entry + (dt/diff (dt/now) + (:created-at entry)))] + (if (and (some? entry) (< age max-age)) + (rx/of (:data entry)) + (->> observable + (rx/tap + (fn [data] + (let [entry {:created-at (dt/now) :data data}] + (swap! cache assoc key entry)))))))) diff --git a/frontend/src/app/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs index ae07021839..a4069406c3 100644 --- a/frontend/src/app/util/code_gen.cljs +++ b/frontend/src/app/util/code_gen.cljs @@ -33,7 +33,7 @@ color {:color (:stroke-color shape) :opacity (:stroke-opacity shape) :gradient (:stroke-color-gradient shape)}] - (if-not (= :none (:stroke-style shape)) + (when-not (= :none (:stroke-style shape)) (str/format "%spx %s %s" width style (uc/color->background color))))) (def styles-data diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 8899e6f23b..3c540dc3e8 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -7,9 +7,8 @@ (ns app.util.color "Color conversion utils." (:require - [cuerdas.core :as str] - [app.common.math :as math] [app.util.object :as obj] + [cuerdas.core :as str] [goog.color :as gcolor])) (defn rgb->str @@ -31,7 +30,7 @@ [v] (try (into [] (gcolor/hexToRgb v)) - (catch :default e [0 0 0]))) + (catch :default _e [0 0 0]))) (defn rgb->hex [[r g b]] @@ -49,7 +48,7 @@ (defn hex->hsl [hex] (try (into [] (gcolor/hexToHsl hex)) - (catch :default e [0 0 0]))) + (catch :default _e [0 0 0]))) (defn hex->hsla [^string data ^number opacity] @@ -96,7 +95,7 @@ c (nth v 2)] (str a a b b c c)) - :default + :else v)) (defn prepend-hash @@ -135,7 +134,7 @@ :else "transparent"))) -(defn multiple? [{:keys [id file-id value color gradient opacity]}] +(defn multiple? [{:keys [id file-id value color gradient]}] (or (= value :multiple) (= color :multiple) (= gradient :multiple) @@ -144,7 +143,7 @@ (defn color? [^string color-str] (and (not (nil? color-str)) - (not (empty? color-str)) + (seq color-str) (gcolor/isValidColor color-str))) (defn parse-color [^string color-str] diff --git a/frontend/src/app/util/data.cljs b/frontend/src/app/util/data.cljs index c070e31c66..7447359fb0 100644 --- a/frontend/src/app/util/data.cljs +++ b/frontend/src/app/util/data.cljs @@ -39,7 +39,7 @@ (reduce #(dissoc! %1 %2) (transient data) keys))) (defn dissoc-in - [m [k & ks :as keys]] + [m [k & ks :as _keys]] (if ks (if-let [nextmap (get m k)] (let [newmap (dissoc-in nextmap ks)] @@ -153,34 +153,6 @@ ;; Other ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn classnames - [& params] - {:pre [(even? (count params))]} - (str/join " " (reduce (fn [acc [k v]] - (if (and k (true? v)) - (conj acc (name k)) - acc)) - [] - (partition 2 params)))) - -;; (defn normalize-attrs -;; [m] -;; (letfn [(transform [[k v]] -;; (cond -;; (or (= k :class) (= k :class-name)) -;; ["className" v] - -;; (or (keyword? k) (string? k)) -;; [(str/camel (name k)) v] - -;; :else -;; [k v])) -;; (walker [x] -;; (if (map? x) -;; (into {} (map tf) x) -;; x))] -;; (walk/postwalk walker m))) - (defn normalize-props [props] (clj->js props :keyword-fn (fn [key] @@ -196,28 +168,3 @@ nm (str/trim (str/lower name))] (str/includes? nm st)))) -;; (defn coalesce -;; [^number v ^number n] -;; (if (.-toFixed v) -;; (js/parseFloat (.toFixed v n)) -;; 0)) - - - -;; (defmacro mirror-map [& fields] -;; (let [keys# (map #(keyword (name %)) fields) -;; vals# fields] -;; (apply hash-map (interleave keys# vals#)))) - -;; (defmacro some->' -;; [x & forms] -;; `(let [x# (p/then' ~x (fn [v#] -;; (when (nil? v#) -;; (throw (ex-info "internal" {::some-interrupt true}))) -;; v#))] -;; (-> (-> x# ~@forms) -;; (p/catch' (fn [e#] -;; (if (::some-interrupt (ex-data e#)) -;; nil -;; (throw e#))))))) - diff --git a/frontend/src/app/util/debug.cljs b/frontend/src/app/util/debug.cljs index d83a3e0543..c3ee88d9f9 100644 --- a/frontend/src/app/util/debug.cljs +++ b/frontend/src/app/util/debug.cljs @@ -1,12 +1,12 @@ (ns app.util.debug "Debugging utils" (:require - [app.util.timers :as timers] - [app.util.object :as obj] [app.common.math :as mth] + [app.util.object :as obj] + [app.util.timers :as timers] [cljs.pprint :refer [pprint]])) -(def debug-options #{:bounding-boxes :group :events :rotation-handler :resize-handler :selection-center #_:simple-selection}) +(def debug-options #{:bounding-boxes :group :events :rotation-handler :resize-handler :selection-center :export :import #_:simple-selection}) ;; These events are excluded when we activate the :events flag (def debug-exclude-events diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 7601435ca6..f05f2a1a5f 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -6,12 +6,13 @@ (ns app.util.dom (:require - [app.common.exceptions :as ex] - [app.common.geom.point :as gpt] - [app.util.object :as obj] - [app.util.globals :as globals] - [cuerdas.core :as str] - [goog.dom :as dom])) + [app.common.exceptions :as ex] + [app.common.geom.point :as gpt] + [app.util.globals :as globals] + [app.util.object :as obj] + [cuerdas.core :as str] + [goog.dom :as dom] + [promesa.core :as p])) ;; --- Deprecated methods @@ -44,6 +45,18 @@ [title] (set! (.-title globals/document) title)) +(defn set-page-style + [style] + (let [head (first (.getElementsByTagName ^js globals/document "head")) + style-str (str/join "\n" + (map (fn [[k v]] + (str (name k) ": " v ";")) + style))] + (.insertAdjacentHTML head "beforeend" + (str "")))) + (defn get-element-by-class ([classname] (dom/getElementByClass classname)) @@ -54,6 +67,10 @@ [id] (dom/getElement id)) +(defn get-elements-by-tag + [node tag] + (.getElementsByTagName node tag)) + (defn stop-propagation [e] (when e @@ -123,7 +140,7 @@ (defn set-value! [node value] - (set! (.-value node) value)) + (set! (.-value ^js node) value)) (defn select-text! [node] @@ -208,6 +225,10 @@ [node] (.focus node)) +(defn blur! + [node] + (.blur node)) + (defn fullscreen? [] (cond @@ -279,13 +300,15 @@ (defn mtype->extension [mtype] ;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types (case mtype - "image/apng" "apng" - "image/avif" "avif" - "image/gif" "gif" - "image/jpeg" "jpg" - "image/png" "png" - "image/svg+xml" "svg" - "image/webp" "webp" + "image/apng" "apng" + "image/avif" "avif" + "image/gif" "gif" + "image/jpeg" "jpg" + "image/png" "png" + "image/svg+xml" "svg" + "image/webp" "webp" + "application/zip" "zip" + "application/penpot" "penpot" nil)) (defn set-attribute [^js node ^string attr value] @@ -311,3 +334,48 @@ (>= (.-left rect) 0) (<= (.-bottom rect) height) (<= (.-right rect) width)))) + +(defn trigger-download-uri + [filename mtype uri] + (let [link (create-element "a") + extension (mtype->extension mtype) + filename (if extension + (str filename "." extension) + filename)] + (obj/set! link "href" uri) + (obj/set! link "download" filename) + (obj/set! (.-style ^js link) "display" "none") + (.appendChild (.-body ^js js/document) link) + (.click link) + (.remove link))) + +(defn trigger-download + [filename blob] + (trigger-download-uri filename (.-type ^js blob) (create-uri blob))) + +(defn save-as + [uri filename mtype description] + + ;; Only chrome supports the save dialog + (if (obj/contains? globals/window "showSaveFilePicker") + (let [extension (mtype->extension mtype) + opts {:suggestedName (str filename "." extension) + :types [{:description description + :accept { mtype [(str "." extension)]}}]}] + + (-> (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts)) + writable (.createWritable file-system) + response (js/fetch uri) + blob (.blob response) + _ (.write writable blob)] + (.close writable)) + (p/catch + #(when-not (and (= (type %) js/DOMException) + (= (.-name %) "AbortError")) + (trigger-download-uri filename mtype uri))))) + + (trigger-download-uri filename mtype uri))) + +(defn left-mouse? [bevent] + (let [event (.-nativeEvent bevent)] + (= 1 (.-which event)))) diff --git a/frontend/src/app/util/dom/dnd.cljs b/frontend/src/app/util/dom/dnd.cljs index c6cf9c6a28..03bb612008 100644 --- a/frontend/src/app/util/dom/dnd.cljs +++ b/frontend/src/app/util/dom/dnd.cljs @@ -7,9 +7,8 @@ (ns app.util.dom.dnd "Drag & Drop interop helpers." (:require - [cuerdas.core :as str] - [app.util.data :refer (read-string)] - [app.util.transit :as t])) + [app.common.transit :as t] + [cuerdas.core :as str])) ;; This is the official documentation for the dnd API: ;; https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API @@ -40,7 +39,7 @@ (defn trace ;; This function is useful to debug the dnd interface behaviour when something weird occurs. [event data label] - (let [currentTarget (.-currentTarget event) + (let [;;currentTarget (.-currentTarget event) relatedTarget (.-relatedTarget event)] (js/console.log label @@ -59,7 +58,7 @@ (let [dt (.-dataTransfer e)] (if (or (str/starts-with? data-type "application") (str/starts-with? data-type "penpot")) - (.setData dt data-type (t/encode data)) + (.setData dt data-type (t/encode-str data)) (.setData dt data-type data)) e))) @@ -112,7 +111,7 @@ (let [dt (.-dataTransfer e)] (if (or (str/starts-with? data-type "penpot") (= data-type "application/json")) - (t/decode (.getData dt data-type)) + (t/decode-str (.getData dt data-type)) (.getData dt data-type))))) (defn get-files diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index 9db9ca89ad..d2b5d5ce99 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -10,17 +10,14 @@ [app.common.spec :as us] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [app.util.timers :as tm] - [beicon.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [potok.core :as ptk] [rumext.alpha :as mf])) ;; --- Handlers Helpers (defn- interpret-problem - [acc {:keys [path pred val via in] :as problem}] + [acc {:keys [path pred via] :as problem}] (cond (and (empty? path) (list? pred) @@ -28,8 +25,8 @@ (let [path (conj path (last (last pred)))] (assoc-in acc path {:code ::missing :type :builtin})) - (and (not (empty? path)) - (not (empty? via))) + (and (seq path) + (seq via)) (assoc-in acc path {:code (last via) :type :builtin}) :else acc)) @@ -37,7 +34,7 @@ (declare create-form-mutator) (defn use-form - [& {:keys [spec validators initial] :as opts}] + [& {:keys [initial] :as opts}] (let [state (mf/useState 0) render (aget state 1) state-ref (mf/use-ref {:data (if (fn? initial) (initial) initial) @@ -126,9 +123,8 @@ (defn on-input-blur [form field] - (fn [event] - (let [target (dom/get-target event) - touched (get @form :touched)] + (fn [_] + (let [touched (get @form :touched)] (when-not (get touched field) (swap! form assoc-in [:touched field] true))))) @@ -136,9 +132,8 @@ (mf/defc field-error [{:keys [form field type] - :or {only (constantly true)} :as props}] - (let [{:keys [code message] :as error} (get-in form [:errors field]) + (let [{:keys [message] :as error} (get-in form [:errors field]) touched? (get-in form [:touched field]) show? (and touched? error message (cond diff --git a/frontend/src/app/util/geom/grid.cljs b/frontend/src/app/util/geom/grid.cljs index bd8f58bd19..97496d18f6 100644 --- a/frontend/src/app/util/geom/grid.cljs +++ b/frontend/src/app/util/geom/grid.cljs @@ -6,8 +6,8 @@ (ns app.util.geom.grid (:require - [app.common.math :as mth] - [app.common.geom.point :as gpt])) + [app.common.geom.point :as gpt] + [app.common.math :as mth])) (def ^:private default-items 12) @@ -35,7 +35,7 @@ margin) gutter (if (= :stretch type) (/ (- width (* item-width size) (* margin 2)) (dec size)) gutter) next-x (fn [cur-val] (+ initial-offset x (* (+ item-width gutter) cur-val))) - next-y (fn [cur-val] y)] + next-y (fn [_] y)] [size item-width item-height next-x next-y])) (defn- calculate-row-grid @@ -49,7 +49,7 @@ :center (/ (- height (* item-height size) (* gutter (dec size))) 2) margin) gutter (if (= :stretch type) (/ (- height (* item-height size) (* margin 2)) (dec size)) gutter) - next-x (fn [cur-val] x) + next-x (fn [_] x) next-y (fn [cur-val] (+ initial-offset y (* (+ item-height gutter) cur-val)))] [size item-width item-height next-x next-y])) @@ -91,7 +91,7 @@ ([shape coord] (mapcat #(grid-snap-points shape % coord) (:grids shape))) - ([shape {:keys [type display params] :as grid} coord] + ([shape {:keys [type params] :as grid} coord] (when (:display grid) (case type :square diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index b9525be109..669a291130 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -6,10 +6,8 @@ (ns app.util.geom.snap-points (:require - [cljs.spec.alpha :as s] - [clojure.set :as set] - [app.common.geom.shapes :as gsh] - [app.common.geom.point :as gpt])) + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh])) (defn- selrect-snap-points [{:keys [x y width height]}] #{(gpt/point x y) diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index a5c4920b0c..fe60147a3c 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -8,11 +8,13 @@ "A http client with rx streams interface." (:require [app.common.data :as d] + [app.common.transit :as t] [app.common.uri :as u] [app.config :as cfg] + [app.util.cache :as c] [app.util.globals :as globals] - [app.util.object :as obj] - [app.util.transit :as t] + [app.util.time :as dt] + [app.util.webapi :as wapi] [beicon.core :as rx] [cuerdas.core :as str] [promesa.core :as p])) @@ -52,8 +54,8 @@ {"x-frontend-version" (:full @cfg/version)}) (defn fetch - [{:keys [method uri query headers body timeout mode omit-default-headers] - :or {timeout 10000 mode :cors headers {}}}] + [{:keys [method uri query headers body mode omit-default-headers] + :or {mode :cors headers {}}}] (rx/Observable.create (fn [subscriber] (let [controller (js/AbortController.) @@ -126,16 +128,16 @@ (defn transit-data [data] (reify IBodyData - (-get-body-data [_] (t/encode data)) + (-get-body-data [_] (t/encode-str data)) (-update-headers [_ headers] (assoc headers "content-type" "application/transit+json")))) (defn conditional-decode-transit - [{:keys [body headers status] :as response}] + [{:keys [body headers] :as response}] (let [contentype (get headers "content-type")] (if (and (str/starts-with? contentype "application/transit+json") (pos? (count body))) - (assoc response :body (t/decode body)) + (assoc response :body (t/decode-str body)) response))) (defn success? @@ -152,6 +154,29 @@ (defn as-promise [observable] - (p/create (fn [resolve reject] - (->> (rx/take 1 observable) - (rx/subs resolve reject))))) + (p/create + (fn [resolve reject] + (->> (rx/take 1 observable) + (rx/subs resolve reject))))) + +(defn fetch-data-uri [uri] + (c/with-cache {:key uri :max-age (dt/duration {:hours 4})} + (->> (send! {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + + (rx/filter #(= 200 (:status %))) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(hash-map uri %))))) + +(defn fetch-text [url] + (c/with-cache {:key url :max-age (dt/duration {:hours 4})} + (->> (send! + {:method :get + :mode :cors + :omit-default-headers true + :uri url + :response-type :text}) + (rx/map :body)))) diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index 6bc479f804..1eaf407e32 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -9,10 +9,8 @@ (:require [app.config :as cfg] [app.util.globals :as globals] - [app.util.storage :refer [storage]] [app.util.object :as obj] - [app.util.transit :as t] - [beicon.core :as rx] + [app.util.storage :refer [storage]] [cuerdas.core :as str] [goog.object :as gobj] [okulary.core :as l] @@ -92,7 +90,7 @@ (deftype C [val] IDeref - (-deref [o] val)) + (-deref [_] val)) (defn ^boolean c? [r] diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs new file mode 100644 index 0000000000..8de2abf379 --- /dev/null +++ b/frontend/src/app/util/import/parser.cljs @@ -0,0 +1,725 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.import.parser + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.uuid :as uuid] + [app.util.color :as uc] + [app.util.json :as json] + [app.util.path.parser :as upp] + [cuerdas.core :as str])) + +(def url-regex + #"url\(#([^\)]*)\)") + +(def uuid-regex + #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}") + +(defn valid? + [root] + (contains? (:attrs root) :xmlns:penpot)) + +(defn branch? + [node] + (and (contains? node :content) + (some? (:content node)))) + +(defn close? + [node] + (and (vector? node) + (= ::close (first node)))) + +(defn find-node + [node tag] + (when (some? node) + (->> node :content (d/seek #(= (:tag %) tag))))) + +(defn find-node-by-id + [id coll] + (->> coll (d/seek #(= id (-> % :attrs :id))))) + +(defn find-all-nodes + [node tag] + (when (some? node) + (->> node :content (filterv #(= (:tag %) tag))))) + +(defn get-data + ([node] + (or (find-node node :penpot:shape) + (find-node node :penpot:page))) + + ([node tag] + (-> (get-data node) + (find-node tag)))) + +(defn get-type + [node] + (if (close? node) + (second node) + (let [data (get-data node)] + (-> (get-in data [:attrs :penpot:type]) + (keyword))))) + +(defn shape? + [node] + (or (close? node) + (some? (get-data node)))) + +(defn get-id + [node] + (let [attr-id (get-in node [:attrs :id]) + id (when (string? attr-id) (re-find uuid-regex attr-id))] + (when (some? id) + (uuid/uuid id)))) + +(defn str->bool + [val] + (when (some? val) (= val "true"))) + +(defn get-meta + ([m att] + (get-meta m att identity)) + ([m att val-fn] + (let [ns-att (->> att d/name (str "penpot:") keyword) + val (or (get-in m [:attrs ns-att]) + (get-in (get-data m) [:attrs ns-att]))] + (when val (val-fn val))))) + +(defn get-children + [node] + (cond-> (:content node) + ;; We add a "fake" node to know when we are leaving the shape children + (shape? node) + (conj [::close (get-type node)]))) + +(defn node-seq + [content] + (->> content (tree-seq branch? get-children))) + +(defn parse-style + "Transform style list into a map" + [style-str] + (if (string? style-str) + (->> (str/split style-str ";") + (map str/trim) + (map #(str/split % ":")) + (group-by first) + (map (fn [[key val]] + (vector (keyword key) (second (first val))))) + (into {})) + style-str)) + +(defn add-attrs + [m attrs] + (reduce-kv + (fn [m k v] + (if (#{:style :data-style} k) + (merge m (parse-style v)) + (assoc m k v))) + m + attrs)) + +(defn without-penpot-prefix + [m] + (let [no-penpot-prefix? + (fn [[k _]] + (not (str/starts-with? (d/name k) "penpot:")))] + (into {} (filter no-penpot-prefix?) m))) + +(defn remove-penpot-prefix + [m] + (into {} + (map (fn [[k v]] + (if (str/starts-with? (d/name k) "penpot:") + [(-> k d/name (str/replace "penpot:" "") keyword) v] + [k v]))) + m)) + +(defn camelize [[k v]] + [(-> k d/name str/camel keyword) v]) + +(defn camelize-keys + [m] + (assert (map? m) (str m)) + + (into {} (map camelize) m)) + +(defn fix-style-attr + [m] + (let [fix-style + (fn [[k v]] + (if (= k :style) + [k (-> v parse-style camelize-keys)] + [k v]))] + + (d/deep-mapm (comp camelize fix-style) m))) + +(defn string->uuid + "Looks in a map for keys or values that have uuid shape and converts them + into uuid objects" + [m] + (letfn [(convert [value] + (cond + (and (string? value) (re-matches uuid-regex value)) + (uuid/uuid value) + + (and (keyword? value) (re-matches uuid-regex (d/name value))) + (uuid/uuid (d/name value)) + + (vector? value) + (mapv convert value) + + :else + value))] + (->> m + (d/deep-mapm + (fn [pair] (->> pair (mapv convert))))))) + +(def search-data-node? #{:rect :image :path :text :circle}) + +(defn get-svg-data + [type node] + + (let [node-attrs (add-attrs {} (:attrs node))] + (cond + (search-data-node? type) + (let [data-tags #{:ellipse :rect :path :text :foreignObject :image}] + (->> node + (node-seq) + (filter #(contains? data-tags (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs))) + + (= type :frame) + (let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))] + (merge (add-attrs {} (:attrs svg-node)) node-attrs)) + + (= type :svg-raw) + (let [svg-content (get-data node :penpot:svg-content) + tag (-> svg-content :attrs :penpot:tag keyword) + + svg-node (if (= :svg tag) + (->> node :content last :content last) + (->> node :content last))] + (merge (add-attrs {} (:attrs svg-node)) node-attrs)) + + :else + node-attrs))) + +(def has-position? #{:frame :rect :image :text}) + +(defn parse-position + [props svg-data] + (let [values (->> (select-keys svg-data [:x :y :width :height]) + (d/mapm (fn [_ val] (d/parse-double val))))] + (d/merge props values))) + +(defn parse-circle + [props svg-data] + (let [values (->> (select-keys svg-data [:cx :cy :rx :ry]) + (d/mapm (fn [_ val] (d/parse-double val))))] + (-> props + (assoc :x (- (:cx values) (:rx values)) + :y (- (:cy values) (:ry values)) + :width (* (:rx values) 2) + :height (* (:ry values) 2))))) + +(defn parse-path + [props center svg-data] + (let [content (upp/parse-path (:d svg-data))] + (-> props + (assoc :content content) + (assoc :center center)))) + +(defn parse-stops + [gradient-node] + (->> gradient-node + (node-seq) + (filter #(= :stop (:tag %))) + (mapv (fn [{{:keys [offset stop-color stop-opacity]} :attrs}] + {:color stop-color + :opacity (d/parse-double stop-opacity) + :offset (d/parse-double offset)})))) + +(defn parse-gradient + [node ref-url] + (let [[_ url] (re-find url-regex ref-url) + gradient-node (->> node (node-seq) (find-node-by-id url)) + stops (parse-stops gradient-node)] + + (when (contains? (:attrs gradient-node) :penpot:gradient) + (cond-> {:stops stops} + (= :linearGradient (:tag gradient-node)) + (assoc :type :linear + :start-x (-> gradient-node :attrs :x1 d/parse-double) + :start-y (-> gradient-node :attrs :y1 d/parse-double) + :end-x (-> gradient-node :attrs :x2 d/parse-double) + :end-y (-> gradient-node :attrs :y2 d/parse-double) + :width 1) + + (= :radialGradient (:tag gradient-node)) + (assoc :type :radial + :start-x (get-meta gradient-node :start-x d/parse-double) + :start-y (get-meta gradient-node :start-y d/parse-double) + :end-x (get-meta gradient-node :end-x d/parse-double) + :end-y (get-meta gradient-node :end-y d/parse-double) + :width (get-meta gradient-node :width d/parse-double)))))) + +(defn add-svg-position [props node] + (let [svg-content (get-data node :penpot:svg-content)] + (cond-> props + (contains? (:attrs svg-content) :penpot:x) + (assoc :x (-> svg-content :attrs :penpot:x d/parse-double)) + + (contains? (:attrs svg-content) :penpot:y) + (assoc :y (-> svg-content :attrs :penpot:y d/parse-double)) + + (contains? (:attrs svg-content) :penpot:width) + (assoc :width (-> svg-content :attrs :penpot:width d/parse-double)) + + (contains? (:attrs svg-content) :penpot:height) + (assoc :height (-> svg-content :attrs :penpot:height d/parse-double))))) + +(defn add-common-data + [props node] + + (let [name (get-meta node :name) + blocked (get-meta node :blocked str->bool) + hidden (get-meta node :hidden str->bool) + transform (get-meta node :transform gmt/str->matrix) + transform-inverse (get-meta node :transform-inverse gmt/str->matrix) + flip-x (get-meta node :flip-x str->bool) + flip-y (get-meta node :flip-y str->bool) + proportion (get-meta node :proportion d/parse-double) + proportion-lock (get-meta node :proportion-lock str->bool) + rotation (get-meta node :rotation d/parse-double) + constraints-h (get-meta node :constraints-h keyword) + constraints-v (get-meta node :constraints-v keyword) + fixed-scroll (get-meta node :fixed-scroll str->bool)] + + (-> props + (assoc :name name) + (assoc :blocked blocked) + (assoc :hidden hidden) + (assoc :flip-x flip-x) + (assoc :flip-y flip-y) + (assoc :proportion proportion) + (assoc :proportion-lock proportion-lock) + (assoc :rotation rotation) + + (cond-> (some? transform) + (assoc :transform transform)) + + (cond-> (some? transform-inverse) + (assoc :transform-inverse transform-inverse)) + + (cond-> (some? constraints-h) + (assoc :constraints-h constraints-h)) + + (cond-> (some? constraints-v) + (assoc :constraints-v constraints-v)) + + (cond-> (some? fixed-scroll) + (assoc :fixed-scroll fixed-scroll))))) + +(defn add-position + [props type node svg-data] + (let [center-x (get-meta node :center-x d/parse-double) + center-y (get-meta node :center-y d/parse-double) + center (gpt/point center-x center-y)] + (cond-> props + (has-position? type) + (parse-position svg-data) + + (= type :svg-raw) + (add-svg-position node) + + (= type :circle) + (parse-circle svg-data) + + (= type :path) + (parse-path center svg-data)))) + +(defn add-fill + [props node svg-data] + + (let [fill (:fill svg-data) + gradient (when (str/starts-with? fill "url") + (parse-gradient node fill))] + (cond-> props + :always + (assoc :fill-color nil + :fill-opacity nil) + + (some? gradient) + (assoc :fill-color-gradient gradient + :fill-color nil + :fill-opacity nil) + + (uc/hex? fill) + (assoc :fill-color fill + :fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double))))) + +(defn add-stroke + [props node svg-data] + + (let [stroke-style (get-meta node :stroke-style keyword) + stroke-alignment (get-meta node :stroke-alignment keyword) + stroke (:stroke svg-data) + gradient (when (str/starts-with? stroke "url") + (parse-gradient node stroke))] + + (cond-> props + :always + (assoc :stroke-alignment stroke-alignment + :stroke-style stroke-style + :stroke-color (-> svg-data :stroke) + :stroke-opacity (-> svg-data :stroke-opacity d/parse-double) + :stroke-width (-> svg-data :stroke-width d/parse-double)) + + (some? gradient) + (assoc :stroke-color-gradient gradient + :stroke-color nil + :stroke-opacity nil) + + (= stroke-alignment :inner) + (update :stroke-width / 2)))) + +(defn add-rect-data + [props node svg-data] + (let [r1 (get-meta node :r1 d/parse-double) + r2 (get-meta node :r2 d/parse-double) + r3 (get-meta node :r3 d/parse-double) + r4 (get-meta node :r4 d/parse-double) + + rx (-> (get svg-data :rx) d/parse-double) + ry (-> (get svg-data :ry) d/parse-double)] + + (cond-> props + (some? r1) + (assoc :r1 r1 :r2 r2 :r3 r3 :r4 r4 + :rx nil :ry nil) + + (and (nil? r1) (some? rx)) + (assoc :rx rx :ry ry)))) + +(defn add-image-data + [props type node] + (let [metadata {:id (get-meta node :media-id) + :width (get-meta node :media-width) + :height (get-meta node :media-height) + :mtype (get-meta node :media-mtype)}] + (cond-> props + (= type :image) + (assoc :metadata metadata) + + (not= type :image) + (assoc :fill-image metadata)))) + +(defn add-text-data + [props node] + (-> props + (assoc :grow-type (get-meta node :grow-type keyword)) + (assoc :content (get-meta node :content (comp string->uuid json/decode))))) + +(defn add-group-data + [props node] + (let [mask? (get-meta node :masked-group str->bool)] + (cond-> props + mask? + (assoc :masked-group? true)))) + +(defn parse-shadow [node] + {:id (uuid/next) + :style (get-meta node :shadow-type keyword) + :hidden (get-meta node :hidden str->bool) + :color {:color (get-meta node :color) + :opacity (get-meta node :opacity d/parse-double)} + :offset-x (get-meta node :offset-x d/parse-double) + :offset-y (get-meta node :offset-y d/parse-double) + :blur (get-meta node :blur d/parse-double) + :spread (get-meta node :spread d/parse-double)}) + +(defn parse-blur [node] + {:id (uuid/next) + :type (get-meta node :blur-type keyword) + :hidden (get-meta node :hidden str->bool) + :value (get-meta node :value d/parse-double)}) + +(defn parse-export [node] + {:type (get-meta node :type keyword) + :suffix (get-meta node :suffix) + :scale (get-meta node :scale d/parse-double)}) + + +(defn parse-grid-node [node] + (let [attrs (-> node :attrs remove-penpot-prefix) + color {:color (:color attrs) + :opacity (-> attrs :opacity d/parse-double)} + + params (-> (d/without-keys attrs [:color :opacity :display :type]) + (d/update-when :size d/parse-double) + (d/update-when :item-length d/parse-double) + (d/update-when :gutter d/parse-double) + (d/update-when :margin d/parse-double) + (assoc :color color))] + {:type (-> attrs :type keyword) + :display (-> attrs :display str->bool) + :params params})) + +(defn parse-grids [node] + (let [grid-node (get-data node :penpot:grids)] + (->> grid-node :content (mapv parse-grid-node)))) + +(defn extract-from-data + ([node tag] + (extract-from-data node tag identity)) + + ([node tag parse-fn] + (let [shape-data (get-data node)] + (->> shape-data + (node-seq) + (filter #(= (:tag %) tag)) + (mapv parse-fn))))) + +(defn add-shadows + [props node] + (let [shadows (extract-from-data node :penpot:shadow parse-shadow)] + (cond-> props + (d/not-empty? shadows) + (assoc :shadow shadows)))) + +(defn add-blur + [props node] + (let [blur (->> (extract-from-data node :penpot:blur parse-blur) (first))] + (cond-> props + (some? blur) + (assoc :blur blur)))) + +(defn add-exports + [props node] + (let [exports (extract-from-data node :penpot:export parse-export)] + (cond-> props + (d/not-empty? exports) + (assoc :exports exports)))) + +(defn add-layer-options + [props svg-data] + (let [blend-mode (get svg-data :mix-blend-mode) + opacity (-> (get svg-data :opacity) d/parse-double)] + + (cond-> props + (some? blend-mode) + (assoc :blend-mode (keyword blend-mode)) + + (some? opacity) + (assoc :opacity opacity)))) + +(defn remove-prefix [s] + (cond-> s + (string? s) + (str/replace (re-pattern (str uuid-regex "-")) ""))) + +(defn get-svg-attrs + [svg-data svg-attrs] + (let [assoc-key + (fn [acc prop] + (let [key (keyword prop)] + (if-let [v (or (get svg-data key) + (get-in svg-data [:attrs key]))] + (assoc acc key (remove-prefix v)) + acc)))] + + (->> (str/split svg-attrs ",") + (reduce assoc-key {})))) + +(defn get-svg-defs + [node] + + (let [svg-import (get-data node :penpot:svg-import)] + (->> svg-import + :content + (filter #(= (:tag %) :penpot:svg-def)) + (map #(vector (-> % :attrs :def-id) + (-> % :content first))) + (into {})))) + +(defn add-svg-attrs + [props node svg-data] + + (let [svg-import (get-data node :penpot:svg-import)] + (if (some? svg-import) + (let [svg-attrs (get-in svg-import [:attrs :penpot:svg-attrs]) + svg-defs (get-in svg-import [:attrs :penpot:svg-defs]) + svg-transform (get-in svg-import [:attrs :penpot:svg-transform]) + viewbox-x (get-in svg-import [:attrs :penpot:svg-viewbox-x]) + viewbox-y (get-in svg-import [:attrs :penpot:svg-viewbox-y]) + viewbox-width (get-in svg-import [:attrs :penpot:svg-viewbox-width]) + viewbox-height (get-in svg-import [:attrs :penpot:svg-viewbox-height])] + + (cond-> props + :true + (assoc :svg-attrs (get-svg-attrs svg-data svg-attrs)) + + (some? viewbox-x) + (assoc :svg-viewbox {:x (d/parse-double viewbox-x) + :y (d/parse-double viewbox-y) + :width (d/parse-double viewbox-width) + :height (d/parse-double viewbox-height)}) + + (some? svg-transform) + (assoc :svg-transform (gmt/str->matrix svg-transform)) + + + (some? svg-defs) + (assoc :svg-defs (get-svg-defs node)))) + + props))) + +(defn add-svg-content + [props node] + (let [svg-content (get-data node :penpot:svg-content) + attrs (-> (:attrs svg-content) (without-penpot-prefix)) + tag (-> svg-content :attrs :penpot:tag keyword) + + node-content + (cond + (= tag :svg) + (->> node :content last :content last :content fix-style-attr) + + (= tag :text) + (-> node :content last :content))] + (assoc + props :content + {:attrs attrs + :tag tag + :content node-content}))) + +(defn add-frame-data [props node] + (let [grids (parse-grids node)] + (cond-> props + (d/not-empty? grids) + (assoc :grids grids)))) + +(defn has-image? + [node] + (let [type (get-type node) + pattern-image + (-> node + (find-node :defs) + (find-node :pattern) + (find-node :image))] + (or (= type :image) + (some? pattern-image)))) + +(defn get-image-name + [node] + (get-in node [:attrs :penpot:name])) + +(defn get-image-data + [node] + (let [pattern-data + (-> node + (find-node :defs) + (find-node :pattern) + (find-node :image) + :attrs) + image-data (get-svg-data :image node) + svg-data (or image-data pattern-data)] + (:xlink:href svg-data))) + +(defn add-library-refs + [props node] + + (let [fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid) + fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid) + stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid) + stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid) + component-id (get-meta node :component-id uuid/uuid) + component-file (get-meta node :component-file uuid/uuid) + shape-ref (get-meta node :shape-ref uuid/uuid) + component-root? (get-meta node :component-root str->bool)] + + (cond-> props + (some? fill-color-ref-id) + (assoc :fill-color-ref-id fill-color-ref-id + :fill-color-ref-file fill-color-ref-file) + + (some? stroke-color-ref-id) + (assoc :stroke-color-ref-id stroke-color-ref-id + :stroke-color-ref-file stroke-color-ref-file) + + (some? component-id) + (assoc :component-id component-id + :component-file component-file) + + component-root? + (assoc :component-root? component-root?) + + (some? shape-ref) + (assoc :shape-ref shape-ref)))) + +(defn parse-data + [type node] + + (when-not (close? node) + (let [svg-data (get-svg-data type node)] + (-> {} + (add-common-data node) + (add-position type node svg-data) + (add-fill node svg-data) + (add-stroke node svg-data) + (add-layer-options svg-data) + (add-shadows node) + (add-blur node) + (add-exports node) + (add-svg-attrs node svg-data) + (add-library-refs node) + + (cond-> (= :svg-raw type) + (add-svg-content node)) + + (cond-> (= :frame type) + (add-frame-data node)) + + (cond-> (= :group type) + (add-group-data node)) + + (cond-> (= :rect type) + (add-rect-data node svg-data)) + + (cond-> (some? (get-in node [:attrs :penpot:media-id])) + (add-image-data type node)) + + (cond-> (= :text type) + (add-text-data node)))))) + +(defn parse-page-data + [node] + (let [style (parse-style (get-in node [:attrs :style])) + background (:background style) + grids (->> (parse-grids node) + (group-by :type) + (d/mapm (fn [_ v] (-> v first :params))))] + (cond-> {} + (some? background) + (assoc-in [:options :background] background) + + (d/not-empty? grids) + (assoc-in [:options :saved-grids] grids)))) + +(defn parse-interactions + [node] + (let [interactions-node (get-data node :penpot:interactions)] + (->> (find-all-nodes interactions-node :penpot:interaction) + (mapv (fn [node] + {:destination (get-meta node :destination uuid/uuid) + :action-type (get-meta node :action-type keyword) + :event-type (get-meta node :event-type keyword)}))))) + diff --git a/frontend/src/app/util/json.cljs b/frontend/src/app/util/json.cljs new file mode 100644 index 0000000000..02ff7d58db --- /dev/null +++ b/frontend/src/app/util/json.cljs @@ -0,0 +1,19 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.json) + +(defn decode + [data] + (-> data + (js/JSON.parse) + (js->clj :keywordize-keys true))) + +(defn encode + [data] + (-> data + (clj->js) + (js/JSON.stringify))) diff --git a/frontend/src/app/util/logging.clj b/frontend/src/app/util/logging.clj index 421a9d16e4..f888f1bc8a 100644 --- a/frontend/src/app/util/logging.clj +++ b/frontend/src/app/util/logging.clj @@ -6,9 +6,10 @@ (ns app.util.logging) -(defn- log-expr [form level keyvals] +(defn- log-expr [_form level keyvals] (let [keyvals-map (apply array-map keyvals) - formatter (::formatter keyvals-map 'identity)] + ;;formatter (::formatter keyvals-map 'identity) + ] `(log ~(::logger keyvals-map (str *ns*)) ~level ~(-> keyvals-map diff --git a/frontend/src/app/util/logging.cljs b/frontend/src/app/util/logging.cljs index 559c911b00..b2bfaaff99 100644 --- a/frontend/src/app/util/logging.cljs +++ b/frontend/src/app/util/logging.cljs @@ -11,11 +11,9 @@ (ns app.util.logging (:require - [goog.log :as glog] - [goog.debug.Console :as Console] + [app.common.exceptions :as ex] [cuerdas.core :as str] - [goog.object :as gobj]) - (:import [goog.debug Console]) + [goog.log :as glog]) (:require-macros [app.util.logging])) (defn- logger-name @@ -158,7 +156,7 @@ specials)))))) (defn default-handler - [{:keys [message exception level logger-name]}] + [{:keys [message level logger-name]}] (let [header-styles (str "font-weight: 600; color: " (level->color level)) normal-styles (str "font-weight: 300; color: " (get colors :gray6)) level-name (level->short-name level) @@ -174,7 +172,7 @@ (doseq [[type n v] specials] (case type :js (js/console.log n v) - :error (if (instance? cljs.core.ExceptionInfo v) + :error (if (ex/ex-info? v) (js/console.error (pr-str v)) (js/console.error v)))) (js/console.groupEnd message)) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index ad6697a428..0abdd9c90d 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -6,11 +6,10 @@ (ns app.util.object "A collection of helpers for work with javascript objects." - (:refer-clojure :exclude [set! get get-in merge clone]) + (:refer-clojure :exclude [set! get get-in merge clone contains?]) (:require - [cuerdas.core :as str] - [goog.object :as gobj] - ["lodash/omit" :as omit])) + ["lodash/omit" :as omit] + [cuerdas.core :as str])) (defn new [] #js {}) @@ -22,22 +21,27 @@ (let [result (get obj k)] (if (undefined? result) default result)))) +(defn contains? + [obj k] + (some? (unchecked-get obj k))) + (defn get-keys [obj] (js/Object.keys ^js obj)) (defn get-in - [obj keys] - (loop [key (first keys) - keys (rest keys) - res obj] - (if (nil? key) - res - (if (nil? res) - res - (recur (first keys) - (rest keys) - (unchecked-get res key)))))) + ([obj keys] + (get-in obj keys nil)) + + ([obj keys default] + (loop [key (first keys) + keys (rest keys) + res obj] + (if (or (nil? key) (nil? res)) + (or res default) + (recur (first keys) + (rest keys) + (unchecked-get res key)))))) (defn without [obj keys] @@ -68,6 +72,14 @@ (unchecked-set obj key value) obj) +(defn update! + [obj key f & args] + (let [found (get obj key ::not-found)] + (if-not (identical? ::not-found found) + (do (unchecked-set obj key (apply f found args)) + obj) + obj))) + (defn- props-key-fn [key] (if (or (= key :class) (= key :class-name)) diff --git a/frontend/src/app/util/path/commands.cljs b/frontend/src/app/util/path/commands.cljs index f284a457c5..84a7725ef8 100644 --- a/frontend/src/app/util/path/commands.cljs +++ b/frontend/src/app/util/path/commands.cljs @@ -7,12 +7,7 @@ (ns app.util.path.commands (:require [app.common.data :as d] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] - [app.util.svg :as usvg] - [cuerdas.core :as str] - [clojure.set :as set] - [app.common.math :as mth])) + [app.common.geom.point :as gpt])) (defn command->point ([prev-pos {:keys [relative params] :as command}] @@ -179,7 +174,7 @@ "Returns the commands involving a point with its indices" [content point] (->> (d/enumerate content) - (filterv (fn [[idx cmd]] (= (command->point cmd) point))))) + (filterv (fn [[_ cmd]] (= (command->point cmd) point))))) (defn prefix->coords [prefix] @@ -192,7 +187,7 @@ (when (and (some? index) (some? prefix) (contains? content index)) - (let [[cx cy :as coords] (prefix->coords prefix)] + (let [[cx cy] (prefix->coords prefix)] (if (= :curve-to (get-in content [index :command])) (gpt/point (get-in content [index :params cx]) (get-in content [index :params cy])) diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index a79a5a7e50..4b0640f4eb 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -6,10 +6,8 @@ (ns app.util.path.format (:require - [app.common.data :as d] [app.util.path.commands :as upc] - [cuerdas.core :as str] - [app.util.path.subpaths :as ups])) + [cuerdas.core :as str])) (defn command->param-list [command] (let [params (:params command)] @@ -47,7 +45,7 @@ (:x params) "," (:y params))))) -(defn command->string [{:keys [command relative params] :as entry}] +(defn command->string [{:keys [command relative] :as entry}] (let [command-str (case command :move-to "M" :close-path "Z" diff --git a/frontend/src/app/util/path/geom.cljs b/frontend/src/app/util/path/geom.cljs index af99972ee2..0478fff8c7 100644 --- a/frontend/src/app/util/path/geom.cljs +++ b/frontend/src/app/util/path/geom.cljs @@ -6,13 +6,8 @@ (ns app.util.path.geom (:require - [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes.path :as gshp] - [app.util.svg :as usvg] - [cuerdas.core :as str] - [clojure.set :as set] - [app.common.math :as mth] [app.util.path.commands :as upc])) (defn calculate-opposite-handler diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs index 09f491555a..7b68caf646 100644 --- a/frontend/src/app/util/path/parser.cljs +++ b/frontend/src/app/util/path/parser.cljs @@ -8,15 +8,11 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] [app.util.path.arc-to-curve :refer [a2c]] [app.util.path.commands :as upc] - [app.util.svg :as usvg] - [cuerdas.core :as str] - [clojure.set :as set] - [app.common.math :as mth] [app.util.path.geom :as upg] - )) + [app.util.svg :as usvg] + [cuerdas.core :as str])) ;; (def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*") @@ -53,7 +49,7 @@ current remain)) (cond-> result - (not (empty? current)) (conj current)))))) + (seq current) (conj current)))))) ;; Path specification ;; https://www.w3.org/TR/SVG11/paths.html @@ -73,7 +69,7 @@ :relative relative :params params})))) -(defmethod parse-command "Z" [cmd] +(defmethod parse-command "Z" [_] [{:command :close-path}]) (defmethod parse-command "L" [cmd] @@ -205,7 +201,7 @@ ;; prev-start : previous move-to necesary for Z commands ;; prev-cc : previous command control point for cubic beziers ;; prev-qc : previous command control point for quadratic curves - (fn [[result prev-pos prev-start prev-cc prev-qc] [command prev]] + (fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]] (let [command (assoc command :prev-pos prev-pos) command diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/frontend/src/app/util/path/shapes_to_path.cljs index a1d42ba47d..0fb979bb16 100644 --- a/frontend/src/app/util/path/shapes_to_path.cljs +++ b/frontend/src/app/util/path/shapes_to_path.cljs @@ -40,14 +40,14 @@ height (* radius 2) c bezier-circle-c - c1x (+ x (* (/ width 2) (- 1 c))) + c1x (+ x (* (/ width 2) (- 1 c))) c2x (+ x (* (/ width 2) (+ 1 c))) c1y (+ y (* (/ height 2) (- 1 c))) c2y (+ y (* (/ height 2) (+ 1 c))) h1 (case corner :top-left (assoc from :y c1y) - :top-right (assoc from :x c2x) + :top-right (assoc from :x c2x) :bottom-right (assoc from :y c2y) :bottom-left (assoc from :x c1x)) @@ -67,14 +67,13 @@ ex (+ x width) ey (+ y height) - pc (gpt/point mx my) p1 (gpt/point mx y) p2 (gpt/point ex my) p3 (gpt/point mx ey) p4 (gpt/point x my) c bezier-circle-c - c1x (+ x (* (/ width 2) (- 1 c))) + c1x (+ x (* (/ width 2) (- 1 c))) c2x (+ x (* (/ width 2) (+ 1 c))) c1y (+ y (* (/ height 2) (- 1 c))) c2y (+ y (* (/ height 2) (+ 1 c)))] @@ -90,7 +89,7 @@ [x y width height r1 r2 r3 r4] (let [p1 (gpt/point x (+ y r1)) p2 (gpt/point (+ x r1) y) - + p3 (gpt/point (+ width x (- r2)) y) p4 (gpt/point (+ width x) (+ y r2)) diff --git a/frontend/src/app/util/path/simplify_curve.cljs b/frontend/src/app/util/path/simplify_curve.cljs index c3a400a118..900d66e784 100644 --- a/frontend/src/app/util/path/simplify_curve.cljs +++ b/frontend/src/app/util/path/simplify_curve.cljs @@ -6,14 +6,7 @@ (ns app.util.path.simplify-curve (:require - [app.common.data :as d] - [app.common.geom.point :as gpt] - [app.common.geom.shapes.path :as gshp] - [app.util.path.path-impl-simplify :as impl-simplify] - [app.util.svg :as usvg] - [cuerdas.core :as str] - [clojure.set :as set] - [app.common.math :as mth])) + [app.util.path.path-impl-simplify :as impl-simplify])) (defn simplify "Simplifies a drawing done with the pen tool" diff --git a/frontend/src/app/util/path/tools.cljs b/frontend/src/app/util/path/tools.cljs index 870ddab857..f6409f2216 100644 --- a/frontend/src/app/util/path/tools.cljs +++ b/frontend/src/app/util/path/tools.cljs @@ -80,13 +80,7 @@ in the same vector that results from te previous->next points but with fixed length." [content point] - (let [make-curve-cmd (fn [cmd h1 h2] - (-> cmd - (update :params assoc - :c1x (:x h1) :c1y (:y h1) - :c2x (:x h2) :c2y (:y h2)))) - - indices (upc/point-indices content point) + (let [indices (upc/point-indices content point) vectors (->> indices (mapv (fn [index] (let [cmd (nth content index) prev-i (dec index) @@ -151,10 +145,10 @@ (let [add-curve (fn [content {:keys [index command prev-p next-c next-i]}] (cond-> content - (and (= :line-to (:command command))) + (= :line-to (:command command)) (update index #(line->curve prev-p %)) - (and (= :line-to (:command next-c))) + (= :line-to (:command next-c)) (update next-i #(line->curve point %))))] (->> vectors (reduce add-curve content)))))) @@ -292,7 +286,7 @@ ;; If have a curve the first handler will be relative to the previous ;; point. We change the handler to the new previous point - (and curve? (not (empty? subpath)) (not= old-prev-point new-prev-point)) + (and curve? (seq subpath) (not= old-prev-point new-prev-point)) (update :params merge last-handler)) head-idx (dec (count result)) @@ -394,7 +388,7 @@ result (cond-> result (and (nil? set-a) (nil? set-b)) (conj #{point-a point-b}) - + (and (some? set-a) (nil? set-b)) (add-to-set set-a point-b) diff --git a/frontend/src/app/util/perf.cljs b/frontend/src/app/util/perf.cljs index 03935f3016..e23401201b 100644 --- a/frontend/src/app/util/perf.cljs +++ b/frontend/src/app/util/perf.cljs @@ -39,7 +39,7 @@ this))) (defn tdigest-summary - [td] + [^js td] (str "samples=" (unchecked-get td "n") "\n" "Q50=" (.percentile td 0.50) "\n" "Q75=" (.percentile td 0.75) "\n" diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs index d7945580d8..dd3078c75e 100644 --- a/frontend/src/app/util/router.cljs +++ b/frontend/src/app/util/router.cljs @@ -7,13 +7,11 @@ (ns app.util.router (:refer-clojure :exclude [resolve]) (:require - [app.common.data :as d] - [app.config :as cfg] [app.common.uri :as u] + [app.config :as cfg] [app.util.browser-history :as bhistory] [app.util.timers :as ts] [beicon.core :as rx] - [cuerdas.core :as str] [goog.events :as e] [potok.core :as ptk] [reitit.core :as r])) @@ -83,7 +81,7 @@ (dissoc state :exception)) ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (ts/asap #(let [router (:router state) history (:history state) @@ -106,7 +104,7 @@ (deftype NavigateNewWindow [id params qparams] ptk/EffectEvent - (effect [_ state stream] + (effect [_ state _] (let [router (:router state) path (resolve router id params qparams) uri (-> (u/uri cfg/public-uri) diff --git a/frontend/src/app/util/simple_math.cljs b/frontend/src/app/util/simple_math.cljs index c92e8d6de2..34d476d455 100644 --- a/frontend/src/app/util/simple_math.cljs +++ b/frontend/src/app/util/simple_math.cljs @@ -6,15 +6,15 @@ (ns app.util.simple-math (:require - [cljs.spec.alpha :as s] - [clojure.string :refer [index-of]] - [cuerdas.core :as str] - [instaparse.core :as insta] - [app.common.data :as d] - [app.common.exceptions :as ex])) + [app.common.data :as d] + [app.common.exceptions :as ex] + [cljs.spec.alpha :as s] + [clojure.string :refer [index-of]] + [cuerdas.core :as str] + [instaparse.core :as insta])) (def parser - (insta/parser + (insta/parser "opt-expr = '' | expr expr = term ( ('+'|'-') expr)* | ('+'|'-'|'*'|'/') factor diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index aecd1f03a4..d7e150c61a 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -6,20 +6,9 @@ (ns app.util.storage (:require - [app.util.transit :as t] - [app.util.timers :as tm] + [app.common.transit :as t] [app.util.globals :as g] - [app.common.exceptions :as ex])) - -(defn- ^boolean is-worker? - [] - (or (= *target* "nodejs") - (not (exists? js/window)))) - -(defn- decode - [v] - (ex/ignoring (t/decode v))) - + [app.util.timers :as tm])) (defn- persist [storage prev curr] @@ -29,8 +18,8 @@ (when (not= curr* prev*) (tm/schedule-on-idle #(if (some? curr*) - (.setItem ^js storage (t/encode key) (t/encode curr*)) - (.removeItem ^js storage (t/encode key))))))) + (.setItem ^js storage (t/encode-str key) (t/encode-str curr*)) + (.removeItem ^js storage (t/encode-str key))))))) (into #{} (concat (keys curr) (keys prev))))) @@ -43,8 +32,8 @@ (let [key (.key ^js storage index) val (.getItem ^js storage key)] (try - (assoc res (t/decode key) (t/decode val)) - (catch :default e + (assoc res (t/decode-str key) (t/decode-str val)) + (catch :default _e res)))) {} (range len))))) diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs index b4d3a352a3..932f900295 100644 --- a/frontend/src/app/util/svg.cljs +++ b/frontend/src/app/util/svg.cljs @@ -6,12 +6,12 @@ (ns app.util.svg (:require - [app.common.uuid :as uuid] [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.uuid :as uuid] [cuerdas.core :as str])) ;; Regex for XML ids per Spec @@ -540,11 +540,6 @@ (str/camel) (keyword)))) - (lowercase-key [key] - (-> (d/name key) - (str/lower) - (keyword))) - (format-styles [style-str] (->> (str/split style-str ";") (map str/trim) @@ -594,7 +589,7 @@ (defn replace-attrs-ids "Replaces the ids inside a property" [attrs ids-mapping] - (if (and ids-mapping (not (empty? ids-mapping))) + (if (and ids-mapping (seq ids-mapping)) (update-attr-ids attrs (fn [id] (get ids-mapping id id))) ;; Ids-mapping is null attrs)) @@ -607,7 +602,7 @@ (reduce visit-node result (:content node))))] (visit-node {} content))) -(defn extract-defs [{:keys [tag attrs content] :as node}] +(defn extract-defs [{:keys [attrs] :as node}] (if-not (map? node) [{} node] @@ -647,7 +642,7 @@ (cond (nil? to-check) result - + (checked? to-check) (recur result checked? @@ -673,7 +668,7 @@ scale-x (/ width svg-width) scale-y (/ height svg-height)] - + (gmt/multiply (gmt/matrix) @@ -736,7 +731,7 @@ (let [process-matrix (fn [[_ type params]] (let [params (->> (re-seq number-regex params) - (filter #(-> % first empty? not)) + (filter #(-> % first seq)) (map (comp d/parse-double first)))] {:type type :params params})) @@ -746,8 +741,6 @@ (reduce gmt/multiply (gmt/matrix) matrices)) (gmt/matrix))) - - (defn format-move [[x y]] (str "M" x " " y)) (defn format-line [[x y]] (str "L" x " " y)) @@ -764,7 +757,7 @@ (str (format-move head) (->> other (map format-line) (str/join " "))))) -(defn polyline->path [{:keys [attrs tag] :as node}] +(defn polyline->path [{:keys [attrs] :as node}] (let [tag :path attrs (-> attrs (dissoc :points) @@ -772,14 +765,14 @@ (assoc node :attrs attrs :tag tag))) -(defn polygon->path [{:keys [attrs tag] :as node}] +(defn polygon->path [{:keys [attrs] :as node}] (let [tag :path attrs (-> attrs (dissoc :points) (assoc :d (str (points->path (:points attrs)) "Z")))] (assoc node :attrs attrs :tag tag))) -(defn line->path [{:keys [attrs tag] :as node}] +(defn line->path [{:keys [attrs] :as node}] (let [tag :path {:keys [x1 y1 x2 y2]} attrs attrs (-> attrs @@ -871,7 +864,7 @@ :ratio (calculate-ratio (:width svg-data) (:height svg-data))}] (letfn [(fix-length [prop-length val] (* (get viewbox prop-length) (/ val 100.))) - + (fix-coord [prop-coord prop-length val] (+ (get viewbox prop-coord) (fix-length prop-length val))) @@ -899,7 +892,7 @@ (fix-percent-attrs-viewbox [attrs] (d/mapm fix-percent-attr-viewbox attrs)) - (fix-percent-attr-numeric [attr-key attr-val] + (fix-percent-attr-numeric [_ attr-val] (let [is-percent? (str/ends-with? attr-val "%")] (if is-percent? (str (let [attr-num (d/parse-double attr-val)] diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index 869e929c12..370f17f2de 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -9,15 +9,7 @@ (:require ["./text_editor_impl.js" :as impl] ["draft-js" :as draft] - [app.common.attrs :as attrs] - [app.common.data :as d] - [app.common.text :as txt] - [app.common.uuid :as uuid] - [app.util.array :as arr] - [app.util.object :as obj] - [app.util.transit :as t] - [clojure.walk :as walk] - [cuerdas.core :as str])) + [app.common.text :as txt])) ;; --- CONVERSION diff --git a/frontend/src/app/util/theme.cljs b/frontend/src/app/util/theme.cljs index deed7644ac..1030c5eea0 100644 --- a/frontend/src/app/util/theme.cljs +++ b/frontend/src/app/util/theme.cljs @@ -8,14 +8,11 @@ (ns app.util.theme "A theme manager." (:require - [cuerdas.core :as str] - [rumext.alpha :as mf] - [beicon.core :as rx] - [goog.object :as gobj] [app.config :as cfg] [app.util.dom :as dom] - [app.util.transit :as t] - [app.util.storage :refer [storage]])) + [app.util.storage :refer [storage]] + [beicon.core :as rx] + [rumext.alpha :as mf])) (defonce theme (get @storage ::theme cfg/default-theme)) (defonce theme-sub (rx/subject)) diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 317ff866e7..a32a0e1a4e 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -6,21 +6,21 @@ (ns app.util.time (:require - [cuerdas.core :as str] - ["luxon" :as lxn] ["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict] - ["date-fns/locale/el" :default dateFnsLocalesEl] - ["date-fns/locale/fr" :default dateFnsLocalesFr] ["date-fns/locale/ca" :default dateFnsLocalesCa] ["date-fns/locale/de" :default dateFnsLocalesDe] - ["date-fns/locale/ro" :default dateFnsLocalesRo] - ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr] + ["date-fns/locale/el" :default dateFnsLocalesEl] ["date-fns/locale/en-US" :default dateFnsLocalesEnUs] - ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] ["date-fns/locale/es" :default dateFnsLocalesEs] - ["date-fns/locale/tr" :default dateFnsLocalesTr] + ["date-fns/locale/fr" :default dateFnsLocalesFr] + ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr] + ["date-fns/locale/ro" :default dateFnsLocalesRo] ["date-fns/locale/ru" :default dateFnsLocalesRu] - [app.util.object :as obj])) + ["date-fns/locale/tr" :default dateFnsLocalesTr] + ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] + ["luxon" :as lxn] + [app.util.object :as obj] + [cuerdas.core :as str])) (def DateTime lxn/DateTime) (def Duration lxn/Duration) diff --git a/frontend/src/app/util/transit.cljs b/frontend/src/app/util/transit.cljs deleted file mode 100644 index 24d3851769..0000000000 --- a/frontend/src/app/util/transit.cljs +++ /dev/null @@ -1,127 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.util.transit - "A lightweight abstraction for transit serialization." - (:require - [cognitect.transit :as t] - [linked.core :as lk] - [linked.set :as lks] - [app.common.data :as d] - [app.common.geom.point :as gpt] - [app.common.geom.matrix :as gmt] - [app.util.time :as dt])) - -(deftype Blob [content] - IDeref - (-deref [_] content)) - -(defn blob? - [v] - (instance? Blob v)) - -(def blob-write-handler - (t/write-handler - (constantly "jsonblob") - (fn [v] (js/JSON.stringify @v)))) - -(def blob-read-handler - (t/read-handler - (fn [value] - (->Blob (js/JSON.parse value))))) - -;; --- Transit adapters - -(def bigint-read-handler - (t/read-handler - (fn [value] - (js/parseInt value 10)))) - -(def point-write-handler - (t/write-handler - (constantly "point") - (fn [v] (into {} v)))) - -(def point-read-handler - (t/read-handler - (fn [value] - (gpt/map->Point value)))) - -(def matrix-write-handler - (t/write-handler - (constantly "matrix") - (fn [v] (into {} v)))) - -(def matrix-read-handler - (t/read-handler - (fn [value] - (gmt/map->Matrix value)))) - -(def ordered-set-write-handler - (t/write-handler - (constantly "ordered-set") - (fn [v] (vec v)))) - -(def ordered-set-read-handler - (t/read-handler #(into (lk/set) %))) - -(def date-read-handler - (t/read-handler (fn [value] (-> value (js/parseInt 10) (dt/datetime))))) - -(def duration-read-handler - (t/read-handler (fn [value] (dt/duration value)))) - -(def date-write-handler - (t/write-handler - (constantly "m") - (fn [v] (str (inst-ms v))))) - -(def duration-write-handler - (t/write-handler - (constantly "duration") - (fn [v] (inst-ms v)))) - -;; --- Transit Handlers - -(def ^:privare +read-handlers+ - {"u" uuid - "n" bigint-read-handler - "ordered-set" ordered-set-read-handler - "jsonblob" blob-read-handler - "matrix" matrix-read-handler - "m" date-read-handler - "duration" duration-read-handler - "point" point-read-handler}) - -(def ^:privare +write-handlers+ - {gmt/Matrix matrix-write-handler - Blob blob-write-handler - dt/DateTime date-write-handler - dt/Duration duration-write-handler - lks/LinkedSet ordered-set-write-handler - gpt/Point point-write-handler}) - -;; --- Public Api - -(defn decode - [data] - (let [r (t/reader :json {:handlers +read-handlers+})] - (t/read r data))) - -(defn encode - [data] - (try - (let [w (t/writer :json-verbose {:handlers +write-handlers+})] - (t/write w data)) - (catch :default e - (throw e)))) - -(defn transit? - "Checks if a string can be decoded with transit" - [str] - (try - (-> str decode nil? not) - (catch js/SyntaxError e false))) diff --git a/frontend/src/app/util/uri.cljs b/frontend/src/app/util/uri.cljs index fe01825439..3f6d6c1bc7 100644 --- a/frontend/src/app/util/uri.cljs +++ b/frontend/src/app/util/uri.cljs @@ -6,8 +6,8 @@ (ns app.util.uri (:require - [cuerdas.core :as str] - [app.util.object :as obj])) + [app.util.object :as obj] + [cuerdas.core :as str])) (defn uri-name [url] (let [query-idx (str/last-index-of url "?") diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 0da4d753a4..e07949567e 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -10,10 +10,8 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.util.object :as obj] - [app.util.transit :as t] [beicon.core :as rx] - [cuerdas.core :as str] - [promesa.core :as p])) + [cuerdas.core :as str])) (defn- file-reader [f] @@ -137,7 +135,7 @@ (rx/create (fn [subs] (let [obs (js/ResizeObserver. - (fn [entries x] + (fn [entries _] (rx/push! subs entries)))] (.observe ^js obs node) (fn [] diff --git a/frontend/src/app/util/websockets.cljs b/frontend/src/app/util/websockets.cljs index 61e06baba3..8bac581684 100644 --- a/frontend/src/app/util/websockets.cljs +++ b/frontend/src/app/util/websockets.cljs @@ -7,12 +7,9 @@ (ns app.util.websockets "A interface to webworkers exposed functionality." (:require - [app.common.uri :as u] - [app.config :as cfg] - [app.util.transit :as t] + [app.common.transit :as t] [beicon.core :as rx] - [goog.events :as ev] - [potok.core :as ptk]) + [goog.events :as ev]) (:import goog.net.WebSocket goog.net.WebSocket.EventType)) @@ -55,4 +52,4 @@ (defn send! [ws msg] - (-send ws (t/encode msg))) + (-send ws (t/encode-str msg))) diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index 4c07a7f083..810d32e294 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -7,21 +7,32 @@ (ns app.util.worker "A lightweight layer on top of webworkers api." (:require - [beicon.core :as rx] + [app.common.transit :as t] [app.common.uuid :as uuid] - [app.util.transit :as t])) + [beicon.core :as rx])) (declare handle-response) (defrecord Worker [instance stream]) -(defn- send-message! [worker {sender-id :sender-id :as message}] - (let [data (t/encode message) - instance (:instance worker)] - (.postMessage instance data) - (->> (:stream worker) - (rx/filter #(= (:reply-to %) sender-id)) - (rx/take 1) - (rx/map handle-response)))) +(defn- send-message! + ([worker message] + (send-message! worker message nil)) + + ([worker {sender-id :sender-id :as message} {:keys [many?] :or {many? false}}] + (let [take-messages + (fn [ob] + (if many? + (rx/take-while #(not (:completed %)) ob) + (rx/take 1 ob))) + + data (t/encode-str message) + instance (:instance worker)] + + (.postMessage instance data) + (->> (:stream worker) + (rx/filter #(= (:reply-to %) sender-id)) + (take-messages) + (rx/map handle-response))))) (defn ask! [worker message] @@ -30,6 +41,14 @@ {:sender-id (uuid/next) :payload message})) +(defn ask-many! + [worker message] + (send-message! + worker + {:sender-id (uuid/next) + :payload message} + {:many? true})) + (defn ask-buffered! [worker message] (send-message! @@ -48,7 +67,7 @@ handle-message (fn [event] (let [data (.-data event) - data (t/decode data)] + data (t/decode-str data)] (if (:error data) (on-error (:error data)) (rx/push! bus data)))) @@ -56,14 +75,14 @@ handle-error (fn [error] (on-error worker (.-data error)))] - + (.addEventListener instance "message" handle-message) (.addEventListener instance "error" handle-error) worker)) (defn- handle-response - [{:keys [payload error dropped] :as response}] + [{:keys [payload error dropped]}] (when-not dropped (if-let [{:keys [data message]} error] (throw (ex-info message data)) diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index 6cc7a6fd82..a3b6dc3f38 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -6,14 +6,63 @@ (ns app.util.zip "Helpers for make zip file (using jszip)." - (:require [vendor.jszip] - [beicon.core :as rx])) + (:require + ["jszip" :as zip] + [app.util.http :as http] + [beicon.core :as rx] + [promesa.core :as p])) -(defn build +(defn compress-files [files] (letfn [(attach-file [zobj [name content]] (.file zobj name content))] - (let [zobj (js/JSZip.)] + (let [zobj (zip.)] (run! (partial attach-file zobj) files) (->> (.generateAsync zobj #js {:type "blob"}) (rx/from))))) + +(defn load-from-url + "Loads the data from a blob url" + [url] + (->> (http/send! + {:uri url + :response-type :blob + :method :get}) + (rx/map :body) + (rx/flat-map zip/loadAsync))) + +(defn- process-file + [entry path type] + (cond + (nil? entry) + (p/rejected (str "File not found: " path)) + + (.-dir entry) + (p/resolved {:dir path}) + + :else + (-> (.async entry type) + (p/then #(hash-map :path path :content %))))) + +(defn get-file + "Gets a single file from the zip archive" + ([zip path] + (get-file zip path "text")) + + ([zip path type] + (-> (.file zip path) + (process-file path type) + (rx/from)))) + +(defn extract-files + "Creates a stream that will emit values for every file in the zip" + [zip] + (let [promises (atom []) + get-file + (fn [path entry] + (let [current (process-file entry path "text")] + (swap! promises conj current)))] + (.forEach zip get-file) + + (->> (rx/from (p/all @promises)) + (rx/flat-map identity)))) diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index 0c21935324..59470139e8 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -6,20 +6,17 @@ (ns app.worker (:require - [cljs.spec.alpha :as s] - [promesa.core :as p] - [beicon.core :as rx] - [cuerdas.core :as str] - [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.uuid :as uuid] + [app.common.transit :as t] + [app.worker.export] [app.worker.impl :as impl] + [app.worker.import] [app.worker.selection] - [app.worker.thumbnails] [app.worker.snaps] - [app.util.object :as obj] - [app.util.transit :as t] - [app.util.worker :as w])) + [app.worker.thumbnails] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [promesa.core :as p])) ;; --- Messages Handling @@ -34,7 +31,7 @@ (s/def ::message (s/keys - :req-opt [::buffer?] + :opt-un [::buffer?] :req-un [::payload ::sender-id])) (def buffer (rx/subject)) @@ -43,42 +40,49 @@ "Process the message and returns to the client" [{:keys [sender-id payload] :as message}] (us/assert ::message message) - (try - (let [result (impl/handler payload)] - (cond - (p/promise? result) - (p/handle result - (fn [msg] - (.postMessage js/self (t/encode - {:reply-to sender-id - :payload msg}))) - (fn [err] - (.postMessage js/self (t/encode - {:reply-to sender-id - :error {:data (ex-data err) - :message (ex-message err)}})))) + (letfn [(post [msg] + (let [msg (-> msg (assoc :reply-to sender-id) (t/encode-str))] + (.postMessage js/self msg))) - (or (rx/observable? result) - (rx/subject? result)) - (throw (ex-info "not implemented" {})) + (reply [result] + (post {:payload result})) - :else - (.postMessage js/self (t/encode - {:reply-to sender-id - :payload result})))) - (catch :default e - (.error js/console "error" e) - (let [message {:reply-to sender-id - :error {:data (ex-data e) - :message (ex-message e)}}] - (.postMessage js/self (t/encode message)))))) + (reply-error [err] + (.error js/console "error" err) + (post {:error {:data (ex-data err) + :message (ex-message err)}})) + + (reply-completed + ([] (reply-completed nil)) + ([msg] (post {:payload msg + :completed true})))] + + (try + (let [result (impl/handler payload) + promise? (p/promise? result) + stream? (or (rx/observable? result) (rx/subject? result))] + + (cond + promise? + (-> result + (p/then reply-completed) + (p/catch reply-error)) + + stream? + (rx/subscribe result reply reply-error reply-completed) + + :else + (reply result))) + + (catch :default err + (reply-error err))))) (defn- drop-message "Sends to the client a notifiction that its messages have been dropped" - [{:keys [sender-id payload] :as message}] + [{:keys [sender-id] :as message}] (us/assert ::message message) - (.postMessage js/self (t/encode {:reply-to sender-id - :dropped true}))) + (.postMessage js/self (t/encode-str {:reply-to sender-id + :dropped true}))) (defn subscribe-buffer-messages "Creates a subscription to process the buffer messages" @@ -94,7 +98,7 @@ ;; we also store the last message processed in order to detect ;; posible infinite loops (rx/scan - (fn [[messages dropped last] message] + (fn [[messages dropped _last] message] (let [cmd (get-in message [:payload :cmd]) ;; The previous message is dropped @@ -136,7 +140,7 @@ [event] (when (nil? (.-source event)) (let [message (.-data event) - message (t/decode message)] + message (t/decode-str message)] (if (:buffer? message) (rx/push! buffer message) (handle-message message))))) @@ -152,4 +156,3 @@ (set! process-message-sub (subscribe-buffer-messages)) (.addEventListener js/self "message" on-message)) - diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs new file mode 100644 index 0000000000..02fae88fee --- /dev/null +++ b/frontend/src/app/worker/export.cljs @@ -0,0 +1,471 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.worker.export + (:require + [app.common.data :as d] + [app.common.text :as ct] + [app.config :as cfg] + [app.main.render :as r] + [app.main.repo :as rp] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.json :as json] + [app.util.zip :as uz] + [app.worker.impl :as impl] + [beicon.core :as rx] + [cuerdas.core :as str])) + +(defn create-manifest + "Creates a manifest entry for the given files" + [team-id file-id export-type files] + (letfn [(format-page [manifest page] + (-> manifest + (assoc (str (:id page)) + {:name (:name page)}))) + + (format-file [manifest file] + (let [name (:name file) + is-shared (:is-shared file) + pages (->> (get-in file [:data :pages]) + (mapv str)) + index (->> (get-in file [:data :pages-index]) + (vals) + (reduce format-page {}))] + (-> manifest + (assoc (str (:id file)) + {:name name + :shared is-shared + :pages pages + :pagesIndex index + :libraries (->> (:libraries file) (into #{}) (mapv str)) + :exportType (d/name export-type) + :hasComponents (d/not-empty? (get-in file [:data :components])) + :hasMedia (d/not-empty? (get-in file [:data :media])) + :hasColors (d/not-empty? (get-in file [:data :colors])) + :hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))] + (let [manifest {:teamId (str team-id) + :fileId (str file-id) + :files (->> (vals files) (reduce format-file {}))}] + (json/encode manifest)))) + +(defn process-pages [file] + (let [pages (get-in file [:data :pages]) + pages-index (get-in file [:data :pages-index])] + (->> pages + (map #(hash-map + :file-id (:id file) + :data (get pages-index %)))))) + +(defn get-page-data + [{file-id :file-id {:keys [id name] :as data} :data}] + (->> (r/render-page data) + (rx/map (fn [markup] + {:id id + :name name + :file-id file-id + :markup markup})))) + +(defn collect-page + [{:keys [id file-id markup] :as page}] + [(str file-id "/" id ".svg") markup]) + +(defn collect-entries [result data keys] + (-> result + (assoc (str (:id data)) + (->> (select-keys data keys) + (d/deep-mapm + (fn [[k v]] + [(-> k str/camel) v])))))) + +(def ^:const color-keys + [:name :color :opacity :gradient :path]) + +(def ^:const typography-keys + [:name :font-family :font-id :font-size :font-style :font-variant-id :font-weight + :letter-spacing :line-height :text-transform :path]) + +(def ^:const media-keys + [:name :mtype :width :height :path]) + +(defn collect-color + [result color] + (collect-entries result color color-keys)) + +(defn collect-typography + [result typography] + (collect-entries result typography typography-keys)) + +(defn collect-media + [result media] + (collect-entries result media media-keys)) + +(defn parse-library-color + [[file-id colors]] + (let [markup + (->> (vals colors) + (reduce collect-color {}) + (json/encode))] + [(str file-id "/colors.json") markup])) + +(defn parse-library-typographies + [[file-id typographies]] + (let [markup + (->> (vals typographies) + (reduce collect-typography {}) + (json/encode))] + [(str file-id "/typographies.json") markup])) + +(defn parse-library-media + [[file-id media]] + (rx/merge + (let [markup + (->> (vals media) + (reduce collect-media {}) + (json/encode))] + (rx/of (vector (str file-id "/media.json") markup))) + + (->> (rx/from (vals media)) + (rx/map #(assoc % :file-id file-id)) + (rx/flat-map + (fn [media] + (let [file-path (str file-id "/media/" (:id media) "." (dom/mtype->extension (:mtype media)))] + (->> (http/send! + {:uri (cfg/resolve-file-media media) + :response-type :blob + :method :get}) + (rx/map :body) + (rx/map #(vector file-path %))))))))) + +(defn parse-library-components + [file] + (->> (r/render-components (:data file)) + (rx/map #(vector (str (:id file) "/components.svg") %)))) + +(defn fetch-file-with-libraries [file-id] + (->> (rx/zip (rp/query :file {:id file-id}) + (rp/query :file-libraries {:file-id file-id})) + (rx/map + (fn [[file file-libraries]] + (let [libraries-ids (->> file-libraries (map :id) (filterv #(not= (:id file) %)))] + (-> file + (assoc :libraries libraries-ids))))))) + +(defn get-component-ref-file + [objects shape] + + (cond + (contains? shape :component-file) + (get shape :component-file) + + (contains? shape :shape-ref) + (recur objects (get objects (:parent-id shape))) + + :else + nil)) + +(defn detach-external-references + [file file-id] + (let [detach-text + (fn [content] + (->> content + (ct/transform-nodes + #(cond-> % + (not= file-id (:fill-color-ref-file %)) + (dissoc :fill-color-ref-id :fill-color-ref-file) + + (not= file-id (:typography-ref-file %)) + (dissoc :typography-ref-id :typography-ref-file))))) + + detach-shape + (fn [objects shape] + (cond-> shape + (not= file-id (:fill-color-ref-file shape)) + (dissoc :fill-color-ref-id :fill-color-ref-file) + + (not= file-id (:stroke-color-ref-file shape)) + (dissoc :stroke-color-ref-id :stroke-color-ref-file) + + (not= file-id (get-component-ref-file objects shape)) + (dissoc :component-id :component-file :shape-ref :component-root?) + + (= :text (:type shape)) + (update :content detach-text))) + + detach-objects + (fn [objects] + (->> objects + (d/mapm #(detach-shape objects %2)))) + + detach-pages + (fn [pages-index] + (->> pages-index + (d/mapm + (fn [_ data] + (-> data + (update :objects detach-objects))))))] + + (-> file + (update-in [:data :pages-index] detach-pages)))) + +(defn make-local-external-references + [file file-id] + (let [detach-text + (fn [content] + (->> content + (ct/transform-nodes + #(cond-> % + (not= file-id (:fill-color-ref-file %)) + (assoc :fill-color-ref-file file-id) + + (not= file-id (:typography-ref-file %)) + (assoc :typography-ref-file file-id))))) + + detach-shape + (fn [shape] + (cond-> shape + (not= file-id (:fill-color-ref-file shape)) + (assoc :fill-color-ref-file file-id) + + (not= file-id (:stroke-color-ref-file shape)) + (assoc :stroke-color-ref-file file-id) + + (not= file-id (:component-file shape)) + (assoc :component-file file-id) + + (= :text (:type shape)) + (update :content detach-text))) + + detach-objects + (fn [objects] + (->> objects + (d/mapm #(detach-shape %2)))) + + detach-pages + (fn [pages-index] + (->> pages-index + (d/mapm + (fn [_ data] + (-> data + (update :objects detach-objects))))))] + (-> file + (update-in [:data :pages-index] detach-pages)))) + +(defn collect-external-references + [file] + + (let [get-text-refs + (fn [content] + (->> content + (ct/node-seq #(or (contains? % :fill-color-ref-id) + (contains? % :typography-ref-id))) + + (mapcat (fn [node] + (cond-> [] + (contains? node :fill-color-ref-id) + (conj {:id (:fill-color-ref-id node) + :file-id (:fill-color-ref-file node)}) + + (contains? node :typography-ref-id) + (conj {:id (:typography-ref-id node) + :file-id (:typography-ref-file node)}) + ))) + + (into []))) + + get-shape-refs + (fn [[_ shape]] + (cond-> [] + (contains? shape :fill-color-ref-id) + (conj {:id (:fill-color-ref-id shape) + :file-id (:fill-color-ref-file shape)}) + + (contains? shape :stroke-color-ref-id) + (conj {:id (:stroke-color-ref-id shape) + :file-id (:stroke-color-ref-file shape)}) + + (contains? shape :component-id) + (conj {:id (:component-id shape) + :file-id (:component-file shape)}) + + (= :text (:type shape)) + (d/concat (get-text-refs (:content shape)))))] + + (->> (get-in file [:data :pages-index]) + (vals) + (mapcat :objects) + (mapcat get-shape-refs) + (filter (comp some? :file-id)) + (filter (comp some? :id)) + (group-by :file-id) + (d/mapm #(mapv :id %2))))) + +(defn merge-assets [target-file assets-files] + (let [external-refs (collect-external-references target-file) + + merge-file-assets + (fn [target file] + (let [colors (-> (get-in file [:data :colors]) + (select-keys (get external-refs (:id file)))) + typographies (-> (get-in file [:data :typographies]) + (select-keys (get external-refs (:id file)))) + media (-> (get-in file [:data :media]) + (select-keys (get external-refs (:id file)))) + components (-> (get-in file [:data :components]) + (select-keys (get external-refs (:id file))))] + (cond-> target + (d/not-empty? colors) + (update-in [:data :colors] merge colors) + + (d/not-empty? typographies) + (update-in [:data :typographies] merge typographies) + + (d/not-empty? media) + (update-in [:data :media] merge media) + + (d/not-empty? components) + (update-in [:data :components] merge components))))] + + (->> assets-files + (reduce merge-file-assets target-file)))) + +(defn process-export + [file-id export-type files] + + (case export-type + :all files + :merge (let [file-list (-> files (d/without-keys [file-id]) vals)] + (-> (select-keys files [file-id]) + (update file-id merge-assets file-list) + (update file-id make-local-external-references file-id) + (update file-id dissoc :libraries))) + :detach (-> (select-keys files [file-id]) + (update file-id detach-external-references file-id) + (update file-id dissoc :libraries)))) + +(defn collect-files + [file-id export-type] + + (letfn [(fetch-dependencies [[files pending]] + (if (empty? pending) + ;; When not pending, we finish the generation + (rx/empty) + + ;; Still pending files, fetch the next one + (let [next (peek pending) + pending (pop pending)] + (if (contains? files next) + ;; The file is already in the result + (rx/of [files pending]) + + (->> (fetch-file-with-libraries next) + (rx/map + (fn [file] + [(-> files + (assoc (:id file) file)) + (as-> pending $ + (reduce conj $ (:libraries file)))])))))))] + (let [files {} + pending [file-id]] + (->> (rx/of [files pending]) + (rx/expand fetch-dependencies) + (rx/last) + (rx/map first) + (rx/map #(process-export file-id export-type %)))))) + +(defn export-file + [team-id file-id export-type] + + (let [files-stream (->> (collect-files file-id export-type) + (rx/share)) + + manifest-stream + (->> files-stream + (rx/map #(create-manifest team-id file-id export-type %)) + (rx/map #(vector "manifest.json" %))) + + render-stream + (->> files-stream + (rx/flat-map vals) + (rx/flat-map process-pages) + (rx/observe-on :async) + (rx/flat-map get-page-data) + (rx/share)) + + colors-stream + (->> files-stream + (rx/flat-map vals) + (rx/map #(vector (:id %) (get-in % [:data :colors]))) + (rx/filter #(d/not-empty? (second %))) + (rx/map parse-library-color)) + + typographies-stream + (->> files-stream + (rx/flat-map vals) + (rx/map #(vector (:id %) (get-in % [:data :typographies]))) + (rx/filter #(d/not-empty? (second %))) + (rx/map parse-library-typographies)) + + media-stream + (->> files-stream + (rx/flat-map vals) + (rx/map #(vector (:id %) (get-in % [:data :media]))) + (rx/filter #(d/not-empty? (second %))) + (rx/flat-map parse-library-media)) + + components-stream + (->> files-stream + (rx/flat-map vals) + (rx/filter #(d/not-empty? (get-in % [:data :components]))) + (rx/flat-map parse-library-components)) + + pages-stream + (->> render-stream + (rx/map collect-page))] + + (rx/merge + (->> render-stream + (rx/map #(hash-map + :type :progress + :file file-id + :data (str "Render " (:file-name %) " - " (:name %))))) + + (->> (rx/merge + manifest-stream + pages-stream + components-stream + media-stream + colors-stream + typographies-stream) + (rx/reduce conj []) + (rx/with-latest-from files-stream) + (rx/flat-map (fn [[data files]] + (->> (uz/compress-files data) + (rx/map #(vector (get files file-id) %))))))))) + +(defmethod impl/handler :export-file + [{:keys [team-id files export-type] :as message}] + + (->> (rx/from files) + (rx/mapcat + (fn [file] + (->> (export-file team-id file export-type) + (rx/map + (fn [value] + (if (contains? value :type) + value + (let [[file export-blob] value] + {:type :finish + :file-id (:id file) + :filename (:name file) + :mtype "application/penpot" + :description "Penpot export (*.penpot)" + :uri (dom/create-uri export-blob)})))) + (rx/catch + (fn [err] + (rx/of {:type :error + :error (str err) + :file-id file})))))))) diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 77032133a6..9dfb3bba26 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -6,9 +6,8 @@ (ns app.worker.impl (:require - [okulary.core :as l] - [app.util.transit :as t] - [app.common.pages.changes :as ch])) + [app.common.pages.changes :as ch] + [okulary.core :as l])) (enable-console-print!) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs new file mode 100644 index 0000000000..d318f9d6a9 --- /dev/null +++ b/frontend/src/app/worker/import.cljs @@ -0,0 +1,454 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.worker.import + (:refer-clojure :exclude [resolve]) + (:require + [app.common.data :as d] + [app.common.file-builder :as fb] + [app.common.pages :as cp] + [app.common.text :as ct] + [app.common.uuid :as uuid] + [app.main.repo :as rp] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.import.parser :as cip] + [app.util.json :as json] + [app.util.logging :as log] + [app.util.zip :as uz] + [app.worker.impl :as impl] + [beicon.core :as rx] + [cuerdas.core :as str] + [tubax.core :as tubax])) + +(log/set-level! :trace) + +;; Upload changes batches size +(def ^:const change-batch-size 100) + +(defn get-file + "Resolves the file inside the context given its id and the data" + ([context type] + (get-file context type nil nil)) + + ([context type id] + (get-file context type id nil)) + + ([context type id media] + (let [file-id (:file-id context) + path (case type + :manifest (str "manifest.json") + :page (str file-id "/" id ".svg") + :colors (str file-id "/colors.json") + :typographies (str file-id "/typographies.json") + :media-list (str file-id "/media.json") + :media (let [ext (dom/mtype->extension (:mtype media))] + (str file-id "/media/" id "." ext)) + :components (str file-id "/components.svg")) + + parse-svg? (and (not= type :media) (str/ends-with? path "svg")) + parse-json? (and (not= type :media) (str/ends-with? path "json")) + no-parse? (or (= type :media) + (not (or parse-svg? parse-json?))) + + file-type (if (or parse-svg? parse-json?) "text" "blob")] + + (log/debug :action "parsing" :path path) + + (cond->> (uz/get-file (:zip context) path file-type) + parse-svg? + (rx/map (comp tubax/xml->clj :content)) + + parse-json? + (rx/map (comp json/decode :content)) + + no-parse? + (rx/map :content))))) + +(defn resolve-factory + "Creates a wrapper around the atom to remap ids to new ids and keep + their relationship so they ids are coherent." + [] + (let [id-mapping-atom (atom {}) + resolve + (fn [id-mapping id] + (assert (uuid? id) (str id)) + (get id-mapping id)) + + set-id + (fn [id-mapping id] + (assert (uuid? id) (str id)) + (cond-> id-mapping + (nil? (resolve id-mapping id)) + (assoc id (uuid/next))))] + + (fn [id] + (when (some? id) + (swap! id-mapping-atom set-id id) + (resolve @id-mapping-atom id))))) + +(defn create-file + "Create a new file on the back-end" + [context] + (let [resolve (:resolve context) + file-id (resolve (:file-id context))] + (rp/mutation + :create-temp-file + {:id file-id + :name (:name context) + :is-shared (:shared context) + :project-id (:project-id context) + :data (-> cp/empty-file-data (assoc :id file-id))}))) + +(defn link-file-libraries + "Create a new file on the back-end" + [context] + (let [resolve (:resolve context) + file-id (resolve (:file-id context)) + libraries (->> context :libraries (mapv resolve))] + (->> (rx/from libraries) + (rx/map #(hash-map :file-id file-id :library-id %)) + (rx/flat-map (partial rp/mutation :link-file-to-library))))) + +(defn persist-file [file] + (rp/mutation :persist-temp-file {:id (:id file)})) + +(defn send-changes + "Creates batches of changes to be sent to the backend" + [file] + (let [revn (atom (:revn file)) + file-id (:id file) + session-id (uuid/next) + changes-batches + (->> (fb/generate-changes file) + (partition change-batch-size change-batch-size nil) + (mapv vec))] + + (rx/concat + (->> (rx/from changes-batches) + (rx/mapcat + #(rp/mutation + :update-file + {:id file-id + :session-id session-id + :revn @revn + :changes %})) + (rx/map first) + (rx/tap #(reset! revn (:revn %)))) + + (rp/mutation :persist-temp-file {:id file-id})))) + +(defn upload-media-files + "Upload a image to the backend and returns its id" + [file-id name data-uri] + + (log/debug :action "uploading" :file-id file-id :name name) + + (->> (http/send! + {:uri data-uri + :response-type :blob + :method :get}) + (rx/map :body) + (rx/map + (fn [blob] + {:name name + :file-id file-id + :content blob + :is-local true})) + (rx/flat-map #(rp/mutation! :upload-file-media-object %)))) + +(defn resolve-text-content [node context] + (let [resolve (:resolve context)] + (->> node + (ct/transform-nodes + (fn [item] + (cond-> item + (uuid? (get item :fill-color-ref-id)) + (d/update-when :fill-color-ref-id resolve) + + (uuid? (get item :fill-color-ref-file)) + (d/update-when :fill-color-ref-file resolve) + + (uuid? (get item :typography-ref-id)) + (d/update-when :typography-ref-id resolve) + + (uuid? (get item :typography-ref-file)) + (d/update-when :typography-ref-file resolve))))))) + +(defn resolve-data-ids + [data type context] + (let [resolve (:resolve context)] + (-> data + (d/update-when :fill-color-ref-id resolve) + (d/update-when :fill-color-ref-file resolve) + (d/update-when :stroke-color-ref-id resolve) + (d/update-when :stroke-color-ref-file resolve) + (d/update-when :component-id resolve) + (d/update-when :component-file resolve) + (d/update-when :shape-ref resolve) + + (cond-> (= type :text) + (d/update-when :content resolve-text-content context))))) + +(defn process-import-node + [context file node] + + (let [type (cip/get-type node) + close? (cip/close? node)] + (if close? + (case type + :frame (fb/close-artboard file) + :group (fb/close-group file) + :svg-raw (fb/close-svg-raw file) + #_default file) + + (let [resolve (:resolve context) + old-id (cip/get-id node) + interactions (->> (cip/parse-interactions node) + (mapv #(update % :destination resolve))) + + data (-> (cip/parse-data type node) + (resolve-data-ids type context) + (cond-> (some? old-id) + (assoc :id (resolve old-id)))) + + file (case type + :frame (fb/add-artboard file data) + :group (fb/add-group file data) + :rect (fb/create-rect file data) + :circle (fb/create-circle file data) + :path (fb/create-path file data) + :text (fb/create-text file data) + :image (fb/create-image file data) + :svg-raw (fb/create-svg-raw file data) + #_default file)] + + ;; We store this data for post-processing after every shape has been + ;; added + (cond-> file + (d/not-empty? interactions) + (assoc-in [:interactions (:id data)] interactions)))))) + +(defn setup-interactions + [file] + + (letfn [(add-interactions + [file [id interactions]] + (->> interactions + (reduce #(fb/add-interaction %1 id %2) file))) + + (process-interactions + [file] + (let [interactions (:interactions file) + file (dissoc file :interactions)] + (->> interactions (reduce add-interactions file))))] + + (-> file process-interactions))) + +(defn resolve-media + [file-id node] + (if (and (not (cip/close? node)) + (cip/has-image? node)) + (let [name (cip/get-image-name node) + data-uri (cip/get-image-data node)] + (->> (upload-media-files file-id name data-uri) + (rx/catch #(do (.error js/console %) + (rx/of node))) + (rx/map + (fn [media] + (-> node + (assoc-in [:attrs :penpot:media-id] (:id media)) + (assoc-in [:attrs :penpot:media-width] (:width media)) + (assoc-in [:attrs :penpot:media-height] (:height media)) + (assoc-in [:attrs :penpot:media-mtype] (:mtype media))))))) + + ;; If the node is not an image just return the node + (->> (rx/of node) + (rx/observe-on :async)))) + +(defn import-page + [context file [page-id page-name content]] + (let [nodes (->> content cip/node-seq) + file-id (:id file) + resolve (:resolve context) + page-data (-> (cip/parse-page-data content) + (assoc :name page-name) + (assoc :id (resolve page-id))) + file (-> file (fb/add-page page-data))] + (->> (rx/from nodes) + (rx/filter cip/shape?) + (rx/mapcat (partial resolve-media file-id)) + (rx/reduce (partial process-import-node context) file) + (rx/map (comp fb/close-page setup-interactions))))) + +(defn import-component [context file node] + (let [resolve (:resolve context) + content (cip/find-node node :g) + file-id (:id file) + old-id (cip/get-id node) + id (resolve old-id) + path (get-in node [:attrs :penpot:path] "") + data (-> (cip/parse-data :group content) + (assoc :path path) + (assoc :id id)) + + file (-> file (fb/start-component data)) + children (cip/node-seq node)] + + (->> (rx/from children) + (rx/filter cip/shape?) + (rx/skip 1) + (rx/skip-last 1) + (rx/mapcat (partial resolve-media file-id)) + (rx/reduce (partial process-import-node context) file) + (rx/map fb/finish-component)))) + +(defn process-pages + [context file] + (let [index (:pages-index context) + get-page-data + (fn [page-id] + [page-id (get-in index [page-id :name])]) + + pages (->> (:pages context) (mapv get-page-data))] + + (->> (rx/from pages) + (rx/mapcat + (fn [[page-id page-name]] + (->> (get-file context :page page-id) + (rx/map (fn [page-data] [page-id page-name page-data]))))) + (rx/concat-reduce (partial import-page context) file)))) + +(defn process-library-colors + [context file] + (if (:has-colors context) + (let [resolve (:resolve context) + add-color + (fn [file [id color]] + (let [color (-> color + (d/update-in-when [:gradient :type] keyword) + (assoc :id (resolve id)))] + (fb/add-library-color file color)))] + (->> (get-file context :colors) + (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/reduce add-color file))) + + (rx/of file))) + +(defn process-library-typographies + [context file] + (if (:has-typographies context) + (let [resolve (:resolve context)] + (->> (get-file context :typographies) + (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/map (fn [[id typography]] + (-> typography + (d/kebab-keys) + (assoc :id (resolve id))))) + (rx/reduce fb/add-library-typography file))) + + (rx/of file))) + +(defn process-library-media + [context file] + (if (:has-media context) + (let [resolve (:resolve context)] + (->> (get-file context :media-list) + (rx/flat-map (comp d/kebab-keys cip/string->uuid)) + (rx/flat-map + (fn [[id media]] + (let [media (assoc media :id (resolve id))] + (->> (get-file context :media id media) + (rx/map (fn [blob] + (let [content (.slice blob 0 (.-size blob) (:mtype media))] + {:name (:name media) + :id (:id media) + :file-id (:id file) + :content content + :is-local false}))) + (rx/flat-map #(rp/mutation! :upload-file-media-object %)) + (rx/map (constantly media)))))) + (rx/reduce fb/add-library-media file))) + + (rx/of file))) + +(defn process-library-components + [context file] + (if (:has-components context) + (let [split-components + (fn [content] (->> (cip/node-seq content) + (filter #(= :symbol (:tag %)))))] + + (->> (get-file context :components) + (rx/flat-map split-components) + (rx/concat-reduce (partial import-component context) file))) + (rx/of file))) + +(defn process-file + [context file] + + (->> (rx/of file) + (rx/flat-map (partial process-pages context)) + (rx/flat-map (partial process-library-colors context)) + (rx/flat-map (partial process-library-typographies context)) + (rx/flat-map (partial process-library-media context)) + (rx/flat-map (partial process-library-components context)) + (rx/flat-map send-changes))) + +(defn create-files + [context files] + + (let [data (group-by :file-id files)] + (rx/concat + (->> (rx/from files) + (rx/map #(merge context %)) + (rx/flat-map + (fn [context] + (->> (create-file context) + (rx/map #(vector % (first (get data (:file-id context))))))))) + + (->> (rx/from files) + (rx/map #(merge context %)) + (rx/flat-map link-file-libraries) + (rx/ignore))))) + +(defmethod impl/handler :analyze-import + [{:keys [files]}] + + (->> (rx/from files) + (rx/flat-map + (fn [uri] + (->> (rx/of uri) + (rx/flat-map uz/load-from-url) + (rx/flat-map #(get-file {:zip %} :manifest)) + (rx/map (comp d/kebab-keys cip/string->uuid)) + (rx/map #(hash-map :uri uri :data %)) + (rx/catch #(rx/of {:uri uri :error (.-message %)}))))))) + +(defmethod impl/handler :import-files + [{:keys [project-id files]}] + + (let [context {:project-id project-id + :resolve (resolve-factory)}] + (->> (create-files context files) + (rx/catch #(.error js/console "IMPORT ERROR" %)) + (rx/flat-map + (fn [[file data]] + (->> (uz/load-from-url (:uri data)) + (rx/map #(-> context (assoc :zip %) (merge data))) + (rx/flat-map #(process-file % file)) + (rx/map + (fn [_] + {:status :import-success + :file-id (:file-id data)})) + + (rx/catch + (fn [err] + (.error js/console "ERROR" (:file-id data) err) + (rx/of {:status :import-error + :file-id (:file-id data) + :error (.-message err) + :error-data (clj->js (.-data err))}))))))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 09cc4041a1..d93fcfaf45 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -6,17 +6,14 @@ (ns app.worker.selection (:require - [cljs.spec.alpha :as s] - [okulary.core :as l] [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.util.quadtree :as qdt] [app.worker.impl :as impl] - [clojure.set :as set])) + [clojure.set :as set] + [okulary.core :as l])) (defonce state (l/atom {})) @@ -66,8 +63,8 @@ changed-ids (into #{} (comp (filter changes?) (filter #(not= % uuid/zero))) - (set/union (keys old-objects) - (keys new-objects))) + (set/union (set (keys old-objects)) + (set (keys new-objects)))) shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) parents-index (cp/generate-child-all-parents-index new-objects shapes) @@ -87,7 +84,7 @@ (create-index new-objects))) (defn- query-index - [{index :index z-index :z-index} rect frame-id include-frames? include-groups? disabled-masks reverse?] + [{index :index z-index :z-index} rect frame-id include-frames? full-frame? include-groups? reverse?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -100,7 +97,11 @@ (case (:type shape) :frame include-frames? :group include-groups? - true))) + true) + + (or (not full-frame?) + (not= :frame (:type shape)) + (gsh/rect-contains-shape? rect shape)))) overlaps? (fn [shape] @@ -137,7 +138,7 @@ (defmethod impl/handler :selection/initialize-index - [{:keys [file-id data] :as message}] + [{:keys [data] :as message}] (letfn [(index-page [state page] (let [id (:id page) objects (:objects page)] @@ -154,10 +155,10 @@ nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?] - :or {include-groups? true disabled-masks #{} reverse? false} :as message}] + [{:keys [page-id rect frame-id include-frames? full-frame? include-groups? reverse?] + :or {include-groups? true reverse? false include-frames? false full-frame? false} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id include-frames? include-groups? disabled-masks reverse?))) + (query-index index rect frame-id include-frames? full-frame? include-groups? reverse?))) (defmethod impl/handler :selection/query-z-index [{:keys [page-id objects ids]}] diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index a2c3954483..a4926731b5 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -7,7 +7,6 @@ (ns app.worker.snaps (:require [app.common.data :as d] - [app.common.pages :as cp] [app.common.uuid :as uuid] [app.util.geom.grid :as gg] [app.util.geom.snap-points :as snap] @@ -20,7 +19,7 @@ (defn process-shape [frame-id coord] (fn [shape] - (let [points (snap/shape-snap-points shape) + (let [points (when-not (:hidden shape) (snap/shape-snap-points shape)) shape-data (->> points (mapv #(vector % (:id shape))))] (if (= (:id shape) frame-id) (d/concat @@ -74,7 +73,7 @@ (d/mapm create-index shapes-data))) ;; Attributes that will change the values of their snap -(def snap-attrs [:x :y :width :height :selrect :grids]) +(def snap-attrs [:x :y :width :height :hidden :selrect :grids]) (defn- update-snap-data [snap-data old-objects new-objects] @@ -99,7 +98,8 @@ changed-ids (into #{} (filter changed?) - (set/union (keys old-objects) (keys new-objects))) + (set/union (set (keys old-objects)) + (set (keys new-objects)))) to-delete (aggregate-data old-objects changed-ids) to-add (aggregate-data new-objects changed-ids) @@ -134,30 +134,25 @@ (reduce add-data $ to-add) (reduce delete-frames $ frames-to-delete)))) -(defn- log-state - "Helper function to print a friendly version of the snap tree. Debugging purposes" - [] - (let [process-frame-data #(d/mapm rt/as-map %) - process-page-data #(d/mapm process-frame-data %)] - (js/console.log "STATE" (clj->js (d/mapm process-page-data @state))))) +;; (defn- log-state +;; "Helper function to print a friendly version of the snap tree. Debugging purposes" +;; [] +;; (let [process-frame-data #(d/mapm rt/as-map %) +;; process-page-data #(d/mapm process-frame-data %)] +;; (js/console.log "STATE" (clj->js (d/mapm process-page-data @state))))) (defn- index-page [state page-id objects] (let [snap-data (initialize-snap-data objects)] (assoc state page-id snap-data))) (defn- update-page [state page-id old-objects new-objects] - (let [changed? #(not= (get old-objects %) (get new-objects %)) - changed-ids (into #{} - (filter changed?) - (set/union (keys old-objects) (keys new-objects))) - - snap-data (get state page-id) + (let [snap-data (get state page-id) snap-data (update-snap-data snap-data old-objects new-objects)] (assoc state page-id snap-data))) ;; Public API (defmethod impl/handler :snaps/initialize-index - [{:keys [file-id data] :as message}] + [{:keys [data] :as message}] ;; Create the index (letfn [(process-page [state page] (let [id (:id page) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index ddf65c6b1a..210dfe31b9 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -6,14 +6,13 @@ (ns app.worker.thumbnails (:require - [rumext.alpha :as mf] - [beicon.core :as rx] - [promesa.core :as p] - [app.main.fonts :as fonts] + ["react-dom/server" :as rds] [app.main.exports :as exports] - [app.worker.impl :as impl] + [app.main.fonts :as fonts] [app.util.http :as http] - ["react-dom/server" :as rds])) + [app.worker.impl :as impl] + [beicon.core :as rx] + [rumext.alpha :as mf])) (defn- handle-response [response] @@ -31,17 +30,12 @@ (defn- request-page [file-id page-id] (let [uri "/api/rpc/query/page"] - (p/create - (fn [resolve reject] - (->> (http/send! {:uri uri - :query {:file-id file-id :id page-id :strip-thumbnails true} - :method :get}) - (rx/map http/conditional-decode-transit) - (rx/mapcat handle-response) - (rx/subs (fn [body] - (resolve body)) - (fn [error] - (reject error)))))))) + (->> (http/send! + {:uri uri + :query {:file-id file-id :id page-id :strip-thumbnails true} + :method :get}) + (rx/map http/conditional-decode-transit) + (rx/mapcat handle-response)))) (defonce cache (atom {})) @@ -57,8 +51,8 @@ (defmethod impl/handler :thumbnails/generate [{:keys [file-id page-id] :as message}] - (p/then - (request-page file-id page-id) - (fn [data] - {:svg (render-page data #{file-id page-id}) - :fonts @fonts/loaded}))) + (->> (request-page file-id page-id) + (rx/map + (fn [data] + {:svg (render-page data #{file-id page-id}) + :fonts @fonts/loaded})))) diff --git a/frontend/test.cljs b/frontend/test.cljs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/tests/app/test_components_basic.cljs b/frontend/test/app/components_basic_test.cljs similarity index 98% rename from frontend/tests/app/test_components_basic.cljs rename to frontend/test/app/components_basic_test.cljs index 2c6eba8886..702b36cadd 100644 --- a/frontend/tests/app/test_components_basic.cljs +++ b/frontend/test/app/components_basic_test.cljs @@ -1,4 +1,4 @@ -(ns app.test-components-basic +(ns app.components-basic-test (:require [app.common.data :as d] [app.common.geom.point :as gpt] @@ -29,7 +29,7 @@ (->> state (the/do-update (dw/select-shape (thp/id :shape1))) - (the/do-watch-update dwl/add-component) + (the/do-watch-update dwl/add-component) (rx/do (fn [new-state] (let [shape1 (thp/get-shape new-state :shape1) @@ -73,7 +73,7 @@ (the/do-update (dw/select-shapes (lks/set (thp/id :shape1) (thp/id :shape2)))) - (the/do-watch-update dwl/add-component) + (the/do-watch-update dwl/add-component) (rx/do (fn [new-state] (let [shape1 (thp/get-shape new-state :shape1) @@ -125,7 +125,7 @@ (->> state (the/do-update (dw/select-shape (thp/id :group1))) - (the/do-watch-update dwl/add-component) + (the/do-watch-update dwl/add-component) (rx/do (fn [new-state] (let [[[group shape1 shape2] diff --git a/frontend/tests/app/test_components_sync.cljs b/frontend/test/app/components_sync_test.cljs similarity index 97% rename from frontend/tests/app/test_components_sync.cljs rename to frontend/test/app/components_sync_test.cljs index e93ab55ed2..048cea7a1e 100644 --- a/frontend/tests/app/test_components_sync.cljs +++ b/frontend/test/app/components_sync_test.cljs @@ -1,18 +1,18 @@ -(ns app.test-components-sync +(ns app.components-sync-test (: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.geom.point :as gpt] [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] [app.main.data.workspace.changes :as dwc] [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.libraries-helpers :as dwlh])) + [app.main.data.workspace.libraries-helpers :as dwlh] + [app.test-helpers.events :as the] + [app.test-helpers.libraries :as thl] + [app.test-helpers.pages :as thp] + [beicon.core :as rx] + [cljs.pprint :refer [pprint]] + [cljs.test :as t :include-macros true] + [linked.core :as lks])) (t/use-fixtures :each {:before thp/reset-idmap!}) diff --git a/frontend/test/app/init_test.cljs b/frontend/test/app/init_test.cljs new file mode 100644 index 0000000000..0e36cdcd04 --- /dev/null +++ b/frontend/test/app/init_test.cljs @@ -0,0 +1,9 @@ +(ns app.init-test + (:require + [cljs.test :as t :include-macros true])) + +(defmethod t/report [:cljs.test/default :end-run-tests] + [m] + (if (t/successful? m) + (set! (.-exitCode js/process) 0) + (set! (.-exitCode js/process) 1))) diff --git a/frontend/tests/app/test_shapes.cljs b/frontend/test/app/shapes_test.cljs similarity index 72% rename from frontend/tests/app/test_shapes.cljs rename to frontend/test/app/shapes_test.cljs index 2b473c02a7..45bf8afb75 100644 --- a/frontend/tests/app/test_shapes.cljs +++ b/frontend/test/app/shapes_test.cljs @@ -1,16 +1,17 @@ -(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])) +(ns app.shapes-test + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.pages.helpers :as cph] + [app.main.data.workspace.libraries :as dwl] + [app.test-helpers.events :as the] + [app.test-helpers.libraries :as thl] + [app.test-helpers.pages :as thp] + [beicon.core :as rx] + [cljs.pprint :refer [pprint]] + [cljs.test :as t :include-macros true] + [clojure.stacktrace :as stk] + [linked.core :as lks])) (t/use-fixtures :each {:before thp/reset-idmap!}) diff --git a/frontend/tests/app/test_helpers/events.cljs b/frontend/test/app/test_helpers/events.cljs similarity index 66% rename from frontend/tests/app/test_helpers/events.cljs rename to frontend/test/app/test_helpers/events.cljs index 106510ab3f..85c6488ad0 100644 --- a/frontend/tests/app/test_helpers/events.cljs +++ b/frontend/test/app/test_helpers/events.cljs @@ -1,14 +1,15 @@ (ns app.test-helpers.events - (:require [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] - [beicon.core :as rx] - [potok.core :as ptk] - [app.common.uuid :as uuid] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] - [app.main.data.workspace :as dw])) + (:require + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [beicon.core :as rx] + [potok.core :as ptk] + [app.common.uuid :as uuid] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] + [app.common.pages.helpers :as cph] + [app.main.data.workspace :as dw])) ;; ---- Helpers to manage global events diff --git a/frontend/tests/app/test_helpers/libraries.cljs b/frontend/test/app/test_helpers/libraries.cljs similarity index 87% rename from frontend/tests/app/test_helpers/libraries.cljs rename to frontend/test/app/test_helpers/libraries.cljs index aa62894459..b93f81f2a5 100644 --- a/frontend/tests/app/test_helpers/libraries.cljs +++ b/frontend/test/app/test_helpers/libraries.cljs @@ -1,16 +1,17 @@ (ns app.test-helpers.libraries - (:require [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] - [beicon.core :as rx] - [potok.core :as ptk] - [app.common.uuid :as uuid] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] - [app.common.pages :as cp] - [app.common.pages.helpers :as cph] - [app.main.data.workspace :as dw] - [app.main.data.workspace.libraries-helpers :as dwlh] - [app.test-helpers.pages :as thp])) + (:require + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [beicon.core :as rx] + [potok.core :as ptk] + [app.common.uuid :as uuid] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] + [app.common.pages.helpers :as cph] + [app.main.data.workspace :as dw] + [app.main.data.workspace.libraries-helpers :as dwlh] + [app.test-helpers.pages :as thp])) ;; ---- Helpers to manage libraries and synchronization diff --git a/frontend/tests/app/test_helpers/pages.cljs b/frontend/test/app/test_helpers/pages.cljs similarity index 100% rename from frontend/tests/app/test_helpers/pages.cljs rename to frontend/test/app/test_helpers/pages.cljs diff --git a/frontend/tests/app/test_util_range_tree.cljs b/frontend/test/app/util/range_tree_test.cljs similarity index 96% rename from frontend/tests/app/test_util_range_tree.cljs rename to frontend/test/app/util/range_tree_test.cljs index 507a8386b7..d97a46c79b 100644 --- a/frontend/tests/app/test_util_range_tree.cljs +++ b/frontend/test/app/util/range_tree_test.cljs @@ -1,8 +1,9 @@ -(ns app.test-util-range-tree - (:require [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] - [app.common.geom.point :as gpt] - [app.util.range-tree :as rt])) +(ns app.util.range-tree-test + (:require + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [app.common.geom.point :as gpt] + [app.util.range-tree :as rt])) (defn check-max-height [tree num-nodes]) (defn check-sorted [tree]) @@ -109,7 +110,7 @@ (rt/insert 50 :e) (rt/update 50 :d :xx))] (t/is (= (rt/get tree 50) [:b :xx :e])))) - + (t/testing "Try to update non-existing element" (let [tree (-> (rt/make-tree) (rt/insert 100 :a) diff --git a/frontend/tests/app/test_util_simple_math.cljs b/frontend/test/app/util/simple_math_test.cljs similarity index 92% rename from frontend/tests/app/test_util_simple_math.cljs rename to frontend/test/app/util/simple_math_test.cljs index bc6d48c74e..4b220a9e17 100644 --- a/frontend/tests/app/test_util_simple_math.cljs +++ b/frontend/test/app/util/simple_math_test.cljs @@ -1,8 +1,9 @@ -(ns app.test-util-simple-math - (:require [cljs.test :as t :include-macros true] - [cljs.pprint :refer [pprint]] - [app.common.math :as cm] - [app.util.simple-math :as sm])) +(ns app.util.simple-math-test + (:require + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [app.common.math :as cm] + [app.util.simple-math :as sm])) (t/deftest test-parser-inst (t/testing "Evaluate an empty string" diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 86bac1358e..4ac165f467 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -1,16 +1,16 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"PO-Revision-Date: 2021-06-18 09:19+0000\n" "Last-Translator: Amine Gdoura \n" -"Language-Team: Arabic " -"\n" +"Language-Team: Arabic \n" "Language: ar\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" -"X-Generator: Weblate 4.7-dev\n" +"X-Generator: Weblate 4.7\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -48,10 +48,6 @@ msgstr "هل نسيت كلمة السر؟" msgid "auth.fullname" msgstr "الاسم بالكامل" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "الرجوع للخلف!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "تسجيل الدخول هنا" @@ -137,7 +133,6 @@ msgid "auth.register-submit" msgstr "إنشاء حساب" #: src/app/main/ui/auth/register.cljs -#, fuzzy msgid "auth.register-subtitle" msgstr "إنه مجاني ، مفتوح المصدر" @@ -190,6 +185,9 @@ msgstr "نمط" msgid "labels.fonts" msgstr "الخطوط" +msgid "labels.go-back" +msgstr "الرجوع للخلف" + msgid "labels.images" msgstr "الصور" @@ -229,4 +227,530 @@ msgid "title.dashboard.fonts" msgstr "الخطوط -٪ s - Penpot" msgid "workspace.viewport.click-to-close-path" -msgstr "انقر لإغلاق المسار" \ No newline at end of file +msgstr "انقر لإغلاق المسار" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.notifications.password-saved" +msgstr "تم حفظ كلمة المرور بنجاح!" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-verified-successfully" +msgstr "تم التحقق من عنوان بريدك الإلكتروني بنجاح" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "dashboard.notifications.email-changed-successfully" +msgstr "تم تحديث عنوان بريدك الإلكتروني بنجاح" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.no-projects-placeholder" +msgstr "ستظهر المشاريع المثبتة هنا" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.no-matches-for" +msgstr "لم يتم العثور على مطابقات ل \"٪s\"" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "مشروع جديد" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.new-project" +msgstr "+ مشروع جديد" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "ملف جديد" + +#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs +msgid "dashboard.new-file" +msgstr "+ ملف جديد" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "الانتقال إلى فريق آخر" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "أنقل ٪s الملفات إلى" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "الانتقال إلى" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.loading-files" +msgstr "تحميل ملفاتك …" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "dashboard.libraries-title" +msgstr "المكتبات المشتركة" + +#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.leave-team" +msgstr "ترك الفريق" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.invite-profile" +msgstr "قم بدعوة فريق" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "لا يزال لديك 0 ملفات هنا" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "تكرير ٪s الملفات" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "تكرير" + +msgid "dashboard.draft-title" +msgstr "مسودة" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "حذف الفريق" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Penpot الخاص بك" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ إنشاء فريق جديد" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(نسخة)" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "تغيير البريد الإلكتروني" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "أضف كمكتبة مشتركة" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "تم إرسال رابط استعادة كلمة المرور إلى صندوق البريد الخاص بك." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "لم يتم التعرف على الحساب الشخصي ، يرجى التحقق قبل المتابعة." + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.email-already-validated" +msgstr "تم التحقق من صحة البريد الإلكتروني." + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs +msgid "errors.email-already-exists" +msgstr "البريد الإلكتروني مستخدم بالفعل" + +#: src/app/main/data/workspace.cljs +msgid "errors.clipboard-not-implemented" +msgstr "لا يمكن للمتصفح إجراء هذه العملية" + +#: src/app/main/ui/dashboard/grid.cljs +#, fuzzy +msgid "ds.updated-at" +msgstr "محدث: ٪s" + +#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs +msgid "ds.confirm-title" +msgstr "هل أنت متأكد؟" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-ok" +msgstr "حسنا" + +#: src/app/main/ui/confirm.cljs +msgid "ds.confirm-cancel" +msgstr "إلغاء الأمر" + +#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.your-penpot" +msgstr "Penpot الخاص بك" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-name" +msgstr "اسمك" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.your-email" +msgstr "البريد الالكتروني" + +#: src/app/main/ui/settings.cljs +#, fuzzy +msgid "dashboard.your-account-title" +msgstr "حسابك الخاص" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +msgid "dashboard.update-settings" +msgstr "تحديث الإعدادات" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.type-something" +msgstr "اكتب لإظهار نتائج البحث" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.title-search" +msgstr "نتائج البحث" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-projects" +msgstr "مشاريع الفريق" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-members" +msgstr "أعضاء الفريق" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.team-info" +msgstr "معلومات الفريق" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.switch-team" +msgstr "تبديل الفريق" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "تم نقل مشروعك بنجاح" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "تم نقل الملفات بنجاح" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "تم نقل ملفك بنجاح" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "تم نسخ مشروعك بنجاح" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "تم تكرار ملفك بنجاح" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "تم حذف مشروعك بنجاح" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "تم حذف ملفك بنجاح" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.show-all-files" +msgstr "إظهار كافة الملفات" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-theme" +msgstr "اختر نمطا" + +#: src/app/main/ui/settings/options.cljs +msgid "dashboard.select-ui-language" +msgstr "حدد لغة واجهة المستخدم" + +#: src/app/main/ui/dashboard/search.cljs +msgid "dashboard.searching-for" +msgstr "البحث عن \"٪s\"…" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.search-placeholder" +msgstr "بحث…" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.remove-shared" +msgstr "إزالة كمكتبة مشتركة" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.remove-account" +msgstr "هل تريد إزالة حسابك؟" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.promote-to-owner" +msgstr "الترقية إلى مالك" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "dashboard.projects-title" +msgstr "المشاريع" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "تثبيت / إلغاء التثبيت" + +#: src/app/main/ui/settings/password.cljs +msgid "dashboard.password-change" +msgstr "تغيير كلمة المرور" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "فتح ملف في علامة تبويب جديدة" + +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.num-of-members" +msgstr "٪s الأعضاء" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "عرض" + +msgid "handoff.attributes.stroke.style.solid" +msgstr "صلب" + +msgid "handoff.attributes.stroke.style.none" +msgstr "لا أحد" + +msgid "handoff.attributes.stroke.style.mixed" +msgstr "مختلط" + +msgid "handoff.attributes.stroke.style.dotted" +msgstr "منقط" + +#, permanent +msgid "handoff.attributes.stroke.alignment.outer" +msgstr "خارج" + +#, permanent +msgid "handoff.attributes.stroke.alignment.inner" +msgstr "داخل" + +#, permanent +msgid "handoff.attributes.stroke.alignment.center" +msgstr "مركز" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "ظل" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "عرض" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.top" +msgstr "أعلى" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.rotation" +msgstr "دوران" + +#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.radius" +msgstr "نصف قطر" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "يسار" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "ارتفاع" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout" +msgstr "تخطيط" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "عرض" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "ارتفاع" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "تحميل صورة المصدر" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "قيمة" + +#: src/app/main/ui/settings/password.cljs +msgid "generic.error" +msgstr "حدث خطأ" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.title" +msgstr "البريد الإلكتروني" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subtitle" +msgstr "" +"يرجى وصف سبب بريدك الإلكتروني ، وتحديد ما إذا كانت مشكلة أم فكرة أم شك. سيرد " +"أحد أعضاء فريقنا في أسرع وقت ممكن." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subject" +msgstr "موضوع" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-title" +msgstr "مناقشات الفريق" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle2" +msgstr "" +"يمكنك طرح الأسئلة والإجابة عليها، إجراء محادثات مفتوحة، ومتابعة القرارات " +"التي تؤثر على المشروع." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle1" +msgstr "انضم إلى منتدى التواصل التعاوني لفريق Penpot." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-go-to" +msgstr "اذهب إلى المناقشات" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.description" +msgstr "وصف" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-subtitle" +msgstr "ترغب في الكلام؟ تحدث معنا في Gitter" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.chat-start" +msgstr "انضم إلى الدردشة" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.wrong-old-password" +msgstr "كلمة المرور القديمة غير صحيحة" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-too-short" +msgstr "يجب ألا تقل كلمة المرور عن 8 أحرف" + +#: src/app/main/ui/settings/password.cljs +msgid "errors.password-invalid-confirmation" +msgstr "يجب أن تتطابق كلمة مرور التأكيد" + +msgid "errors.network" +msgstr "تعذر الاتصال بخادم الواجهة الخلفية." + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "errors.media-type-not-allowed" +msgstr "يبدو أن هذه ليست صورة صالحة." + +#: src/app/main/data/workspace/persistence.cljs +msgid "errors.media-too-large" +msgstr "الصورة كبيرة جدا بحيث لا يمكن إدراجها (يجب أن تكون أقل من 5mb)." + +msgid "errors.media-format-unsupported" +msgstr "تنسيق الصورة غير مدعوم (يجب أن يكون svg أو jpg أو png)." + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "تم تعطيل مصادقة LDAP." + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "لون غير صالح" + +#: src/app/main/ui/auth/login.cljs +msgid "errors.google-auth-not-enabled" +msgstr "المصادقة مع جوجل تعطلت في الخلفية" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs +msgid "errors.generic" +msgstr "حدث خطأ ما." + +#: src/app/main/ui/settings/change_email.cljs +msgid "errors.email-invalid-confirmation" +msgstr "يجب أن يتطابق البريد الإلكتروني للتأكيد" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "مشرف" + +msgid "labels.accept" +msgstr "إقبل" + +msgid "history.alert-message" +msgstr "أنت ترى الإصدار٪ s" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "معلومات" + +msgid "handoff.tabs.code.selected.text" +msgstr "نص" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.rect" +msgstr "رباعي" + +msgid "handoff.tabs.code.selected.path" +msgstr "مسار" + +msgid "handoff.tabs.code.selected.image" +msgstr "صورة" + +msgid "handoff.tabs.code.selected.curve" +msgstr "منحنى" + +msgid "handoff.tabs.code.selected.circle" +msgstr "دائرة" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "شفرة" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "الأحرف الكبيرة" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "أحرف صغيرة" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-transform" +msgstr "تغيير النص" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "مسطر" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "يتوسطه خط" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-decoration" +msgstr "زخرفة النص" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "ارتفاع الخط" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.letter-spacing" +msgstr "تباعد الحروف" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "نوع الخط" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.spread" +msgstr "S" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-y" +msgstr "Y" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 0944d45743..44ac3aa5d3 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -1,38 +1,43 @@ msgid "" msgstr "" +"PO-Revision-Date: 2021-06-01 00:38+0000\n" +"Last-Translator: Antonio \n" +"Language-Team: Catalan \n" "Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.7-dev\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" -msgstr "Ja tens un compte?" +msgstr "Ja teniu un compte?" #: src/app/main/ui/auth/register.cljs msgid "auth.check-your-email" msgstr "" -"Revisa el teu email i fes click al link per verificar i començar a " -"utilitzar Penpot." +"Reviseu el correu i cliqueu l'enllaç per verificar i començar a utilitzar el " +"Penpot." #: src/app/main/ui/auth/recovery.cljs msgid "auth.confirm-password" -msgstr "Confirmar contrasenya" +msgstr "Confirmeu la contrasenya" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.create-demo-account" -msgstr "Crea un compte de proba" +msgstr "Creeu un compte de prova" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.create-demo-profile" -msgstr "Vols probar-ho?" +msgstr "Ho voleu provar?" #: src/app/main/ui/auth/register.cljs msgid "auth.demo-warning" msgstr "" -"Aquest es un servei de PROBA. NO HO UTILITZIS per feina real, els projectes " -"seran esborrats periòdicament." +"Aquest és un servei de PROVA. NO L'UTILITZEU en feines reals, ja que els " +"projectes s'esborraran periòdicament." #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs msgid "auth.email" @@ -40,51 +45,47 @@ msgstr "Correu electrònic" #: src/app/main/ui/auth/login.cljs msgid "auth.forgot-password" -msgstr "Has oblidat la contrasenya?" +msgstr "Heu oblidat la contrasenya?" #: src/app/main/ui/auth/register.cljs msgid "auth.fullname" msgstr "Nom complet" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Tornar" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" -msgstr "Inicia sessió aquí" +msgstr "Inicieu la sessió aquí" #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" -msgstr "Accedir" +msgstr "Entreu" #: src/app/main/ui/auth/login.cljs msgid "auth.login-subtitle" -msgstr "Introdueix les teves dades aquí" +msgstr "Introduïu les vostres dades a continuació" #: src/app/main/ui/auth/login.cljs msgid "auth.login-title" -msgstr "Encantats de tornar a veure't" +msgstr "Ens agrada tornar-vos a veure!" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" -msgstr "Accedir amb Github" +msgstr "Entreu amb GitHub" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.login-with-gitlab-submit" -msgstr "Accedir amb Gitlab" +msgstr "Entreu amb GitLab" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-ldap-submit" -msgstr "Accedir amb LDAP" +msgstr "Identifiqueu-vos amb LDAP" #: src/app/main/ui/auth/recovery.cljs msgid "auth.new-password" -msgstr "Introdueix la nova contrasenya" +msgstr "Escriviu la nova contrasenya" #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" -msgstr "El codi de recuperació no és vàlid" +msgstr "El codi de recuperació no és vàlid." #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.password-changed-succesfully" @@ -93,16 +94,15 @@ msgstr "La contrasenya s'ha canviat correctament" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.profile-not-verified" msgstr "" -"El perfil encara no s'ha verificat, si us plau verifica-ho abans de " -"continuar." +"El perfil encara no ha estat verificat. Verifiqueu-lo abans de continuar." #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.notifications.recovery-token-sent" -msgstr "Hem enviat un link de recuperació de contrasenya al teu email." +msgstr "S'ha enviat un enllaç de recuperació de contrasenya al vostre correu." #: src/app/main/ui/auth/verify_token.cljs msgid "auth.notifications.team-invitation-accepted" -msgstr "T'has unit al equip" +msgstr "Us heu unit a l'equip correctament" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.password" @@ -114,94 +114,94 @@ msgstr "Com a mínim 8 caràcters" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-submit" -msgstr "Recuperar contrasenya" +msgstr "Recupera la contrasenya" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-subtitle" -msgstr "T'enviarem un correu electrónic amb instruccions" +msgstr "Rebreu un correu electrònic amb instruccions" #: src/app/main/ui/auth/recovery_request.cljs msgid "auth.recovery-request-title" -msgstr "Has oblidat la teva contrasenya?" +msgstr "Heu oblidat la contrasenya?" #: src/app/main/ui/auth/recovery.cljs msgid "auth.recovery-submit" -msgstr "Canvia la teva contrasenya" +msgstr "Canvieu la contrasenya" #: src/app/main/ui/auth/login.cljs msgid "auth.register" -msgstr "Encara no tens compte?" +msgstr "No teniu un compte?" #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs msgid "auth.register-submit" -msgstr "Crea un compte" +msgstr "Creeu un compte" #: src/app/main/ui/auth/register.cljs msgid "auth.register-subtitle" -msgstr "Es gratuit, es Open Source" +msgstr "És gratuït, és de codi obert" #: src/app/main/ui/auth/register.cljs msgid "auth.register-title" -msgstr "Crea un compte" +msgstr "Creeu un compte" #: src/app/main/ui/auth.cljs msgid "auth.sidebar-tagline" -msgstr "La solució de codi obert per disenyar i prototipar" +msgstr "La solució de codi obert per dissenyar i prototipar." #: src/app/main/ui/auth/register.cljs msgid "auth.verification-email-sent" -msgstr "Em enviat un correu de verificació a" +msgstr "S'ha enviat un correu de verificació a" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" -msgstr "Afegeix una Biblioteca Compartida" +msgstr "Afegeix una biblioteca compartida" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.change-email" -msgstr "Canviar correu" +msgstr "Canvia el correu" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.create-new-team" -msgstr "+ Crear un nou equip" +msgstr "+ Crea un nou equip" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.default-team-name" -msgstr "El teu Penpot" +msgstr "El vostre Penpot" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.delete-team" -msgstr "Suprimir equip" +msgstr "Suprimeix l'equip" msgid "dashboard.draft-title" msgstr "Esborrany" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.empty-files" -msgstr "Encara no hi ha cap arxiu aquí" +msgstr "Encara no teniu cap arxiu aquí" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" -msgstr "Convidar a l'equip" +msgstr "Convida a l'equip" #: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.leave-team" -msgstr "Abandonar l'equip" +msgstr "Deixa l'equip" #: src/app/main/ui/dashboard/libraries.cljs msgid "dashboard.libraries-title" -msgstr "Biblioteques Compartides" +msgstr "Biblioteques compartides" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.loading-files" -msgstr "carregan els teus fitxers" +msgstr "S'estan carregant els fitxers…" #: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs msgid "dashboard.new-file" -msgstr "+ Nou Arxiu" +msgstr "+ Fitxer nou" #: src/app/main/ui/dashboard/projects.cljs msgid "dashboard.new-project" -msgstr "+ Nou projecte" +msgstr "+ Projecte nou" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" @@ -213,11 +213,11 @@ msgstr "Els projectes fixats apareixeran aquí" #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-changed-successfully" -msgstr "La teva adreça de correu s'ha actualizat" +msgstr "S'ha actualitzat l'adreça de correu" #: src/app/main/ui/auth/verify_token.cljs msgid "dashboard.notifications.email-verified-successfully" -msgstr "La teva adreça de correu ha sigut verificada" +msgstr "S'ha verificat l'adreça de correu" #: src/app/main/ui/settings/password.cljs msgid "dashboard.notifications.password-saved" @@ -241,11 +241,11 @@ msgstr "Promoure a propietari" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.remove-account" -msgstr "Vols esborrar el teu compte?" +msgstr "Voleu eliminar el vostre compte?" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.remove-shared" -msgstr "Elimina com Biblioteca Compartida" +msgstr "Elimina com a biblioteca compartida" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.search-placeholder" @@ -253,7 +253,7 @@ msgstr "Cerca…" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.searching-for" -msgstr "S'está cercant “%s“…" +msgstr "S'està cercant “%s“…" #: src/app/main/ui/settings/options.cljs msgid "dashboard.select-ui-language" @@ -265,11 +265,11 @@ msgstr "Selecciona un tema" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.show-all-files" -msgstr "Veure tots els fitxers" +msgstr "Mostra tots els fitxers" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.switch-team" -msgstr "Cambiar d'equip" +msgstr "Canvia d'equip" #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.team-info" @@ -285,47 +285,47 @@ msgstr "Projectes de l'equip" #: src/app/main/ui/settings/options.cljs msgid "dashboard.theme-change" -msgstr "Tema de l'interfície" +msgstr "Tema de la interfície" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.title-search" -msgstr "Membres de l'equip" +msgstr "Resultats de la cerca" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.type-something" -msgstr "Escriu per cercar resultats" +msgstr "Escriviu per cercar resultats" #: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" -msgstr "Actualitzar opcions" +msgstr "Actualitza la configuració" #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" -msgstr "El teu compte" +msgstr "El vostre compte" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.your-email" -msgstr "Correu electrónic" +msgstr "Correu electrònic" #: src/app/main/ui/settings/profile.cljs msgid "dashboard.your-name" -msgstr "El teu nom" +msgstr "Nom" #: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.your-penpot" -msgstr "El teu Penpot" +msgstr "El vostre Penpot" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-cancel" -msgstr "Cancel·lar" +msgstr "Cancel·la" #: src/app/main/ui/confirm.cljs msgid "ds.confirm-ok" -msgstr "Ok" +msgstr "D'acord" #: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs msgid "ds.confirm-title" -msgstr "Estàs segur?" +msgstr "N'esteu segur?" #: src/app/main/ui/dashboard/grid.cljs msgid "ds.updated-at" @@ -333,19 +333,19 @@ msgstr "Actualitzat: %s" #: src/app/main/data/workspace.cljs msgid "errors.clipboard-not-implemented" -msgstr "El teu navegador no pot realitzar aquesta operació" +msgstr "El vostre navegador no pot fer aquesta operació" #: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs msgid "errors.email-already-exists" -msgstr "El correu ja està en ús" +msgstr "Aquest correu ja està en ús" #: src/app/main/ui/auth/verify_token.cljs msgid "errors.email-already-validated" -msgstr "El correu ja està validat" +msgstr "Aquest correu ja està validat." #: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.email-has-permanent-bounces" -msgstr "El correu «%s» té molts informes de rebot permanents" +msgstr "El correu «%s» té molts informes de retorn permanents." #: src/app/main/ui/settings/change_email.cljs msgid "errors.email-invalid-confirmation" @@ -357,31 +357,32 @@ msgstr "Alguna cosa ha anat malament" #: src/app/main/ui/auth/login.cljs msgid "errors.google-auth-not-enabled" -msgstr "L'autenticació amb google ha estat desactivada a aquest servidor" +msgstr "L'autenticació amb Google està desactivada en aquest servidor" msgid "errors.media-format-unsupported" -msgstr "El format d'imatge no està suportat (deu ser svg, jpg o png)," +msgstr "El format d'imatge no està suportat (ha de ser SVG, JPG o PNG)." #: src/app/main/data/workspace/persistence.cljs msgid "errors.media-too-large" -msgstr "La imatge es massa gran (ha de tenir menys de 5 mb)." +msgstr "La imatge és massa gran (ha de ser inferior a 5 MB)." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-mismatch" -msgstr "Sembla que el contingut de la imatge no coincideix amb l'extensió del arxiu" +msgstr "" +"Sembla que el contingut de la imatge no coincideix amb l'extensió del fitxer." #: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs msgid "errors.media-type-not-allowed" -msgstr "La imatge no sembla pas vàlida" +msgstr "Sembla que la imatge no és vàlida." #: src/app/main/ui/dashboard/team.cljs msgid "errors.member-is-muted" msgstr "" -"El perfil que estàs invitant té els emails mutejats (per informes de spam o " -"rebots alts" +"El perfil que estàs convidant té els missatges de correu silenciats (per " +"informes de correu brossa o de retorns alts)." msgid "errors.network" -msgstr "Impossible connectar amb el servidor principal" +msgstr "No es pot connectar amb el servidor principal." #: src/app/main/ui/settings/password.cljs msgid "errors.password-invalid-confirmation" @@ -389,15 +390,17 @@ msgstr "La contrasenya de confirmació ha de coincidir" #: src/app/main/ui/settings/password.cljs msgid "errors.password-too-short" -msgstr "La contrasenya ha de tenir 8 com a mínim 8 caràcters" +msgstr "La contrasenya ha de tenir 8 caràcters com a mínim" #: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs msgid "errors.profile-is-muted" -msgstr "El teu perfil te els emails mutejats (per informes de spam o rebots alts)." +msgstr "" +"El teu perfil té els missatges de correu silenciats (per informes de correu " +"brossa o de retorn alts)." #: src/app/main/ui/auth/register.cljs msgid "errors.registration-disabled" -msgstr "El registre està desactivat actualment" +msgstr "El registre està desactivat" #: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs msgid "errors.unexpected-error" @@ -429,7 +432,7 @@ msgstr "Descripció" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discussions-go-to" -msgstr "" +msgstr "Ves als debats" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discussions-subtitle1" @@ -443,7 +446,7 @@ msgstr "" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discussions-title" -msgstr "" +msgstr "Debats de l'equip" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" @@ -467,6 +470,9 @@ msgstr "S'ha produït un error" msgid "labels.accept" msgstr "Acceptar" +msgid "labels.go-back" +msgstr "Enrere" + msgid "labels.recent" msgstr "Recent" @@ -479,4 +485,335 @@ msgstr "Projectes - %s - Penpot" #: src/app/main/ui/dashboard/search.cljs msgid "title.dashboard.search" -msgstr "Cerca - %s - Penpot" \ No newline at end of file +msgstr "Cerca - %s - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "errors.token-expired" +msgstr "El codi ha caducat" + +msgid "errors.terms-privacy-agreement-invalid" +msgstr "" +"Heu d'acceptar les nostres condicions del servei i la política de privacitat." + +#: src/app/main/ui/auth/login.cljs +msgid "errors.ldap-disabled" +msgstr "L'autenticació LDAP està inhabilitada." + +#: src/app/main/ui/components/color_input.cljs +msgid "errors.invalid-color" +msgstr "El color no és vàlid" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-move-project" +msgstr "S'ha mogut el projecte" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-files" +msgstr "S'han mogut els fitxers" + +#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-move-file" +msgstr "S'ha mogut el fitxer" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-duplicate-project" +msgstr "S'ha eliminat el projecte" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-duplicate-file" +msgstr "S'ha duplicat el fitxer" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.success-delete-project" +msgstr "S'ha eliminat el projecte" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.success-delete-file" +msgstr "S'ha eliminat el fitxer" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "dashboard.pin-unpin" +msgstr "Fixa/Deixa de fixar" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.open-in-new-tab" +msgstr "Obre el fitxer en una pestanya nova" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-project-prefix" +msgstr "Projecte nou" + +#: src/app/main/data/dashboard.cljs +msgid "dashboard.new-file-prefix" +msgstr "Fitxer nou" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-other-team" +msgstr "Mou a un altre equip" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to-multi" +msgstr "Mou %s fitxers a" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.move-to" +msgstr "Mou a" + +#, markdown +msgid "dashboard.fonts.hero-text2" +msgstr "" +"Només podeu pujar tipus de lletra de la vostra propietat o dels que tingueu " +"una llicència que us permeti utilitzar-los al Penpot. Teniu més informació a " +"la secció de drets de contingut de les [Condicions del servei del " +"Penpot](https://penpot.app/terms.html). També podeu llegir sobre [" +"llicenciament de tipus de lletra](https://www.typography.com/faq)." + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Els tipus de lletra web que pengeu aquí s'afegiran a la llista de famílies " +"tipogràfiques disponibles a les propietats del text dels fitxers d'aquest " +"equip. Els tipus de lletra amb el mateix nom de família s'agruparan en **una " +"sola família tipogràfica**. Podeu pujar tipus de lletra amb aquests formats: " +"**TTF, OTF i WOFF** (només és necessari un)." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Duplica %s fitxers" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Duplica" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(còpia)" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"En crear un nou compte, accepteu les nostres condicions del servei i la " +"política de privacitat." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Entreu amb OpenID (SSO)" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Entreu amb Google" + +#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs +msgid "labels.admin" +msgstr "Administració" + +msgid "history.alert-message" +msgstr "Esteu veient la versió %s" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.info" +msgstr "Informació" + +msgid "handoff.tabs.code.selected.text" +msgstr "Text" + +msgid "handoff.tabs.code.selected.svg-raw" +msgstr "SVG" + +msgid "handoff.tabs.code.selected.rect" +msgstr "Rectangle" + +msgid "handoff.tabs.code.selected.path" +msgstr "Camí" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code.selected.multiple" +msgstr "%s seleccionats" + +msgid "handoff.tabs.code.selected.image" +msgstr "Imatge" + +msgid "handoff.tabs.code.selected.group" +msgstr "Grup" + +msgid "handoff.tabs.code.selected.frame" +msgstr "Taula de treball" + +msgid "handoff.tabs.code.selected.curve" +msgstr "Corba" + +msgid "handoff.tabs.code.selected.circle" +msgstr "Cercle" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.letter-spacing" +msgstr "Espaiat de la lletra" + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "Codi" + +msgid "handoff.attributes.typography.text-transform.uppercase" +msgstr "Majúscules" + +msgid "handoff.attributes.typography.text-transform.titlecase" +msgstr "Inicials en majúscules" + +msgid "handoff.attributes.typography.text-transform.none" +msgstr "Cap" + +msgid "handoff.attributes.typography.text-transform.lowercase" +msgstr "Minúscules" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-transform" +msgstr "Transformació del text" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "Subratllat" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "Barrat" + +msgid "handoff.attributes.typography.text-decoration.none" +msgstr "Cap" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-decoration" +msgstr "Decoració del text" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "Alçada de la línia" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "Estil de la lletra" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-size" +msgstr "Mida de la lletra" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-family" +msgstr "Família tipogràfica" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography" +msgstr "Tipografia" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "Amplada" + +msgid "handoff.attributes.stroke.style.solid" +msgstr "Sòlid" + +msgid "handoff.attributes.stroke.style.none" +msgstr "Cap" + +msgid "handoff.attributes.stroke.style.mixed" +msgstr "Mesclat" + +msgid "handoff.attributes.stroke.style.dotted" +msgstr "Puntejat" + +#, permanent +msgid "handoff.attributes.stroke.alignment.outer" +msgstr "Exterior" + +#, permanent +msgid "handoff.attributes.stroke.alignment.inner" +msgstr "Interior" + +#, permanent +msgid "handoff.attributes.stroke.alignment.center" +msgstr "Centre" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke" +msgstr "Traç" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.spread" +msgstr "S" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-y" +msgstr "Y" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-x" +msgstr "X" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.blur" +msgstr "B" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "Ombra" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "Amplada" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.top" +msgstr "Superior" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.rotation" +msgstr "Rotació" + +#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.radius" +msgstr "Radi" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "Esquerra" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "Alçada" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout" +msgstr "Disposició" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.width" +msgstr "Amplada" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.height" +msgstr "Alçada" + +#: src/app/main/ui/handoff/attributes/image.cljs +msgid "handoff.attributes.image.download" +msgstr "Baixa la imatge original" + +#: src/app/main/ui/handoff/attributes/fill.cljs +msgid "handoff.attributes.fill" +msgstr "Emplenat" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.rgba" +msgstr "RGBA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hsla" +msgstr "HSLA" + +#: src/app/main/ui/handoff/attributes/common.cljs +msgid "handoff.attributes.color.hex" +msgstr "HEX" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur.value" +msgstr "Valor" + +#: src/app/main/ui/handoff/attributes/blur.cljs +msgid "handoff.attributes.blur" +msgstr "Difuminat" diff --git a/frontend/translations/da.po b/frontend/translations/da.po index 2847eee0e6..1f15d4514c 100644 --- a/frontend/translations/da.po +++ b/frontend/translations/da.po @@ -51,10 +51,6 @@ msgstr "Glemt adgangskode?" msgid "auth.fullname" msgstr "Fulde Navn" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Gå tilbage!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Log på her" @@ -452,6 +448,9 @@ msgstr "Stil" msgid "labels.fonts" msgstr "Skrifttyper" +msgid "labels.go-back" +msgstr "Gå tilbage!" + msgid "labels.installed-fonts" msgstr "Installeret skrifttyper" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index c5179aefad..4763752014 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1,9 +1,9 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-05-13 08:44+0000\n" -"Last-Translator: Andrey Antukh \n" -"Language-Team: German " -"\n" +"PO-Revision-Date: 2021-05-25 12:31+0000\n" +"Last-Translator: Yannik Rödel \n" +"Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -51,10 +51,6 @@ msgstr "Passwort vergessen?" msgid "auth.fullname" msgstr "Vollständiger Name" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Zurück!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Hier einloggen" @@ -823,6 +819,9 @@ msgstr "Feedback gesendet" msgid "labels.give-feedback" msgstr "Feedback geben" +msgid "labels.go-back" +msgstr "Zurück!" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Erledigte Kommentare ausblenden" @@ -1306,6 +1305,10 @@ msgstr "Seite bearbeiten" msgid "viewer.header.fullscreen" msgstr "Vollbildmodus" +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interaktionen" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Link kopieren" @@ -1328,7 +1331,7 @@ msgstr "Jeder mit dem Link hat Zugriff" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.title" -msgstr "Link teilen" +msgstr "Prototyp teilen" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" @@ -2409,4 +2412,31 @@ msgid "workspace.updates.update" msgstr "Aktualisieren" msgid "workspace.viewport.click-to-close-path" -msgstr "Klicken Sie, um den Pfad zu schließen" \ No newline at end of file +msgstr "Klicken Sie, um den Pfad zu schließen" + +#, markdown +msgid "dashboard.fonts.hero-text1" +msgstr "" +"Jede Webschriftart, die Sie hier hochladen, wird der Liste der Schriftarten " +"hinzugefügt, die in den Texteigenschaften der Dateien dieses Teams verfügbar " +"ist. Schriftarten mit dem gleichen Schriftfamilien-Namen werden als **eine " +"einzige Schriftfamilie** gruppiert. Sie können Schriftarten in den folgenden " +"Formaten hochladen: **TTF, OTF und WOFF** (nur eine wird benötigt)." + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "%s Dateien duplizieren" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Wenn Sie ein neues Konto erstellen, stimmen Sie unseren Nutzungsbedingungen " +"und Datenschutzrichtlinien zu." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Einloggen mit OpenID (SSO)" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Einloggen mit Google" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index d5b7822500..b8a58c0116 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -46,10 +46,6 @@ msgstr "Ξεχάσατε τον κωδικό;" msgid "auth.fullname" msgstr "Πλήρες όνομα" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Πίσω" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Συνδεθείτε εδώ" @@ -823,6 +819,9 @@ msgstr "Εστάλη γνώμη" msgid "labels.give-feedback" msgstr "Δώστε μας τη γνώμη σας" +msgid "labels.go-back" +msgstr "Πίσω" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Απόκρυψη επιλυμένων σχολίων" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 323724446c..3c07c0e8a7 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -49,10 +49,6 @@ msgstr "Forgot password?" msgid "auth.fullname" msgstr "Full Name" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Go back!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Login here" @@ -93,6 +89,10 @@ msgstr "Login with OpenID (SSO)" msgid "auth.new-password" msgstr "Type a new password" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "I agree to subscribe to the Penpot mailing list." + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" msgstr "The recovery token is invalid." @@ -206,6 +206,47 @@ msgstr "Duplicate %s files" msgid "dashboard.empty-files" msgstr "You still have no files here" +msgid "dashboard.export-multi" +msgstr "Export %s files" + +msgid "dashboard.export-single" +msgstr "Export file" + +msgid "dashboard.export.detail" +msgstr "* Might include components, graphics, colors and/or typographies." + +msgid "dashboard.export.explain" +msgstr "" +"One or more files that you want to export are using shared libraries. What " +"do you want to do with their assets*?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"files with shared libraries will be included in the export, maintaining " +"their linkage." + +msgid "dashboard.export.options.all.title" +msgstr "Export shared libraries" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Shared libraries will not be included in the export and no assets will be " +"added to the library. " + +msgid "dashboard.export.options.detach.title" +msgstr "Treat shared library assets as basic objects" + +msgid "dashboard.export.options.merge.message" +msgstr "" +"Your file will be exported with all external assets merged into the file " +"library." + +msgid "dashboard.export.options.merge.title" +msgstr "Include shared library assets in file libraries" + +msgid "dashboard.export.title" +msgstr "Export files" + msgid "dashboard.fonts.deleted-placeholder" msgstr "Font deleted" @@ -229,6 +270,18 @@ msgstr "" "Service](https://penpot.app/terms.html). You also might want to read about " "[font licensing](https://www.typography.com/faq)." +msgid "dashboard.import" +msgstr "Import files" + +msgid "dashboard.import.analyze-error" +msgstr "Oops! We couldn't import this file" + +msgid "dashboard.import.import-error" +msgstr "There was a problem importing the file. The file wasn't imported." + +msgid "dashboard.import.import-message" +msgstr "%s files have been imported succesfully." + #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" msgstr "Invite to team" @@ -304,6 +357,9 @@ msgstr "%s members" msgid "dashboard.open-in-new-tab" msgstr "Open file in a new tab" +msgid "dashboard.options" +msgstr "Options" + #: src/app/main/ui/settings/password.cljs msgid "dashboard.password-change" msgstr "Change password" @@ -799,6 +855,9 @@ msgstr "You are seeing version %s" msgid "labels.accept" msgstr "Accept" +msgid "labels.close" +msgstr "Close" + msgid "labels.add-custom-font" msgstr "Add custom font" @@ -838,6 +897,9 @@ msgstr "Confirm password" msgid "labels.content" msgstr "Content" +msgid "labels.continue" +msgstr "Continue" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Create" @@ -889,6 +951,9 @@ msgstr "Editor" msgid "labels.email" msgstr "Email" +msgid "labels.export" +msgstr "Export" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" msgstr "Feedback disabled" @@ -913,6 +978,9 @@ msgstr "Fonts" msgid "labels.give-feedback" msgstr "Give feedback" +msgid "labels.go-back" +msgstr "Go back" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Hide resolved comments" @@ -1000,6 +1068,9 @@ msgstr "Old password" msgid "labels.only-yours" msgstr "Only yours" +msgid "labels.or" +msgstr "or" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.owner" msgstr "Owner" @@ -1454,13 +1525,17 @@ msgid "viewer.header.dont-show-interactions" msgstr "Don't show interactions" #: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.edit-page" -msgstr "Edit page" +msgid "viewer.header.edit-file" +msgstr "Edit file" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.fullscreen" msgstr "Full Screen" +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interactions" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Copy link" @@ -1483,7 +1558,7 @@ msgstr "Anyone with the link will have access" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.title" -msgstr "Share link" +msgstr "Share prototype" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" @@ -1596,6 +1671,10 @@ msgstr "No assets found" msgid "workspace.assets.rename" msgstr "Rename" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "Rename group" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" msgstr "Search assets" @@ -1646,6 +1725,10 @@ msgstr "Ag" msgid "workspace.assets.typography.text-transform" msgstr "Text Transform" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "Ungroup" + #: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Linear gradient" @@ -1872,6 +1955,46 @@ msgstr "Canvas background" msgid "workspace.options.component" msgstr "Component" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "Constraints" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "Bottom" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "Center" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "Fix when scrolling" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "Left" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.leftright" +msgstr "Left & Right" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "Right" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "Scale" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "Top" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.topbottom" +msgstr "Top & Bottom" + #: src/app/main/ui/workspace/sidebar/options.cljs msgid "workspace.options.design" msgstr "Design" @@ -2626,4 +2749,4 @@ msgid "workspace.updates.update" msgstr "Update" msgid "workspace.viewport.click-to-close-path" -msgstr "Click to close the path" \ No newline at end of file +msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b3a28baf65..d6c2bc4e97 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-04-14 13:44+0000\n" -"Last-Translator: Andrés Moya \n" +"PO-Revision-Date: 2021-06-18 09:19+0000\n" +"Last-Translator: andy \n" "Language-Team: Spanish " "\n" "Language: es\n" @@ -9,7 +9,7 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.7-dev\n" +"X-Generator: Weblate 4.7\n" #: src/app/main/ui/auth/register.cljs msgid "auth.already-have-account" @@ -51,10 +51,6 @@ msgstr "¿Olvidaste tu contraseña?" msgid "auth.fullname" msgstr "Nombre completo" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Volver" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Entra aquí" @@ -69,7 +65,7 @@ msgstr "Introduce tus datos aquí" #: src/app/main/ui/auth/login.cljs msgid "auth.login-title" -msgstr "Encantados de volverte a ver" +msgstr "¡Encantados de verte de nuevo!" #: src/app/main/ui/auth/login.cljs msgid "auth.login-with-github-submit" @@ -95,6 +91,10 @@ msgstr "Entrar con OpenID (SSO)" msgid "auth.new-password" msgstr "Introduce la nueva contraseña" +#: src/app/main/ui/auth/register.cljs +msgid "auth.newsletter-subscription" +msgstr "Estoy de acuerdo en suscribirme a la lista de correo de Penpot" + #: src/app/main/ui/auth/recovery.cljs msgid "auth.notifications.invalid-token-error" msgstr "El código de recuperación no es válido." @@ -210,6 +210,41 @@ msgstr "Duplicar %s archivos" msgid "dashboard.empty-files" msgstr "Todavía no hay ningún archivo aquí" +msgid "dashboard.export.detail" +msgstr "* Pueden incluir components, gráficos, colores y/o tipografias." + +msgid "dashboard.export.explain" +msgstr "" +"Uno o mas ficheros que quieres exportar usan librerias compartidas. ¿Qué " +"quieres hacer con los recursos*?" + +msgid "dashboard.export.options.all.message" +msgstr "" +"ficheros con librerias compartidas se inclurán en el paquete de exportación " +"y mantendrán los enlaces." + +msgid "dashboard.export.options.all.title" +msgstr "Exportar librerias compartidas" + +msgid "dashboard.export.options.detach.message" +msgstr "" +"Las librerias compartidas no se incluirán en la exportación y ningún " +"recurso será incluido en la librería." + +msgid "dashboard.export.options.detach.title" +msgstr "Usar los recursos como objetos básicos." + +msgid "dashboard.export.options.merge.message" +msgstr "" +"Tu fichero será exportado con todos los recursos dentro de la libreria del " +"propio fichero." + +msgid "dashboard.export.options.merge.title" +msgstr "Incluir librerias compartidas dentro de las librerias del fichero." + +msgid "dashboard.export.title" +msgstr "Exportar ficheros" + msgid "dashboard.fonts.deleted-placeholder" msgstr "Fuente eliminada." @@ -219,19 +254,29 @@ msgstr "Aun no tienes fuentes personalizadas." #, markdown msgid "dashboard.fonts.hero-text1" msgstr "" -"Any web font you upload here will be added to the font family list " -"available at the text properties of the files of this team. Fonts with the " -"same font family name will be grouped as a **single font family**. You can " -"upload fonts with the following formats: **TTF, OTF and WOFF** (only one " -"will be needed)." +"Cualquier fuente personalizada añadida aquí aparecerá en la lista de " +"familias de fuentes disponible en las propiedades de texto de los archivos " +"de este equipo. Las fuentes con el mismo nombre de familia serán agrupadas " +"como una **única familia de fuentes**. Se pueden cargar fuentes con los " +"siguientes formatos: **TTF, OTF and WOFF** (con uno es suficiente)." #, markdown msgid "dashboard.fonts.hero-text2" msgstr "" -"You should only upload fonts you own or have license to use in Penpot. Find " -"out more in the Content rights section of [Penpot's Terms of " -"Service](https://penpot.app/terms.html). You also might want to read about " -"[font licensing](2)." +"Sólo deberías cargar fuentes que te pertenecen o de las que tienes una " +"licencia que te permita usarlas en Penpot. Encuentra más información en la " +"sección de Derechos de Contenido: [Penpot's Terms of Service](1). También " +"te puede interesar leer más sobre licencias tipográficas: [font " +"licensing](2)." + +msgid "dashboard.import.analyze-error" +msgstr "¡Vaya! No hemos podido importar el fichero." + +msgid "dashboard.import.import-error" +msgstr "Hubo un problema importando el fichero. No se ha creado el fichero." + +msgid "dashboard.import.import-message" +msgstr "%s ficheros han sido importados con éxito." #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" @@ -350,7 +395,7 @@ msgstr "Selecciona un tema" #: src/app/main/ui/dashboard/grid.cljs msgid "dashboard.show-all-files" -msgstr "Ver todos los ficheros" +msgstr "Ver todos los archivos" #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.success-delete-file" @@ -474,7 +519,7 @@ msgstr "Autenticación con google esta dehabilitada en el servidor" #: src/app/main/ui/components/color_input.cljs msgid "errors.invalid-color" -msgstr "Color inválido" +msgstr "Color no válido" #: src/app/main/ui/auth/login.cljs msgid "errors.ldap-disabled" @@ -570,10 +615,12 @@ msgstr "Entra al foro colaborativo de Penpot" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discussions-subtitle2" msgstr "" +"Puedes hacer preguntas y dar respuestas, participar en conversaciones " +"abiertas y hacer seguimiento de decisones que afectan al proyecto." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.discussions-title" -msgstr "" +msgstr "Debates de equipo" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subject" @@ -582,6 +629,9 @@ msgstr "Asunto" #: src/app/main/ui/settings/feedback.cljs msgid "feedback.subtitle" msgstr "" +"Por favor describe el motivo de tu mensaje, especificando si es un " +"problema, una idea o una duda. Alguien de nuestro equipo responderá tan " +"pronto como sea posible." #: src/app/main/ui/settings/feedback.cljs msgid "feedback.title" @@ -805,6 +855,9 @@ msgstr "Estás viendo la versión %s" msgid "labels.accept" msgstr "Aceptar" +msgid "labels.close" +msgstr "Cerrar" + msgid "labels.add-custom-font" msgstr "Añadir fuentes personalizada" @@ -844,6 +897,9 @@ msgstr "Confirmar contraseña" msgid "labels.content" msgstr "Contenido" +msgid "labels.continue" +msgstr "Continuar" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Crear" @@ -852,6 +908,10 @@ msgstr "Crear" msgid "labels.create-team" msgstr "Crea un nuevo equipo" +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.create-team.placeholder" +msgstr "Introduce un nuevo nombre de equipo" + msgid "labels.custom-fonts" msgstr "Fuentes personalizadas" @@ -891,6 +951,9 @@ msgstr "Editor" msgid "labels.email" msgstr "Correo electrónico" +msgid "labels.export" +msgstr "Exportar" + #: src/app/main/ui/settings/feedback.cljs msgid "labels.feedback-disabled" msgstr "El modulo de recepción de opiniones esta deshabilitado." @@ -915,6 +978,9 @@ msgstr "Fuentes" msgid "labels.give-feedback" msgstr "Danos tu opinión" +msgid "labels.go-back" +msgstr "Volver" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Ocultar comentarios resueltos" @@ -1002,6 +1068,9 @@ msgstr "Contraseña anterior" msgid "labels.only-yours" msgstr "Sólo los tuyos" +msgid "labels.or" +msgstr "o" + #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs msgid "labels.owner" msgstr "Dueño" @@ -1039,7 +1108,7 @@ msgstr "Renombrar" #: src/app/main/ui/dashboard/team_form.cljs msgid "labels.rename-team" -msgstr "Renomba el equipo" +msgstr "Renombra el equipo" #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" @@ -1061,7 +1130,7 @@ msgstr "Enviar" #: src/app/main/ui/settings/feedback.cljs msgid "labels.sending" -msgstr "Enviando..." +msgstr "Enviando…" #: src/app/main/ui/static.cljs msgid "labels.service-unavailable.desc-message" @@ -1100,13 +1169,13 @@ msgid "labels.update-team" msgstr "Actualiza el equipo" msgid "labels.upload" -msgstr "Subir" +msgstr "Cargar" msgid "labels.upload-custom-fonts" -msgstr "Subir fuente" +msgstr "Cargar fuente" msgid "labels.uploading" -msgstr "Subiendo..." +msgstr "Subiendo…" #: src/app/main/ui/dashboard/team.cljs msgid "labels.viewer" @@ -1218,8 +1287,8 @@ msgstr "Eliminando estilo de fuente" msgid "modals.delete-font.message" msgstr "" -"Estas seguro de querer eliminar esta fuente? Dejara de cargar si es usada " -"en algun fichero." +"¿Seguro que quieres eliminar esta fuente? Si está siendo usada en algún " +"fichero no se cargará." msgid "modals.delete-font.title" msgstr "Eliminando fuente" @@ -1375,6 +1444,10 @@ msgstr "Perfil guardado correctamente!" msgid "notifications.validation-email-sent" msgstr "Verificación de email enviada a %s. Comprueba tu correo." +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "Ir al login" + #: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs msgid "settings.multiple" msgstr "Varios" @@ -1444,13 +1517,17 @@ msgid "viewer.header.dont-show-interactions" msgstr "No mostrar interacciones" #: src/app/main/ui/viewer/header.cljs -msgid "viewer.header.edit-page" -msgstr "Editar página" +msgid "viewer.header.edit-file" +msgstr "Editar archivo" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.fullscreen" msgstr "Pantalla completa" +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interacciones" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Copiar enlace" @@ -1473,7 +1550,7 @@ msgstr "Cualquiera con el enlace podrá acceder" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.title" -msgstr "Enlace" +msgstr "Compartir prototipo" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" @@ -1588,6 +1665,10 @@ msgstr "No se encontraron recursos" msgid "workspace.assets.rename" msgstr "Renombrar" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename-group" +msgstr "Renombrar grupo" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "workspace.assets.search" msgstr "Buscar recursos" @@ -1638,6 +1719,10 @@ msgstr "Ag" msgid "workspace.assets.typography.text-transform" msgstr "Transformar texto" +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.ungroup" +msgstr "Desagrupar" + #: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs msgid "workspace.gradients.linear" msgstr "Degradado lineal" @@ -1740,7 +1825,7 @@ msgstr "Añadir" #: src/app/main/ui/workspace/libraries.cljs msgid "workspace.libraries.colors" -msgstr "%s colors" +msgstr "%s colores" #: src/app/main/ui/workspace/colorpalette.cljs msgid "workspace.libraries.colors.big-thumbnails" @@ -1864,6 +1949,46 @@ msgstr "Color de fondo" msgid "workspace.options.component" msgstr "Componente" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints" +msgstr "Restricciones" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.bottom" +msgstr "Abajo" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.center" +msgstr "Centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.fix-when-scrolling" +msgstr "Fijo al desplazar" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.left" +msgstr "Izquierda" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.leftright" +msgstr "Izq. y Der." + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.right" +msgstr "Derecha" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.scale" +msgstr "Escalar" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.top" +msgstr "Arriba" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.constraints.topbottom" +msgstr "Arriba y Abajo" + #: src/app/main/ui/workspace/sidebar/options.cljs msgid "workspace.options.design" msgstr "Diseño" @@ -2620,4 +2745,4 @@ msgid "workspace.updates.update" msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" -msgstr "Pulsar para cerrar la ruta" \ No newline at end of file +msgstr "Pulsar para cerrar la ruta" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 34e70bab41..b43da10802 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -51,10 +51,6 @@ msgstr "Mot de passe oublié ?" msgid "auth.fullname" msgstr "Nom complet" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Retour !" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Se connecter ici" @@ -737,6 +733,9 @@ msgstr "Adresse e‑mail" msgid "labels.give-feedback" msgstr "Donnez votre avis" +msgid "labels.go-back" +msgstr "Retour" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Masquer les commentaires résolus" @@ -1202,6 +1201,10 @@ msgstr "Modifier la page" msgid "viewer.header.fullscreen" msgstr "Plein écran" +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interactions" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Copier le lien" @@ -1224,7 +1227,7 @@ msgstr "Toute personne disposant du lien aura accès" #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.title" -msgstr "Lien de partage" +msgstr "Partager le prototype" #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.show-interactions" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index b1fc23594e..5210b494db 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -1,6 +1,210 @@ msgid "" msgstr "" -"X-Generator: Weblate\n" +"PO-Revision-Date: 2021-05-23 21:33+0000\n" +"Last-Translator: luthfi azhari \n" +"Language-Team: Indonesian \n" +"Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" \ No newline at end of file +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.7-dev\n" + +#: src/app/main/ui/dashboard/grid.cljs +msgid "dashboard.empty-files" +msgstr "Anda belum memiliki file disini" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate-multi" +msgstr "Duplikasi % file" + +#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.duplicate" +msgstr "Duplikat" + +msgid "dashboard.draft-title" +msgstr "Konsep" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.delete-team" +msgstr "Hapus tim" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.default-team-name" +msgstr "Penpot anda" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "dashboard.create-new-team" +msgstr "+ Buat tim baru" + +#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs +msgid "dashboard.copy-suffix" +msgstr "(salin)" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.change-email" +msgstr "Ubah email" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "dashboard.add-shared" +msgstr "Tambahkan sebagai pustaka bersama" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.verification-email-sent" +msgstr "Kami mengirim verifikasi ke surel anda" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-title" +msgstr "Buat akun baru" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.register-subtitle" +msgstr "Ini gratis, Open Source" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.register-submit" +msgstr "Buat akun baru" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.register" +msgstr "Tidak ada akun?" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.recovery-submit" +msgstr "Ubah kata sandi anda" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-title" +msgstr "Lupa kata sandi?" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-subtitle" +msgstr "Kami akan mengirimi anda surel dengan intruksi" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.recovery-request-submit" +msgstr "Pemulihan Kata sandi" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-length-hint" +msgstr "Paling tidak 8 karakter" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.check-your-email" +msgstr "" +"Cek surel anda dan klik pada tautan tersebut untuk verifikasi dan mulai " +"gunakan Penpot." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.confirm-password" +msgstr "Konfirmasi kata sandi" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.terms-privacy-agreement" +msgstr "" +"Ketika membuat akun baru, anda menyetujui persyaratan layanan dan kebijakan " +"privasi kami." + +#: src/app/main/ui/auth.cljs +msgid "auth.sidebar-tagline" +msgstr "Solusi open-source untuk desain dan pembuatan prototype." + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.password" +msgstr "Kata sandi" + +#: src/app/main/ui/auth/verify_token.cljs +msgid "auth.notifications.team-invitation-accepted" +msgstr "Berhasil bergabung dengan tim" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.recovery-token-sent" +msgstr "Link pemulihan kata sandi berhasil dikirim ke kotak masuk anda." + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.notifications.profile-not-verified" +msgstr "" +"Akun belum terverifikasi, harap verifikasi profile anda sebelum melanjutkan." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.password-changed-succesfully" +msgstr "Kata sandi berhasil diubah" + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.notifications.invalid-token-error" +msgstr "Token pemulihan tidak valid." + +#: src/app/main/ui/auth/recovery.cljs +msgid "auth.new-password" +msgstr "Ketikkan kata sandi baru" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-oidc-submit" +msgstr "Masuk dengan OpenID (SSO)" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-ldap-submit" +msgstr "Masuk dengan LDAP" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-google-submit" +msgstr "Masuk dengan Google" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-gitlab-submit" +msgstr "Masuk dengan Gitlab" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-with-github-submit" +msgstr "Masuk dengan Github" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-title" +msgstr "Senang bertemu denganmu lagi!" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-subtitle" +msgstr "Masukkan detail anda di bawah ini" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.demo-warning" +msgstr "" +"Ini layanan DEMO, JANGAN GUNAKAN untuk pekerjaan nyata, project ini akan di " +"hapus secara berkala." + +#: src/app/main/ui/auth/login.cljs +msgid "auth.login-submit" +msgstr "Masuk" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.login-here" +msgstr "Masuk disini" + +#: src/app/main/ui/auth/recovery_request.cljs +msgid "auth.go-back-to-login" +msgstr "Kembali!" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.fullname" +msgstr "Nama Lengkap" + +#: src/app/main/ui/auth/login.cljs +msgid "auth.forgot-password" +msgstr "Lupa kata sandi?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.email" +msgstr "Surel" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-profile" +msgstr "Ingin mencobanya?" + +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Buat akun demo" + +#: src/app/main/ui/auth/register.cljs +msgid "auth.already-have-account" +msgstr "Sudah memiliki akun?" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index a00125a3d2..ae9e3d988b 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -1,9 +1,9 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-05-17 21:32+0000\n" +"PO-Revision-Date: 2021-06-15 18:34+0000\n" "Last-Translator: Eranot \n" -"Language-Team: Portuguese (Brazil) " -"\n" +"Language-Team: Portuguese (Brazil) \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -51,10 +51,6 @@ msgstr "Esqueceu a senha?" msgid "auth.fullname" msgstr "Nome completo" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Voltar!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Entrar aqui" @@ -697,6 +693,9 @@ msgstr "Fontes" msgid "labels.give-feedback" msgstr "Enviar feedback" +msgid "labels.go-back" +msgstr "Voltar" + msgid "labels.icons" msgstr "Ícones" @@ -925,4 +924,332 @@ msgstr "Provedores de fonte - %s - Penpot" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "Fontes - %s - Penpot" \ No newline at end of file +msgstr "Fontes - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "Configurações - %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "Membros - %s - Penpot" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "Perfil - Penpot" + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "Senha - Penpot" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "Configurações - Penpot" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "Dê sua opinião - Penpot" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot - Liberdade de design para equipes" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "Bibliotecas Compartilhadas - %s - Penpot" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "Pesquisar - %s - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "Projetos - %s - Penpot" + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "settings.multiple" +msgstr "Misto" + +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "Ir para a página de login" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "E-mail de verificação enviado para %s. Verifique seu e-mail!" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "Perfil salvo com sucesso!" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" +msgstr "" +"Você não pode deletar seu perfil. Designe um novo proprietário para suas " +"equipes antes de continuar." + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "Convite enviado com sucesso" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "Atualizar Componente em uma Biblioteca Compartilhada" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.hint" +msgstr "" +"Você está prestes a atualizar um Componente em uma Biblioteca Compartilhada. " +"Isso pode afetar outros arquivos que a utilizam." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.hint" +msgstr "" +"Depois de removida como Biblioteca Compartilhada, os Componentes deste " +"arquivo deixarão de estar disponível para serem usados com o resto de seus " +"arquivos." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint1" +msgstr "Você é o proprietário de %s." + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "Excluir membro da equipe" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.message" +msgstr "Tem certeza de que deseja excluir este membro da equipe?" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.solid" +msgstr "Sólido" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.outer" +msgstr "Fora" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "Misturado" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.inner" +msgstr "Dentro" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "Pontilhada" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "Tracejada" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "Centro" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +msgid "workspace.options.size-presets" +msgstr "Predefinições de tamanho" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "Tamanho" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "Sombra" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "Sombra interior" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "Sombra projetada" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.blur" +msgstr "Borrar" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "Rotação" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "Cantos individuais" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "Todos cantos" + +msgid "workspace.options.radius" +msgstr "Raio" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "Protótipo" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "Posição" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.none" +msgstr "Nenhum" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.navigate-to" +msgstr "Navegar para" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "Camadas selecionadas" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "Cor" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.title" +msgstr "Grades & Layouts" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "Quadrado" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "Linhas" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "Largura" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "Usar padrão" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "Superior" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "Esticar" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "Direita" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "Esquerda" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "Centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "Inferior" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Tipo" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "Tamanho" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "Definir como padrão" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "Linhas" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "Margem" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "Altura" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "Colunas" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "Colunas" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "Automático" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "Preencher" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.exporting-object" +msgstr "Exportando…" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "Sufixo" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export-object" +msgstr "Exportar forma" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export" +msgstr "Exportar" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "Design" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Componente" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "" + +msgid "workspace.options.blur-options.layer-blur" +msgstr "Camada" + +msgid "workspace.options.blur-options.background-blur" +msgstr "Fundo" + +msgid "workspace.library.store" +msgstr "Bibliotecas da loja" + +msgid "workspace.library.own" +msgstr "Minhas bibliotecas" + +msgid "workspace.library.libraries" +msgstr "Bibliotecas" + +msgid "workspace.library.all" +msgstr "Todas bibliotecas" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 6823ed2731..f12369e267 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -52,10 +52,6 @@ msgstr "Ai uitat parola?" msgid "auth.fullname" msgstr "Numele complet" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Întoarce-te!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Conectează-te" @@ -907,6 +903,9 @@ msgstr "Fonturi" msgid "labels.give-feedback" msgstr "Lasă un feedback" +msgid "labels.go-back" +msgstr "Întoarce-te" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Ascunde comentariile rezolvate" @@ -1441,6 +1440,10 @@ msgstr "Editează pagina" msgid "viewer.header.fullscreen" msgstr "Ecran complet" +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.interactions" +msgstr "Interacţiunile" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.share.copy-link" msgstr "Copiază link" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 9118ae5359..4d681bf2b1 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -40,10 +40,6 @@ msgstr "Забыли пароль?" msgid "auth.fullname" msgstr "Полное имя" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Назад!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Войти здесь" @@ -359,6 +355,9 @@ msgstr "Email" msgid "labels.give-feedback" msgstr "Дать обратную связь" +msgid "labels.go-back" +msgstr "Назад" + msgid "labels.icons" msgstr "Иконки" @@ -563,6 +562,10 @@ msgstr "На странице не найдено ни одного кадра" msgid "viewer.frame-not-found" msgstr "Кадры не найдены." +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header-interactions" +msgstr "взаимодействия" + #: src/app/main/ui/viewer/header.cljs msgid "viewer.header.dont-show-interactions" msgstr "Не показывать взаимодействия" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index a947e22230..55e5587d08 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1,9 +1,9 @@ msgid "" msgstr "" -"PO-Revision-Date: 2021-05-17 21:32+0000\n" -"Last-Translator: Gizem Akgüney \n" -"Language-Team: Turkish " -"\n" +"PO-Revision-Date: 2021-06-01 00:38+0000\n" +"Last-Translator: Çağlar Yeşilyurt \n" +"Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -51,10 +51,6 @@ msgstr "Parolanı mı unuttun?" msgid "auth.fullname" msgstr "Tam Adın" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "Geri dön!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "Buradan giriş yap" @@ -212,10 +208,10 @@ msgstr "Burada hiç dosyan yok" msgid "dashboard.fonts.hero-text2" msgstr "" "Sadece kendinize ait veya Penpot'ta kullanılabilecek bir lisansa sahip olan " -"fontları yükleyebilirsiniz. [Penpot's Terms of Service] içindeki İçerik " -"hakları bölümünden detaylı bilgi alabilirsiniz " -"(https://penpot.app/terms.html). Ayrıca [font licensing](2) hakkında daha " -"fazla bilgi almak isteyebilirsiniz." +"yazi tiplerini yükleyebilirsiniz. [Penpot'un Kullanım Şartları] içindeki " +"İçerik hakları bölümünden detaylı bilgi alabilirsiniz (https://penpot.app/" +"terms.html). Ayrıca [yazı tipi lisanslama](https://www.typography.com/faq) " +"hakkında daha fazla bilgi almak isteyebilirsiniz." #: src/app/main/ui/dashboard/team.cljs msgid "dashboard.invite-profile" @@ -263,7 +259,7 @@ msgstr "Yeni Proje" #: src/app/main/ui/dashboard/search.cljs msgid "dashboard.no-matches-for" -msgstr "%s için hiç sonuç bulunamadı" +msgstr "\"%s\" için sonuç bulunamadı" #: src/app/main/ui/dashboard/sidebar.cljs msgid "dashboard.no-projects-placeholder" @@ -739,6 +735,9 @@ msgstr "Fontlar" msgid "labels.give-feedback" msgstr "Geri bildirimde bulun" +msgid "labels.go-back" +msgstr "Geri dön" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "Çözülmüş yorumları gizle" @@ -870,7 +869,7 @@ msgstr "Gönder" #: src/app/main/ui/settings/feedback.cljs msgid "labels.sending" -msgstr "Gönderiliyor..." +msgstr "Gönderiliyor…" #: src/app/main/ui/static.cljs msgid "labels.service-unavailable.desc-message" @@ -908,7 +907,7 @@ msgid "labels.upload" msgstr "Yükle" msgid "labels.uploading" -msgstr "Yükleniyor..." +msgstr "Yükleniyor…" msgid "modals.delete-font.message" msgstr "" @@ -920,4 +919,1513 @@ msgstr "Fontu sil" #: src/app/main/ui/dashboard/fonts.cljs msgid "title.dashboard.fonts" -msgstr "Fontlar - %s - Penpot" \ No newline at end of file +msgstr "Fontlar - %s - Penpot" + +msgid "labels.manage-fonts" +msgstr "Fontları yönet" + +#: src/app/main/ui/static.cljs +msgid "labels.internal-error.main-message" +msgstr "İç Hata" + +msgid "labels.font-providers" +msgstr "Font sağlayıcısı" + +msgid "labels.custom-fonts" +msgstr "Özel Fontlar" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.main-message" +msgstr "Hatalı Ağ Geçidi" + +#: src/app/main/ui/static.cljs +msgid "labels.bad-gateway.desc-message" +msgstr "" +"Görünüşe göre biraz beklemen ve yeniden denemen gerekiyor; sunucularımızda " +"küçük bir bakım yapıyoruz." + +#: src/app/main/ui/handoff/right_sidebar.cljs +msgid "handoff.tabs.code" +msgstr "Kod" + +msgid "handoff.attributes.typography.text-decoration.underline" +msgstr "Altı Çizili" + +msgid "handoff.attributes.typography.text-decoration.strikethrough" +msgstr "Üstü Çizili" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.line-height" +msgstr "Satır Yüksekliği" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.letter-spacing" +msgstr "Harf Aralığı" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-style" +msgstr "Font Stili" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-size" +msgstr "Font Boyutu" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.font-family" +msgstr "Font Ailesi" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography" +msgstr "Tipografi" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke.width" +msgstr "Genişlik" + +msgid "handoff.attributes.stroke.style.solid" +msgstr "Düz" + +msgid "handoff.attributes.stroke.style.none" +msgstr "Hiçbiri" + +msgid "handoff.attributes.stroke.style.mixed" +msgstr "Karışık" + +msgid "handoff.attributes.stroke.style.dotted" +msgstr "Noktalı" + +#, permanent +msgid "handoff.attributes.stroke.alignment.center" +msgstr "Merkezi" + +#, permanent +msgid "handoff.attributes.stroke.alignment.outer" +msgstr "Dışarıda" + +#, permanent +msgid "handoff.attributes.stroke.alignment.inner" +msgstr "İçinde" + +#: src/app/main/ui/handoff/attributes/stroke.cljs +msgid "handoff.attributes.stroke" +msgstr "Çerçeve" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-y" +msgstr "Y" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.offset-x" +msgstr "X" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow" +msgstr "Gölge" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.width" +msgstr "Genişlik" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.top" +msgstr "Üst" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.rotation" +msgstr "Döndür" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.left" +msgstr "Sol" + +#: src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.height" +msgstr "Yükseklik" + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.subtitle" +msgstr "" +"Lütfen bir sorun, fikir ya da kuşkunuzu açıklayarak e-postanızın nedenini " +"belirtin. Ekibimizin bir üyesi en kısa sürede yanıt verecektir." + +#: src/app/main/ui/settings/feedback.cljs +msgid "feedback.discussions-subtitle2" +msgstr "" +"Soru sorabilir ve soruları cevaplayabilir, açık uçlu tartışmalar yapabilir " +"ve projeyi etkileyen kararları takip edebilirsin." + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.typography" +msgstr "%s tipografi" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography-tooltip" +msgstr "Tüm tipografileri ayır" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.libraries.text.multiple-typography" +msgstr "Çoklu tipografiler" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.small-thumbnails" +msgstr "Küçük önizlemeler" + +#: src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.big-thumbnails" +msgstr "Büyük önizlemeler" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-variant-id" +msgstr "Çeşit" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.typography" +msgstr "Tipografiler" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "title.viewer" +msgstr "%s - Görünüm modu - Penpot" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.hint" +msgstr "" +"Paylaşılmış bir kütüphanedeki bileşeni güncellemek üzeresin. Onu kullanan " +"diğer dosyalar etkilenebilir." + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.accept" +msgstr "Bileşeni güncelle" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.navigate-to" +msgstr "Git" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-a-shape" +msgstr "" +"Diğer çalışma yüzeyine bağlantı taşımak için bir şekil, çalışma yüzeyi ya da " +"grup seçin." + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-rtl" +msgstr "Sağdan sola" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.direction-ltr" +msgstr "Soldan sağa" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.titlecase" +msgstr "İlk Harfi Büyük" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-vertical" +msgstr "Dikey ters çevir" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flip-horizontal" +msgstr "Yatay ters çevir" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.go-to-edit" +msgstr "Düzenlemek için biçim kütüphane dosyasına gidin" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hcenter" +msgstr "Yatay olarak ortaya hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.stretch" +msgstr "Ger" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.right" +msgstr "Sağ" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.left" +msgstr "Sol" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.center" +msgstr "Orta" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.bottom" +msgstr "Alt" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type" +msgstr "Tür" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.size" +msgstr "Boyut" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.set-default" +msgstr "Varsayılan olarak belirle" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.rows" +msgstr "Satırlar" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.margin" +msgstr "Kenar Boşluğu" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.height" +msgstr "Yükseklik" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.columns" +msgstr "Sütunlar" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.column" +msgstr "Sütunlar" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.auto" +msgstr "Otomatik" + +#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +msgid "workspace.options.fill" +msgstr "Doldur" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.exporting-object" +msgstr "Dışarı aktarılıyor…" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs +msgid "workspace.options.export.suffix" +msgstr "Son ek" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export-object" +msgstr "Şekli dışarı aktar" + +#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs +msgid "workspace.options.export" +msgstr "Dışarı Aktar" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.design" +msgstr "Tasarım" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.options.component" +msgstr "Bileşen" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-snap-grid" +msgstr "Izgaraya tutturmayı kapat" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-scale-text" +msgstr "Metin ölçeklendirmeyi kapat" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.disable-dynamic-alignment" +msgstr "Dinamik hizalamayı kapat" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.radial" +msgstr "Dairesel degrade" + +#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs +msgid "workspace.gradients.linear" +msgstr "Doğrusal degrade" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.create-group" +msgstr "Grup oluştur" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.components" +msgstr "Bileşenler" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.colors" +msgstr "Renkler" + +msgid "workspace.assets.box-filter-graphics" +msgstr "Grafikler" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.box-filter-all" +msgstr "Tüm varlıklar" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.assets" +msgstr "Varlıklar" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vtop" +msgstr "Üste hizala" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vdistribute" +msgstr "Dikeyde dağıt" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hdistribute" +msgstr "Yatayda dağıt" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.edit-page" +msgstr "Sayfayı düzenle" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.dont-show-interactions" +msgstr "Etkileşimleri gösterme" + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.frame-not-found" +msgstr "Çerçeve bulunmadı." + +#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs +msgid "viewer.empty-state" +msgstr "Sayfada çerçeve bulunmuyor." + +#: src/app/main/ui/workspace.cljs +msgid "title.workspace" +msgstr "%s - Penpot" + +#: src/app/main/ui/dashboard/fonts.cljs +msgid "title.dashboard.font-providers" +msgstr "Yazıtipi Sağlayıcıları - %s - Penpot" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.cancel" +msgstr "İptal" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.remove-shared-confirm.accept" +msgstr "Paylaşılmış Kütüphane olarak kaldır" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.title" +msgstr "Sahip olarak terfi et" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.message" +msgstr "Bu kullanıcıyı sahip olarak terfi etmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.title" +msgstr "Takımdan ayrılmak" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint2" +msgstr "Ayrılmadan önce terfi etmek için başka bir üye seçin" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.mixed" +msgstr "Karışık" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.screen" +msgstr "Ekran" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.saturation" +msgstr "Doygunluk" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.overlay" +msgstr "Üst katman" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.normal" +msgstr "Normal" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color-burn" +msgstr "Renk yanması" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.curve" +msgstr "Eğri (%s)" + +msgid "workspace.path.actions.snap-nodes" +msgstr "Düğümleri tuttur (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.strikethrough" +msgstr "Üstü çizili" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.line-height" +msgstr "Satır yüksekliği" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.letter-spacing" +msgstr "Harf Aralıkları" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-fixed" +msgstr "Sabit" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-width" +msgstr "Otomatik genişlik" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.grow-auto-height" +msgstr "Otomatik yükseklik" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.google" +msgstr "Google" + +msgid "workspace.options.text-options.decoration" +msgstr "Süsleme" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-top" +msgstr "Üste hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-right" +msgstr "Sağa hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-middle" +msgstr "Merkeze hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-left" +msgstr "Sola hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-justify" +msgstr "İki yana yasla" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-center" +msgstr "Ortaya hizala" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.align-bottom" +msgstr "Alta hizala" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.frame" +msgstr "Çalışma Yüzeyi (%s)" + +msgid "workspace.undo.entry.single.frame" +msgstr "çalışma yüzeyi" + +msgid "workspace.undo.entry.multiple.frame" +msgstr "çalışma yüzeyi" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.select-artboard" +msgstr "Çalışma yüzeyi seç" + +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"Birisini takımın sahibi yapmadan takımı bırakamazsın. Takımı silmek " +"isteyebilirsin." + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.title" +msgstr "Takım üyesini sil" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.message" +msgstr "Bu üyeyi takımdan silmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.delete-team-member-confirm.accept" +msgstr "Üyeyi sil" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.title" +msgstr "Takımın silinmesi" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.message" +msgstr "" +"Bu takımı silmek istediğinden emin misin? Takımla ilişkili dosyalar ve " +"projeler kalıcı olarak silinecektir." + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.delete-team-confirm.accept" +msgstr "Takımı sil" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.title" +msgstr "Projeyi sil" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.message" +msgstr "Bu projeyi silmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/project_menu.cljs +msgid "modals.delete-project-confirm.accept" +msgstr "Projeyi sil" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.title" +msgstr "Sayfayı sil" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "modals.delete-page.body" +msgstr "Bu sayfayı silmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.title" +msgstr "%s dosyanın silinmesi" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.message" +msgstr "%s dosyayı silmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-multi-confirm.accept" +msgstr "Dosyalar sil" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.title" +msgstr "Dosya siliniyor" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.message" +msgstr "Bu dosyayı silmek istediğinden emin misin?" + +#: src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.delete-file-confirm.accept" +msgstr "Dosyayı sil" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.title" +msgstr "Konuşmayı sil" + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.message" +msgstr "" +"Bu konuşmayı silmek istediğinden emin misin? Konudaki tüm yorumlar silinecek." + +#: src/app/main/ui/comments.cljs +msgid "modals.delete-comment-thread.accept" +msgstr "Konuşmayı sil" + +msgid "handoff.tabs.code.selected.frame" +msgstr "Çalışma yüzeyi" + +msgid "handoff.attributes.typography.text-transform.titlecase" +msgstr "İlk Harfleri Büyük" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-transform" +msgstr "Metin Dönüşümü" + +#: src/app/main/ui/handoff/attributes/text.cljs +msgid "handoff.attributes.typography.text-decoration" +msgstr "Metin Süsleme" + +#: src/app/main/ui/handoff/attributes/shadow.cljs +msgid "handoff.attributes.shadow.shorthand.spread" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vcenter" +msgstr "Dikey olarak ortaya hizala" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.vbottom" +msgstr "Alta hizala" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hright" +msgstr "Sağa hizala" + +#: src/app/main/ui/workspace/sidebar/align.cljs +msgid "workspace.align.hleft" +msgstr "Sola hizala" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.sitemap" +msgstr "Site haritası" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions-on-click" +msgstr "Tıklamada etkileşimleri göster" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.show-interactions" +msgstr "Etkileşimleri göster" + +#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.title" +msgstr "Bağlantıyı paylaş" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.subtitle" +msgstr "Bağlantıya sahip herkes erişebilecek" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.remove-link" +msgstr "Bağlantıyı kaldır" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.placeholder" +msgstr "Paylaşım adresi burada görünecek" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.create-link" +msgstr "Bağlantı oluştur" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.share.copy-link" +msgstr "Bağlantıyı kopyala" + +#: src/app/main/ui/viewer/header.cljs +msgid "viewer.header.fullscreen" +msgstr "Tam Ekran" + +#: src/app/main/ui/dashboard/files.cljs +msgid "title.dashboard.files" +msgstr "%s - Penpot" + +#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "settings.multiple" +msgstr "Karışık" + +#: src/app/main/ui/auth/recovery.cljs +msgid "profile.recovery.go-to-login" +msgstr "Giriş yap" + +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "" +"%s adresine doğrulama e-postası gönderildi. E-postalarınızı kontrol edin!" + +#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs +msgid "notifications.profile-saved" +msgstr "Profil başarıyla kaydedildi!" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "notifications.profile-deletion-not-allowed" +msgstr "Profilini silemezsin. Önce takımlarını birine atamalsın." + +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-email-sent" +msgstr "Davet başarıyla iletildi" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "modals.update-remote-component.message" +msgstr "Paylaşılmış bir kütüphanede bir bileşen güncelle" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-settings" +msgstr "Ayarlar * %s - Penpot" + +#: src/app/main/ui/dashboard/team.cljs +msgid "title.team-members" +msgstr "Üyeler - %s - Penpot" + +#: src/app/main/ui/settings/profile.cljs +msgid "title.settings.profile" +msgstr "Profil - Penpot" + +#: src/app/main/ui/settings/password.cljs +msgid "title.settings.password" +msgstr "Parola - Penpot" + +#: src/app/main/ui/settings/options.cljs +msgid "title.settings.options" +msgstr "Ayarlar - Penpot" + +#: src/app/main/ui/settings/feedback.cljs +msgid "title.settings.feedback" +msgstr "Geri bildirimde bulun - Penpot" + +#: src/app/main/ui/dashboard/search.cljs +msgid "title.dashboard.search" +msgstr "Ara - %s - Penpot" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-libraries-need-sync" +msgstr "Güncelleme gerektiren Paylaşılmış Kütüphane bulunmuyor" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.group" +msgstr "Gölge grubu" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title" +msgstr "Gölge" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsetx" +msgstr "X" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.go-main" +msgstr "Ana bileşen dosyasına git" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.front" +msgstr "En öne getir" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.forward" +msgstr "Öne getir" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.edit" +msgstr "Düzenle" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.duplicate" +msgstr "Çoğalt" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete" +msgstr "Sil" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.cut" +msgstr "Kes" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.create-component" +msgstr "Bileşen oluştur" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.copy" +msgstr "Kopyala" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.backward" +msgstr "En arkaya gönder" + +msgid "workspace.path.actions.move-nodes" +msgstr "Düğümleri taşı (%s)" + +msgid "workspace.path.actions.merge-nodes" +msgstr "Düğümleri birleştir (%s)" + +msgid "workspace.path.actions.make-curve" +msgstr "Eğriye (%s)" + +msgid "workspace.path.actions.make-corner" +msgstr "Köşeye (%s)" + +msgid "workspace.path.actions.join-nodes" +msgstr "Düğümleri birleştir (%s)" + +msgid "workspace.path.actions.draw-nodes" +msgstr "Düğüm çiz (%s)" + +msgid "workspace.path.actions.delete-node" +msgstr "Düğüm sil (%s)" + +msgid "workspace.options.text-options.vertical-align" +msgstr "Düşey hizalama" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.uppercase" +msgstr "Büyük Harf" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.underline" +msgstr "Altı Çizili" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +#, fuzzy +msgid "workspace.options.text-options.title-selection" +msgstr "Metin seçimi" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title-group" +msgstr "Grup metni" + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs +msgid "workspace.options.text-options.title" +msgstr "Metin" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.lowercase" +msgstr "Küçük harf" + +msgid "workspace.undo.entry.multiple.group" +msgstr "gruplar" + +msgid "workspace.undo.entry.multiple.curve" +msgstr "eğriler" + +msgid "workspace.undo.entry.multiple.component" +msgstr "bileşenler" + +msgid "workspace.undo.entry.multiple.color" +msgstr "renk varlıkları" + +msgid "workspace.undo.entry.multiple.circle" +msgstr "daireler" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.move" +msgstr "Nesneler taşındı" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.modify" +msgstr "%s düzenlendi" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.delete" +msgstr "%s silindi" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.empty" +msgstr "Şu ana kadar değişim geçmişi yok" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.text" +msgstr "Metin (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.move" +msgstr "Taşı" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.image" +msgstr "Resim (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.ellipse" +msgstr "Elips (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.comments" +msgstr "Yorumlar (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.assets" +msgstr "Varlıklar(%s)" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs +msgid "workspace.sidebar.sitemap" +msgstr "Sayfalar" + +#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs +msgid "workspace.sidebar.options.svg-attrs.title" +msgstr "SVG Öznitelikleri İçeri Aktarıldı" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.layers" +msgstr "Katmanlar (%s)" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.there-are-updates" +msgstr "Paylaşılmış kütüphanelerde güncellemeler mevcut" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.dismiss" +msgstr "Gözardı et" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.unknown" +msgstr "%s üstündeki işlem" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.title" +msgstr "Geçmiş" + +msgid "workspace.undo.entry.single.text" +msgstr "metin" + +msgid "workspace.undo.entry.single.shape" +msgstr "şekil" + +msgid "workspace.undo.entry.single.rect" +msgstr "dikdörtgen" + +msgid "workspace.undo.entry.single.page" +msgstr "sayfa" + +msgid "workspace.undo.entry.single.multiple" +msgstr "nesne" + +msgid "workspace.undo.entry.single.media" +msgstr "grafik varlığı" + +msgid "workspace.undo.entry.single.image" +msgstr "resim" + +msgid "workspace.undo.entry.single.group" +msgstr "grup" + +msgid "workspace.undo.entry.single.curve" +msgstr "eğri" + +msgid "workspace.undo.entry.single.component" +msgstr "bileşen" + +msgid "workspace.undo.entry.single.color" +msgstr "renk varlığı" + +msgid "workspace.undo.entry.single.circle" +msgstr "daire" + +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.new" +msgstr "Yeni %s" + +msgid "workspace.undo.entry.multiple.text" +msgstr "metinler" + +msgid "workspace.undo.entry.multiple.shape" +msgstr "şekiller" + +msgid "workspace.undo.entry.multiple.rect" +msgstr "dikdörtgenler" + +msgid "workspace.undo.entry.multiple.page" +msgstr "sayfalar" + +msgid "workspace.undo.entry.multiple.multiple" +msgstr "nesneler" + +msgid "workspace.undo.entry.multiple.media" +msgstr "grafik varlığı" + +#: src/app/main/data/workspace/libraries.cljs +msgid "workspace.updates.update" +msgstr "Güncelle" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dotted" +msgstr "Noktalı" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.dashed" +msgstr "Çizgili" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke.center" +msgstr "Merkez" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.blend-mode.color" +msgstr "Renk" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.square" +msgstr "Kare" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.row" +msgstr "Satırlar" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.width" +msgstr "Genişlik" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.use-default" +msgstr "Varsayılanı kullan" + +#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs +msgid "workspace.options.grid.params.type.top" +msgstr "Üst" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.title" +msgstr "Bulanıklık" + +msgid "workspace.options.blur-options.layer-blur" +msgstr "Katman" + +msgid "workspace.options.blur-options.background-blur" +msgstr "Arkaplan" + +msgid "workspace.library.own" +msgstr "Kütüphanelerim" + +msgid "workspace.library.libraries" +msgstr "Kütüphaneler" + +msgid "workspace.library.all" +msgstr "Tüm kütüphaneler" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.updates" +msgstr "GÜNCELLEMELER" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.update" +msgstr "Güncelle" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.search-shared-libraries" +msgstr "Paylaşılmış kütüphane ara" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.shared-libraries" +msgstr "PAYLAŞILMIŞ KÜTÜPHANELER" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-shared-libraries-available" +msgstr "Paylaşılmış Kütüphane bulunmuyor" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.no-matches-for" +msgstr "“%s“ için eşleşme bulunmadı" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.components" +msgstr "%s bileşen" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.library" +msgstr "KÜTÜPHANE" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.libraries" +msgstr "KÜTÜPHANELER" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.in-this-file" +msgstr "BU DOSYADAKİ KÜTÜPHANELER" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.graphics" +msgstr "%s grafik" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.file-library" +msgstr "Dosya kütüphanesi" + +#: src/app/main/ui/workspace/colorpicker.cljs +msgid "workspace.libraries.colors.save-color" +msgstr "Renk biçimini kaydet" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.recent-colors" +msgstr "Son renkler" + +#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs +msgid "workspace.libraries.colors.file-library" +msgstr "Dosya kütüphanesi" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.colors" +msgstr "%s renk" + +#: src/app/main/ui/workspace/libraries.cljs +msgid "workspace.libraries.add" +msgstr "Ekle" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.viewer" +msgstr "Görünüm modu (%s)" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.unsaved" +msgstr "Kaydedilmemiş değişiklikler" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saving" +msgstr "Kaydediliyor" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.saved" +msgstr "Kaydedildi" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.save-error" +msgstr "Kaydetmede hata" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-rules" +msgstr "Cetveli göster" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-palette" +msgstr "Renk paletini göster" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-layers" +msgstr "Katmanları göster" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-grid" +msgstr "Izgarayı göster" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.show-assets" +msgstr "Varlıkları göster" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.select-all" +msgstr "Tümünü seç" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-rules" +msgstr "Cetveli gizle" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-palette" +msgstr "Renk paletini gizle" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-layers" +msgstr "Katmanları gizle" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-grid" +msgstr "Izgaraları gizle" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.hide-assets" +msgstr "Varlıkları gizle" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.text-transform" +msgstr "Metin Dönüşümü" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.line-height" +msgstr "Satır Yüksekliği" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.letter-spacing" +msgstr "Harf Boşluğu" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-size" +msgstr "Boyut" + +#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.assets.typography.font-id" +msgstr "Yazı tipi" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.shared" +msgstr "PAYLAŞILDI" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.selected-count" +msgid_plural "workspace.assets.selected-count" +msgstr[0] "Tek öge seçildi" +msgstr[1] "%s öge seçildi" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.search" +msgstr "Varlık ara" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.rename" +msgstr "Yeniden adlandır" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.not-found" +msgstr "Varlık bulunmadı" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.libraries" +msgstr "Kütüphaneler" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group-name" +msgstr "Grup adı" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.group" +msgstr "Grup" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.graphics" +msgstr "Grafikler" + +#: src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.file-library" +msgstr "Dosya kütüphanesi" + +#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.edit" +msgstr "Düzenle" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.duplicate" +msgstr "Çoğalt" + +#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "workspace.assets.delete" +msgstr "Sil" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.promote-owner-confirm.accept" +msgstr "Terfi et" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.message" +msgstr "Bu takımdan ayrılmak istediğinden emin misin?" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-confirm.accept" +msgstr "Takımdan ayrıl" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.title" +msgstr "Terfi etmek için bir üye seçin" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.select-memeber-to-promote" +msgstr "Terfi etmek için bir üye seçin" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.promote-and-leave" +msgstr "Terfi et ve ayrıl" + +#: src/app/main/ui/dashboard/sidebar.cljs +msgid "modals.leave-and-reassign.hint1" +msgstr "%s sahibisiniz." + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member.title" +msgstr "Takıma katılma daveti gönder" + +#: src/app/main/ui/dashboard/team.cljs +msgid "modals.invite-member-confirm.accept" +msgstr "Davet gönder" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.info" +msgstr "Hesabını silerek tüm projelerini ve arşivlerini kaybedeceksin." + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.title" +msgstr "Hesabını silmek istediğinden emin misin?" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.confirm" +msgstr "Evet, hesabımı sil" + +#: src/app/main/ui/settings/delete_account.cljs +msgid "modals.delete-account.cancel" +msgstr "İptal et ve hesabımı koru" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.title" +msgstr "E-postanızı değiştirin" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.submit" +msgstr "E-postayı değiştir" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.new-email" +msgstr "Yeni e-posta" + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.info" +msgstr "" +"“%s” e-posta adresinize kimliğinizi doğrulamak için bir e-posta göndereceğiz." + +#: src/app/main/ui/settings/change_email.cljs +msgid "modals.change-email.confirm-email" +msgstr "Yeni e-postayı doğrulayın" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.message" +msgstr "Paylaşılmış Kütüphane olarak “%s” Ekle" + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.hint" +msgstr "" +"Paylaşılmış Kütüphane olarak eklenince, bu dosya kütüphanesindeki varlıklar " +"diğer dosyalarınızdan da ulaşılabilecek." + +#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs +msgid "modals.add-shared-confirm.accept" +msgstr "Paylaşılmış Kütüphane olarak Ekle" + +#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs +msgid "media.loading" +msgstr "Resim yükleniyor…" + +#: src/app/main/ui/comments.cljs +msgid "labels.write-new-comment" +msgstr "Yeni yorum yaz" + +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.viewer" +msgstr "Görüntüler" + +msgid "labels.upload-custom-fonts" +msgstr "Özel yazı tipi yükle" + +#: src/app/main/ui/dashboard/team_form.cljs +msgid "labels.update-team" +msgstr "Takımı güncelle" + +msgid "labels.icons" +msgstr "Simgeler" + +#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs +msgid "handoff.attributes.layout.radius" +msgstr "Yarı Çap" + +#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs +msgid "title.default" +msgstr "Penpot * Takımlar için Özgür Tasarım" + +#: src/app/main/ui/dashboard/libraries.cljs +msgid "title.dashboard.shared-libraries" +msgstr "Paylaşılmış Kütüphaneler - %s - Penpot" + +#: src/app/main/ui/dashboard/projects.cljs +msgid "title.dashboard.projects" +msgstr "Projeler - %s - Penpot" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-snap-grid" +msgstr "Izgaraya tuttur" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-scale-text" +msgstr "Metin ölçeklendirmeyi etkinleştir" + +#: src/app/main/ui/workspace/header.cljs +msgid "workspace.header.menu.enable-dynamic-alignment" +msgstr "Dinamik hizalamayı etkinleştir" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.size" +msgstr "Boyut" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.title.multiple" +msgstr "Gölge seçimi" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.spread" +msgstr "Yayılma" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.offsety" +msgstr "Y" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.inner-shadow" +msgstr "İç gölge" + +#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +msgid "workspace.options.shadow-options.drop-shadow" +msgstr "Kabartı gölgesi" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.rotation" +msgstr "Döndür" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.single-corners" +msgstr "Tek köşe" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.radius.all-corners" +msgstr "Tüm köşeler" + +msgid "workspace.options.radius" +msgstr "Yarı çap" + +#: src/app/main/ui/workspace/sidebar/options.cljs +msgid "workspace.options.prototype" +msgstr "Prototip" + +#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.position" +msgstr "Konum" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.none" +msgstr "Hiç biri" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.multiple" +msgstr "Seçili katmanlar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title.group" +msgstr "Katman grubu" + +#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +msgid "workspace.options.layer-options.title" +msgstr "Katman" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.sidebar.history" +msgstr "Geçmiş (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.update-main" +msgstr "Ana bileşeni güncelle" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.unlock" +msgstr "Çöz" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.ungroup" +msgstr "Grubu dağıt" + +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show-main" +msgstr "Ana bileşeni göster" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.show" +msgstr "Göster" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.paste" +msgstr "Yapıştır" + +#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.mask" +msgstr "Maskele" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.lock" +msgstr "Kilitle" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.hide" +msgstr "Gizle" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.group" +msgstr "Grup" + +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.back" +msgstr "Arkaya gönder" + +msgid "workspace.path.actions.separate-nodes" +msgstr "Düğümleri ayır (%s)" + +msgid "workspace.path.actions.add-node" +msgstr "Düğüm ekle (%s)" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.use-play-button" +msgstr "" +"Prototip görünümünü çalıştırmak için başlıktaki oynatma düğmesini kullan." + +#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +msgid "workspace.options.text-options.none" +msgstr "Hiçbiri" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.rect" +msgstr "Dikdörtgen (%s)" + +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.color-palette" +msgstr "Renk Paketi (%s)" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 481883c522..37b1d70051 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -42,10 +42,6 @@ msgstr "忘记密码?" msgid "auth.fullname" msgstr "全名" -#: src/app/main/ui/auth/recovery_request.cljs -msgid "auth.go-back-to-login" -msgstr "返回!" - #: src/app/main/ui/auth/register.cljs msgid "auth.login-here" msgstr "在这里登录" @@ -751,6 +747,9 @@ msgstr "反馈已发出" msgid "labels.give-feedback" msgstr "提交反馈" +msgid "labels.go-back" +msgstr "返回" + #: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs msgid "labels.hide-resolved-comments" msgstr "隐藏已决定的评论" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index caa556a85b..a0ac4205e6 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -70,11 +70,6 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" @@ -99,23 +94,25 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -134,7 +131,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@~3.1.1: +anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -149,24 +146,11 @@ append-buffer@^1.0.2: dependencies: buffer-equal "^1.0.0" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -208,11 +192,6 @@ array-each@^1.0.0, array-each@^1.0.1: resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-initial@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" @@ -302,16 +281,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async-settle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" @@ -342,14 +311,14 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^10.2.4: - version "10.2.5" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.5.tgz#096a0337dbc96c0873526d7fef5de4428d05382d" - integrity sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA== + version "10.3.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.1.tgz#954214821d3aa06692406c6a0a9e9d401eafbed2" + integrity sha512-L8AmtKzdiRyYg7BUXJTzigmhbQRCXFKz6SA1Lqo0+AR2FBbQ4aTAPFSDlOutnFkjhiz8my4agGXog1xlMjPJ6A== dependencies: - browserslist "^4.16.3" - caniuse-lite "^1.0.30001196" + browserslist "^4.16.6" + caniuse-lite "^1.0.30001243" colorette "^1.2.2" - fraction.js "^4.0.13" + fraction.js "^4.1.1" normalize-range "^0.1.2" postcss-value-parser "^4.1.0" @@ -430,13 +399,6 @@ bintrees@1.0.1: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -554,7 +516,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.16.3: +browserslist@^4.16.6: version "4.16.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== @@ -646,19 +608,6 @@ callsites@^2.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" @@ -669,28 +618,17 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219: - version "1.0.30001221" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz#b916721ddf59066cfbe96c5c9a77cf7ae5c52e65" - integrity sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g== +caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001243: + version "1.0.30001244" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001244.tgz#a6dc49ad5fa02d81d04373ec3f5ceabc3da06abf" + integrity sha512-Wb4UFZPkPoJoKKVfELPWytRzpemjP/s0pe22NriANru1NoI+5bGNxzKtk7edYL8rmCWTfQO8eRiF0pn1Dqzx7Q== caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -699,20 +637,28 @@ chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -"chokidar@>=3.0.0 <4.0.0": - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== dependencies: - anymatch "~3.1.1" + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0": + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" chokidar@^2.0.0: version "2.1.8" @@ -776,15 +722,6 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -852,12 +789,19 @@ color-convert@^1.9.0, color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -958,11 +902,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -994,14 +933,14 @@ copy-props@^2.0.1: is-plain-object "^5.0.0" core-js-pure@^3.0.0: - version "3.11.2" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c" - integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ== + version "3.13.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.0.tgz#9d267fb47d1d7046cfbc05e7b67bb235b6735355" + integrity sha512-7VTvXbsMxROvzPAVczLgfizR8CyYnvWPrb1eGrtlZAJfjQWEHLofVfCKljLHdpazTfpaziRORwUH/kfGDKvpdA== core-js@^3.6.4: - version "3.11.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.11.2.tgz#af087a43373fc6e72942917c4a4c3de43ed574d6" - integrity sha512-3tfrrO1JpJSYGKnd9LKTBPqgUES/UYiCzMKeqwR1+jF16q4kD1BY2NvqkfuzXwQ6+CIWm55V9cjD7PQd+hijdw== + version "3.13.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.0.tgz#58ca436bf01d6903aee3d364089868d0d89fe58d" + integrity sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -1056,14 +995,6 @@ cross-fetch@^3.0.4: dependencies: node-fetch "2.6.1" -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1164,13 +1095,6 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -1186,10 +1110,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.21.3: - version "2.21.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b" - integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw== +date-fns@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4" + integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg== dateformat@^3.0.3: version "3.0.3" @@ -1226,7 +1150,7 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -1282,11 +1206,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -1404,9 +1323,9 @@ editorconfig@^0.15.3: sigmund "^1.0.1" electron-to-chromium@^1.3.723: - version "1.3.726" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz#6d3c577e5f5a48904ba891464740896c05e3bdb1" - integrity sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w== + version "1.3.774" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.774.tgz#4d6661a23119e35151646c9543b346bb3beca423" + integrity sha512-Fggh17Q1yyv1uMzq8Qn1Ci58P50qcRXMXd2MBcB9sxo6rJxjUutWcNw8uCm3gFWMdcblBO6mDT5HzX/RVRRECA== elliptic@^6.5.3: version "6.5.4" @@ -1421,11 +1340,6 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - enabled@2.0.x: version "2.0.0" resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" @@ -1457,10 +1371,10 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.2, es-abstract@^1.18.0-next.2: - version "1.18.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== +es-abstract@^1.17.2, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: + version "1.18.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" + integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -1470,14 +1384,14 @@ es-abstract@^1.17.2, es-abstract@^1.18.0-next.2: has-symbols "^1.0.2" is-callable "^1.2.3" is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" + is-regex "^1.1.3" + is-string "^1.0.6" + object-inspect "^1.10.3" object-keys "^1.1.1" object.assign "^4.1.2" string.prototype.trimend "^1.0.4" string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" + unbox-primitive "^1.0.1" es-to-primitive@^1.2.1: version "1.2.1" @@ -1534,7 +1448,7 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -1837,10 +1751,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -fraction.js@^4.0.13: - version "4.0.13" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" - integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== +fraction.js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" + integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg== fragment-cache@^0.2.1: version "0.2.1" @@ -1879,57 +1793,21 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.3.1: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" @@ -1939,11 +1817,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -1981,7 +1854,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@~5.1.0: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2029,10 +1902,10 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.1.1, glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2061,15 +1934,6 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" -globule@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" - integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - glogg@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" @@ -2158,19 +2022,18 @@ gulp-rename@^2.0.0: resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c" integrity sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ== -gulp-sass@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-4.1.0.tgz#486d7443c32d42bf31a6b1573ebbdaa361de7427" - integrity sha512-xIiwp9nkBLcJDpmYHbEHdoWZv+j+WtYaKD6Zil/67F3nrAaZtWYN5mDwerdo7EvcdBenSAj7Xb2hx2DqURLGdA== +gulp-sass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-5.0.0.tgz#c338fc021e450a51ae977fea9014eda331ce66b7" + integrity sha512-J0aH0/2N4+2szGCeut0ktGHK0Wg8L9uWivuigrl7xv+nhxozBQRAKLrhnDDaTa3FeUWYtgT8w4RlgdhRy5v16w== dependencies: - chalk "^2.3.0" - lodash "^4.17.11" - node-sass "^4.8.3" + chalk "^4.1.1" + lodash "^4.17.20" plugin-error "^1.0.1" - replace-ext "^1.0.0" - strip-ansi "^4.0.0" - through2 "^2.0.0" - vinyl-sourcemaps-apply "^0.2.0" + replace-ext "^2.0.0" + strip-ansi "^6.0.0" + transfob "^1.0.0" + vinyl-sourcemaps-apply "^0.2.1" gulp-sourcemaps@^3.0.0: version "3.0.0" @@ -2228,13 +2091,6 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -2245,16 +2101,16 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -2323,10 +2179,10 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -highlight.js@^10.6.0: - version "10.7.2" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" - integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg== +highlight.js@^11.0.1: + version "11.1.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.1.0.tgz#0198f7326e64ddfbea5f76b00e84ab542cf24ae8" + integrity sha512-X9VVhYKHQPPuwffO8jk4bP/FVj+ibNCy3HxZZNDXFtJrq4O5FdcdCDRIkDis5MiMnjh7UwEdHgRZJcHFYdzDdA== hmac-drbg@^1.0.1: version "1.0.1" @@ -2364,9 +2220,9 @@ https-browserify@^1.0.0: integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" @@ -2375,6 +2231,11 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + immutable@~3.7.4: version "3.7.6" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" @@ -2402,18 +2263,6 @@ import-from@^2.1.0: dependencies: resolve-from "^3.0.0" -in-publish@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c" - integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ== - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2422,7 +2271,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2490,9 +2339,9 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-bigint@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" + integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== is-binary-path@^1.0.0: version "1.0.1" @@ -2509,11 +2358,11 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-boolean-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" + integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" is-buffer@^1.1.5: version "1.1.6" @@ -2526,9 +2375,9 @@ is-callable@^1.1.4, is-callable@^1.2.3: integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== is-core-module@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887" - integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw== + version "2.5.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" + integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== dependencies: has "^1.0.3" @@ -2547,9 +2396,9 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" + integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== is-descriptor@^0.1.0: version "0.1.6" @@ -2591,11 +2440,6 @@ is-extglob@^2.1.0, is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -2633,9 +2477,9 @@ is-negative-zero@^2.0.1: integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" + integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== is-number@^3.0.0: version "3.0.0" @@ -2671,13 +2515,13 @@ is-promise@^2.2.2: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== +is-regex@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== dependencies: call-bind "^1.0.2" - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-relative@^1.0.0: version "1.0.0" @@ -2696,17 +2540,17 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.5, is-string@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" + integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-typedarray@~1.0.0: version "1.0.0" @@ -2762,20 +2606,14 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -js-base64@^2.1.8: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -js-beautify@^1.13.5: - version "1.13.13" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.13.tgz#756907d1728f329f2b84c42efd56ad17514620bf" - integrity sha512-oH+nc0U5mOAqX8M5JO1J0Pw/7Q35sAdOsM5W3i87pir9Ntx6P/5Gx1xLNoK+MGyvHk4rqqRCE4Oq58H6xl2W7A== +js-beautify@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d" + integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ== dependencies: config-chain "^1.1.12" editorconfig "^0.15.3" glob "^7.1.3" - mkdirp "^1.0.4" nopt "^5.0.0" "js-tokens@^3.0.0 || ^4.0.0": @@ -2838,6 +2676,16 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jszip@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" + integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + just-debounce@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" @@ -2920,6 +2768,13 @@ lead@^1.0.0: dependencies: flush-write-stream "^1.0.2" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + liftoff@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -3058,7 +2913,7 @@ lodash.pluck@^3.1.2: lodash.isarray "^3.0.0" lodash.map "^3.0.0" -lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.10: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3081,15 +2936,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^4.0.1, lru-cache@^4.1.5: +lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -3105,9 +2952,9 @@ lru-queue@^0.1.0: es5-ext "~0.10.2" luxon@^1.26.0: - version "1.26.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578" - integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A== + version "1.28.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" + integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== make-iterator@^1.0.0: version "1.0.1" @@ -3128,11 +2975,6 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - map-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" @@ -3145,10 +2987,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0" - integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA== +marked@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== matchdep@^2.0.0: version "2.0.0" @@ -3202,22 +3044,6 @@ memoizee@0.4.X: next-tick "^1.1.0" timers-ext "^0.1.7" -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3245,17 +3071,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.47.0: - version "1.47.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" - integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== +mime-db@1.48.0: + version "1.48.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.30" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" - integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== + version "2.1.31" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== dependencies: - mime-db "1.47.0" + mime-db "1.48.0" mimic-fn@^2.0.0: version "2.1.0" @@ -3272,7 +3098,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -3284,7 +3110,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.3, minimist@^1.2.5: +minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -3304,7 +3130,7 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.1: +mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -3363,7 +3189,7 @@ mute-stdout@^1.0.0: resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== -nan@^2.12.1, nan@^2.13.2: +nan@^2.12.1: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== @@ -3410,24 +3236,6 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -3458,39 +3266,9 @@ node-libs-browser@^2.2.1: vm-browserify "^1.0.1" node-releases@^1.1.71: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== - -node-sass@^4.8.3: - version "4.14.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" - integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash "^4.17.15" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.13.2" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "2.2.5" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" + version "1.1.73" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" + integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== nopt@^5.0.0: version "5.0.0" @@ -3499,7 +3277,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -3540,16 +3318,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -3567,7 +3335,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@4.X, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -3581,10 +3349,10 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.9.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30" - integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA== +object-inspect@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -3651,14 +3419,13 @@ object.reduce@^1.0.0: make-iterator "^1.0.0" object.values@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" - integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" + integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - has "^1.0.3" + es-abstract "^1.18.2" once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" @@ -3694,11 +3461,6 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - os-locale@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" @@ -3715,19 +3477,6 @@ os-locale@^3.0.0: lcid "^2.0.0" mem "^4.0.0" -os-tmpdir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -3762,7 +3511,7 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -3850,9 +3599,9 @@ path-key@^2.0.0, path-key@^2.0.1: integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-root-regex@^0.1.0: version "0.1.2" @@ -3912,9 +3661,9 @@ phantomjs-prebuilt@^2.1.16: which "^1.2.10" picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" - integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== pify@^2.0.0: version "2.3.0" @@ -3987,14 +3736,14 @@ postcss@^7.0.16: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.2.15: - version "8.2.15" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" - integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q== +postcss@^8.3.5: + version "8.3.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" + integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA== dependencies: colorette "^1.2.2" nanoid "^3.1.23" - source-map "^0.6.1" + source-map-js "^0.6.2" pretty-hrtime@^1.0.0: version "1.0.3" @@ -4144,7 +3893,7 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -react-dom@~17.0.1: +react-dom@~17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -4175,7 +3924,7 @@ react-virtualized@^9.22.3: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" -react@~17.0.1: +react@~17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -4209,7 +3958,7 @@ read-pkg@^1.0.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4231,10 +3980,10 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" @@ -4250,14 +3999,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -4303,18 +4044,16 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - replace-ext@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== +replace-ext@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06" + integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug== + replace-homedir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c" @@ -4331,7 +4070,7 @@ request-progress@^2.0.1: dependencies: throttleit "^1.0.0" -request@^2.81.0, request@^2.87.0, request@^2.88.0: +request@^2.81.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -4367,11 +4106,6 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" @@ -4410,13 +4144,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4432,10 +4159,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rxjs@~7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.1.tgz#5f41c4f991cea550471fc5d215727390103702c7" - integrity sha512-wViQ4Vgps1xJwqWIBooMNN44usCSthL7wCUl4qWqrVjhGfWyVyXcxlYzfDKkJKACQvZMTOft/jJ3RkbwK1j9QQ== +rxjs@~7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.2.0.tgz#5cd12409639e9514a71c9f5f9192b2c4ae94de31" + integrity sha512-aX8w9OpKrQmiPKfT1bqETtUr9JygIz6GZ+gql8v7CijClsP0laoFUdKzxFAoWuRdSlOdU2+crss+cMf+cqMTnw== dependencies: tslib "~2.1.0" @@ -4461,20 +4188,10 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-graph@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" - integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^13.3.2" - -sass@^1.32.8: - version "1.32.12" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.12.tgz#a2a47ad0f1c168222db5206444a30c12457abb9f" - integrity sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA== +sass@^1.35.1: + version "1.35.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.2.tgz#b732314fcdaf7ef8d0f1698698adc378043cb821" + integrity sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw== dependencies: chokidar ">=3.0.0 <4.0.0" @@ -4491,14 +4208,6 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -4511,16 +4220,16 @@ semver-greatest-satisfied-range@^1.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -4549,17 +4258,17 @@ shadow-cljs-jar@1.3.2: resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== -shadow-cljs@2.12.6: - version "2.12.6" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.6.tgz#039539fdc35a19c2f2cd15792ae17e7928f97428" - integrity sha512-dNw989EFQki/59kD8Cd8b6HIpBTqPj9ksWIvSg6hI1bgezZHT0oHfJH5UIbXPD+dnVLvbOnDnfOMWYH6ozalcA== +shadow-cljs@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.1.tgz#9f4b642efafeb84480396f46af2a8e59351d5986" + integrity sha512-X0ueBJksdBg5FIuFOFguyZtAP9gzZZI6lmednxQ/eOsN9tGhpTXh5Y8/7lGzkfIFXxONe9THZx4f2m4JX5jBYA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" shadow-cljs-jar "1.3.2" source-map-support "^0.4.15" which "^1.3.1" - ws "^3.0.0" + ws "^7.4.6" shebang-command@^1.2.0: version "1.2.0" @@ -4664,6 +4373,11 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -4703,13 +4417,6 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - source-map@^0.5.1, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -4747,9 +4454,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + version "3.0.9" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" + integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -4796,13 +4503,6 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== - dependencies: - readable-stream "^2.0.1" - stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -4848,7 +4548,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -4856,15 +4556,6 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string.prototype.codepointat@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" @@ -4914,12 +4605,12 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== dependencies: - ansi-regex "^4.1.0" + ansi-regex "^5.0.0" strip-bom-string@^1.0.0: version "1.0.0" @@ -4938,13 +4629,6 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -4952,11 +4636,6 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0, supports-color@^5.4.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4971,6 +4650,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -5025,15 +4711,6 @@ svgo@^1.1.1: unquote "~1.1.1" util.promisify "~1.0.0" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - tdigest@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" @@ -5160,23 +4837,16 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -trim-newlines@^1.0.0: +transfob@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + resolved "https://registry.yarnpkg.com/transfob/-/transfob-1.0.0.tgz#c7fc27a5b5430ad486267ae666d923f74a0ab320" + integrity sha1-x/wnpbVDCtSGJnrmZtkj90oKsyA= triple-beam@^1.2.0, triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" - tslib@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" @@ -5219,12 +4889,7 @@ ua-parser-js@^0.7.18, ua-parser-js@^0.7.28: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - -unbox-primitive@^1.0.0: +unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== @@ -5420,7 +5085,7 @@ vinyl-sourcemap@^1.1.0: remove-bom-buffer "^3.0.0" vinyl "^2.0.0" -vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1: +vinyl-sourcemaps-apply@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU= @@ -5465,20 +5130,13 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - winston-transport@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" @@ -5510,28 +5168,15 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^3.0.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" +ws@^7.4.6: + version "7.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" + integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== xmldom@0.1.27: version "0.1.27" @@ -5560,7 +5205,7 @@ y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +"y18n@^3.2.1 || ^4.0.0": version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== @@ -5578,14 +5223,6 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" @@ -5612,22 +5249,6 @@ yargs@^12.0.2: y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - yargs@^7.1.0: version "7.1.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db" diff --git a/version.txt b/version.txt index 48afc73f06..d9c26f23be 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.5-alpha +1.7.0-alpha