From da6675c91e6d2e1e7face5094a982f3cd1147f9b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Jun 2022 15:26:40 +0200 Subject: [PATCH 01/86] :books: Update changelog --- CHANGES.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e82b1dc2fe..e6df55de0e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,13 @@ ## :rocket: Next ### :boom: Breaking changes +### :sparkles: New features +### :bug: Bugs fixed +### :arrow_up: Deps updates +### :heart: Community contributions by (Thank you!) + +## 1.14.0-beta + ### :sparkles: New features - Added shortcut panel in workspace [Taiga #36](https://tree.taiga.io/project/penpot/us/36) @@ -16,9 +23,6 @@ - Remove deprecated menu options [Taiga #3333](https://tree.taiga.io/project/penpot/issue/3333) - Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384) -### :arrow_up: Deps updates -### :heart: Community contributions by (Thank you!) - ## 1.13.4-beta ### :bug: Bugs fixed From a8c3ac630dad86731eda8ec6bd11fb3dd44494df Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Jun 2022 15:27:55 +0200 Subject: [PATCH 02/86] :arrow_up: Update to version 1.15.0-beta --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index e1ad24de7c..f1eb667f4f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.14.0-beta +1.15.0-beta From 97bf20dd4cc630ec9eea5eae4efcb5774a88ae23 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 11:18:02 +0200 Subject: [PATCH 03/86] :arrow_up: Update dependencies --- backend/deps.edn | 20 ++++++++++---------- common/deps.edn | 12 ++++++------ common/package.json | 2 +- common/yarn.lock | 8 ++++---- docker/devenv/Dockerfile | 8 ++++---- exporter/deps.edn | 6 +++--- exporter/package.json | 2 +- exporter/yarn.lock | 8 ++++---- frontend/deps.edn | 12 ++++++------ frontend/package.json | 2 +- frontend/shadow-cljs.edn | 2 +- frontend/yarn.lock | 8 ++++---- manage.sh | 2 +- 13 files changed, 46 insertions(+), 46 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index 34e62ccb9d..b9591d8271 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -1,13 +1,13 @@ {:deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.10.3"} + org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/core.async {:mvn/version "1.5.648"} ;; Logging org.zeromq/jeromq {:mvn/version "0.5.2"} com.taoensso/nippy {:mvn/version "3.1.1"} - com.github.luben/zstd-jni {:mvn/version "1.5.2-2"} + com.github.luben/zstd-jni {:mvn/version "1.5.2-3"} org.clojure/data.fressian {:mvn/version "1.0.0"} io.prometheus/simpleclient {:mvn/version "0.15.0"} @@ -17,23 +17,23 @@ org.eclipse.jetty/jetty-servlet]} io.prometheus/simpleclient_httpserver {:mvn/version "0.15.0"} - io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.1.8.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} - com.github.seancorfield/next.jdbc {:mvn/version "1.2.772"} - metosin/reitit-core {:mvn/version "0.5.16"} - org.postgresql/postgresql {:mvn/version "42.3.3"} + com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"} + metosin/reitit-core {:mvn/version "0.5.18"} + org.postgresql/postgresql {:mvn/version "42.4.0"} com.zaxxer/HikariCP {:mvn/version "5.0.1"} funcool/datoteka {:mvn/version "2.0.0"} buddy/buddy-hashers {:mvn/version "1.8.158"} buddy/buddy-sign {:mvn/version "3.4.333"} - org.jsoup/jsoup {:mvn/version "1.14.3"} + org.jsoup/jsoup {:mvn/version "1.15.1"} org.im4java/im4java {:mvn/version "1.4.0"} org.lz4/lz4-java {:mvn/version "1.8.0"} @@ -43,11 +43,11 @@ io.sentry/sentry {:mvn/version "5.6.1"} dawran6/emoji {:mvn/version "0.1.5"} - markdown-clj/markdown-clj {:mvn/version "1.11.0"} + markdown-clj/markdown-clj {:mvn/version "1.11.1"} ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.17.136"}} + software.amazon.awssdk/s3 {:mvn/version "2.17.209"}} :paths ["src" "resources" "target/classes"] :aliases @@ -64,7 +64,7 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.7.7" :git/sha "1474ad6"}} + {io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}} :ns-default build} :test diff --git a/common/deps.edn b/common/deps.edn index 019017255f..01cb47c926 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,9 +1,9 @@ {:deps - {org.clojure/clojure {:mvn/version "1.10.3"} + {org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/data.json {:mvn/version "2.4.0"} org.clojure/tools.cli {:mvn/version "1.0.206"} - metosin/jsonista {:mvn/version "0.3.5"} - org.clojure/clojurescript {:mvn/version "1.11.4"} + metosin/jsonista {:mvn/version "0.3.6"} + org.clojure/clojurescript {:mvn/version "1.11.57"} ;; Logging org.apache.logging.log4j/log4j-api {:mvn/version "2.17.2"} @@ -13,7 +13,7 @@ org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.2"} org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} - selmer/selmer {:mvn/version "1.12.50"} + selmer/selmer {:mvn/version "1.12.51"} criterium/criterium {:mvn/version "0.4.6"} expound/expound {:mvn/version "0.9.0"} @@ -33,7 +33,7 @@ com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} ;; exception printing - fipp/fipp {:mvn/version "0.6.25"} + fipp/fipp {:mvn/version "0.6.26"} io.aviso/pretty {:mvn/version "1.1.1"} environ/environ {:mvn/version "1.2.0"}} :paths ["src"] @@ -42,7 +42,7 @@ {:extra-deps {org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} - thheller/shadow-cljs {:mvn/version "2.17.8"} + thheller/shadow-cljs {:mvn/version "2.19.3"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} diff --git a/common/package.json b/common/package.json index 990c172add..d8e1a16c07 100644 --- a/common/package.json +++ b/common/package.json @@ -13,7 +13,7 @@ "test": "yarn run compile-test && yarn run run-test" }, "devDependencies": { - "shadow-cljs": "2.17.8", + "shadow-cljs": "2.19.3", "source-map-support": "^0.5.19", "ws": "^7.4.6" } diff --git a/common/yarn.lock b/common/yarn.lock index 22f689b46e..83766bc491 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -533,10 +533,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.17.3: - version "2.17.3" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.3.tgz#748e31f67cffdc401691c0cd1bf733a1da53ab5d" - integrity sha512-GxyczUuCtACq/uEOvdTc61wT/aDOZFy8G/AGc322uTX/oUiZaeTJrwpClXe+0+e7VKG9E9RCqP/cjuG3cAG0fw== +shadow-cljs@2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.3.tgz#115a33917f8bca1495e0f815dca7ec3957f669af" + integrity sha512-9TsTCRlmR8m1g2ekwblgomRUgJpbifQI99VlRrlH9NMqEzklev3zYAD1dvy4d5h8BoAhgdxOOEg7ld2d45CWTA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 98ae0514fa..4523423ed5 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v16.14.2 \ - CLOJURE_VERSION=1.11.0.1100 \ - CLJKONDO_VERSION=2022.03.09 \ - BABASHKA_VERSION=0.8.0 \ +ENV NODE_VERSION=v16.15.1 \ + CLOJURE_VERSION=1.11.1.1124 \ + CLJKONDO_VERSION=2022.05.31 \ + BABASHKA_VERSION=0.8.156 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 diff --git a/exporter/deps.edn b/exporter/deps.edn index b171da17cd..5351336d2d 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -1,9 +1,9 @@ {:paths ["src" "vendor" "resources" "test"] :deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.10.3"} + org.clojure/clojure {:mvn/version "1.11.1"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.5.16"} + metosin/reitit-core {:mvn/version "0.5.18"} funcool/beicon {:mvn/version "2021.07.05-1"} } :aliases @@ -15,7 +15,7 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.17.8"}}} + {thheller/shadow-cljs {:mvn/version "2.19.3"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/exporter/package.json b/exporter/package.json index 83f70bfca1..def47f8350 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -21,7 +21,7 @@ "xregexp": "^5.0.2" }, "devDependencies": { - "shadow-cljs": "^2.17.8", + "shadow-cljs": "^2.19.3", "source-map-support": "^0.5.21" } } diff --git a/exporter/yarn.lock b/exporter/yarn.lock index a33887816b..4f240216ea 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -1098,10 +1098,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.17.8: - version "2.17.8" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.8.tgz#7ee27ccf7585991f6c042f66f07f17582c0b70af" - integrity sha512-O39cLA7ukEh+OeH1yZlaWjGFinPOsDD87TetAWPe1QBD9TZQ0Ail+2ovaXeAyZpJ+6Z37joFfival+LNuCgsmQ== +shadow-cljs@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.3.tgz#115a33917f8bca1495e0f815dca7ec3957f669af" + integrity sha512-9TsTCRlmR8m1g2ekwblgomRUgJpbifQI99VlRrlH9NMqEzklev3zYAD1dvy4d5h8BoAhgdxOOEg7ld2d45CWTA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" diff --git a/frontend/deps.edn b/frontend/deps.edn index a1d592e326..c94b5246b2 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -5,7 +5,7 @@ org.clojure/clojure {:mvn/version "1.10.3"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.5.17"} + metosin/reitit-core {:mvn/version "0.5.18"} funcool/beicon {:mvn/version "2021.07.05-1"} funcool/okulary {:mvn/version "2022.04.11-16"} @@ -13,9 +13,9 @@ funcool/rumext {:mvn/version "2022.04.19-148"} funcool/tubax {:mvn/version "2021.05.20-0"} - instaparse/instaparse {:mvn/version "1.4.10"} - garden/garden {:mvn/version "1.3.10"} - + instaparse/instaparse {:mvn/version "1.4.12"} + garden/garden {:git/url "https://github.com/noprompt/garden" + :git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"} } :aliases @@ -32,9 +32,9 @@ :dev {:extra-paths ["dev"] :extra-deps - {thheller/shadow-cljs {:mvn/version "2.17.8"} + {thheller/shadow-cljs {:mvn/version "2.19.3"} org.clojure/tools.namespace {:mvn/version "RELEASE"} - cider/cider-nrepl {:mvn/version "0.28.3"}}} + cider/cider-nrepl {:mvn/version "0.28.4"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/frontend/package.json b/frontend/package.json index 104be46654..d1e140ca70 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,7 +48,7 @@ "prettier": "^2.6.1", "rimraf": "^3.0.0", "sass": "^1.49.9", - "shadow-cljs": "2.17.8" + "shadow-cljs": "2.19.3" }, "dependencies": { "@sentry/browser": "^6.17.4", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 60985de8c4..9945e0ac39 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -29,7 +29,7 @@ :depends-on #{:shared}}} :compiler-options - {:output-feature-set :es8 + {:output-feature-set :es2020 :output-wrapper false :warnings {:fn-deprecated false}} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index fee26840af..f3c3236e2f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5013,10 +5013,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.17.8: - version "2.17.8" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.17.8.tgz#7ee27ccf7585991f6c042f66f07f17582c0b70af" - integrity sha512-O39cLA7ukEh+OeH1yZlaWjGFinPOsDD87TetAWPe1QBD9TZQ0Ail+2ovaXeAyZpJ+6Z37joFfival+LNuCgsmQ== +shadow-cljs@2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.3.tgz#115a33917f8bca1495e0f815dca7ec3957f669af" + integrity sha512-9TsTCRlmR8m1g2ekwblgomRUgJpbifQI99VlRrlH9NMqEzklev3zYAD1dvy4d5h8BoAhgdxOOEg7ld2d45CWTA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" diff --git a/manage.sh b/manage.sh index c86ef23a7c..293a074b24 100755 --- a/manage.sh +++ b/manage.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -ex export ORGANIZATION="penpotapp"; export DEVENV_IMGNAME="$ORGANIZATION/devenv"; From 65e99cabbfe147ca9f317a341c76d4954f1beb36 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 11:18:35 +0200 Subject: [PATCH 04/86] :paperclip: Fix linter issues Related to the linter update on devenv --- backend/src/app/db.clj | 2 +- backend/src/app/rpc/mutations/ldap.clj | 3 ++- backend/src/app/storage/s3.clj | 3 ++- backend/src/app/util/emails.clj | 6 ++++-- frontend/src/app/main/ui/shapes/attrs.cljs | 14 +++++++------- frontend/src/app/main/ui/shapes/custom_stroke.cljs | 2 +- frontend/src/app/main/ui/shapes/export.cljs | 4 ++-- frontend/src/app/main/ui/shapes/group.cljs | 8 ++++---- frontend/src/app/main/ui/shapes/shape.cljs | 3 +-- frontend/src/app/main/ui/shapes/svg_defs.cljs | 3 +-- frontend/src/app/main/ui/shapes/text/fo_text.cljs | 4 ++-- .../src/app/main/ui/viewer/handoff/render.cljs | 8 ++++---- frontend/src/app/util/object.cljs | 4 ++-- 13 files changed, 33 insertions(+), 31 deletions(-) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index c874fb9ccc..9b923495b5 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -213,7 +213,7 @@ [& args] `(jdbc/with-transaction ~@args)) -(defn ^Connection open +(defn open [pool] (jdbc/get-connection pool)) diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj index b4cc37afb6..8ff5c23058 100644 --- a/backend/src/app/rpc/mutations/ldap.clj +++ b/backend/src/app/rpc/mutations/ldap.clj @@ -27,7 +27,8 @@ (s/def ::info-data (s/keys :req-un [::fullname ::email ::backend])) -(defn ^java.lang.AutoCloseable connect +(defn connect + ^java.lang.AutoCloseable [] (let [params {:ssl? (cfg/get :ldap-ssl) :startTLS? (cfg/get :ldap-starttls) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index f169f6bcf0..9c2129638f 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -130,7 +130,8 @@ (def default-timeout (dt/duration {:seconds 30})) -(defn- ^Region lookup-region +(defn- lookup-region + ^Region [region] (Region/of (name region))) diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj index 7136a288cb..dca9b732ea 100644 --- a/backend/src/app/util/emails.clj +++ b/backend/src/app/util/emails.clj @@ -30,7 +30,8 @@ [v] (InternetAddress/parse ^String v)) -(defn- ^Message$RecipientType resolve-recipient-type +(defn- resolve-recipient-type + ^Message$RecipientType [type] (case type :to Message$RecipientType/TO @@ -157,7 +158,8 @@ (.setDebug session debug) session)) -(defn ^MimeMessage smtp-message +(defn smtp-message + ^MimeMessage [cfg message] (let [^Session session (smtp-session cfg)] (build-message cfg session message))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 3eb2d13771..32353e9f7f 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -169,7 +169,7 @@ [svg-attrs svg-styles] (extract-svg-attrs render-id svg-defs svg-attrs) - styles (-> (obj/get props "style" (obj/new)) + styles (-> (obj/get props "style" (obj/create)) (obj/merge! svg-styles) (add-layer-props shape)) @@ -211,24 +211,24 @@ (defn extract-style-attrs [shape] - (-> (obj/new) + (-> (obj/create) (add-style-attrs shape))) (defn extract-fill-attrs [fill-data render-id index type] - (let [fill-styles (-> (obj/get fill-data "style" (obj/new)) + (let [fill-styles (-> (obj/get fill-data "style" (obj/create)) (add-fill fill-data render-id index type))] - (-> (obj/new) + (-> (obj/create) (obj/set! "style" fill-styles)))) (defn extract-stroke-attrs [stroke-data index render-id] - (let [stroke-styles (-> (obj/get stroke-data "style" (obj/new)) + (let [stroke-styles (-> (obj/get stroke-data "style" (obj/create)) (add-stroke stroke-data render-id index))] - (-> (obj/new) + (-> (obj/create) (obj/set! "style" stroke-styles)))) (defn extract-border-radius-attrs [shape] - (-> (obj/new) + (-> (obj/create) (add-border-radius shape))) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 5c5782b027..775a2eb6d6 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -428,7 +428,7 @@ elem-name (obj/get child "type") render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-ctx)) stroke-id (dm/fmt "strokes-%" (:id shape)) - stroke-props (-> (obj/new) + stroke-props (-> (obj/create) (obj/set! "id" stroke-id) (cond-> ;; There is a blur diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index c5b548ccf1..c59b46a6b9 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -262,7 +262,7 @@ (when (= (:type shape) :svg-raw) (let [shape (-> shape (d/update-in-when [:content :attrs :style] str->style)) props - (-> (obj/new) + (-> (obj/create) (obj/set! "penpot:x" (:x shape)) (obj/set! "penpot:y" (:y shape)) (obj/set! "penpot:width" (:width shape)) @@ -328,7 +328,7 @@ (mf/defc export-data [{:keys [shape]}] - (let [props (-> (obj/new) (add-data shape) (add-library-refs shape))] + (let [props (-> (obj/create) (add-data shape) (add-library-refs shape))] [:> "penpot:shape" props (export-shadow-data shape) (export-blur-data shape) diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 403a9f2a0d..88dc6186d8 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -34,18 +34,18 @@ ; Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805 [clip-wrapper clip-props] (if masked-group? - ["g" (-> (obj/new) + ["g" (-> (obj/create) (obj/set! "clipPath" (clip-url render-id mask)))] [mf/Fragment nil]) [mask-wrapper mask-props] (if masked-group? - ["g" (-> (obj/new) - (obj/set! "mask" (mask-url render-id mask)))] + ["g" (-> (obj/create) + (obj/set! "mask" (mask-url render-id mask)))] [mf/Fragment nil]) ;; This factory is generic, it's used for viewer, workspace and handoff. - ;; These props are generated in viewer wrappers only, in the rest of the + ;; These props are generated in viewer wrappers only, in the rest of the ;; cases these props will be nil, not affecting the code. delta (unchecked-get props "delta") fixed? (unchecked-get props "fixed?")] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 6ec138194c..c22da29d89 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -55,9 +55,8 @@ type (:type shape) render-id (mf/use-memo #(str (uuid/next))) filter-id (str "filter_" render-id) - styles (-> (obj/new) + styles (-> (obj/create) (obj/set! "pointerEvents" pointer-events) - (cond-> (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index d127361cb3..d46007c9f2 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -11,7 +11,6 @@ [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] [app.main.ui.shapes.filters :as f] - [app.util.object :as obj] [app.util.svg :as usvg] [rumext.alpha :as mf])) @@ -68,7 +67,7 @@ [wrapper wrapper-props] (if (= tag :mask) ["g" #js {:className "svg-mask-wrapper" :transform (str transform)}] - [mf/Fragment (obj/new)])] + [mf/Fragment #js {}])] [:> (name tag) (clj->js attrs) [:> wrapper wrapper-props diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 1e6a259d19..8f3f53575d 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -212,11 +212,11 @@ :y y :id id :data-colors (->> colors (str/join ",")) - :data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify)) + :data-mapping (-> color-mapping-inverse clj->js js/JSON.stringify) :transform transform :width (if (#{:auto-width} grow-type) 100000 width) :height (if (#{:auto-height :auto-width} grow-type) 100000 height) - :style (-> (obj/new) (attrs/add-layer-props shape)) + :style (-> (obj/create) (attrs/add-layer-props shape)) :ref ref} ;; We use a class here because react has a bug that won't use the appropriate selector for ;; `background-clip` diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index a016077cc8..d46c2eabdb 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -89,7 +89,7 @@ childs (mapv #(get objects %) (:shapes shape)) shape (geom/transform-shape shape) - props (-> (obj/new) + props (-> (obj/create) (obj/merge! props) (obj/merge! #js {:shape shape :childs childs}))] @@ -105,7 +105,7 @@ [props] (let [shape (unchecked-get props "shape") childs (mapv #(get objects %) (:shapes shape)) - props (-> (obj/new) + props (-> (obj/create) (obj/merge! props) (obj/merge! #js {:childs childs}))] [:> group-wrapper props])))) @@ -121,7 +121,7 @@ (let [shape (unchecked-get props "shape") children (->> (cph/get-children-ids objects (:id shape)) (select-keys objects)) - props (-> (obj/new) + props (-> (obj/create) (obj/merge! props) (obj/merge! #js {:childs children}))] [:> bool-wrapper props])))) @@ -136,7 +136,7 @@ [props] (let [shape (unchecked-get props "shape") childs (mapv #(get objects %) (:shapes shape)) - props (-> (obj/new) + props (-> (obj/create) (obj/merge! props) (obj/merge! #js {:childs childs}))] [:> svg-raw-wrapper props])))) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index 0abdd9c90d..325cfe3449 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -6,12 +6,12 @@ (ns app.util.object "A collection of helpers for work with javascript objects." - (:refer-clojure :exclude [set! get get-in merge clone contains?]) + (:refer-clojure :exclude [set! new get get-in merge clone contains?]) (:require ["lodash/omit" :as omit] [cuerdas.core :as str])) -(defn new [] #js {}) +(defn create [] #js {}) (defn get ([obj k] From e5cb5860a8befe6ef200ffa852494fbe2f9c7ef1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 13:01:31 +0200 Subject: [PATCH 05/86] :arrow_up: Update cuerdas dep (fixes dm/str nil handling) --- common/deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/deps.edn b/common/deps.edn index 01cb47c926..b267ccefcf 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -22,7 +22,7 @@ java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/promesa {:mvn/version "8.0.450"} - funcool/cuerdas {:mvn/version "2022.03.27-397"} + funcool/cuerdas {:mvn/version "2022.06.13-401"} lambdaisland/uri {:mvn/version "1.13.95" :exclusions [org.clojure/data.json]} From 1174590af430a13af5a0f819b6af5f95255f306e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 13:10:36 +0200 Subject: [PATCH 06/86] :paperclip: Add hack for devtools unhandled rejection --- frontend/src/app/main/errors.cljs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 604b0b039c..9bacf35c58 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -184,14 +184,19 @@ (defn on-unhandled-error [error] - (if (instance? ExceptionInfo error) - (-> error ex-data ptk/handle-error) - (let [hint (ex-message error) - msg (dm/str "Unhandled Internal Error: " hint)] - (ts/schedule #(st/emit! (rt/assign-exception error))) - (js/console.group msg) - (ex/ignoring (js/console.error error)) - (js/console.groupEnd msg)))) + (letfn [(is-ignorable-exception? [cause] + (condp = (ex-message cause) + "Possible side-effect in debug-evaluate" true + false))] + (if (instance? ExceptionInfo error) + (-> error ex-data ptk/handle-error) + (when-not (is-ignorable-exception? error) + (let [hint (ex-message error) + msg (dm/str "Unhandled Internal Error: " hint)] + (ts/schedule #(st/emit! (rt/assign-exception error))) + (js/console.group msg) + (ex/ignoring (js/console.error error)) + (js/console.groupEnd msg)))))) (defonce uncaught-error-handler (letfn [(on-error [event] From 65753cdc17a453bec94bf56a517c366ef2b31066 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 13:42:32 +0200 Subject: [PATCH 07/86] :arrow_up: Update yetti dep (fix multipart field size validation params handling) --- backend/deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/deps.edn b/backend/deps.edn index b9591d8271..506510a99e 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -20,7 +20,7 @@ io.lettuce/lettuce-core {:mvn/version "6.1.8.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} - funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9" + funcool/yetti {:git/tag "v9.2" :git/sha "4ddcc03" :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} From 6190ce9b35b20f79dc8d66765ca28bcd7e0432ed Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jun 2022 14:44:40 +0200 Subject: [PATCH 08/86] :bug: Add missing resolver to frontend docker image --- docker/images/files/nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/images/files/nginx.conf b/docker/images/files/nginx.conf index 52922f3a04..35863895be 100644 --- a/docker/images/files/nginx.conf +++ b/docker/images/files/nginx.conf @@ -38,6 +38,8 @@ http { gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json; + resolver 127.0.0.11; + map $http_upgrade $connection_upgrade { default upgrade; '' close; From 9ae55283553fd6e12c26eb1056e0febaa648a04d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 14 Jun 2022 11:07:10 +0200 Subject: [PATCH 09/86] :arrow_up: Update im4java version to our internal fork version It fixes the v7 compatibility issues. Now, adding the -Dim4java.useV7=true property to the java command when executing the penpot backend bundle it switches to use the `magick` (ImageMagick v7 CLI) instead of `convert` and `identify`. --- backend/deps.edn | 4 +++- backend/scripts/repl | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/deps.edn b/backend/deps.edn index 506510a99e..1ceba4b524 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -34,7 +34,9 @@ buddy/buddy-sign {:mvn/version "3.4.333"} org.jsoup/jsoup {:mvn/version "1.15.1"} - org.im4java/im4java {:mvn/version "1.4.0"} + org.im4java/im4java {:git/tag "1.4.0-penpot-2" :git/sha "e2b3e16" + :git/url "https://github.com/penpot/im4java"} + org.lz4/lz4-java {:mvn/version "1.8.0"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} diff --git a/backend/scripts/repl b/backend/scripts/repl index 49e105305b..4e2c583947 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -40,6 +40,9 @@ export OPTIONS=" -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints"; +# Uncomment for use the ImageMagick v7.x +# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; + export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" From 20f357d75d1ada9b58924f795e0f858bb2987c54 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 14 Jun 2022 12:12:53 +0200 Subject: [PATCH 10/86] :sparkles: Make the region param optional on s3 storage backend Defaulting to the eu-central-1 --- backend/src/app/storage/s3.clj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index 9c2129638f..c5c4a68196 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -59,7 +59,7 @@ ;; --- BACKEND INIT -(s/def ::region #{:eu-central-1}) +(s/def ::region ::us/keyword) (s/def ::bucket ::us/string) (s/def ::prefix ::us/string) (s/def ::endpoint ::us/string) @@ -68,9 +68,10 @@ (s/keys :opt-un [::region ::bucket ::prefix ::endpoint ::wrk/executor])) (defmethod ig/prep-key ::backend - [_ {:keys [prefix] :as cfg}] + [_ {:keys [prefix region] :as cfg}] (cond-> (d/without-nils cfg) - prefix (assoc :prefix prefix))) + (some? prefix) (assoc :prefix prefix) + (nil? region) (assoc :region :eu-central-1))) (defmethod ig/init-key ::backend [_ cfg] From 199360efa6c675a42a91c0b7729bdfd5c66c546f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Jun 2022 12:18:39 +0200 Subject: [PATCH 11/86] :paperclip: Update default repl script --- backend/scripts/repl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/scripts/repl b/backend/scripts/repl index 49e105305b..d7d60368ba 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -24,9 +24,8 @@ mc mb penpot-s3/penpot -p export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv -export PENPOT_ASSETS_STORAGE_BACKEND=assets-fs +export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 -export PENPOT_STORAGE_ASSETS_S3_REGION=eu-central-1 export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot export OPTIONS=" From dce479bc4b52513941cecc7b3733fd8112045469 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Jun 2022 12:19:16 +0200 Subject: [PATCH 12/86] :sparkles: Make the pool initialization process and defaults reusable And add the ability to skip pool initialization if no enough data is provided. Mainly for initialize pools based on configuration for not essential/dynamic services. --- backend/src/app/db.clj | 72 ++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 9b923495b5..6604df6b56 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -55,54 +55,66 @@ (s/def ::migrations map?) (s/def ::name keyword?) (s/def ::password ::us/string) -(s/def ::read-only ::us/boolean) (s/def ::uri ::us/not-empty-string) (s/def ::username ::us/string) (s/def ::validation-timeout ::us/integer) +(s/def ::read-only? ::us/boolean) -(defmethod ig/pre-init-spec ::pool [_] - (s/keys :req-un [::uri ::name +(s/def ::pool-options + (s/keys :opt-un [::uri ::name ::min-size ::max-size ::connection-timeout - ::validation-timeout] - :opt-un [::migrations + ::validation-timeout + ::migrations ::username ::password ::mtx/metrics - ::read-only])) + ::read-only?])) + +(def defaults + {:name :main + :min-size 0 + :max-size 30 + :connection-timeout 10000 + :validation-timeout 10000 + :idle-timeout 120000 ; 2min + :max-lifetime 1800000 ; 30m + :read-only? false}) (defmethod ig/prep-key ::pool [_ cfg] - (merge {:name :main - :min-size 0 - :max-size 30 - :connection-timeout 10000 - :validation-timeout 10000 - :idle-timeout 120000 ; 2min - :max-lifetime 1800000 ; 30m - :read-only false} - (d/without-nils cfg))) + (merge defaults (d/without-nils cfg))) + +;; Don't validate here, just validate that a map is received. +(defmethod ig/pre-init-spec ::pool [_] ::pool-options) (defmethod ig/init-key ::pool - [_ {:keys [migrations name read-only] :as cfg}] - (l/info :hint "initialize connection pool" - :name (d/name name) - :uri (:uri cfg) - :read-only read-only - :with-credentials (and (contains? cfg :username) - (contains? cfg :password)) - :min-size (:min-size cfg) - :max-size (:max-size cfg)) + [_ {:keys [migrations read-only? uri] :as cfg}] + (if uri + (let [pool (create-pool cfg)] + (l/info :hint "initialize connection pool" + :name (d/name (:name cfg)) + :uri uri + :read-only read-only? + :with-credentials (and (contains? cfg :username) + (contains? cfg :password)) + :min-size (:min-size cfg) + :max-size (:max-size cfg)) + (when-not read-only? + (some->> (seq migrations) (apply-migrations! pool))) + pool) - (let [pool (create-pool cfg)] - (when-not read-only - (some->> (seq migrations) (apply-migrations! pool))) - pool)) + (do + (l/warn :hint "unable to initialize pool, missing url" + :name (d/name (:name cfg)) + :read-only read-only?) + nil))) (defmethod ig/halt-key! ::pool [_ pool] - (.close ^HikariDataSource pool)) + (when pool + (.close ^HikariDataSource pool))) (defn- apply-migrations! [pool migrations] @@ -126,7 +138,7 @@ (.setJdbcUrl (str "jdbc:" uri)) (.setPoolName (d/name (:name cfg))) (.setAutoCommit true) - (.setReadOnly (:read-only cfg)) + (.setReadOnly (:read-only? cfg)) (.setConnectionTimeout (:connection-timeout cfg)) (.setValidationTimeout (:validation-timeout cfg)) (.setIdleTimeout (:idle-timeout cfg)) From adf2d82a52749bad970ed5103c57f86207c285e5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Jun 2022 12:21:23 +0200 Subject: [PATCH 13/86] :tada: Add proper logging reports on audit-log-archive task --- backend/src/app/loggers/audit.clj | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 8b5a8c2ec7..8fb9ce6b99 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -257,12 +257,16 @@ (ex/raise :type :internal :code :task-not-configured :hint "archive task not configured, missing uri")) + (when enabled - (loop [] - (let [res (archive-events cfg)] - (when (= res :continue) - (aa/thread-sleep 200) - (recur)))))))) + (loop [total 0] + (let [n (archive-events cfg)] + (if n + (do + (aa/thread-sleep 200) + (recur (+ total n))) + (when (pos? total) + (l/trace :hint "events chunk archived" :num total))))))))) (def sql:retrieve-batch-of-audit-log "select * from audit_log @@ -332,7 +336,7 @@ (l/debug :action "archive-events" :uri uri :events (count events)) (when (send events) (mark-as-archived conn rows) - :continue)))))) + (count events))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GC Task From 8f7fd21454d5fdc88633b9157502355d7cf22536 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 9 Jun 2022 13:43:52 +0200 Subject: [PATCH 14/86] :sparkles: New layout and layout item menur --- .../resources/images/icons/auto-direction.svg | 3 + frontend/resources/images/icons/auto-fill.svg | 3 + .../images/icons/auto-fix-layout.svg | 3 + frontend/resources/images/icons/auto-gap.svg | 3 + frontend/resources/images/icons/auto-hug.svg | 3 + .../images/icons/auto-margin-side.svg | 3 + .../resources/images/icons/auto-margin.svg | 3 + .../images/icons/auto-padding-side.svg | 3 + .../resources/images/icons/auto-padding.svg | 3 + .../images/icons/auto-row-column.svg | 3 + .../resources/images/icons/space-around.svg | 3 + .../resources/images/icons/space-between.svg | 3 + .../resources/styles/common/framework.scss | 24 ++ .../partials/sidebar-element-options.scss | 272 +++++++++++++- frontend/src/app/main/constants.cljs | 2 + frontend/src/app/main/ui/icons.cljs | 11 + .../sidebar/options/menus/frame_grid.cljs | 2 +- .../sidebar/options/menus/layout.cljs | 353 ++++++++++++++++++ .../sidebar/options/menus/layout_item.cljs | 190 ++++++++++ .../sidebar/options/shapes/bool.cljs | 10 +- .../sidebar/options/shapes/circle.cljs | 10 +- .../sidebar/options/shapes/frame.cljs | 15 +- .../sidebar/options/shapes/group.cljs | 30 +- .../sidebar/options/shapes/image.cljs | 10 +- .../sidebar/options/shapes/multiple.cljs | 57 ++- .../sidebar/options/shapes/path.cljs | 10 +- .../sidebar/options/shapes/rect.cljs | 11 +- .../sidebar/options/shapes/svg_raw.cljs | 10 +- .../sidebar/options/shapes/text.cljs | 17 +- frontend/src/app/util/dom.cljs | 14 + frontend/translations/ar.po | 4 - frontend/translations/ca.po | 4 - frontend/translations/de.po | 4 - frontend/translations/el.po | 4 - frontend/translations/en.po | 152 +++++++- frontend/translations/es.po | 152 +++++++- frontend/translations/fr.po | 4 - frontend/translations/he.po | 4 - frontend/translations/pl.po | 4 - frontend/translations/pt_BR.po | 4 - frontend/translations/ro.po | 4 - frontend/translations/ru.po | 4 - frontend/translations/tr.po | 4 - frontend/translations/zh_CN.po | 4 - 44 files changed, 1337 insertions(+), 99 deletions(-) create mode 100644 frontend/resources/images/icons/auto-direction.svg create mode 100644 frontend/resources/images/icons/auto-fill.svg create mode 100644 frontend/resources/images/icons/auto-fix-layout.svg create mode 100644 frontend/resources/images/icons/auto-gap.svg create mode 100644 frontend/resources/images/icons/auto-hug.svg create mode 100644 frontend/resources/images/icons/auto-margin-side.svg create mode 100644 frontend/resources/images/icons/auto-margin.svg create mode 100644 frontend/resources/images/icons/auto-padding-side.svg create mode 100644 frontend/resources/images/icons/auto-padding.svg create mode 100644 frontend/resources/images/icons/auto-row-column.svg create mode 100644 frontend/resources/images/icons/space-around.svg create mode 100644 frontend/resources/images/icons/space-between.svg create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/layout.cljs create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs diff --git a/frontend/resources/images/icons/auto-direction.svg b/frontend/resources/images/icons/auto-direction.svg new file mode 100644 index 0000000000..d002174b69 --- /dev/null +++ b/frontend/resources/images/icons/auto-direction.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-fill.svg b/frontend/resources/images/icons/auto-fill.svg new file mode 100644 index 0000000000..344e188d72 --- /dev/null +++ b/frontend/resources/images/icons/auto-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-fix-layout.svg b/frontend/resources/images/icons/auto-fix-layout.svg new file mode 100644 index 0000000000..f2be41986b --- /dev/null +++ b/frontend/resources/images/icons/auto-fix-layout.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-gap.svg b/frontend/resources/images/icons/auto-gap.svg new file mode 100644 index 0000000000..307740492a --- /dev/null +++ b/frontend/resources/images/icons/auto-gap.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-hug.svg b/frontend/resources/images/icons/auto-hug.svg new file mode 100644 index 0000000000..f72fbd3c88 --- /dev/null +++ b/frontend/resources/images/icons/auto-hug.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-margin-side.svg b/frontend/resources/images/icons/auto-margin-side.svg new file mode 100644 index 0000000000..087ae82504 --- /dev/null +++ b/frontend/resources/images/icons/auto-margin-side.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-margin.svg b/frontend/resources/images/icons/auto-margin.svg new file mode 100644 index 0000000000..4777ce3b1f --- /dev/null +++ b/frontend/resources/images/icons/auto-margin.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-padding-side.svg b/frontend/resources/images/icons/auto-padding-side.svg new file mode 100644 index 0000000000..d4c56746b1 --- /dev/null +++ b/frontend/resources/images/icons/auto-padding-side.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-padding.svg b/frontend/resources/images/icons/auto-padding.svg new file mode 100644 index 0000000000..e546d0631c --- /dev/null +++ b/frontend/resources/images/icons/auto-padding.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/auto-row-column.svg b/frontend/resources/images/icons/auto-row-column.svg new file mode 100644 index 0000000000..9023c8fcf0 --- /dev/null +++ b/frontend/resources/images/icons/auto-row-column.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/space-around.svg b/frontend/resources/images/icons/space-around.svg new file mode 100644 index 0000000000..94e6e8de4c --- /dev/null +++ b/frontend/resources/images/icons/space-around.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/space-between.svg b/frontend/resources/images/icons/space-between.svg new file mode 100644 index 0000000000..639040b0b4 --- /dev/null +++ b/frontend/resources/images/icons/space-between.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 37e5c275e9..888b2d101d 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -450,6 +450,30 @@ ul.slider-dots { } } + &.maxW { + &::after { + content: attr(alt); + } + } + + &.minW { + &::after { + content: attr(alt); + } + } + + &.maxH { + &::after { + content: attr(alt); + } + } + + &.minH { + &::after { + content: attr(alt); + } + } + &.large { min-width: 7rem; } diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index f678044e19..ba53f45189 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -625,7 +625,9 @@ } } -.radius-options { +.radius-options, +.padding-options, +.margin-options { align-items: center; border: 1px solid $color-gray-60; border-radius: 4px; @@ -634,7 +636,9 @@ padding: 8px; width: 64px; - .radius-icon { + .radius-icon, + .padding-icon, + .margin-icon { display: flex; align-items: center; @@ -1583,3 +1587,267 @@ } } } + +.layout-menu { + svg { + height: 16px; + width: 16px; + fill: $color-gray-30; + } + .direction-gap { + display: flex; + justify-content: space-between; + .direction { + display: flex; + .dir { + margin-right: 4px; + display: flex; + justify-content: center; + align-items: center; + background: none; + border: none; + cursor: pointer; + + &.right { + svg { + transform: rotate(180deg); + } + } + &.top { + svg { + transform: rotate(-90deg); + } + } + &.bottom { + svg { + transform: rotate(90deg); + } + } + &.active, + &:hover { + svg { + fill: $color-primary; + } + } + } + } + .gap { + display: flex; + justify-content: center; + align-items: center; + .icon { + display: flex; + justify-content: center; + align-items: center; + margin-right: 7px; + &.rotated { + transform: rotate(90deg); + } + &.activated { + svg { + fill: $color-primary; + } + } + } + input { + font-size: 0.75rem; + min-width: 0; + padding: 0.25rem; + width: 50px; + height: 20px; + margin: 0; + } + } + } + + .layout-container { + border: 1px solid $color-gray-60; + border-radius: 3px; + margin: 5px 0; + .layout-entry { + display: flex; + align-items: center; + color: $color-gray-20; + height: 38px; + cursor: pointer; + &:hover { + svg { + fill: $color-primary; + } + } + } + + .layout-info { + font-size: $fs12; + width: 100%; + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + &::first-letter { + text-transform: capitalize; + } + } + + .layout-body { + display: flex; + align-items: center; + margin: 7px; + .selects-wrapper { + width: 100%; + margin-left: 12px; + select { + text-transform: capitalize; + } + option { + text-transform: capitalize; + } + } + + .orientation-grid { + background-color: $color-gray-60; + .button-wrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + width: 47px; + height: 47px; + border: 1px solid $color-gray-30; + margin: 12px; + .orientation { + background: none; + border: none; + height: 14px; + width: 14px; + display: flex; + justify-content: center; + align-items: center; + padding: 2px; + cursor: pointer; + + .icon { + display: flex; + justify-content: center; + align-items: center; + svg { + fill: none; + height: 10px; + width: 10px; + } + &.rotated { + transform: rotate(90deg); + } + } + + &.active { + .icon { + svg { + fill: $color-primary; + } + } + } + &:hover { + .icon { + svg { + fill: $color-gray-20; + } + } + } + } + } + &.col { + .button-wrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + .orientation { + height: 100%; + justify-content: space-between; + flex-direction: column; + } + } + } + &.row { + .button-wrapper { + display: grid; + grid-template-rows: 1fr 1fr 1fr; + grid-template-columns: 1fr; + .orientation { + width: 100%; + justify-content: space-between; + padding: 2px; + } + } + } + } + } + } +} + +.layout-item-menu { + .layout-behavior { + display: flex; + align-items: center; + margin: 9px 0; + .button-wrapper { + border: 1px solid $color-gray-60; + border-radius: 4px; + display: flex; + align-items: center; + .behavior-btn { + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + .icon { + display: flex; + justify-content: center; + align-items: center; + height: 26px; + width: 26px; + svg { + height: 16px; + width: 16px; + fill: $color-gray-30; + } + } + &:hover, + &.activated { + svg { + fill: $color-primary; + } + } + } + &.horizontal { + margin-right: 8px; + svg { + transform: rotate(90deg); + } + } + } + } + .advanced-ops { + display: flex; + align-items: center; + cursor: pointer; + font-size: $fs12; + color: $color-gray-30; + &:hover { + svg { + fill: $color-primary; + } + } + } + .advanced-ops-body { + display: grid; + grid-template-columns: 1fr 1fr; + .input-element { + width: 100%; + &::after { + width: 100px; + } + } + } +} diff --git a/frontend/src/app/main/constants.cljs b/frontend/src/app/main/constants.cljs index 8247b92962..84087c5e08 100644 --- a/frontend/src/app/main/constants.cljs +++ b/frontend/src/app/main/constants.cljs @@ -23,3 +23,5 @@ :grid-alignment true :background "var(--color-white)"}) +(def has-layout-item false) + diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 078d767bdc..14d531418e 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -26,8 +26,17 @@ (def arrow-slide (icon-xref :arrow-slide)) (def artboard (icon-xref :artboard)) (def at (icon-xref :at)) +(def auto-direction (icon-xref :auto-direction)) +(def auto-fill (icon-xref :auto-fill)) (def auto-fix (icon-xref :auto-fix)) +(def auto-fix-layout (icon-xref :auto-fix-layout)) +(def auto-gap (icon-xref :auto-gap)) (def auto-height (icon-xref :auto-height)) +(def auto-hug (icon-xref :auto-hug)) +(def auto-margin-side (icon-xref :auto-margin-side)) +(def auto-margin (icon-xref :auto-margin)) +(def auto-padding (icon-xref :auto-padding)) +(def auto-padding-side (icon-xref :auto-padding-side)) (def auto-width (icon-xref :auto-width)) (def bool-difference (icon-xref :boolean-difference)) (def bool-exclude (icon-xref :boolean-exclude)) @@ -152,6 +161,8 @@ (def size-vert (icon-xref :size-vert)) (def sort-ascending (icon-xref :sort-ascending)) (def sort-descending (icon-xref :sort-descending)) +(def space-around (icon-xref :space-around)) +(def space-between (icon-xref :space-between)) (def strikethrough (icon-xref :strikethrough)) (def stroke (icon-xref :stroke)) (def switch (icon-xref :switch)) 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 0f99378352..6569802d95 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 @@ -245,7 +245,7 @@ handle-create-grid (mf/use-fn (mf/deps id) #(st/emit! (dw/add-frame-grid id)))] [:div.element-set [:div.element-set-title - [:span (tr "workspace.options.grid.title")] + [:span (tr "workspace.options.grid.grid-title")] [:div.add-page {:on-click handle-create-grid} i/close]] (when (seq (:grids shape)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout.cljs new file mode 100644 index 0000000000..54745a0edb --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout.cljs @@ -0,0 +1,353 @@ +;; 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.layout + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [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]] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(def layout-attrs + [:layout ;; true if active, false if not + :layout-dir ;; :right, :left, :top, :bottom + :gap ;; number could be negative + :layout-type ;; :packed, :space-between, :space-around + :wrap-type ;; :wrap, :no-wrap + :padding-type ;; :simple, :multiple + :padding ;; {:p1 num :p2 num :p3 num :p4 num} number could be negative + :h-orientation ;; :top, :center, :bottom + :v-orientation]) ;; :left, :center, :right + +(def grid-pos [[:top :left] + [:top :center] + [:top :right] + [:center :left] + [:center :center] + [:center :right] + [:bottom :left] + [:bottom :center] + [:bottom :right]]) +(def grid-rows [:top :center :bottom]) +(def grid-cols [:left :center :right]) + +(mf/defc direction-row + [{:keys [dir saved-dir set-direction] :as props}] + [:button.dir.tooltip.tooltip-bottom + {:class (dom/classnames :active (= saved-dir dir) + :left (= :left dir) + :right (= :right dir) + :top (= :top dir) + :bottom (= :bottom dir)) + :key (dm/str "direction-" dir) + :alt (tr (dm/str "workspace.options.layout.direction." (d/name dir))) + :on-click #(set-direction dir)} + i/auto-direction]) + +(mf/defc orientation-grid + [{:keys [manage-orientation test-values get-icon] :as props}] + (let [dir (:layout-dir @test-values) + type (:layout-type @test-values) + is-col? (or (= dir :top) + (= dir :bottom)) + saved-pos [(:h-orientation @test-values) (:v-orientation @test-values)]] + (if (= type :packed) + [:div.orientation-grid + [:div.button-wrapper + (for [[pv ph] grid-pos] + [:button.orientation + {:on-click (partial manage-orientation pv ph type) + :class (dom/classnames + :active (= [pv ph] saved-pos) + :top (= :top pv) + :center (= :center pv) + :bottom (= :bottom pv) + :left (= :left ph) + :center (= :center ph) + :right (= :right ph)) + :key (dm/str pv ph)} + [:span.icon + {:class (dom/classnames + :rotated is-col?)} + (get-icon dir type pv ph)]])]] + + (if is-col? + [:div.orientation-grid.col + [:div.button-wrapper + (for [col grid-cols] + [:button.orientation + {:on-click (partial manage-orientation :top col type) + :class (dom/classnames + :active (= col (second saved-pos)) + :top (= :left col) + :centered (= :center col) + :bottom (= :right col))} + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type nil col)] + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type nil col)] + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type nil col)]])]] + + [:div.orientation-grid.row + [:div.button-wrapper + (for [row grid-rows] + [:button.orientation + {:on-click (partial manage-orientation row :left type) + :class (dom/classnames + :active (= row (first saved-pos)) + :top (= :top row) + :centered (= :center row) + :bottom (= :bottom row))} + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type row nil)] + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type row nil)] + [:span.icon + {:class (dom/classnames :rotated is-col?)} + (get-icon dir type row nil)]])]])))) + +(mf/defc padding-section + [{:keys [test-values change-padding-style select-all on-padding-change] :as props}] + + (let [padding-type (:padding-type @test-values)] + + [:div.row-flex + [:div.padding-options + [:div.padding-icon.tooltip.tooltip-bottom + {:class (dom/classnames :selected (= padding-type :simple)) + :alt (tr "workspace.options.layout.padding-simple") + :on-click #(change-padding-style :simple)} + i/auto-padding] + [:div.padding-icon.tooltip.tooltip-bottom + {:class (dom/classnames :selected (= padding-type :multiple)) + :alt (tr "workspace.options.layout.padding") + :on-click #(change-padding-style :multiple)} + i/auto-padding-side]] + + (cond + (= padding-type :simple) + [:div.tooltip.tooltip-bottom + {:alt (tr "workspace.options.layout.padding-all")} + [:div.input-element.mini + + [:> numeric-input + {:placeholder "--" + :on-click select-all + :on-change (partial on-padding-change :simple) + :value (:p1 (:padding @test-values))}]]] + + (= padding-type :multiple) + (for [num [:p1 :p2 :p3 :p4]] + [:div.tooltip.tooltip-bottom + {:key (dm/str "padding-" (d/name num)) + :alt (case num + :p1 (tr "workspace.options.layout.top") + :p2 (tr "workspace.options.layout.right") + :p3 (tr "workspace.options.layout.bottom") + :p4 (tr "workspace.options.layout.left"))} + [:div.input-element.mini + [:> numeric-input + {:placeholder "--" + :on-click select-all + :on-change (partial on-padding-change num) + :value (num (:padding @test-values))}]]]))])) + + + +(mf/defc layout-menu + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]} + [{:keys [_ids _type _values] :as props}] + (let [test-values (mf/use-state {:layout false + :layout-dir nil + :gap 0 + :layout-type nil + :wrap-type nil + :padding-type nil + :padding {:p1 0 :p2 0 :p3 0 :p4 0} + :h-orientation nil + :v-orientation nil}) + + open? (mf/use-state false) + gap-selected? (mf/use-state false) + toggle-open (fn [] (swap! open? not)) + + on-add-layout + (fn [_] + (reset! test-values {:layout true + :layout-dir :left + :gap 0 + :layout-type :packed + :wrap-type :wrap + :padding-type :simple + :padding {:p1 0 :p2 0 :p3 0 :p4 0} + :h-orientation :top + :v-orientation :left})) + on-remove-layout + (fn [_] + (reset! test-values {:layout false + :layout-dir nil + :gap 0 + :layout-type nil + :wrap-type nil + :padding-type nil + :padding {:p1 0 :p2 0 :p3 0 :p4 0} + :h-orientation nil + :v-orientation nil}) + (reset! open? false)) + + set-direction + (fn [dir] + (swap! test-values assoc :layout-dir dir)) + + set-gap + (fn [event] + (swap! test-values assoc :gap event)) + + change-padding-style + (fn [type] + (swap! test-values assoc :padding-type type)) + + select-all #(dom/select-target %) + + select-all-gap #(do (reset! gap-selected? true) + (dom/select-target %)) + + on-padding-change + (fn [type val] + (if (= type :simple) + (swap! test-values assoc :padding {:p1 val :p2 val :p3 val :p4 val}) + (swap! test-values assoc-in [:padding type] val))) + + handle-change-type + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target) + value (keyword value)] + (swap! test-values assoc :layout-type value))) + + handle-wrap-type + (mf/use-callback + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target) + value (keyword value)] + (swap! test-values assoc :wrap-type value)))) + + manage-orientation + (fn [h v] + (swap! test-values assoc :h-orientation h :v-orientation v)) + + get-icon + (fn [dir layout-type v h] + (let [col? (= dir (or :left :right)) + manage-text-icon + (if col? + (case h + :left i/text-align-left + :center i/text-align-center + :right i/text-align-right + i/text-align-center) + + (case v + :top i/text-align-left + :center i/text-align-center + :bottom i/text-align-right + i/text-align-center))] + (case layout-type + :packed manage-text-icon + :space-around i/space-around + :space-between i/space-between))) + + layout-info + (fn [] + (let [type (:layout-type @test-values) + dir (:layout-dir @test-values) + is-col? (or (= dir :top) + (= dir :bottom)) + h (:v-orientation @test-values) + v (:h-orientation @test-values) + wrap (:wrap-type @test-values) + orientation (if (= type :packed) + (dm/str (tr (dm/str "workspace.options.layout.v." (d/name v))) ", " (tr (dm/str "workspace.options.layout.h." (d/name h))) ", ") + (if is-col? + (dm/str (tr (dm/str "workspace.options.layout.h." (d/name h))) ", ") + (dm/str (tr (dm/str "workspace.options.layout.v." (d/name v))) ", ")))] + + (dm/str orientation + (str/replace (tr (dm/str "workspace.options.layout." (d/name type))) "-" " ") ", " + (str/replace (tr (dm/str "workspace.options.layout." (d/name wrap))) "-" " "))))] + + [:div.element-set + [:div.element-set-title + [:* + [:span (tr "workspace.options.layout.title")] + (if (= true (:layout @test-values)) + [:div.add-page {:on-click on-remove-layout} i/minus] + [:div.add-page {:on-click on-add-layout} i/close])]] + + (when (= true (:layout @test-values)) + [:div.element-set-content.layout-menu + ;; DIRECTION-GAP + [:div.direction-gap + [:div.direction + [:* + (for [dir [:left :right :bottom :top]] + [:& direction-row {:dir dir + :saved-dir (:layout-dir @test-values) + :set-direction set-direction}])]] + [:div.gap.tooltip.tooltip-bottom-left + {:alt (tr "workspace.options.layout.gap")} + [:span.icon + {:class (dom/classnames + :rotated (or (= (:layout-dir @test-values) :top) + (= (:layout-dir @test-values) :bottom)) + :activated (= @gap-selected? true))} + i/auto-gap] + [:> numeric-input {:no-validate true + :placeholder "--" + :on-click select-all-gap + :on-change set-gap + :on-blur #(reset! gap-selected? false) + :value (:gap @test-values)}]]] + + ;; LAYOUT FLEX + [:div.layout-container + [:div.layout-entry.tooltip.tooltip-bottom + {:on-click toggle-open + :alt (layout-info)} + [:div.element-set-actions-button i/actions] + [:div.layout-info + (layout-info)]] + (when (= true @open?) + [:div.layout-body + [:& orientation-grid {:manage-orientation manage-orientation :test-values test-values :get-icon get-icon}] + + [:div.selects-wrapper + [:select.input-select {:value (d/name (:layout-type @test-values)) + :on-change handle-change-type} + [:option {:value "packed" :label (tr "workspace.options.layout.packed")}] + [:option {:value "space-between" :label (tr "workspace.options.layout.space-between")}] + [:option {:value "space-around" :label (tr "workspace.options.layout.space-around")}]] + + [:select.input-select {:value (d/name (:wrap-type @test-values)) + :on-change handle-wrap-type} + [:option {:value "wrap" :label (tr "workspace.options.layout.wrap")}] + [:option {:value "no-wrap" :label (tr "workspace.options.layout.no-wrap")}]]]])] + + [:& padding-section {:test-values test-values + :change-padding-style change-padding-style + :select-all select-all + :on-padding-change on-padding-change}]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs new file mode 100644 index 0000000000..448211b1fa --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -0,0 +1,190 @@ +;; 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.layout-item + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [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 layout-item-attrs + [:margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} + :margin-type ;; :simple :multiple + :h-behavior ;; :fill :fix :auto + :v-behavior ;; :fill :fix :auto + :max-h ;; num + :min-h ;; num + :max-w ;; num + :min-w ]) ;; num +(mf/defc margin-section + [{:keys [test-values change-margin-style select-all on-margin-change] :as props}] + + (let [margin-type (:margin-type @test-values)] + + [:div.row-flex + [:div.margin-options + [:div.margin-icon.tooltip.tooltip-bottom + {:class (dom/classnames :selected (= margin-type :simple)) + :alt (tr "workspace.options.layout.margin-simple") + :on-click #(change-margin-style :simple)} + i/auto-margin] + [:div.margin-icon.tooltip.tooltip-bottom + {:class (dom/classnames :selected (= margin-type :multiple)) + :alt (tr "workspace.options.layout.margin") + :on-click #(change-margin-style :multiple)} + i/auto-margin-side]] + + (cond + (= margin-type :simple) + [:div.tooltip.tooltip-bottom + {:alt (tr "workspace.options.layout.margin-all")} + [:div.input-element.mini + + [:> numeric-input + {:placeholder "--" + :on-click select-all + :on-change (partial on-margin-change :simple) + :value (:m1 (:margin @test-values))}]]] + + (= margin-type :multiple) + [:* + (for [num [:m1 :m2 :m3 :m4]] + [:div.tooltip.tooltip-bottom + {:class (dm/str "margin-" (d/name num)) + :key (dm/str "margin-" (d/name num)) + :alt (case num + :m1 (tr "workspace.options.layout.top") + :m2 (tr "workspace.options.layout.right") + :m3 (tr "workspace.options.layout.bottom") + :m4 (tr "workspace.options.layout.left"))} + [:div.input-element.mini + [:> numeric-input + {:placeholder "--" + :on-click select-all + :on-change (partial on-margin-change num) + :value (num (:margin @test-values))}]]])])])) + +(mf/defc element-behavior + [{:keys [is-layout-container? is-layout-item? h-behavior v-behavior on-change-behavior] :as props}] + (let [auto? is-layout-container? + fill? (and (= true is-layout-item?) (not= true is-layout-container?))] + + [:div.layout-behavior + [:div.button-wrapper.horizontal + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "horizontal fix" + :class (dom/classnames :activated (= h-behavior :fix)) + :on-click #(on-change-behavior :h :fix)} + [:span.icon i/auto-fix-layout]] + (when fill? + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "horizontal fill" + :class (dom/classnames :activated (= h-behavior :fill)) + :on-click #(on-change-behavior :h :fill)} + [:span.icon i/auto-fill]]) + (when auto? + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "horizontal auto" + :class (dom/classnames :activated (= h-behavior :auto)) + :on-click #(on-change-behavior :h :auto)} + [:span.icon i/auto-hug]])] + [:div.button-wrapper + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "vertical fix" + :class (dom/classnames :activated (= v-behavior :fix)) + :on-click #(on-change-behavior :v :fix)} + [:span.icon i/auto-fix-layout]] + (when fill? + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "vertical fill" + :class (dom/classnames :activated (= v-behavior :fill)) + :on-click #(on-change-behavior :v :fill)} + [:span.icon i/auto-fill]]) + (when auto? + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "vertical auto" + :class (dom/classnames :activated (= v-behavior :auto)) + :on-click #(on-change-behavior :v :auto)} + [:span.icon i/auto-hug]])]])) + +(mf/defc layout-item-menu + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]} + [{:keys [_ids _type _values] :as props}] + (let [test-values (mf/use-state {:margin {:m1 0 :m2 0 :m3 0 :m4 0} + :margin-type :simple + :h-behavior :fill + :v-behavior :fill + :max-h 100 + :min-h 100 + :max-w 100 + :min-w 100}) + open? (mf/use-state false) + toggle-open (fn [] (swap! open? not)) + is-layout-container? true + is-layout-item? true + change-margin-style + (fn [type] + (swap! test-values assoc :margin-type type)) + + select-all #(dom/select-target %) + + on-margin-change + (fn [type val] + (if (= type :simple) + (swap! test-values assoc :margin {:m1 val :m2 val :m3 val :m4 val}) + (swap! test-values assoc-in [:margin type] val))) + + on-change-behavior + (fn [dir value] + (if (= dir :h) + (swap! test-values assoc :h-behavior value) + (swap! test-values assoc :v-behavior value))) + + on-size-change + (fn [measure value] + (swap! test-values assoc measure value))] + [:div.element-set + [:div.element-set-title + [:span (tr "workspace.options.layout-item.title")]] + [:div.element-set-content.layout-item-menu + [:& element-behavior {:is-layout-container? is-layout-container? + :is-layout-item? is-layout-item? + :v-behavior (:v-behavior @test-values) + :h-behavior (:h-behavior @test-values) + :on-change-behavior on-change-behavior}] + [:div.margin [:& margin-section {:test-values test-values + :change-margin-style change-margin-style + :select-all select-all + :on-margin-change on-margin-change}]] + [:div.advanced-ops-container + [:div.advanced-ops.toltip.tooltip-bottom + {:on-click toggle-open + :alt (tr "workspace.options.layout-item.advanced-ops")} + [:div.element-set-actions-button i/actions] + [:span (tr "workspace.options.layout-item.advanced-ops")]]] + (when (= true @open?) + [:div.advanced-ops-body + (for [item [:max-h :min-h :max-w :min-w]] + [:div.input-element + {:alt (tr (dm/str "workspace.options.layout-item." (d/name item))) + :title (tr (dm/str "workspace.options.layout-item." (d/name item))) + :class (dom/classnames "maxH" (= item :max-h) + "minH" (= item :min-h) + "maxW" (= item :max-w) + "minW" (= item :min-w)) + :key item} + [:> numeric-input + {:no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :on-click select-all + :on-change (partial on-size-change item) + :value (item @test-values)}]])])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index 23367cdda4..69517bad68 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -6,10 +6,12 @@ (ns app.main.ui.workspace.sidebar.options.shapes.bool (:require + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -22,12 +24,18 @@ measure-values (select-keys shape measure-attrs) stroke-values (select-keys shape stroke-attrs) layer-values (select-keys shape layer-attrs) - constraint-values (select-keys shape constraint-attrs)] + constraint-values (select-keys shape constraint-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-values}] [:& layer-menu {:ids ids 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 5dbeada465..845db69616 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,10 +6,12 @@ (ns app.main.ui.workspace.sidebar.options.shapes.circle (:require + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -23,12 +25,18 @@ measure-values (select-keys shape measure-attrs) stroke-values (select-keys shape stroke-attrs) layer-values (select-keys shape layer-attrs) - constraint-values (select-keys shape constraint-attrs)] + constraint-values (select-keys shape constraint-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-values}] [:& layer-menu {:ids ids 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 18ef3950d8..79e1bb2688 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,10 +6,13 @@ (ns app.main.ui.workspace.sidebar.options.shapes.frame (:require + [app.main.constants :refer [has-layout-item]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape 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.layout :refer [layout-attrs layout-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-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]] @@ -21,12 +24,22 @@ type (:type shape) stroke-values (select-keys shape stroke-attrs) layer-values (select-keys shape layer-attrs) - measure-values (select-keys shape measure-attrs)] + measure-values (select-keys shape measure-attrs) + layout-values (select-keys shape layout-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids [(:id shape)] :values measure-values :type type :shape shape}] + (when has-layout-item + [:& layout-menu {:type type :ids [(:id shape)] :values layout-values}]) + + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& layer-menu {:ids ids :type type :values layer-values}] 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 f74b444930..3f27507e26 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 @@ -7,12 +7,15 @@ (ns app.main.ui.workspace.sidebar.options.shapes.group (:require [app.common.data :as d] + [app.main.constants :refer [has-layout-item]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-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.layout :refer [layout-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-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]] @@ -30,20 +33,27 @@ 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) - [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)]] + [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)] + [layout-ids layout-values] (get-attrs [shape] objects :layout) + [layout-item-ids layout-item-values] (get-attrs [shape] objects :layout-item) + ] [:div.options [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] [:& component-menu {:ids comp-ids :values comp-values}] + (when-not (empty? layout-ids) + [:& layout-menu {:type type :ids layout-ids :values layout-values}]) + (when has-layout-item + [:& layout-item-menu {:type type :ids layout-item-ids :values layout-item-values}]) [:& constraints-menu {:ids constraint-ids :values constraint-values}] [:& layer-menu {:type type :ids layer-ids :values layer-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 077ecda693..b6d37e3a53 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,10 +6,12 @@ (ns app.main.ui.workspace.sidebar.options.shapes.image (:require + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -23,12 +25,18 @@ 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)] + stroke-values (select-keys shape stroke-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-values}] 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 b7273638d4..97e30ca303 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 @@ -11,6 +11,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages.common :as cpc] [app.common.text :as txt] + [app.main.constants :refer [has-layout-item]] [app.main.ui.hooks :as hooks] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] @@ -18,6 +19,8 @@ [app.main.ui.workspace.sidebar.options.menus.exports :refer [exports-attrs exports-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.layout :refer [layout-attrs layout-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-attrs shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] @@ -130,15 +133,17 @@ :exports :shape}}) (def group->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 - :exports exports-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 + :exports exports-attrs + :layout layout-attrs + :layout-item layout-item-attrs}) (def shadow-keys [:style :color :offset-x :offset-y :blur :spread]) @@ -242,17 +247,20 @@ all-types (into #{} (map :type shapes)) has-text? (contains? all-types :text) - + [measure-ids measure-values] (get-attrs shapes objects :measure) - [layer-ids layer-values - constraint-ids constraint-values - fill-ids fill-values - shadow-ids shadow-values - blur-ids blur-values - stroke-ids stroke-values - text-ids text-values - exports-ids exports-values] + + [layer-ids layer-values + constraint-ids constraint-values + fill-ids fill-values + shadow-ids shadow-values + blur-ids blur-values + stroke-ids stroke-values + text-ids text-values + exports-ids exports-values + layout-ids layout-values + layout-item-ids layout-item-values] (mf/use-memo (mf/deps objects-no-measures) (fn [] @@ -266,12 +274,21 @@ (get-attrs shapes objects-no-measures :blur) (get-attrs shapes objects-no-measures :stroke) (get-attrs shapes objects-no-measures :text) - (get-attrs shapes objects-no-measures :exports)])))] + (get-attrs shapes objects-no-measures :exports) + (get-attrs shapes objects-no-measures :layout) + (get-attrs shapes objects-no-measures :layout-item) + ])))] [:div.options (when-not (empty? measure-ids) [:& measures-menu {:type type :all-types all-types :ids measure-ids :values measure-values :shape shapes}]) + (when-not (empty? layout-ids) + [:& layout-menu {:type type :ids layout-ids :values layout-values}]) + + (when has-layout-item + [:& layout-item-menu {:type type :ids layout-item-ids :values layout-item-values}]) + (when-not (empty? constraint-ids) [:& constraints-menu {:ids constraint-ids :values constraint-values}]) @@ -284,7 +301,7 @@ (when-not (empty? stroke-ids) [:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values :disable-stroke-style has-text?}]) - + (when-not (empty? shapes) [:& color-selection-menu {:type type :shapes (vals objects-no-measures)}]) 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 be8bb8e1e8..b7f4265fae 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,10 +6,12 @@ (ns app.main.ui.workspace.sidebar.options.shapes.path (:require + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -23,12 +25,18 @@ measure-values (select-keys shape measure-attrs) stroke-values (select-keys shape stroke-attrs) layer-values (select-keys shape layer-attrs) - constraint-values (select-keys shape constraint-attrs)] + constraint-values (select-keys shape constraint-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-values}] [:& layer-menu {:ids ids 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 825e8e3eba..49773a0698 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,10 +6,12 @@ (ns app.main.ui.workspace.sidebar.options.shapes.rect (:require + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -25,13 +27,18 @@ 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)] + stroke-values (select-keys shape stroke-attrs) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] - + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-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 c21726de83..331cc6d558 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 @@ -8,9 +8,11 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.main.constants :refer [has-layout-item]] [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.layout-item :refer [layout-item-attrs layout-item-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]] @@ -97,7 +99,8 @@ 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)] + stroke-values (get-stroke-values shape) + layout-item-values (select-keys shape layout-item-attrs)] (when (contains? svg-elements tag) [:* @@ -105,6 +108,11 @@ :type type :values measure-values :shape shape}] + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values constraint-values}] 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 f90ff6b321..54b8032453 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 @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.text (:require [app.common.data :as d] + [app.main.constants :refer [has-layout-item]] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] @@ -14,6 +15,7 @@ [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-menu fill-attrs]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.layout-item :refer [layout-item-attrs layout-item-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]] @@ -56,7 +58,8 @@ (dwt/current-text-values {:editor-state editor-state :shape shape - :attrs text-attrs}))] + :attrs text-attrs})) + layout-item-values (select-keys shape layout-item-attrs)] [:* [:& measures-menu @@ -64,7 +67,11 @@ :type type :values (select-keys shape measure-attrs) :shape shape}] - + (when has-layout-item + [:& layout-item-menu {:ids ids + :type type + :values layout-item-values + :shape shape}]) [:& constraints-menu {:ids ids :values (select-keys shape constraint-attrs)}] @@ -72,7 +79,7 @@ [:& layer-menu {:ids ids :type type :values layer-values}] - + [:& text-menu {:ids ids :type type @@ -97,6 +104,4 @@ [:& blur-menu {:ids ids - :values (select-keys shape [:blur])}] - - ])) + :values (select-keys shape [:blur])}]])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index dfa9572fd9..dd02530596 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -102,6 +102,20 @@ (when (some? event) (.-target event))) +(defn select-target + "Extract the target from event instance and select it" + [^js event] + (when (some? event) + (-> event + (.-target) + (.-select)))) + +(defn select-node + "Select element by node" + [^js node] + (when (some? node) + (.-select node))) + (defn get-current-target "Extract the current target from event instance (different from target when event triggered in a child of the subscribing element)." diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 06a8a28687..9b64bb5ad6 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -2034,10 +2034,6 @@ msgstr "الصفوف" msgid "workspace.options.grid.square" msgstr "مربع" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "الشبكة والتخطيطات" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "ملء المجموعة" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 66f55e803c..2d0eff1421 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -2527,10 +2527,6 @@ msgstr "Files" msgid "workspace.options.grid.square" msgstr "Quadrat" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Quadrícula i disposicions" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Emplenament del grup" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 3f7645b39b..b6486cbd6b 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -2813,10 +2813,6 @@ msgstr "Zeile" msgid "workspace.options.grid.square" msgstr "Quadrat" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Raster & Layouts" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Gruppe füllen" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 7db6d07fd2..389f8cb217 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -1706,10 +1706,6 @@ msgstr "Σειρές" msgid "workspace.options.grid.square" msgstr "τετράγωνο" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Πλέγμα & Διατάξεις" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Συμπλήρωση ομάδας" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 628e35997e..5e36646de4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3134,8 +3134,8 @@ msgid "workspace.options.grid.square" msgstr "Square" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Grid & Layouts" +msgid "workspace.options.grid.grid-title" +msgstr "Grid" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" @@ -3432,6 +3432,154 @@ msgstr "Group layers" msgid "workspace.options.layer-options.title.multiple" msgstr "Selected layers" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.title" +msgstr "Layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.left" +msgstr "Row" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.right" +msgstr "Reverse row" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.top" +msgstr "Reverse column" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.bottom" +msgstr "Column" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "Gap" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.left" +msgstr "left" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.right" +msgstr "right" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.center" +msgstr "center" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.top" +msgstr "top" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.bottom" +msgstr "bottom" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.center" +msgstr "center" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "packed" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "space between" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "space around" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.wrap" +msgstr "wrap" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.no-wrap" +msgstr "no wrap" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "Simple padding" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "Padding" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-all" +msgstr "All sides" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.top" +msgstr "Top" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.right" +msgstr "Right" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.bottom" +msgstr "Bottom" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.left" +msgstr "Left" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title" +msgstr "Element resizing" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-simple" +msgstr "Simple margin" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin" +msgstr "Margin" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-all" +msgstr "All sides" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.advanced-ops" +msgstr "Advanced options" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.max-h" +msgstr "Max.Height" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.max-w" +msgstr "Max.Width" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.min-h" +msgstr "Min.Height" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.min-w" +msgstr "Min.Width" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.max-h" +msgstr "Maximum height" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.max-w" +msgstr "Maximum width" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.min-h" +msgstr "Minimum height" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.min-w" +msgstr "Minimum width" + #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.more-colors" msgstr "More colors" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 17a5d933dd..64520a989f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3296,8 +3296,8 @@ msgid "workspace.options.grid.square" msgstr "Cuadros" #: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Rejilla & Estructuras" +msgid "workspace.options.grid.grid-title" +msgstr "Rejilla" #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" @@ -3594,6 +3594,154 @@ msgstr "Capas de grupo" msgid "workspace.options.layer-options.title.multiple" msgstr "Capas seleccionadas" +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.title" +msgstr "Layout" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.left" +msgstr "Fila" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.right" +msgstr "Fila invertida" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.top" +msgstr "Columna invertida" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.direction.bottom" +msgstr "Columna" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.gap" +msgstr "Espacio" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.left" +msgstr "izquierda" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.right" +msgstr "derecha" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.h.center" +msgstr "centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.top" +msgstr "arriba" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.bottom" +msgstr "abajo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.v.center" +msgstr "centro" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.packed" +msgstr "juntar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-between" +msgstr "espaciar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.space-around" +msgstr "separar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.wrap" +msgstr "agrupar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.no-wrap" +msgstr "no agrupar" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-simple" +msgstr "Padding sencillo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding" +msgstr "Padding" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.padding-all" +msgstr "Todos" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.top" +msgstr "Arriba" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.right" +msgstr "Derecha" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.bottom" +msgstr "Abajo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs +msgid "workspace.options.layout.left" +msgstr "Izquierda" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title" +msgstr "Redimensionado de elemento" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-simple" +msgstr "Margen sencillo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin" +msgstr "Margen" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout.margin-all" +msgstr "Todos" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.advanced-ops" +msgstr "Opciones avanzadas" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.max-h" +msgstr "AlturaMax." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.max-w" +msgstr "AnchoMax." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.min-h" +msgstr "AlturaMin." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.min-w" +msgstr "AnchoMin." + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.max-h" +msgstr "Altura máxima" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.max-w" +msgstr "Ancho máximo" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.min-h" +msgstr "Altura mínima" + +#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +msgid "workspace.options.layout-item.title.min-w" +msgstr "Ancho mínimo" + #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.more-colors" msgstr "Más colores" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 75778b523a..72a3fc1bc8 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -2186,10 +2186,6 @@ msgstr "Lignes" msgid "workspace.options.grid.square" msgstr "Carré" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Grille & Calques" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Remplissage de groupe" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index ca7d6b7a39..7129f0cd26 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -2754,10 +2754,6 @@ msgstr "שורות" msgid "workspace.options.grid.square" msgstr "ריבוע" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "רשת ופריסות" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "מילוי קבוצה" diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po index d3f5e10a8d..70a4ed3798 100644 --- a/frontend/translations/pl.po +++ b/frontend/translations/pl.po @@ -2740,10 +2740,6 @@ msgstr "Rzędy" msgid "workspace.options.grid.square" msgstr "Kwadrat" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Siatka i układy" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Wypełnienie grupy" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index ce1c152f25..af91a41853 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -1627,10 +1627,6 @@ msgstr "Linhas" msgid "workspace.options.grid.square" msgstr "Quadrado" -#: 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/layer.cljs msgid "workspace.options.layer-options.blend-mode.color" msgstr "Cor" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index f0447a8d45..ab6d47f7a0 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -1919,10 +1919,6 @@ msgstr "Rânduri" msgid "workspace.options.grid.square" msgstr "Pătrat" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Grilă & Layout" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Group fill" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 605a3c69a1..50cd08e0a6 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -2034,10 +2034,6 @@ msgstr "Строки" msgid "workspace.options.grid.square" msgstr "Квадрат" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Сетка и Макеты" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Заливка для группы" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index e777f23dd8..130cf57b6f 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -2813,10 +2813,6 @@ msgstr "Satırlar" msgid "workspace.options.grid.square" msgstr "Kare" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "Izgara ve Yerleşim Düzenleri" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "Grubu doldur" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 219906d33c..d6b44316a0 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -2497,10 +2497,6 @@ msgstr "行" msgid "workspace.options.grid.square" msgstr "正方形" -#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs -msgid "workspace.options.grid.title" -msgstr "网格与布局" - #: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs msgid "workspace.options.group-fill" msgstr "编组填充" From ae468ecdf28b2ba465d5a0f4b4c619b413fa2ea1 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 8 Jun 2022 12:12:23 +0200 Subject: [PATCH 15/86] :tada: Improvements on view mode --- CHANGES.md | 1 + frontend/resources/images/icons/go-next.svg | 1 + frontend/resources/images/icons/go-prev.svg | 1 + frontend/resources/images/icons/reset.svg | 1 + .../styles/main/partials/viewer-header.scss | 29 ++- .../styles/main/partials/viewer.scss | 118 ++++++++++ frontend/src/app/main/data/comments.cljs | 7 +- frontend/src/app/main/data/viewer.cljs | 12 + .../src/app/main/data/viewer/shortcuts.cljs | 4 +- frontend/src/app/main/ui/icons.cljs | 3 + frontend/src/app/main/ui/viewer.cljs | 214 +++++++++++------- frontend/src/app/main/ui/viewer/comments.cljs | 33 ++- frontend/src/app/main/ui/viewer/handoff.cljs | 7 +- frontend/src/app/main/ui/viewer/header.cljs | 13 +- .../src/app/main/ui/workspace/comments.cljs | 23 +- frontend/translations/en.po | 3 + frontend/translations/es.po | 3 + 17 files changed, 359 insertions(+), 114 deletions(-) create mode 100644 frontend/resources/images/icons/go-next.svg create mode 100644 frontend/resources/images/icons/go-prev.svg create mode 100644 frontend/resources/images/icons/reset.svg diff --git a/CHANGES.md b/CHANGES.md index 8d58bb61cf..8fd1bbad58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - Multiple team invitations on onboarding [Taiga #3084](https://tree.taiga.io/project/penpot/us/3084) - Change text properties position at the sidebar [Taiga #3047](https://tree.taiga.io/project/penpot/us/3047) - Group assets by drag and drop [Taiga #2831](https://tree.taiga.io/project/penpot/us/2831) +- View mode improvements to enable access and use in different conditions [Taiga #3023](https://tree.taiga.io/project/penpot/us/3023) ### :bug: Bugs fixed - Fix menu file not accessible in certain conditions [Taiga #3385](https://tree.taiga.io/project/penpot/issue/3385) diff --git a/frontend/resources/images/icons/go-next.svg b/frontend/resources/images/icons/go-next.svg new file mode 100644 index 0000000000..ed48fbcde5 --- /dev/null +++ b/frontend/resources/images/icons/go-next.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/go-prev.svg b/frontend/resources/images/icons/go-prev.svg new file mode 100644 index 0000000000..6737a43abd --- /dev/null +++ b/frontend/resources/images/icons/go-prev.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/reset.svg b/frontend/resources/images/icons/reset.svg new file mode 100644 index 0000000000..59dd005eae --- /dev/null +++ b/frontend/resources/images/icons/reset.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 378019f3ce..1c64546ff9 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -3,11 +3,12 @@ background-color: $color-gray-50; border-bottom: 1px solid $color-gray-60; display: grid; - grid-template-columns: 1fr auto 1fr; + grid-template-columns: 45% 10% 45%; height: 48px; padding: 0 $size-4 0 55px; position: relative; justify-content: space-between; + width: 100vw; a { font-size: $fs12; @@ -15,6 +16,7 @@ .nav-zone { justify-content: flex-start; + width: 100%; } .main-icon { @@ -54,10 +56,25 @@ > * { margin-left: $size-5; + @media only screen and (max-width: 1366px) { + margin-left: 0.5rem; + } } .btn-primary { flex-shrink: 0; + svg { + display: none; + } + @media only screen and (max-width: 1366px) { + padding: 0 0.5rem; + svg { + display: inline-block; + } + span { + display: none; + } + } } .view-options { @@ -105,6 +122,7 @@ display: flex; padding: $size-1; position: relative; + width: 100%; .icon { display: flex; @@ -119,9 +137,13 @@ } } + .breadcrumb { + display: grid; + grid-template-columns: auto 10px auto 10px auto; + } + .breadcrumb, .current-frame { - display: flex; position: relative; > span { @@ -140,7 +162,8 @@ } .current-frame { - display: flex; + display: grid; + grid-template-columns: 14px 1fr; span { color: $color-white; margin-right: $size-1; diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index 4f9f13fe14..cc4b727301 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -21,6 +21,107 @@ overflow: auto; + & .viewer-go-prev, + & .viewer-go-next { + position: absolute; + height: 100%; + display: flex; + align-items: center; + + .arrow { + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: $color-gray-50; + width: 24px; + height: 24px; + cursor: pointer; + fill: $color-gray-30; + + svg { + width: 12px; + height: 12px; + } + + &:hover { + background: $color-primary; + fill: $color-black; + } + } + } + + & .viewer-go-next { + right: 0; + padding-right: 29px; + svg { + margin-left: 2px; + } + } + + & .viewer-go-next.right-bar { + right: 256px; + } + + & .viewer-go-prev { + left: 0; + padding-left: 29px; + svg { + margin-right: 2px; + } + } + + & .viewer-go-prev.left-bar { + left: 256px; + } + + & .viewer-bottom { + position: absolute; + bottom: 0; + height: 50px; + width: 100%; + display: flex; + justify-content: space-between; + + &.left-bar { + width: calc(100% - 512px); + } + + .reset { + display: flex; + align-items: center; + border-radius: 12px; + background: $color-gray-50; + width: 24px; + height: 24px; + cursor: pointer; + fill: $color-gray-30; + margin-left: 29px; + + svg { + margin-left: 4px; + width: 15px; + height: 15px; + } + + &:hover { + background: $color-primary; + fill: $color-black; + } + } + + .counter { + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + background: $color-gray-50; + width: 67px; + height: 25px; + fill: $color-gray-20; + } + } + & .viewer-wrapper { position: relative; } @@ -42,6 +143,23 @@ transform-origin: center; } } + + & .viewer-wrapper-out { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + & .comments-right-sidebar { + position: absolute; + right: 0; + top: 50px; + width: 256px; + height: 100%; + } } .viewport-container { diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 36a9a9e719..3dd8741c00 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -243,7 +243,7 @@ (update :workspace-drawing dissoc :comment))))) (defn update-filters - [{:keys [mode show] :as params}] + [{:keys [mode show list] :as params}] (ptk/reify ::update-filters ptk/UpdateEvent (update [_ state] @@ -254,7 +254,10 @@ (assoc :mode mode) (some? show) - (assoc :show show))))))) + (assoc :show show) + + (some? list) + (assoc :list list))))))) (s/def ::create-draft-params (s/keys :req-un [::page-id ::file-id ::position])) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index c79909eecc..7dbbe296cc 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -303,6 +303,18 @@ (dcm/close-thread) (rt/nav :viewer pparams (assoc qparams :index (inc index))))))))) + +(def select-first-frame + (ptk/reify ::select-first-frame + ptk/WatchEvent + (watch [_ state _] + (let [route (:route state) + qparams (:query-params route) + pparams (:path-params route)] + (rx/of + (dcm/close-thread) + (rt/nav :viewer pparams (assoc qparams :index 0))))))) + (s/def ::interactions-mode #{:hide :show :show-on-click}) (defn set-interactions-mode diff --git a/frontend/src/app/main/data/viewer/shortcuts.cljs b/frontend/src/app/main/data/viewer/shortcuts.cljs index 4c3aa333ec..68971ea27e 100644 --- a/frontend/src/app/main/data/viewer/shortcuts.cljs +++ b/frontend/src/app/main/data/viewer/shortcuts.cljs @@ -42,12 +42,12 @@ :fn #(st/emit! dv/toggle-fullscreen)} :next-frame {:tooltip ds/left-arrow - :command "left" + :command ["left" "up"] :subsections [:general-viewer] :fn #(st/emit! dv/select-prev-frame)} :prev-frame {:tooltip ds/right-arrow - :command "right" + :command ["right" "down"] :subsections [:general-viewer] :fn #(st/emit! dv/select-next-frame)} diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 078d767bdc..0dc78f6518 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -67,6 +67,8 @@ (def full-screen-off (icon-xref :full-screen-off)) (def grid (icon-xref :grid)) (def grid-snap (icon-xref :grid-snap)) +(def go-next (icon-xref :go-next)) +(def go-prev (icon-xref :go-prev)) (def help (icon-xref :help)) (def icon-empty (icon-xref :icon-empty)) (def icon-filter (icon-xref :filter)) @@ -134,6 +136,7 @@ (def radius-4 (icon-xref :radius-4)) (def recent (icon-xref :recent)) (def redo (icon-xref :redo)) +(def reset (icon-xref :reset)) (def rotate (icon-xref :rotate)) (def ruler (icon-xref :ruler)) (def ruler-tool (icon-xref :ruler-tool)) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index c148c3db11..4b84d5c032 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -24,7 +24,7 @@ [app.main.ui.shapes.filters :as filters] [app.main.ui.share-link] [app.main.ui.static :as static] - [app.main.ui.viewer.comments :refer [comments-layer]] + [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] [app.main.ui.viewer.handoff :as handoff] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.interactions :as interactions] @@ -32,6 +32,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.webapi :as wapi] + [cuerdas.core :as str] [goog.events :as events] [rumext.alpha :as mf])) @@ -60,6 +61,113 @@ :height (* height zoom) :vbox (str "0 0 " width " " height)}))) + +(mf/defc viewer-pagination + [{:keys [index num-frames left-bar right-bar] :as props}] + [:* + (when (pos? index) + [:div.viewer-go-prev {:class (when left-bar "left-bar")} + [:div.arrow {:on-click #(st/emit! dv/select-prev-frame)} i/go-prev]]) + (when (< (+ index 1) num-frames) + [:div.viewer-go-next {:class (when right-bar "right-bar")} + [:div.arrow {:on-click #(st/emit! dv/select-next-frame)} i/go-next]]) + [:div.viewer-bottom {:class (when left-bar "left-bar")} + [:div.reset {:on-click #(st/emit! dv/select-first-frame)} i/reset] + [:div.counter (str/join " / " [(+ index 1) num-frames])] + [:span]]]) + + +(mf/defc viewer-wrapper + [{:keys [wrapper-size scroll orig-frame orig-viewport-ref orig-size page file users current-viewport-ref + size frame interactions-mode overlays zoom close-overlay section index] :as props}] + (let [{clist :list} (mf/deref refs/comments-local) + show-comments-list (and (= section :comments) (= :show clist))] + [:* + [:& viewer-pagination {:index index :num-frames (count (:frames page)) :right-bar show-comments-list}] + + (when show-comments-list + [:& comments-sidebar {:users users :frame frame :page page}]) + + [:div.viewer-wrapper + {:style {:width (:width wrapper-size) + :height (:height wrapper-size)}} + [:& (mf/provider ctx/scroll-ctx) {:value @scroll} + [:div.viewer-clipper + [:* + (when orig-frame + [:div.viewport-container + {:ref orig-viewport-ref + :style {:width (:width orig-size) + :height (:height orig-size) + :position "relative"}} + + [:& interactions/viewport + {:frame orig-frame + :base-frame orig-frame + :frame-offset (gpt/point 0 0) + :size orig-size + :page page + :file file + :users users + :interactions-mode :hide}]]) + + [:div.viewport-container + {:ref current-viewport-ref + :style {:width (:width size) + :height (:height size) + :position "relative"}} + + [:& interactions/viewport + {:frame frame + :base-frame frame + :frame-offset (gpt/point 0 0) + :size size + :page page + :file file + :users users + :interactions-mode interactions-mode}] + + (for [overlay overlays] + (let [size-over (calculate-size (:frame overlay) zoom)] + [:* + (when (or (:close-click-outside overlay) + (:background-overlay overlay)) + [:div.viewer-overlay-background + {:class (dom/classnames + :visible (:background-overlay overlay)) + :style {:width (:width wrapper-size) + :height (:height wrapper-size) + :position "absolute" + :left 0 + :top 0} + :on-click #(when (:close-click-outside overlay) + (close-overlay (:frame overlay)))}]) + [:div.viewport-container.viewer-overlay + + {:id (str "overlay-" (-> overlay :frame :id)) + :style {:width (:width size-over) + :height (:height size-over) + :left (* (:x (:position overlay)) zoom) + :top (* (:y (:position overlay)) zoom)}} + [:& interactions/viewport + {:frame (:frame overlay) + :base-frame frame + :frame-offset (:position overlay) + :size size-over + :page page + :file file + :users users + :interactions-mode interactions-mode}]]]))]] + + (when (= section :comments) + [:& comments-layer {:file file + :users users + :frame frame + :page page + :zoom zoom}])]]]])) + + + (mf/defc viewer [{:keys [params data]}] @@ -120,11 +228,6 @@ (when (= section :comments) (st/emit! (dcm/close-thread))))) - close-overlay - (mf/use-callback - (fn [frame] - (st/emit! (dv/close-overlay (:id frame))))) - set-up-new-size (mf/use-callback (fn [_] @@ -288,85 +391,28 @@ :page page :file file :section section - :local local}] + :local local + :index index + :viewer-pagination viewer-pagination}] - [:* - [:div.viewer-wrapper - {:style {:width (:width wrapper-size) - :height (:height wrapper-size)}} - [:& (mf/provider ctx/scroll-ctx) {:value @scroll} - [:div.viewer-clipper - [:* - (when orig-frame - [:div.viewport-container - {:ref orig-viewport-ref - :style {:width (:width orig-size) - :height (:height orig-size) - :position "relative"}} - - [:& interactions/viewport - {:frame orig-frame - :base-frame orig-frame - :frame-offset (gpt/point 0 0) - :size orig-size - :page page - :file file - :users users - :interactions-mode :hide}]]) - - [:div.viewport-container - {:ref current-viewport-ref - :style {:width (:width size) - :height (:height size) - :position "relative"}} - - [:& interactions/viewport - {:frame frame - :base-frame frame - :frame-offset (gpt/point 0 0) - :size size - :page page - :file file - :users users - :interactions-mode interactions-mode}] - - (for [overlay overlays] - (let [size-over (calculate-size (:frame overlay) zoom)] - [:* - (when (or (:close-click-outside overlay) - (:background-overlay overlay)) - [:div.viewer-overlay-background - {:class (dom/classnames - :visible (:background-overlay overlay)) - :style {:width (:width wrapper-size) - :height (:height wrapper-size) - :position "absolute" - :left 0 - :top 0} - :on-click #(when (:close-click-outside overlay) - (close-overlay (:frame overlay)))}]) - [:div.viewport-container.viewer-overlay - {:id (str "overlay-" (str (:id (:frame overlay)))) - :style {:width (:width size-over) - :height (:height size-over) - :left (* (:x (:position overlay)) zoom) - :top (* (:y (:position overlay)) zoom)}} - [:& interactions/viewport - {:frame (:frame overlay) - :base-frame frame - :frame-offset (:position overlay) - :size size-over - :page page - :file file - :users users - :interactions-mode interactions-mode}]]]))]] - - (when (= section :comments) - [:& comments-layer {:file file - :users users - :frame frame - :page page - :zoom zoom}])]]]]))]]])) + + [:& viewer-wrapper + {:wrapper-size wrapper-size + :scroll scroll + :orig-frame orig-frame + :orig-viewport-ref orig-viewport-ref + :orig-size orig-size + :page page + :file file + :users users + :current-viewport-ref current-viewport-ref + :size size + :frame frame + :interactions-mode interactions-mode + :overlays overlays + :zoom zoom + :section section + :index index}]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index c813485fbd..ad29c0e409 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -15,6 +15,7 @@ [app.main.ui.comments :as cmt] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] + [app.main.ui.workspace.comments :as wc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [okulary.core :as l] @@ -22,7 +23,7 @@ (mf/defc comments-menu [] - (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) + (let [{cmode :mode cshow :show clist :list} (mf/deref refs/comments-local) show-dropdown? (mf/use-state false) toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) @@ -36,7 +37,12 @@ update-show (mf/use-callback (fn [mode] - (st/emit! (dcm/update-filters {:show mode}))))] + (st/emit! (dcm/update-filters {:show mode})))) + + update-list + (mf/use-callback + (fn [show-list] + (st/emit! (dcm/update-filters {:list show-list}))))] [:div.view-options {:on-click toggle-dropdown} [:span.label (tr "labels.comments")] @@ -59,7 +65,14 @@ [:li {:class (dom/classnames :selected (= :pending cshow)) :on-click #(update-show (if (= :pending cshow) :all :pending))} [:span.icon i/tick] - [:span.label (tr "labels.hide-resolved-comments")]]]]])) + [:span.label (tr "labels.hide-resolved-comments")]] + + [:hr] + + [:li {:class (dom/classnames :selected (= :show clist)) + :on-click #(update-list (if (= :show clist) :hide :show))} + [:span.icon i/tick] + [:span.label (tr "labels.show-comments-list")]]]]])) (defn- frame-contains? @@ -156,3 +169,17 @@ :on-cancel on-draft-cancel :on-submit on-draft-submit :zoom zoom}])]]])) + + +(mf/defc comments-sidebar + [{:keys [users frame page]}] + (let [profile (mf/deref refs/profile) + cstate (mf/deref refs/comments-local) + threads-map (mf/deref threads-ref) + threads (->> (vals threads-map) + (dcm/apply-filters cstate profile) + (filter (fn [{:keys [position]}] + (frame-contains? frame position))))] + [:aside.settings-bar.settings-bar-right.comments-right-sidebar + [:div.settings-bar-inside + [:& wc/comments-sidebar {:users users :threads threads :page-id (:id page)}]]])) diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs index 4a00631ea6..a17fecc13d 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -6,13 +6,13 @@ (ns app.main.ui.viewer.handoff (:require - [app.main.data.viewer :as dv] + [app.main.data.viewer :as dv] [app.main.store :as st] [app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] [app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]] [app.util.dom :as dom] - [app.util.keyboard :as kbd] + [app.util.keyboard :as kbd] [goog.events :as events] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -25,7 +25,7 @@ (st/emit! (dv/select-shape (:id frame))))) (mf/defc viewport - [{:keys [local file page frame]}] + [{:keys [local file page frame index viewer-pagination]}] (let [on-mouse-wheel (fn [event] (when (kbd/mod? event) @@ -58,6 +58,7 @@ :local local :page page}] [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} + [:& viewer-pagination {:index index :num-frames (count (:frames page)) :left-bar true :right-bar true}] [:div.handoff-svg-container [:& render-frame-svg {:frame frame :page page :local local}]]] diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index ad5f2e49cf..33d2df8009 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -107,18 +107,17 @@ i/full-screen)] (when (:is-admin permissions) - [:span.btn-primary {:on-click open-share-dialog} (tr "labels.share-prototype")]) + [:span.btn-primary {:on-click open-share-dialog} i/export [:span (tr "labels.share-prototype")]]) (when (:can-edit permissions) [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])])) (mf/defc header-sitemap - [{:keys [project file page frame index] :as props}] + [{:keys [project file page frame] :as props}] (let [project-name (:name project) file-name (:name file) page-name (:name page) frame-name (:name frame) - total (count (:frames page)) show-dropdown? (mf/use-state false) toggle-thumbnails @@ -151,7 +150,7 @@ [:span "/"] [:span.page-name page-name] - [:span.icon i/arrow-down] + [:& dropdown {:show @show-dropdown? :on-close close-dropdown} @@ -161,12 +160,12 @@ :on-click (partial navigate-to id)} (get-in file [:data :pages-index id :name])])]]] + [:span.icon {:on-click open-dropdown} i/arrow-down] [:div.current-frame {:on-click toggle-thumbnails} [:span.label "/"] - [:span.label frame-name] - [:span.icon i/arrow-down] - [:span.counters (str (inc index) " / " total)]]])) + [:span.label frame-name]] + [:span.icon {:on-click toggle-thumbnails} i/arrow-down]])) (mf/defc header diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index 322aa473c5..3963cf9330 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -57,20 +57,23 @@ [:span.label (tr "labels.hide-resolved-comments")]]])) (mf/defc comments-sidebar - [] + [{:keys [users threads page-id]}] (let [threads-map (mf/deref refs/threads-ref) profile (mf/deref refs/profile) - users (mf/deref refs/users) + users-refs (mf/deref refs/users) + users (or users users-refs) local (mf/deref refs/comments-local) options? (mf/use-state false) + threads (if (nil? threads) + (->> (vals threads-map) + (sort-by :modified-at) + (reverse) + (dcm/apply-filters local profile)) + threads) + tgroups (->> threads + (dcm/group-threads-by-page)) - tgroups (->> (vals threads-map) - (sort-by :modified-at) - (reverse) - (dcm/apply-filters local profile) - (dcm/group-threads-by-page)) - - page-id (mf/use-ctx ctx/current-page-id) + page-id (or page-id (mf/use-ctx ctx/current-page-id)) on-thread-click (mf/use-callback @@ -87,7 +90,7 @@ [:div.comments-section.comment-threads-section [:div.workspace-comment-threads-sidebar-header - [:div.label "Comments"] + [:div.label (tr "labels.comments")] [:div.options {:on-click #(reset! options? true)} [:div.label (case (:mode local) (nil :all) (tr "labels.all") diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 628e35997e..c243eec84a 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1182,6 +1182,9 @@ msgstr "Help Center" msgid "labels.hide-resolved-comments" msgstr "Hide resolved comments" +msgid "labels.show-comments-list" +msgstr "Show comments list" + msgid "labels.icons" msgstr "Icons" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 17a5d933dd..7d64b1f483 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1246,6 +1246,9 @@ msgstr "Centro de ayuda" msgid "labels.hide-resolved-comments" msgstr "Ocultar comentarios resueltos" +msgid "labels.show-comments-list" +msgstr "Mostrar lista de comentarios" + msgid "labels.icons" msgstr "Iconos" From a9303c37c4ab7edd1412298ba3c7d4c3b4290135 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 31 May 2022 11:21:00 +0200 Subject: [PATCH 16/86] :sparkles: Allow for nested frames --- common/src/app/common/pages.cljc | 4 +- common/src/app/common/pages/changes.cljc | 2 +- common/src/app/common/pages/common.cljc | 2 + common/src/app/common/pages/focus.cljc | 15 +- common/src/app/common/pages/helpers.cljc | 95 ++++++- common/src/app/common/pages/indices.cljc | 16 +- common/src/app/common/spec/shape.cljc | 9 +- frontend/src/app/main/constants.cljs | 167 ++++++++++++ frontend/src/app/main/data/workspace.cljs | 24 +- .../src/app/main/data/workspace/common.cljs | 32 +-- .../app/main/data/workspace/drawing/box.cljs | 7 +- .../src/app/main/data/workspace/groups.cljs | 1 - .../src/app/main/data/workspace/indices.cljs | 78 ++++++ .../data/workspace/indices/object_tree.cljs | 28 ++ frontend/src/app/main/ui/shapes/frame.cljs | 12 +- .../app/main/ui/workspace/shapes/frame.cljs | 7 +- .../sidebar/options/menus/measures.cljs | 249 +++++------------- .../app/main/ui/workspace/viewport/hooks.cljs | 3 +- .../main/ui/workspace/viewport/selection.cljs | 2 +- .../main/ui/workspace/viewport/widgets.cljs | 20 +- frontend/src/app/worker/selection.cljs | 38 ++- frontend/translations/en.po | 6 + frontend/translations/es.po | 6 + 23 files changed, 540 insertions(+), 283 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/indices.cljs create mode 100644 frontend/src/app/main/data/workspace/indices/object_tree.cljs diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index 0e6a91a942..e500d372d6 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -26,8 +26,8 @@ (dm/export focus/is-in-focus?) ;; Indices -(dm/export indices/calculate-z-index) -(dm/export indices/update-z-index) +#_(dm/export indices/calculate-z-index) +#_(dm/export indices/update-z-index) (dm/export indices/generate-child-all-parents-index) (dm/export indices/generate-child-parent-index) (dm/export indices/create-clip-index) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 7d511d9ff6..393791cd45 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -211,7 +211,7 @@ (let [invalid-targets (calculate-invalid-targets objects shape-id)] (and (contains? objects shape-id) (not (invalid-targets parent-id)) - (cph/valid-frame-target? objects parent-id shape-id)))) + #_(cph/valid-frame-target? objects parent-id shape-id)))) (insert-items [prev-shapes index shapes] (let [prev-shapes (or prev-shapes [])] diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index c412176f64..8756ac4720 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -82,6 +82,8 @@ :r1 :r2 :r3 :r4 :selrect :points + :show-content + :hide-in-viewer :opacity :blend-mode diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/pages/focus.cljc index df0f2d351f..f4dda2b977 100644 --- a/common/src/app/common/pages/focus.cljc +++ b/common/src/app/common/pages/focus.cljc @@ -13,21 +13,16 @@ (defn focus-objects [objects focus] - (let [[ids-with-children z-index] + (let [ids-with-children (when (d/not-empty? focus) - [(into (conj focus uuid/zero) - (mapcat (partial cph/get-children-ids objects)) - focus) - (cpi/calculate-z-index objects)]) - - sort-by-z-index - (fn [coll] - (->> coll (sort-by (fn [a b] (- (get z-index a) (get z-index b))))))] + (into (conj focus uuid/zero) + (mapcat (partial cph/get-children-ids objects)) + focus))] (cond-> objects (some? ids-with-children) (-> (select-keys ids-with-children) - (assoc-in [uuid/zero :shapes] (sort-by-z-index focus)))))) + (assoc-in [uuid/zero :shapes] (cph/sort-z-index objects focus)))))) (defn filter-not-focus [objects focus ids] diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index b768680eb1..901b43812d 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -141,6 +141,38 @@ (keep lookup))))) (defn get-frames-ids + "Retrieves all frame objects as vector. It is not implemented in + function of `get-immediate-children` for performance reasons. This + function is executed in the render hot path." + [objects] + (let [lookup (d/getf objects) + xform (comp (remove #(= uuid/zero %)) + (keep lookup) + (filter frame-shape?) + (map :id))] + (->> (keys objects) + (into [] xform)))) + +(defn get-frames + "Retrieves all frame objects as vector. It is not implemented in + function of `get-immediate-children` for performance reasons. This + function is executed in the render hot path." + [objects] + (let [lookup (d/getf objects) + xform (comp (remove #(= uuid/zero %)) + (keep lookup) + (filter frame-shape?))] + (->> (keys objects) + (into [] xform)))) + +(defn get-nested-frames + [objects frame-id] + (into #{} + (comp (filter frame-shape?) + (map :id)) + (get-children objects frame-id))) + +(defn get-root-frames-ids "Retrieves all frame objects as vector. It is not implemented in function of `get-immediate-children` for performance reasons. This function is executed in the render hot path." @@ -152,7 +184,7 @@ (->> (:shapes (lookup uuid/zero)) (into [] xform)))) -(defn get-frames +(defn get-root-frames "Retrieves all frame objects as vector. It is not implemented in function of `get-immediate-children` for performance reasons. This function is executed in the render hot path." @@ -163,15 +195,62 @@ (->> (:shapes (lookup uuid/zero)) (into [] xform)))) +(defn- get-base + [objects id-a id-b] + + (let [parents-a (reverse (get-parents-seq objects id-a)) + parents-b (reverse (get-parents-seq objects id-b)) + + [base base-child-a base-child-b] + (loop [parents-a (rest parents-a) + parents-b (rest parents-b) + base uuid/zero] + (if (not= (first parents-a) (first parents-b)) + [base (first parents-a) (first parents-b)] + (recur (rest parents-a) (rest parents-b) (first parents-a)))) + + index-base-a (when base-child-a (get-position-on-parent objects base-child-a)) + index-base-b (when base-child-b (get-position-on-parent objects base-child-b))] + + [base index-base-a index-base-b])) + +(defn is-shape-over-shape? + [objects base-shape-id over-shape-id] + + (let [[base parent-a parent-b] (get-base objects base-shape-id over-shape-id)] + (cond + (= base base-shape-id) + ;; over-shape is a child of base-shape. Will be over if base is a root-frame + (= uuid/zero (get-in objects [base-shape-id :parent-id])) + + (= base over-shape-id) + (not= uuid/zero (get-in objects [over-shape-id :parent-id])) + + :else + (< parent-a parent-b)))) + +(defn sort-z-index + [objects ids] + (letfn [(comp [id-a id-b] + (cond + (= id-a id-b) 0 + (is-shape-over-shape? objects id-a id-b) 1 + :else -1))] + (sort comp ids))) + (defn frame-id-by-position [objects position] - (let [frames (get-frames objects)] - (or - (->> frames - (reverse) - (d/seek #(and position (gsh/has-point? % position))) - :id) - uuid/zero))) + (let [frames (->> (get-frames objects) + (filter #(and position (gsh/has-point? % position)))) + + top-frame + (reduce (fn [current-top frame] + (if (is-shape-over-shape? objects (:id current-top) (:id frame)) + frame + current-top)) + (first frames) + (rest frames))] + (or (:id top-frame) uuid/zero))) (declare indexed-shapes) diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc index 3a5a852eed..571de36138 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -11,10 +11,12 @@ [app.common.uuid :as uuid] [clojure.set :as set])) -(defn calculate-frame-z-index +#_(defn calculate-frame-z-index [z-index frame-id base-idx objects] - (let [is-frame? (fn [id] (= :frame (get-in objects [id :type]))) + (let [is-root-frame? (fn [id] + (and (= :frame (get-in objects [id :type])) + (= uuid/zero (get-in objects [id :parent-id])))) children (or (get-in objects [frame-id :shapes]) [])] (if (empty? children) @@ -25,8 +27,8 @@ z-index z-index] (let [children (get-in objects [current :shapes]) - is-frame? (is-frame? current) - pending (if (not is-frame?) + is-root-frame? (is-root-frame? current) + pending (if (not is-root-frame?) (d/concat-vec pending children) pending)] @@ -41,12 +43,12 @@ ;; internal z-index. To calculate the "final" z-index we add the shape z-index with ;; the z-index of its frame. This way we can update the z-index per frame without ;; the need of recalculate all the frames -(defn calculate-z-index +#_(defn calculate-z-index "Given a collection of shapes calculates their z-index. Greater index means is displayed over other shapes with less index." [objects] - (let [frames (cph/get-frames objects) + (let [frames (cph/get-root-frames objects) by-frame (cph/objects-by-frame objects) frame-base-idx (d/update-vals by-frame count) @@ -57,7 +59,7 @@ (fn [z-index {:keys [id]}] (calculate-frame-z-index z-index id (get frame-base-idx id) objects)) z-index)))) -(defn update-z-index +#_(defn update-z-index "Updates the z-index given a set of ids to change and the old and new objects representations" [z-index changed-ids old-objects new-objects] diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc index 42ae67f60e..0c1deb156b 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/spec/shape.cljc @@ -52,6 +52,8 @@ (s/def ::fill-color-ref-id (s/nilable uuid?)) (s/def ::hide-fill-on-export boolean?) +(s/def ::show-content boolean?) +(s/def ::hide-in-viewer boolean?) (s/def ::file-thumbnail boolean?) (s/def ::masked-group? boolean?) @@ -254,8 +256,7 @@ :internal.shape.text.position-data/rtl :internal.shape.text.position-data/text :internal.shape.text.position-data/text-decoration - :internal.shape.text.position-data/text-transform] - )) + :internal.shape.text.position-data/text-transform])) (s/def :internal.shape.text.position-data/x ::us/safe-number) (s/def :internal.shape.text.position-data/y ::us/safe-number) @@ -303,7 +304,9 @@ (defmethod shape-spec :frame [_] (s/and ::shape-attrs (s/keys :opt-un [::file-thumbnail - ::hide-fill-on-export]))) + ::hide-fill-on-export + ::show-content + ::hide-in-viewer]))) (s/def ::shape (s/and (s/multi-spec shape-spec :type) diff --git a/frontend/src/app/main/constants.cljs b/frontend/src/app/main/constants.cljs index 84087c5e08..5f9e844862 100644 --- a/frontend/src/app/main/constants.cljs +++ b/frontend/src/app/main/constants.cljs @@ -25,3 +25,170 @@ (def has-layout-item false) +(def size-presets + [{:name "APPLE"} + {:name "iPhone 12/12 Pro" + :width 390 + :height 844} + {:name "iPhone 12 Mini" + :width 360 + :height 780} + {:name "iPhone 12 Pro Max" + :width 428 + :height 926} + {:name "iPhone X/XS/11 Pro" + :width 375 + :height 812} + {:name "iPhone XS Max/XR/11" + :width 414 + :height 896} + {:name "iPhone 6/7/8 Plus" + :width 414 + :height 736} + {:name "iPhone 6/7/8/SE2" + :width 375 + :height 667} + {:name "iPhone 5/SE" + :width 320 + :height 568} + {:name "iPad" + :width 768 + :height 1024} + {:name "iPad Pro 10.5in" + :width 834 + :height 1112} + {:name "iPad Pro 12.9in" + :width 1024 + :height 1366} + {:name "Watch 44mm" + :width 368 + :height 448} + {:name "Watch 42mm" + :width 312 + :height 390} + {:name "Watch 40mm" + :width 324 + :height 394} + {:name "Watch 38mm" + :width 272 + :height 340} + + {:name "ANDROID"} + {:name "Mobile" + :width 360 + :height 640} + {:name "Tablet" + :width 768 + :height 1024} + {:name "Google Pixel 4a/5" + :width 393 + :height 851} + {:name "Samsung Galaxy S20+" + :width 384 + :height 854} + {:name "Samsung Galaxy A71/A51" + :width 412 + :height 914} + + {:name "MICROSOFT"} + {:name "Surface Pro 3" + :width 1440 + :height 960} + {:name "Surface Pro 4/5/6/7" + :width 1368 + :height 912} + + {:name "ReMarkable"} + {:name "Remarkable 2" + :width 840 + :height 1120} + + {:name "WEB"} + {:name "Web 1280" + :width 1280 + :height 800} + {:name "Web 1366" + :width 1366 + :height 768} + {:name "Web 1024" + :width 1024 + :height 768} + {:name "Web 1920" + :width 1920 + :height 1080} + + {:name "PRINT (96dpi)"} + {:name "A0" + :width 3179 + :height 4494} + {:name "A1" + :width 2245 + :height 3179} + {:name "A2" + :width 1587 + :height 2245} + {:name "A3" + :width 1123 + :height 1587} + {:name "A4" + :width 794 + :height 1123} + {:name "A5" + :width 559 + :height 794} + {:name "A6" + :width 397 + :height 559} + {:name "Letter" + :width 816 + :height 1054} + {:name "DIN Lang" + :width 835 + :height 413} + + {:name "SOCIAL MEDIA"} + {:name "Instagram profile" + :width 320 + :height 320} + {:name "Instagram post" + :width 1080 + :height 1080} + {:name "Instagram story" + :width 1080 + :height 1920} + {:name "Facebook profile" + :width 720 + :height 720} + {:name "Facebook cover" + :width 820 + :height 312} + {:name "Facebook post" + :width 1200 + :height 630} + {:name "LinkedIn profile" + :width 400 + :height 400} + {:name "LinkedIn cover" + :width 1584 + :height 396} + {:name "LinkedIn post" + :width 1200 + :height 627} + {:name "Twitter profile" + :width 400 + :height 400} + {:name "Twitter header" + :width 1500 + :height 500} + {:name "Twitter post" + :width 1024 + :height 512} + {:name "YouTube profile" + :width 800 + :height 800} + {:name "YouTube banner" + :width 2560 + :height 1440} + {:name "YouTube thumb" + :width 1280 + :height 720}]) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 6ada6cc962..abb8c22441 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -6,6 +6,8 @@ (ns app.main.data.workspace (:require + [app.main.data.workspace.indices :as dwidx] + [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] @@ -127,7 +129,8 @@ team-id (dm/get-in bundle [:project :team-id])] (rx/merge (rx/of (dwn/initialize team-id file-id) - (dwp/initialize-file-persistence file-id)) + (dwp/initialize-file-persistence file-id) + (dwidx/start-indexing file-id)) (->> stream (rx/filter #(= ::dwc/index-initialized %)) @@ -194,6 +197,7 @@ (watch [_ _ _] (rx/merge (rx/of (dwn/finalize file-id)) + (rx/of (dwidx/stop-indexing file-id)) (->> (rx/of ::dwp/finalize) (rx/observe-on :async)))))) @@ -1214,16 +1218,14 @@ ;; selected and its parents objects (cph/selected-subtree objects selected) - z-index (cp/calculate-z-index objects) - z-values (->> selected - (map #(vector % - (+ (get z-index %) - (get z-index (get-in objects [% :frame-id])))))) - selected - (->> z-values - (sort-by second) - (map first) - (into (d/ordered-set)))] + ;;z-index (cp/calculate-z-index objects) + ;;z-values (->> selected + ;; (map #(vector % + ;; (+ (get z-index %) + ;; (get z-index (get-in objects [% :frame-id])))))) + + selected (-> (cph/sort-z-index objects selected) + (into (d/ordered-set)))] (assoc data :selected selected))) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index bdc2b61103..5032a70fe9 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -261,25 +261,21 @@ (defn get-shape-layer-position [objects selected attrs] - (if (= :frame (:type attrs)) - ;; Frames are always positioned on the root frame - [uuid/zero uuid/zero nil] + ;; Calculate the frame over which we're drawing + (let [position @ms/mouse-position + frame-id (:frame-id attrs (cph/frame-id-by-position objects position)) + shape (when-not (empty? selected) + (cph/get-base-shape objects selected))] - ;; Calculate the frame over which we're drawing - (let [position @ms/mouse-position - frame-id (:frame-id attrs (cph/frame-id-by-position objects position)) - shape (when-not (empty? selected) - (cph/get-base-shape objects selected))] + ;; When no shapes has been selected or we're over a different frame + ;; we add it as the latest shape of that frame + (if (or (not shape) (not= (:frame-id shape) frame-id)) + [frame-id frame-id nil] - ;; When no shapes has been selected or we're over a different frame - ;; we add it as the latest shape of that frame - (if (or (not shape) (not= (:frame-id shape) frame-id)) - [frame-id frame-id nil] - - ;; Otherwise, we add it to next to the selected shape - (let [index (cph/get-position-on-parent objects (:id shape)) - {:keys [frame-id parent-id]} shape] - [frame-id parent-id (inc index)]))))) + ;; Otherwise, we add it to next to the selected shape + (let [index (cph/get-position-on-parent objects (:id shape)) + {:keys [frame-id parent-id]} shape] + [frame-id parent-id (inc index)])))) (defn make-new-shape [attrs objects selected] @@ -325,7 +321,7 @@ selected) changes (-> (pcb/empty-changes it page-id) - (pcb/add-object shape {:index (when (= :frame (:type shape)) 0)}))] + (pcb/add-object shape #_{:index (when (= :frame (:type shape)) 0)}))] (rx/concat (rx/of (dch/commit-changes changes) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index c2eeefc703..b5dcaf1077 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -65,12 +65,7 @@ focus (:workspace-focus-selected state) zoom (get-in state [:workspace-local :zoom] 1) - frames (cph/get-frames objects) - fid (or (->> frames - (filter #(gsh/has-point? % initial)) - first - :id) - uuid/zero) + fid (cph/frame-id-by-position objects initial) shape (-> state (get-in [:workspace-drawing :object]) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index fc7d905be3..3dd032e0c9 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -21,7 +21,6 @@ [objects selected] (->> selected (map #(get objects %)) - (filter #(not= :frame (:type %))) (map #(assoc % ::index (cph/get-position-on-parent objects (:id %)))) (sort-by ::index))) diff --git a/frontend/src/app/main/data/workspace/indices.cljs b/frontend/src/app/main/data/workspace/indices.cljs new file mode 100644 index 0000000000..4cf4fb39a4 --- /dev/null +++ b/frontend/src/app/main/data/workspace/indices.cljs @@ -0,0 +1,78 @@ +;; 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.indices + (:require + [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.indices.object-tree :as dwi-object-tree] + [app.main.refs :as refs] + [app.main.data.workspace.changes :as dwc] + [beicon.core :as rx] + [app.common.data :as d] + [potok.core :as ptk])) + +(def stop-indexing? (ptk/type? ::stop-indexing)) + +(def objects-changes #{:add-obj :mod-obj :del-obj :mov-objects}) + +(defn stop-indexing + [file-id] + (ptk/reify ::stop-indexing + ptk/UpdateEvent + (update [_ state] + (-> state + (dissoc :index-object-tree))))) + +(defn process-changes + "Simplify changes so we have only the type of operation and the ids" + [changes] + (->> changes + (filter #(contains? objects-changes (:type %))) + (mapcat (fn [{:keys [type id shapes]}] + (if (some? shapes) + (->> shapes (map #(vector type %))) + [[type id]]))))) + +(defn update-indexing + [change-type shape-id old-objects new-objects] + (ptk/reify ::update-indexing + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state)] + (-> state + (update :index-object-tree dwi-object-tree/update-index shape-id change-type old-objects new-objects)))))) + +(defn start-indexing + [file-id] + + (ptk/reify ::start-indexing + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state)] + (-> state + (assoc :index-object-tree (dwi-object-tree/init-index objects))))) + + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (->> stream (rx/filter stop-indexing?) (rx/take 1)) + objects-delta (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/buffer 2 1))] + (->> stream + (rx/filter dwc/commit-changes?) + (rx/flat-map #(->> % deref :changes process-changes)) + (rx/with-latest-from objects-delta) + (rx/map (fn [[[type id] [objects-old objects-new]]] + (update-indexing type id objects-old objects-new))) + #_(rx/tap (fn [[[type id] [objects-old objects-new]]] + (let [obj-old (get objects-old id) + obj-new (get objects-new id)] + (prn ">change" (or (:name obj-old) (:name obj-new))) + (prn " > " type) + (.log js/console " >" (clj->js obj-old)) + (.log js/console " >" (clj->js obj-new)) + + ))) + (rx/take-until stopper) + (rx/ignore)))))) diff --git a/frontend/src/app/main/data/workspace/indices/object_tree.cljs b/frontend/src/app/main/data/workspace/indices/object_tree.cljs new file mode 100644 index 0000000000..e772b0ad8e --- /dev/null +++ b/frontend/src/app/main/data/workspace/indices/object_tree.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.main.data.workspace.indices.object-tree + (:require + [app.common.pages.helpers :as cph] + )) + +(defn objects-tree + [objects] + + + + ) + +(defn init-index + [objects] + + + + ) + +(defn update-index + [index shape-id change-type old-objects new-objects] + ) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 8e4d078662..3275e6ab71 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -45,7 +45,7 @@ [props] (let [shape (obj/get props "shape")] (when (:thumbnail shape) - (let [{:keys [x y width height]} shape + (let [{:keys [x y width height show-content]} shape transform (gsh/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! @@ -59,8 +59,9 @@ render-id (mf/use-ctx muc/render-ctx)] [:* - [:g {:clip-path (frame-clip-url shape render-id)} - [:& frame-clip-def {:shape shape :render-id render-id}] + [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id))} + (when (not show-content) + [:& frame-clip-def {:shape shape :render-id render-id}]) [:& shape-fills {:shape shape} (if path? [:> :path props] @@ -88,7 +89,7 @@ [props] (let [childs (unchecked-get props "childs") shape (unchecked-get props "shape") - {:keys [x y width height]} shape + {:keys [x y width height show-content]} shape transform (gsh/transform-matrix shape) @@ -104,7 +105,8 @@ render-id (mf/use-ctx muc/render-ctx)] [:* - [:g {:clip-path (frame-clip-url shape render-id)} + [:g {:clip-path (when (not show-content) + (frame-clip-url shape render-id))} [:& shape-fills {:shape shape} (if path? [:> :path props] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index ec70154f7e..7f23daf08f 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.workspace.shapes.frame (:require + [app.main.store :as st] + [app.main.data.workspace.state-helpers :as wsh] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.uuid :as uuid] @@ -45,8 +47,7 @@ [new-props old-props] (and (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) - (= (unchecked-get new-props "shape") (unchecked-get old-props "shape")) - (= (unchecked-get new-props "objects") (unchecked-get old-props "objects")))) + (= (unchecked-get new-props "shape") (unchecked-get old-props "shape")))) (defn frame-wrapper-factory [shape-wrapper] @@ -59,7 +60,7 @@ (let [shape (unchecked-get props "shape") thumbnail? (unchecked-get props "thumbnail?") - objects (unchecked-get props "objects") + objects (wsh/lookup-page-objects @st/state) render-id (mf/use-memo #(str (uuid/next))) fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects)) 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 0977b1a632..8983398070 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,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.measures (:require + [app.main.constants :refer [size-presets]] [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.spec.radius :as ctr] @@ -30,12 +31,14 @@ :rx :ry :r1 :r2 :r3 :r4 :selrect - :points]) + :points + :show-content + :hide-in-viewer]) (def ^:private type->options {:bool #{:size :position :rotation} :circle #{:size :position :rotation} - :frame #{:presets :size :position :radius} + :frame #{:presets :size :position :radius :clip-content :show-in-viewer} :group #{:size :position :rotation} :image #{:size :position :rotation :radius} :path #{:size :position :rotation} @@ -43,8 +46,6 @@ :svg-raw #{:size :position :rotation} :text #{:size :position :rotation}}) -(declare +size-presets+) - ;; -- User/drawing coords (mf/defc measures-menu [{:keys [ids ids-with-children values type all-types shape] :as props}] @@ -103,6 +104,9 @@ radius-multi? (mf/use-state nil) radius-input-ref (mf/use-ref nil) + clip-content-ref (mf/use-ref nil) + show-in-viewer-ref (mf/use-ref nil) + on-preset-selected (fn [width height] (st/emit! (udw/update-dimensions ids :width width) @@ -146,13 +150,13 @@ change-radius (mf/use-callback - (mf/deps ids-with-children) - (fn [update-fn] - (dch/update-shapes ids-with-children - (fn [shape] - (if (ctr/has-radius? shape) - (update-fn shape) - shape))))) + (mf/deps ids-with-children) + (fn [update-fn] + (dch/update-shapes ids-with-children + (fn [shape] + (if (ctr/has-radius? shape) + (update-fn shape) + shape))))) on-switch-to-radius-1 (mf/use-callback @@ -200,17 +204,38 @@ on-radius-r3-change #(on-radius-4-change % :r3) on-radius-r4-change #(on-radius-4-change % :r4) + on-change-clip-content + (mf/use-callback + (mf/deps ids) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (dch/update-shapes ids (fn [shape]) (assoc shape :show-content (not value)))))) + + on-change-clip-content + (mf/use-callback + (mf/deps ids) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :show-content (not value)))))))) + + on-change-show-in-viewer + (mf/use-callback + (mf/deps ids) + (fn [event] + (let [value (-> event dom/get-target dom/checked?)] + (st/emit! (dch/update-shapes ids (fn [shape] (assoc shape :hide-in-viewer (not value)))))))) + select-all #(-> % (dom/get-target) (.select))] - (mf/use-layout-effect - (mf/deps radius-mode @radius-multi?) - (fn [] - (when (and (= radius-mode :radius-1) - (= @radius-multi? false)) - ;; when going back from radius-multi to normal radius-1, - ;; restore focus to the newly created numeric-input - (let [radius-input (mf/ref-val radius-input-ref)] - (dom/focus! radius-input))))) + (mf/use-layout-effect + (mf/deps radius-mode @radius-multi?) + (fn [] + (when (and (= radius-mode :radius-1) + (= @radius-multi? false)) + ;; when going back from radius-multi to normal radius-1, + ;; restore focus to the newly created numeric-input + (let [radius-input (mf/ref-val radius-input-ref)] + (dom/focus! radius-input))))) [:* [:div.element-set @@ -226,7 +251,7 @@ [:& dropdown {:show @show-presets-dropdown? :on-close #(reset! show-presets-dropdown? false)} [:ul.custom-select-dropdown - (for [size-preset +size-presets+] + (for [size-preset size-presets] (if-not (:width size-preset) [:li.dropdown-label {:key (:name size-preset)} [:span (:name size-preset)]] @@ -367,172 +392,30 @@ :min 0 :on-click select-all :on-change on-radius-r4-change - :value (:r4 values)}]]])])]]])) + :value (:r4 values)}]]])]) - (def +size-presets+ - [{:name "APPLE"} - {:name "iPhone 12/12 Pro" - :width 390 - :height 844} - {:name "iPhone 12 Mini" - :width 360 - :height 780} - {:name "iPhone 12 Pro Max" - :width 428 - :height 926} - {:name "iPhone X/XS/11 Pro" - :width 375 - :height 812} - {:name "iPhone XS Max/XR/11" - :width 414 - :height 896} - {:name "iPhone 6/7/8 Plus" - :width 414 - :height 736} - {:name "iPhone 6/7/8/SE2" - :width 375 - :height 667} - {:name "iPhone 5/SE" - :width 320 - :height 568} - {:name "iPad" - :width 768 - :height 1024} - {:name "iPad Pro 10.5in" - :width 834 - :height 1112} - {:name "iPad Pro 12.9in" - :width 1024 - :height 1366} - {:name "Watch 44mm" - :width 368 - :height 448} - {:name "Watch 42mm" - :width 312 - :height 390} - {:name "Watch 40mm" - :width 324 - :height 394} - {:name "Watch 38mm" - :width 272 - :height 340} + (when (options :clip-content) + [:div.input-checkbox + [:input {:type "checkbox" + :id "clip-content" + :ref clip-content-ref + :checked (not (:show-content values)) + :on-change on-change-clip-content}] - {:name "ANDROID"} - {:name "Mobile" - :width 360 - :height 640} - {:name "Tablet" - :width 768 - :height 1024} - {:name "Google Pixel 4a/5" - :width 393 - :height 851} - {:name "Samsung Galaxy S20+" - :width 384 - :height 854} - {:name "Samsung Galaxy A71/A51" - :width 412 - :height 914} + [:label {:for "clip-content"} + (tr "workspace.options.clip-content")]]) - {:name "MICROSOFT"} - {:name "Surface Pro 3" - :width 1440 - :height 960} - {:name "Surface Pro 4/5/6/7" - :width 1368 - :height 912} + (when (options :show-in-viewer) + [:div.input-checkbox + [:input {:type "checkbox" + :id "show-in-viewer" + :ref show-in-viewer-ref + :checked (not (:hide-in-viewer values)) + :on-change on-change-show-in-viewer}] - {:name "ReMarkable"} - {:name "Remarkable 2" - :width 840 - :height 1120} + [:label {:for "show-in-viewer"} + (tr "workspace.options.show-in-viewer")]]) - {:name "WEB"} - {:name "Web 1280" - :width 1280 - :height 800} - {:name "Web 1366" - :width 1366 - :height 768} - {:name "Web 1024" - :width 1024 - :height 768} - {:name "Web 1920" - :width 1920 - :height 1080} + ]]])) - {:name "PRINT (96dpi)"} - {:name "A0" - :width 3179 - :height 4494} - {:name "A1" - :width 2245 - :height 3179} - {:name "A2" - :width 1587 - :height 2245} - {:name "A3" - :width 1123 - :height 1587} - {:name "A4" - :width 794 - :height 1123} - {:name "A5" - :width 559 - :height 794} - {:name "A6" - :width 397 - :height 559} - {:name "Letter" - :width 816 - :height 1054} - {:name "DIN Lang" - :width 835 - :height 413} - {:name "SOCIAL MEDIA"} - {:name "Instagram profile" - :width 320 - :height 320} - {:name "Instagram post" - :width 1080 - :height 1080} - {:name "Instagram story" - :width 1080 - :height 1920} - {:name "Facebook profile" - :width 720 - :height 720} - {:name "Facebook cover" - :width 820 - :height 312} - {:name "Facebook post" - :width 1200 - :height 630} - {:name "LinkedIn profile" - :width 400 - :height 400} - {:name "LinkedIn cover" - :width 1584 - :height 396} - {:name "LinkedIn post" - :width 1200 - :height 627} - {:name "Twitter profile" - :width 400 - :height 400} - {:name "Twitter header" - :width 1500 - :height 500} - {:name "Twitter post" - :width 1024 - :height 512} - {:name "YouTube profile" - :width 800 - :height 800} - {:name "YouTube banner" - :width 2560 - :height 1440} - {:name "YouTube thumb" - :width 1280 - :height 720}]) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 80c600da1c..dd4123d489 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -172,6 +172,7 @@ over-shapes-stream (mf/deps page-id objects) (fn [ids] + #_(prn "??hover-ids" (->> ids (map #(get-in objects [% :name])))) (let [is-group? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) @@ -221,7 +222,7 @@ [objects hover-ids selected active-frames zoom transform vbox] (let [frame? #(= :frame (get-in objects [% :type])) - all-frames (mf/use-memo (mf/deps objects) #(cph/get-frames-ids objects)) + all-frames (mf/use-memo (mf/deps objects) #(cph/get-root-frames-ids objects)) selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected))) xf-selected-frame (comp (remove frame?) (map #(get-in objects [% :frame-id]))) selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected)) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index dcac1a492f..7b1a5ebd38 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -316,7 +316,7 @@ :color color} props (map->obj (merge common-props props))] (case type - :rotation (when (not= :frame (:type shape)) [:> rotation-handler props]) + :rotation [:> rotation-handler props] :resize-point [:> resize-point-handler props] :resize-side [:> resize-side-handler props])))]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 67c58faa94..e62258148b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.widgets (:require + [app.common.uuid :as uuid] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -182,15 +183,16 @@ [:g.frame-titles (for [frame frames] - [:& frame-title {:key (dm/str "frame-title-" (:id frame)) - :frame frame - :selected? (contains? selected (:id frame)) - :zoom zoom - :show-artboard-names? show-artboard-names? - :modifiers modifiers - :on-frame-enter on-frame-enter - :on-frame-leave on-frame-leave - :on-frame-select on-frame-select}])])) + (when (= (:frame-id frame) uuid/zero) + [:& frame-title {:key (dm/str "frame-title-" (:id frame)) + :frame frame + :selected? (contains? selected (:id frame)) + :zoom zoom + :show-artboard-names? show-artboard-names? + :modifiers modifiers + :on-frame-enter on-frame-enter + :on-frame-leave on-frame-leave + :on-frame-select on-frame-select}]))])) (mf/defc frame-flow [{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 692cf61ce6..dddee6f558 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -86,12 +86,17 @@ index (reduce index-shape initial-quadtree shapes) - z-index (cp/calculate-z-index objects)] + ;;z-index (cp/calculate-z-index objects) + ] - {:index index :z-index z-index :bounds bounds})) + {:index index + ;;:z-index z-index + :bounds bounds})) (defn- update-index - [{index :index z-index :z-index :as data} old-objects new-objects] + [{index :index + ;; z-index :z-index + :as data} old-objects new-objects] (let [changes? (fn [id] (not= (get old-objects id) (get new-objects id))) @@ -112,12 +117,16 @@ index-shape (make-index-shape new-objects parents-index clip-parents-index) index (reduce index-shape new-index shapes) - z-index (cp/update-z-index z-index changed-ids old-objects new-objects)] + ;;z-index (cp/update-z-index z-index changed-ids old-objects new-objects) + ] - (assoc data :index index :z-index z-index))) + (assoc data :index index ;;:z-index z-index + ))) (defn- query-index - [{index :index z-index :z-index} rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?] + [{index :index + ;;z-index :z-index + } rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -145,10 +154,10 @@ (fn [clip-parents] (->> clip-parents (some (comp not overlaps?)) not)) - add-z-index - (fn [{:keys [id frame-id] :as shape}] - (assoc shape :z (+ (get z-index id) - (get z-index frame-id 0)))) + ;;add-z-index + ;;(fn [{:keys [id frame-id] :as shape}] + ;; (assoc shape :z (+ (get z-index id) + ;; (get z-index frame-id 0)))) ;; Shapes after filters of overlapping and criteria matching-shapes @@ -160,14 +169,15 @@ (filter (if clip-children? (comp overlaps-parent? :clip-parents) (constantly true))) - (map add-z-index)) + #_(map add-z-index)) result) - keyfn (if reverse? (comp - :z) :z)] + ;;keyfn (if reverse? (comp - :z) :z) + ] (into (d/ordered-set) (->> matching-shapes - (sort-by keyfn) + #_(sort-by keyfn) (map :id))))) @@ -208,7 +218,7 @@ (when-let [index (get @state page-id)] (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?))) -(defmethod impl/handler :selection/query-z-index +#_(defmethod impl/handler :selection/query-z-index [{:keys [page-id objects ids]}] (when-let [{z-index :z-index} (get @state page-id)] (->> ids (map #(+ (get z-index %) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 156b7ffc7d..67d209c241 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3890,6 +3890,12 @@ msgstr "X" msgid "workspace.options.y" msgstr "Y" +msgid "workspace.options.clip-content" +msgstr "Clip content" + +msgid"workspace.options.show-in-viewer" +msgstr "Show in view mode" + msgid "workspace.path.actions.add-node" msgstr "Add node (%s)" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 7d521607a2..b6ec5136c3 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -4058,6 +4058,12 @@ msgstr "X" msgid "workspace.options.y" msgstr "Y" +msgid "workspace.options.clip-content" +msgstr "Truncar contenido" + +msgid"workspace.options.show-in-viewer" +msgstr "Mostrar en modo visualización" + msgid "workspace.path.actions.add-node" msgstr "Añadir nodo (%s)" From aa584e6d35141f565a4ddb95e26b247b7700360c Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 31 May 2022 16:56:14 +0200 Subject: [PATCH 17/86] :recycle: Refactor transform matrix --- common/src/app/common/geom/shapes.cljc | 1 + .../app/common/geom/shapes/transforms.cljc | 18 ++++++++-- common/src/app/common/pages/common.cljc | 1 + .../src/app/main/data/workspace/common.cljs | 8 ++--- .../app/main/data/workspace/drawing/box.cljs | 22 +++++++----- .../data/workspace/libraries_helpers.cljs | 6 ++-- .../app/main/data/workspace/selection.cljs | 10 +++--- frontend/src/app/main/errors.cljs | 10 ++++-- frontend/src/app/main/ui/hooks/resize.cljs | 34 +++++++++++-------- frontend/src/app/main/ui/shapes/circle.cljs | 4 +-- frontend/src/app/main/ui/shapes/fills.cljs | 2 +- frontend/src/app/main/ui/shapes/frame.cljs | 12 ++++--- .../src/app/main/ui/shapes/gradients.cljs | 6 ++-- frontend/src/app/main/ui/shapes/image.cljs | 2 +- frontend/src/app/main/ui/shapes/rect.cljs | 2 +- frontend/src/app/main/ui/shapes/svg_raw.cljs | 2 +- .../src/app/main/ui/shapes/text/fo_text.cljs | 4 +-- .../src/app/main/ui/shapes/text/svg_text.cljs | 2 +- .../app/main/ui/viewer/handoff/render.cljs | 8 ++--- frontend/src/app/main/ui/viewer/shapes.cljs | 12 +++---- .../shapes/frame/dynamic_modifiers.cljs | 5 +-- .../shapes/text/text_edition_outline.cljs | 4 +-- .../src/app/main/ui/workspace/viewport.cljs | 2 +- .../main/ui/workspace/viewport/outline.cljs | 2 +- .../main/ui/workspace/viewport/selection.cljs | 10 +++--- .../main/ui/workspace/viewport/widgets.cljs | 15 ++++---- 26 files changed, 118 insertions(+), 86 deletions(-) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index ed5c52aa9e..90debd442c 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -159,6 +159,7 @@ (dm/export gtr/move) (dm/export gtr/absolute-move) (dm/export gtr/transform-matrix) +(dm/export gtr/transform-str) (dm/export gtr/inverse-transform-matrix) (dm/export gtr/transform-point-center) (dm/export gtr/transform-rect) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 6fd41c01a2..a334b33172 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -7,6 +7,7 @@ (ns app.common.geom.shapes.transforms (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gco] @@ -143,16 +144,29 @@ ([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] + ([{:keys [flip-x flip-y transform] :as shape} {:keys [no-flip]} shape-center] (-> (gmt/matrix) (gmt/translate shape-center) - (gmt/multiply (:transform shape (gmt/matrix))) + (cond-> (some? transform) + (gmt/multiply transform)) + (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 transform-str + ([shape] + (transform-str shape nil)) + + ([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip]}] + (when (and (some? shape) + (or (some? transform) + (and (not no-flip) flip-x) + (and (not no-flip) flip-y))) + (dm/str (transform-matrix shape))))) + (defn inverse-transform-matrix ([shape] (let [shape-center (or (gco/center-shape shape) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 8756ac4720..1b73deac24 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -80,6 +80,7 @@ :x :y :rx :ry :r1 :r2 :r3 :r4 + :rotation :selrect :points :show-content diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 5032a70fe9..5bc125ce1e 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -338,11 +338,9 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - to-move-shapes (->> (cph/get-immediate-children objects) - (remove cph/frame-shape?) - (d/enumerate) - (filterv (comp shapes :id second)) - (mapv second)) + to-move-shapes (into [] + (map (d/getf objects)) + (reverse (cph/sort-z-index objects shapes))) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index b5dcaf1077..6de3726a60 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -67,15 +67,19 @@ fid (cph/frame-id-by-position objects initial) - shape (-> state - (get-in [:workspace-drawing :object]) - (cp/setup-shape {:x (:x initial) - :y (:y initial) - :width 0.01 - :height 0.01}) - (assoc :frame-id fid) - (assoc :initialized? true) - (assoc :click-draw? true))] + shape (get-in state [:workspace-drawing :object]) + shape (-> shape + (cp/setup-shape {:x (:x initial) + :y (:y initial) + :width 0.01 + :height 0.01}) + (cond-> (and (cph/frame-shape? shape) + (not= fid uuid/zero)) + (assoc :fills [] :show-content true :hide-in-viewer true)) + + (assoc :frame-id fid) + (assoc :initialized? true) + (assoc :click-draw? true))] (rx/concat ;; Add shape to drawing state (rx/of #(assoc-in state [:workspace-drawing :object] shape)) diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 5172481ca5..7fcb2cefa3 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] @@ -158,7 +158,7 @@ (cond-> new-shape true (as-> $ - (geom/move $ delta) + (gsh/move $ delta) (assoc $ :frame-id frame-id) (assoc $ :parent-id (or (:parent-id $) (:frame-id $))) @@ -1150,7 +1150,7 @@ origin-root-pos (shape-pos origin-root) dest-root-pos (shape-pos dest-root) delta (gpt/subtract dest-root-pos origin-root-pos)] - (geom/move shape delta))) + (gsh/move shape delta))) (defn- make-change [container change] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 5a039e34ef..eff690a40b 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] @@ -264,7 +264,7 @@ ;; in the later vector position selected (->> children reverse - (d/seek #(geom/has-point? % position)))] + (d/seek #(gsh/has-point? % position)))] (when selected (rx/of (select-shape (:id selected)))))))) @@ -325,7 +325,7 @@ :frame-id uuid/zero :shapes []) (dissoc :use-for-thumbnail?) - (geom/move delta) + (gsh/move delta) (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) changes (-> (pcb/add-object changes new-frame) @@ -360,7 +360,7 @@ :parent-id parent-id :frame-id frame-id) (dissoc :shapes) - (geom/move delta) + (gsh/move delta) (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) changes (-> (pcb/add-object changes new-obj {:ignore-touched true}) @@ -412,7 +412,7 @@ (fn [g frame] (let [new-id (ids-map (:id frame)) new-frame (-> frame - (geom/move delta)) + (gsh/move delta)) new-guides (->> guides (vals) (filter #(= (:frame-id %) (:id frame))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 9bacf35c58..19dba98dc9 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -7,6 +7,7 @@ (ns app.main.errors "Generic error handling" (:require + [cuerdas.core :as str] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -200,9 +201,12 @@ (defonce uncaught-error-handler (letfn [(on-error [event] - (.preventDefault ^js event) - (some-> (unchecked-get event "error") - (on-unhandled-error)))] + ;; EvalError is a debug error that happens for unknown reason + (when-not (str/includes? (.-message event) "EvalError") + (.error js/console event) + (.preventDefault ^js event) + (some-> (unchecked-get event "error") + (on-unhandled-error))))] (.addEventListener glob/window "error" on-error) (fn [] (.removeEventListener glob/window "error" on-error)))) diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index bd72680709..a2c52cd362 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.hooks.resize (:require + [app.main.ui.hooks :as hooks] [app.common.geom.point :as gpt] [app.common.logging :as log] [app.main.ui.context :as ctx] @@ -77,26 +78,31 @@ (let [prev-val-ref (mf/use-ref nil) current-observer-ref (mf/use-ref nil) + callback-ref (hooks/use-update-var {:callback callback}) + ;; We use the ref as a callback when the dom node is ready (or change) node-ref (mf/use-callback - (mf/deps callback) (fn [^js node] - (let [^js current-observer (mf/ref-val current-observer-ref) - ^js prev-val (mf/ref-val prev-val-ref)] + (when (some? node) + (let [^js current-observer (mf/ref-val current-observer-ref) + ^js prev-val (mf/ref-val prev-val-ref)] - (when (and (not= prev-val node) (some? current-observer)) - (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) - (.disconnect current-observer) - (mf/set-ref-val! current-observer-ref nil)) + (when (and (not= prev-val node) (some? current-observer)) + (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) + (.disconnect current-observer) + (mf/set-ref-val! current-observer-ref nil)) - (when (and (not= prev-val node) (some? node)) - (let [^js observer - (js/ResizeObserver. #(callback last-resize-type (dom/get-client-size node)))] - (mf/set-ref-val! current-observer-ref observer) - (log/debug :action "observe" :js/node node :js/observer observer) - (.observe observer node)))) - (mf/set-ref-val! prev-val-ref node)))] + (when (and (not= prev-val node) (some? node)) + (let [^js observer + (js/ResizeObserver. + #(let [callback (get @callback-ref :callback)] + (callback last-resize-type (dom/get-client-size node))))] + (mf/set-ref-val! current-observer-ref observer) + (log/debug :action "observe" :js/node node :js/observer observer) + (.observe observer node)))) + + (mf/set-ref-val! prev-val-ref node))))] (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index 5924765404..f5db9bf3ed 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -6,7 +6,7 @@ (ns app.main.ui.shapes.circle (:require - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] @@ -17,7 +17,7 @@ [props] (let [shape (unchecked-get props "shape") {:keys [x y width height]} shape - transform (geom/transform-matrix shape) + transform (gsh/transform-str shape) cx (+ x (/ width 2)) cy (+ y (/ height 2)) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 788a433153..36cc2c24b1 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -41,7 +41,7 @@ (cfg/resolve-file-media (:fill-image shape))) embed (embed/use-data-uris [uri]) - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) ;; When true the image has not loaded yet loading? (and (some? uri) (not (contains? embed uri))) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 3275e6ab71..5eb258c544 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -28,12 +28,14 @@ [{:keys [shape render-id]}] (when (= :frame (:type shape)) (let [{:keys [x y width height]} shape + transform (gsh/transform-str shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x x :y y :width width - :height height})) + :height height + :transform transform})) path? (some? (.-d props))] [:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"} (if path? @@ -46,12 +48,12 @@ (let [shape (obj/get props "shape")] (when (:thumbnail shape) (let [{:keys [x y width height show-content]} shape - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x x :y y - :transform (str transform) + :transform transform :width width :height height :className "frame-background"})) @@ -91,13 +93,13 @@ shape (unchecked-get props "shape") {:keys [x y width height show-content]} shape - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x x :y y - :transform (str transform) + :transform transform :width width :height height :className "frame-background"})) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index aaee2d5291..a57678bf47 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -27,13 +27,15 @@ (obj/set! "penpot:width" (:width gradient)))) (mf/defc linear-gradient [{:keys [id gradient shape]}] - (let [transform (when (= :path (:type shape)) (gsh/transform-matrix shape nil (gpt/point 0.5 0.5))) + (let [transform (when (= :path (:type shape)) + (gsh/transform-matrix shape nil (gpt/point 0.5 0.5))) + base-props #js {:id id :x1 (:start-x gradient) :y1 (:start-y gradient) :x2 (:end-x gradient) :y2 (:end-y gradient) - :gradientTransform transform} + :gradientTransform (dm/str transform)} include-metadata? (mf/use-ctx ed/include-metadata-ctx) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 5a80f78444..c13fa3a205 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -18,7 +18,7 @@ (let [shape (unchecked-get props "shape") {:keys [x y width height]} shape - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! (attrs/extract-border-radius-attrs shape)) (obj/merge! diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index 8c8a24e792..e75a804eeb 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -17,7 +17,7 @@ [props] (let [shape (unchecked-get props "shape") {:keys [x y width height]} shape - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 690ee60220..188390cac5 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -60,7 +60,7 @@ (obj/set! "preserveAspectRatio" "none"))] [:& (mf/provider svg-ids-ctx) {:value ids-mapping} - [:g.svg-raw {:transform (dm/str (gsh/transform-matrix shape))} + [:g.svg-raw {:transform (gsh/transform-str shape)} [:> "svg" attrs children]]])) (mf/defc svg-element diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 8f3f53575d..158b6c1173 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -8,7 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] @@ -192,7 +192,7 @@ ::mf/forward-ref true} [props ref] (let [shape (obj/get props "shape") - transform (str (geom/transform-matrix shape)) + transform (gsh/transform-str shape) {:keys [id x y width height content]} shape grow-type (obj/get props "grow-type") ;; This is only needed in workspace diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index 4c8054c247..7a5fad4d2f 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -60,7 +60,7 @@ {:keys [x y width height position-data]} shape - transform (str (gsh/transform-matrix shape {:no-flip true})) + transform (gsh/transform-str shape {:no-flip true}) ;; These position attributes are not really necesary but they are convenient for for the export group-props (-> #js {:transform transform diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index d46c2eabdb..30df848d7c 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -7,7 +7,7 @@ (ns app.main.ui.viewer.handoff.render "The main container for a frame in handoff mode" (:require - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.main.data.viewer :as dv] [app.main.store :as st] @@ -87,7 +87,7 @@ [props] (let [shape (unchecked-get props "shape") childs (mapv #(get objects %) (:shapes shape)) - shape (geom/transform-shape shape) + shape (gsh/transform-shape shape) props (-> (obj/create) (obj/merge! props) @@ -166,8 +166,8 @@ (mf/use-memo (mf/deps objects) #(svg-raw-container-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (-> (geom/transform-shape shape) - (geom/translate-to-frame frame)) + (let [shape (-> (gsh/transform-shape shape) + (gsh/translate-to-frame frame)) opts #js {:shape shape :frame frame}] (case (:type shape) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 4e28186b72..3398a463e8 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -8,7 +8,7 @@ "The main container for a frame in viewer mode" (:require [app.common.data :as d] - [app.common.geom.shapes :as geom] + [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.spec.interactions :as cti] [app.main.data.viewer :as dv] @@ -204,7 +204,7 @@ :stroke-width (if interactions-show? 1 0) :fill-opacity (if interactions-show? 0.2 0) :style {:pointer-events (when frame? "none")} - :transform (geom/transform-matrix shape)}]))) + :transform (gsh/transform-str shape)}]))) (defn generic-wrapper-factory "Wrap some svg shape and add interaction controls" @@ -306,7 +306,7 @@ [props] (let [shape (obj/get props "shape") childs (mapv #(get objects %) (:shapes shape)) - shape (geom/transform-shape shape) + shape (gsh/transform-shape shape) props (obj/merge! #js {} props #js {:shape shape :childs childs @@ -384,9 +384,9 @@ (mf/use-memo (mf/deps objects) #(svg-raw-container-factory objects))] (when (and shape (not (:hidden shape))) - (let [shape (-> (geom/transform-shape shape) - (geom/translate-to-frame frame) - (cond-> fixed? (geom/move delta))) + (let [shape (-> (gsh/transform-shape shape) + (gsh/translate-to-frame frame) + (cond-> fixed? (gsh/move delta))) opts #js {:shape shape :objects objects}] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index ae25c9177b..1722af34e7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -89,8 +89,9 @@ frame? [shape-node (dom/query shape-node ".frame-children") - (dom/query (str "#thumbnail-container-" id)) - (dom/query (str "#thumbnail-" id))] + (dom/query (dm/str "#thumbnail-container-" id)) + (dom/query (dm/str "#thumbnail-" id)) + (dom/query (dm/str "#frame-title-" id))] ;; For groups we don't want to transform the whole group but only ;; its filters/masks diff --git a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs index ba2bfa059e..ad76c2f65a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs @@ -23,7 +23,7 @@ (some? text-modifier) (dwt/apply-text-modifier text-modifier)) - transform (gsh/transform-matrix shape {:no-flip true}) + transform (gsh/transform-str shape {:no-flip true}) {:keys [x y width height]} shape] [:rect.main.viewport-selrect @@ -31,7 +31,7 @@ :y y :width width :height height - :transform (str transform) + :transform transform :style {:stroke "var(--color-select)" :stroke-width (/ 1 zoom) :fill "none"}}])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 88ee7e26b4..38b2b7e1fd 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -311,7 +311,7 @@ :zoom zoom}]) [:& widgets/frame-titles - {:objects objects-modified + {:objects base-objects :selected selected :zoom zoom :modifiers modifiers diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index 95d58b0167..ae79dcbbcb 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -23,7 +23,7 @@ zoom (obj/get props "zoom" 1) color (unchecked-get props "color") - transform (gsh/transform-matrix shape) + transform (gsh/transform-str shape) path? (= :path (:type shape)) path-data (mf/use-memo diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 7b1a5ebd38..4a1e4f7de6 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -170,7 +170,7 @@ :height size :fill (if (debug? :handlers) "blue" "none") :stroke-width 0 - :transform (str transform) + :transform (dm/str transform) :on-mouse-down on-rotate}])) (mf/defc resize-point-handler @@ -224,7 +224,7 @@ height (/ resize-side-height zoom) offset-y (if (= align :outside) (- height) (- (/ height 2))) target-y (+ y offset-y) - transform-str (str (gmt/multiply transform (gmt/rotate-matrix angle (gpt/point x y))))] + transform-str (dm/str (gmt/multiply transform (gmt/rotate-matrix angle (gpt/point x y))))] [:g.resize-handler (when show-handler? [:circle {:r (/ resize-point-radius zoom) @@ -271,13 +271,13 @@ current-transform (mf/deref refs/current-transform) selrect (:selrect shape) - transform (gsh/transform-matrix shape {:no-flip true})] + transform (gsh/transform-str shape {:no-flip true})] (when (not (#{:move :rotate} current-transform)) [:g.controls {:pointer-events (if disable-handlers "none" "visible")} ;; Selection rect [:& selection-rect {:rect selrect - :transform (str transform) + :transform transform :zoom zoom :color color :on-move-selected on-move-selected @@ -327,7 +327,7 @@ (let [{:keys [x y width height]} shape] [:g.controls [:rect.main {:x x :y y - :transform (str (gsh/transform-matrix shape)) + :transform (gsh/transform-str shape) :width width :height height :pointer-events "visible" diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index e62258148b..314233ae46 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -94,9 +94,11 @@ (mf/defc frame-title {::mf/wrap [mf/memo]} [{:keys [frame modifiers selected? zoom show-artboard-names? on-frame-enter on-frame-leave on-frame-select]}] - (let [{:keys [width x y]} (gsh/transform-shape frame) + (let [{:keys [width x y]} frame label-pos (gpt/point x (- y (/ 10 zoom))) + frame-transform (gsh/transform-str frame) + on-mouse-down (mf/use-callback (mf/deps (:id frame) on-frame-select) @@ -137,11 +139,10 @@ text-pos-x (if (:use-for-thumbnail? frame) 15 0)] (when (not (:hidden frame)) - [:* + [:g {:id (dm/str "frame-title-" (:id frame)) + :transform frame-transform} (when (:use-for-thumbnail? frame) - [:g {:transform (str (when (and selected? modifiers) - (str (:displacement modifiers) " ")) - (text-transform label-pos zoom))} + [:g {:transform (dm/str (text-transform label-pos zoom))} [:svg {:x 0 :y -9 :width 12 @@ -155,9 +156,7 @@ :width width :height 20 :class "workspace-frame-label" - :transform (str (when (and selected? modifiers) - (str (:displacement modifiers) " ")) - (text-transform label-pos zoom)) + :transform (dm/str (text-transform label-pos zoom)) :style {:fill (when selected? "var(--color-primary-dark)")} :visibility (if show-artboard-names? "visible" "hidden") :on-mouse-down on-mouse-down From 688ec2589a9795ce05fc005f5556e24fb63533e2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 31 May 2022 21:32:51 +0200 Subject: [PATCH 18/86] :sparkles: Changes in selection feedback --- frontend/src/app/main/data/workspace.cljs | 4 +-- frontend/src/app/main/refs.cljs | 7 ++-- .../src/app/main/ui/workspace/viewport.cljs | 14 ++++---- .../main/ui/workspace/viewport/actions.cljs | 7 ++-- .../app/main/ui/workspace/viewport/hooks.cljs | 36 +++++++++++++------ 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index abb8c22441..ef20955e17 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1224,8 +1224,8 @@ ;; (+ (get z-index %) ;; (get z-index (get-in objects [% :frame-id])))))) - selected (-> (cph/sort-z-index objects selected) - (into (d/ordered-set)))] + selected (->> (cph/sort-z-index objects selected) + (into (d/ordered-set)))] (assoc data :selected selected))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 91117c6223..4e69988277 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -306,8 +306,11 @@ (fn [{:keys [modifiers objects]}] (let [keys (->> modifiers (keys) - (filter #(or (= frame-id %) - (= frame-id (get-in objects [% :frame-id])))))] + (filter (fn [id] + (let [shape (get objects id)] + (or (= frame-id id) + (and (= frame-id (:frame-id shape)) + (not (= :frame (:type shape)))))))))] (select-keys modifiers keys))) workspace-modifiers-with-objects =)) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 38b2b7e1fd..1f55733c0b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -155,7 +155,12 @@ show-draw-area? drawing-obj show-gradient-handlers? (= (count selected) 1) show-grids? (contains? layout :display-grid) - show-outlines? (and (nil? transform) (not edition) (not drawing-obj) (not (#{:comments :path :curve} drawing-tool))) + + show-outlines? (and (nil? transform) + (not edition) + (not drawing-obj) + (not (#{:comments :path :curve} drawing-tool))) + show-pixel-grid? (and (contains? layout :show-pixel-grid) (>= zoom 8)) show-text-editor? (and editing-shape (= :text (:type editing-shape))) @@ -279,12 +284,7 @@ [:& outline/shape-outlines {:objects base-objects :selected selected - :hover (cond - (and @hover (or @mod? (not= :frame (:type @hover)))) - #{(:id @hover)} - - @frame-hover - #{@frame-hover}) + :hover #{(:id @hover) @frame-hover} :edition edition :zoom zoom}]) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 32aaf6e0c7..5501998e68 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -52,7 +52,6 @@ left-click? (and (not panning) (= 1 (.-which event))) middle-click? (and (not panning) (= 2 (.-which event))) - frame? (= :frame type) selected? (contains? selected id)] (cond @@ -96,7 +95,7 @@ drawing-tool (st/emit! (dd/start-drawing drawing-tool)) - (or (not id) (and frame? (not selected?)) mod?) + (or (not id) mod?) (st/emit! (dw/handle-area-selection shift? mod?)) (not drawing-tool) @@ -157,12 +156,10 @@ alt? (kbd/alt? event) meta? (kbd/meta? event) mod? (kbd/mod? event) - hovering? (some? @hover) - frame? (= :frame (:type @hover))] + hovering? (some? @hover)] (st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?)) (when (and hovering? - (or (not frame?) mod?) (not @space?) (not edition) (not drawing-path?) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index dd4123d489..71da7118dc 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -10,6 +10,7 @@ [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] + [app.common.data :as d] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -112,6 +113,9 @@ hover-disabled-ref (mf/use-ref hover-disabled?) focus-ref (mf/use-ref focus) + last-point-ref (mf/use-var nil) + mod-str (mf/use-memo #(rx/subject)) + query-point (mf/use-callback (mf/deps page-id) @@ -133,15 +137,23 @@ (mf/use-memo (fn [] (rx/merge + ;; This stream works to "refresh" the outlines when the control is pressed + ;; but the mouse has not been moved from its position. + (->> mod-str + (rx/observe-on :async) + (rx/map #(deref last-point-ref))) + (->> move-stream ;; When transforming shapes we stop querying the worker (rx/filter #(not (some? (mf/ref-val transform-ref)))) - (rx/merge-map query-point)) + (rx/merge-map query-point) + (rx/tap #(reset! last-point-ref %))) (->> move-stream ;; When transforming shapes we stop querying the worker (rx/filter #(some? (mf/ref-val transform-ref))) - (rx/map (constantly nil))))))] + (rx/map (constantly nil)) + (rx/tap #(reset! last-point-ref %))))))] ;; Refresh the refs on a value change (mf/use-effect @@ -154,7 +166,9 @@ (mf/use-effect (mf/deps @mod?) - #(mf/set-ref-val! mod-ref @mod?)) + (fn [] + (rx/push! mod-str :update) + (mf/set-ref-val! mod-ref @mod?))) (mf/use-effect (mf/deps selected) @@ -172,10 +186,11 @@ over-shapes-stream (mf/deps page-id objects) (fn [ids] - #_(prn "??hover-ids" (->> ids (map #(get-in objects [% :name])))) - (let [is-group? - (fn [id] - (contains? #{:group :bool} (get-in objects [id :type]))) + (let [ids (into + (d/ordered-set) + (cph/sort-z-index objects ids)) + + grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) selected (mf/ref-val selected-ref) focus (mf/ref-val focus-ref) @@ -188,12 +203,11 @@ (into (filter #(group-empty-space? % objects ids)) ids) mod? - (into (filter is-group?) ids)) + (into (filter grouped?) ids)) hover-shape (->> ids - (filter (comp not remove-id?)) - (filter #(or (empty? focus) - (cp/is-in-focus? objects focus %))) + (remove remove-id?) + (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) (first) (get objects))] (reset! hover hover-shape) From 8c5cc446b07987f53b4df5ea1f76eefe3ddde1d6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 1 Jun 2022 15:33:47 +0200 Subject: [PATCH 19/86] :sparkles: Improved hover behavior --- common/src/app/common/pages/focus.cljc | 1 - common/src/app/common/pages/helpers.cljc | 50 ++++++++--- common/src/app/common/pages/indices.cljc | 84 ++----------------- frontend/src/app/main/data/workspace.cljs | 7 +- .../app/main/data/workspace/drawing/box.cljs | 2 +- .../src/app/main/data/workspace/indices.cljs | 19 ++--- .../data/workspace/indices/object_tree.cljs | 20 +---- frontend/src/app/main/errors.cljs | 2 +- frontend/src/app/main/render.cljs | 2 +- frontend/src/app/main/ui/hooks/resize.cljs | 2 +- .../app/main/ui/workspace/shapes/frame.cljs | 4 +- .../sidebar/options/menus/measures.cljs | 9 +- .../src/app/main/ui/workspace/viewport.cljs | 11 ++- .../main/ui/workspace/viewport/actions.cljs | 5 +- .../app/main/ui/workspace/viewport/hooks.cljs | 28 +++---- .../main/ui/workspace/viewport/outline.cljs | 13 +-- .../main/ui/workspace/viewport/widgets.cljs | 4 +- frontend/src/app/worker/selection.cljs | 71 +++++----------- 18 files changed, 117 insertions(+), 217 deletions(-) diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/pages/focus.cljc index f4dda2b977..a7ca0f4951 100644 --- a/common/src/app/common/pages/focus.cljc +++ b/common/src/app/common/pages/focus.cljc @@ -8,7 +8,6 @@ (:require [app.common.data :as d] [app.common.pages.helpers :as cph] - [app.common.pages.indices :as cpi] [app.common.uuid :as uuid])) (defn focus-objects diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 901b43812d..5107be901e 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -18,14 +18,24 @@ ;; GENERIC SHAPE SELECTORS AND PREDICATES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn root-frame? +(defn root? [{:keys [id type]}] - (and (= type :frame) - (= id uuid/zero))) + (and (= type :frame) (= id uuid/zero))) + +(defn root-frame? + ([objects id] + (root-frame? (get objects id))) + + ([{:keys [frame-id type]}] + (and (= type :frame) + (= frame-id uuid/zero)))) (defn frame-shape? - [{:keys [type]}] - (= type :frame)) + ([objects id] + (frame-shape? (get objects id))) + + ([{:keys [type]}] + (= type :frame))) (defn group-shape? [{:keys [type]}] @@ -230,13 +240,29 @@ (< parent-a parent-b)))) (defn sort-z-index - [objects ids] - (letfn [(comp [id-a id-b] - (cond - (= id-a id-b) 0 - (is-shape-over-shape? objects id-a id-b) 1 - :else -1))] - (sort comp ids))) + ([objects ids] + (sort-z-index objects ids nil)) + + ([objects ids {:keys [bottom-frames?]}] + (letfn [(comp [id-a id-b] + (let [type-a (dm/get-in objects [id-a :type]) + type-b (dm/get-in objects [id-b :type])] + (cond + (and bottom-frames? (= :frame type-a) (not= :frame type-b)) + 1 + + (and bottom-frames? (not= :frame type-a) (= :frame type-b)) + -1 + + (= id-a id-b) + 0 + + (is-shape-over-shape? objects id-a id-b) + 1 + + :else + -1)))] + (sort comp ids)))) (defn frame-id-by-position [objects position] diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc index 571de36138..f8b85bec01 100644 --- a/common/src/app/common/pages/indices.cljc +++ b/common/src/app/common/pages/indices.cljc @@ -8,78 +8,7 @@ (:require [app.common.data :as d] [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] - [clojure.set :as set])) - -#_(defn calculate-frame-z-index - [z-index frame-id base-idx objects] - - (let [is-root-frame? (fn [id] - (and (= :frame (get-in objects [id :type])) - (= uuid/zero (get-in objects [id :parent-id])))) - children (or (get-in objects [frame-id :shapes]) [])] - - (if (empty? children) - z-index - (loop [current (peek children) - pending (pop children) - current-idx base-idx - z-index z-index] - - (let [children (get-in objects [current :shapes]) - is-root-frame? (is-root-frame? current) - pending (if (not is-root-frame?) - (d/concat-vec pending children) - pending)] - - (if (empty? pending) - (assoc z-index current current-idx) - (recur (peek pending) - (pop pending) - (dec current-idx) - (assoc z-index current current-idx)))))))) - -;; The z-index is really calculated per-frame. Every frame will have its own -;; internal z-index. To calculate the "final" z-index we add the shape z-index with -;; the z-index of its frame. This way we can update the z-index per frame without -;; the need of recalculate all the frames -#_(defn calculate-z-index - "Given a collection of shapes calculates their z-index. Greater index - means is displayed over other shapes with less index." - [objects] - - (let [frames (cph/get-root-frames objects) - - by-frame (cph/objects-by-frame objects) - frame-base-idx (d/update-vals by-frame count) - - z-index (calculate-frame-z-index {} uuid/zero (get frame-base-idx uuid/zero) objects)] - (->> frames - (reduce - (fn [z-index {:keys [id]}] - (calculate-frame-z-index z-index id (get frame-base-idx id) objects)) z-index)))) - -#_(defn update-z-index - "Updates the z-index given a set of ids to change and the old and new objects - representations" - [z-index changed-ids old-objects new-objects] - - (let [old-frames (into #{} (map #(get-in old-objects [% :frame-id])) changed-ids) - new-frames (into #{} (map #(get-in new-objects [% :frame-id])) changed-ids) - - changed-frames (set/union old-frames new-frames) - - frames (->> (cph/get-frames new-objects) - (map :id) - (filter #(contains? changed-frames %))) - - by-frame (cph/objects-by-frame new-objects) - frame-base-idx (d/update-vals by-frame count) - z-index (calculate-frame-z-index z-index uuid/zero (get frame-base-idx uuid/zero) new-objects)] - - (->> frames - (reduce (fn [z-index id] - (calculate-frame-z-index z-index id (get frame-base-idx id) new-objects)) z-index)))) + [app.common.uuid :as uuid])) (defn generate-child-parent-index [objects] @@ -104,11 +33,16 @@ "Retrieves the mask information for an object" [objects parents-index] (let [retrieve-clips - (fn [_ parents] + (fn [parents] (let [lookup-object (fn [id] (get objects id)) get-clip-parents (fn [shape] (cond-> [] + (and (= :frame (:type shape)) + (not (:show-content shape)) + (not= uuid/zero (:id shape))) + (conj shape) + (:masked-group? shape) (conj (get objects (->> shape :shapes first))) @@ -119,5 +53,5 @@ (comp (map lookup-object) (mapcat get-clip-parents)) parents)))] - (->> parents-index - (d/mapm retrieve-clips)))) + (-> parents-index + (d/update-vals retrieve-clips)))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ef20955e17..9f8f9deb5f 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -6,8 +6,6 @@ (ns app.main.data.workspace (:require - [app.main.data.workspace.indices :as dwidx] - [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] @@ -34,6 +32,7 @@ [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] + [app.main.data.workspace.indices :as dwidx] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.layout :as layout] @@ -130,7 +129,7 @@ (rx/merge (rx/of (dwn/initialize team-id file-id) (dwp/initialize-file-persistence file-id) - (dwidx/start-indexing file-id)) + (dwidx/start-indexing)) (->> stream (rx/filter #(= ::dwc/index-initialized %)) @@ -197,7 +196,7 @@ (watch [_ _ _] (rx/merge (rx/of (dwn/finalize file-id)) - (rx/of (dwidx/stop-indexing file-id)) + (rx/of (dwidx/stop-indexing)) (->> (rx/of ::dwp/finalize) (rx/observe-on :async)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 6de3726a60..bdffbe1f70 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -75,7 +75,7 @@ :height 0.01}) (cond-> (and (cph/frame-shape? shape) (not= fid uuid/zero)) - (assoc :fills [] :show-content true :hide-in-viewer true)) + (assoc :fills [] :hide-in-viewer true)) (assoc :frame-id fid) (assoc :initialized? true) diff --git a/frontend/src/app/main/data/workspace/indices.cljs b/frontend/src/app/main/data/workspace/indices.cljs index 4cf4fb39a4..739a3f9e8b 100644 --- a/frontend/src/app/main/data/workspace/indices.cljs +++ b/frontend/src/app/main/data/workspace/indices.cljs @@ -6,12 +6,11 @@ (ns app.main.data.workspace.indices (:require - [app.main.data.workspace.state-helpers :as wsh] - [app.main.data.workspace.indices.object-tree :as dwi-object-tree] - [app.main.refs :as refs] [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.indices.object-tree :as dwi-object-tree] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.refs :as refs] [beicon.core :as rx] - [app.common.data :as d] [potok.core :as ptk])) (def stop-indexing? (ptk/type? ::stop-indexing)) @@ -19,7 +18,7 @@ (def objects-changes #{:add-obj :mod-obj :del-obj :mov-objects}) (defn stop-indexing - [file-id] + [] (ptk/reify ::stop-indexing ptk/UpdateEvent (update [_ state] @@ -41,13 +40,11 @@ (ptk/reify ::update-indexing ptk/UpdateEvent (update [_ state] - (let [objects (wsh/lookup-page-objects state)] - (-> state - (update :index-object-tree dwi-object-tree/update-index shape-id change-type old-objects new-objects)))))) + (-> state + (update :index-object-tree dwi-object-tree/update-index shape-id change-type old-objects new-objects))))) (defn start-indexing - [file-id] - + [] (ptk/reify ::start-indexing ptk/UpdateEvent (update [_ state] @@ -56,7 +53,7 @@ (assoc :index-object-tree (dwi-object-tree/init-index objects))))) ptk/WatchEvent - (watch [_ state stream] + (watch [_ _ stream] (let [stopper (->> stream (rx/filter stop-indexing?) (rx/take 1)) objects-delta (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/buffer 2 1))] (->> stream diff --git a/frontend/src/app/main/data/workspace/indices/object_tree.cljs b/frontend/src/app/main/data/workspace/indices/object_tree.cljs index e772b0ad8e..b7aaad28da 100644 --- a/frontend/src/app/main/data/workspace/indices/object_tree.cljs +++ b/frontend/src/app/main/data/workspace/indices/object_tree.cljs @@ -4,25 +4,13 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.main.data.workspace.indices.object-tree - (:require - [app.common.pages.helpers :as cph] - )) +(ns app.main.data.workspace.indices.object-tree) (defn objects-tree - [objects] - - - - ) + [_objects]) (defn init-index - [objects] - - - - ) + [_objects]) (defn update-index - [index shape-id change-type old-objects new-objects] - ) + [_index _shape-id _change-type _old-objects _new-objects]) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 19dba98dc9..c23092c516 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -7,7 +7,6 @@ (ns app.main.errors "Generic error handling" (:require - [cuerdas.core :as str] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -21,6 +20,7 @@ [app.util.i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as ts] + [cuerdas.core :as str] [potok.core :as ptk])) (defn on-error diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 74729dbdf6..bfb14c5d05 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -169,7 +169,7 @@ [objects object-id] (let [object (get objects object-id) object (cond->> object - (cph/root-frame? object) + (cph/root? object) (adapt-root-frame objects)) ;; Replace the previous object with the new one diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index a2c52cd362..d640fd81b4 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -6,10 +6,10 @@ (ns app.main.ui.hooks.resize (:require - [app.main.ui.hooks :as hooks] [app.common.geom.point :as gpt] [app.common.logging :as log] [app.main.ui.context :as ctx] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.storage :refer [storage]] [rumext.alpha :as mf])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 7f23daf08f..bef488434a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,14 +6,14 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.main.store :as st] - [app.main.data.workspace.state-helpers :as wsh] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.uuid :as uuid] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.fonts :as fonts] [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.shapes.embed :as embed] 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 8983398070..4d05ca5843 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,10 +6,10 @@ (ns app.main.ui.workspace.sidebar.options.menus.measures (:require - [app.main.constants :refer [size-presets]] [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.spec.radius :as ctr] + [app.main.constants :refer [size-presets]] [app.main.data.workspace :as udw] [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] @@ -204,13 +204,6 @@ on-radius-r3-change #(on-radius-4-change % :r3) on-radius-r4-change #(on-radius-4-change % :r4) - on-change-clip-content - (mf/use-callback - (mf/deps ids) - (fn [event] - (let [value (-> event dom/get-target dom/checked?)] - (dch/update-shapes ids (fn [shape]) (assoc shape :show-content (not value)))))) - on-change-clip-content (mf/use-callback (mf/deps ids) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 1f55733c0b..54ecef54b0 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] [app.main.refs :as refs] [app.main.ui.context :as ctx] [app.main.ui.hooks :as ui-hooks] @@ -156,6 +157,7 @@ show-gradient-handlers? (= (count selected) 1) show-grids? (contains? layout :display-grid) + show-frame-outline? (= transform :move) show-outlines? (and (nil? transform) (not edition) (not drawing-obj) @@ -280,6 +282,14 @@ (when show-text-editor? [:& editor/text-editor-svg {:shape editing-shape}]) + (when show-frame-outline? + [:& outline/shape-outlines + {:objects base-objects + :hover #{(->> @hover-ids + (filter #(cph/frame-shape? base-objects %)) + (first))} + :zoom zoom}]) + (when show-outlines? [:& outline/shape-outlines {:objects base-objects @@ -314,7 +324,6 @@ {:objects base-objects :selected selected :zoom zoom - :modifiers modifiers :show-artboard-names? show-artboard-names? :on-frame-enter on-frame-enter :on-frame-leave on-frame-leave diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 5501998e68..b02b804897 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -50,9 +50,7 @@ mod? (kbd/mod? event) left-click? (and (not panning) (= 1 (.-which event))) - middle-click? (and (not panning) (= 2 (.-which event))) - - selected? (contains? selected id)] + middle-click? (and (not panning) (= 2 (.-which event)))] (cond middle-click? @@ -155,7 +153,6 @@ shift? (kbd/shift? event) alt? (kbd/alt? event) meta? (kbd/meta? event) - mod? (kbd/mod? event) hovering? (some? @hover)] (st/emit! (ms/->MouseEvent :click ctrl? shift? alt? meta?)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 71da7118dc..6dca71f82a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -6,11 +6,11 @@ (ns app.main.ui.workspace.viewport.hooks (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] - [app.common.data :as d] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -130,8 +130,7 @@ :page-id page-id :rect rect :include-frames? true - :clip-children? (not mod?) - :reverse? true}))))) ;; we want the topmost shape to be selected first + :clip-children? (not mod?)}))))) over-shapes-stream (mf/use-memo @@ -145,14 +144,7 @@ (->> move-stream ;; When transforming shapes we stop querying the worker - (rx/filter #(not (some? (mf/ref-val transform-ref)))) (rx/merge-map query-point) - (rx/tap #(reset! last-point-ref %))) - - (->> move-stream - ;; When transforming shapes we stop querying the worker - (rx/filter #(some? (mf/ref-val transform-ref))) - (rx/map (constantly nil)) (rx/tap #(reset! last-point-ref %))))))] ;; Refresh the refs on a value change @@ -186,21 +178,25 @@ over-shapes-stream (mf/deps page-id objects) (fn [ids] - (let [ids (into + (let [selected (mf/ref-val selected-ref) + focus (mf/ref-val focus-ref) + mod? (mf/ref-val mod-ref) + + ids (into (d/ordered-set) - (cph/sort-z-index objects ids)) + (cph/sort-z-index objects ids {:bottom-frames? mod?})) grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) - selected (mf/ref-val selected-ref) - focus (mf/ref-val focus-ref) - mod? (mf/ref-val mod-ref) remove-xfm (mapcat #(cph/get-parent-ids objects %)) remove-id? (cond-> (into #{} remove-xfm selected) (not mod?) - (into (filter #(group-empty-space? % objects ids)) ids) + (into + (filter #(or (and (cph/root-frame? objects %) (d/not-empty? (get-in objects [% :shapes]))) + (group-empty-space? % objects ids))) + ids) mod? (into (filter grouped?) ids)) diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index ae79dcbbcb..d1f6eaf46b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -8,8 +8,6 @@ (:require [app.common.exceptions :as ex] [app.common.geom.shapes :as gsh] - [app.common.pages.helpers :as cph] - [app.main.refs :as refs] [app.util.object :as obj] [app.util.path.format :as upf] [clojure.set :as set] @@ -41,7 +39,7 @@ common {:fill "none" :stroke color - :strokeWidth (/ 1 zoom) + :strokeWidth (/ 2 zoom) :pointerEvents "none" :transform transform} @@ -82,15 +80,12 @@ [props] (let [selected (or (obj/get props "selected") #{}) hover (or (obj/get props "hover") #{}) + objects (obj/get props "objects") edition (obj/get props "edition") zoom (obj/get props "zoom") - transform (mf/deref refs/current-transform) - - outlines-ids (->> (set/union selected hover) - (cph/clean-loops objects)) - + outlines-ids (set/union selected hover) show-outline? (fn [shape] (and (not (:hidden shape)) (not (:blocked shape)))) @@ -100,6 +95,6 @@ (filterv show-outline?) (filter some?))] - [:g.outlines {:display (when (some? transform) "none")} + [:g.outlines [:& shape-outlines-render {:shapes shapes :zoom zoom}]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 314233ae46..fc840f671f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -6,11 +6,11 @@ (ns app.main.ui.workspace.viewport.widgets (:require - [app.common.uuid :as uuid] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] @@ -93,7 +93,7 @@ (mf/defc frame-title {::mf/wrap [mf/memo]} - [{:keys [frame modifiers selected? zoom show-artboard-names? on-frame-enter on-frame-leave on-frame-select]}] + [{:keys [frame selected? zoom show-artboard-names? on-frame-enter on-frame-leave on-frame-select]}] (let [{:keys [width x y]} frame label-pos (gpt/point x (- y (/ 10 zoom))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index dddee6f558..b5576d2911 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -84,19 +84,12 @@ index-shape (make-index-shape objects parents-index clip-parents-index) initial-quadtree (qdt/create (clj->js bounds)) - index (reduce index-shape initial-quadtree shapes) + index (reduce index-shape initial-quadtree shapes)] - ;;z-index (cp/calculate-z-index objects) - ] - - {:index index - ;;:z-index z-index - :bounds bounds})) + {:index index :bounds bounds})) (defn- update-index - [{index :index - ;; z-index :z-index - :as data} old-objects new-objects] + [{index :index :as data} old-objects new-objects] (let [changes? (fn [id] (not= (get old-objects id) (get new-objects id))) @@ -115,18 +108,12 @@ new-index (qdt/remove-all index changed-ids) index-shape (make-index-shape new-objects parents-index clip-parents-index) - index (reduce index-shape new-index shapes) + index (reduce index-shape new-index shapes)] - ;;z-index (cp/update-z-index z-index changed-ids old-objects new-objects) - ] - - (assoc data :index index ;;:z-index z-index - ))) + (assoc data :index index))) (defn- query-index - [{index :index - ;;z-index :z-index - } rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?] + [{index :index} rect frame-id full-frame? include-frames? ignore-groups? clip-children?] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -152,33 +139,18 @@ overlaps-parent? (fn [clip-parents] - (->> clip-parents (some (comp not overlaps?)) not)) - - ;;add-z-index - ;;(fn [{:keys [id frame-id] :as shape}] - ;; (assoc shape :z (+ (get z-index id) - ;; (get z-index frame-id 0)))) - - ;; Shapes after filters of overlapping and criteria - matching-shapes - (into [] - (comp (map #(unchecked-get % "data")) - (filter match-criteria?) - (filter overlaps?) - (filter (comp overlaps? :frame)) - (filter (if clip-children? - (comp overlaps-parent? :clip-parents) - (constantly true))) - #_(map add-z-index)) - result) - - ;;keyfn (if reverse? (comp - :z) :z) - ] + (->> clip-parents (some (comp not overlaps?)) not))] + ;; Shapes after filters of overlapping and criteria (into (d/ordered-set) - (->> matching-shapes - #_(sort-by keyfn) - (map :id))))) + (comp (map #(unchecked-get % "data")) + (filter match-criteria?) + (filter overlaps?) + (filter (if clip-children? + (comp overlaps-parent? :clip-parents) + (constantly true))) + (map :id)) + result))) (defmethod impl/handler :selection/initialize-index @@ -213,13 +185,8 @@ nil) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id reverse? full-frame? include-frames? ignore-groups? clip-children?] - :or {reverse? false full-frame? false include-frames? false clip-children? true} :as message}] + [{:keys [page-id rect frame-id full-frame? include-frames? ignore-groups? clip-children?] + :or {full-frame? false include-frames? false clip-children? true} :as message}] (when-let [index (get @state page-id)] - (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?))) + (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children?))) -#_(defmethod impl/handler :selection/query-z-index - [{:keys [page-id objects ids]}] - (when-let [{z-index :z-index} (get @state page-id)] - (->> ids (map #(+ (get z-index %) - (get z-index (get-in objects [% :frame-id]))))))) From c8ad379bf8eba4ea46dd3d5dcf42fa4857ebe4e3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 3 Jun 2022 15:56:50 +0200 Subject: [PATCH 20/86] :sparkles: Adapted viewer for new frames --- common/src/app/common/pages/helpers.cljc | 5 ++ frontend/src/app/main/data/viewer.cljs | 9 +-- .../src/app/main/data/workspace/common.cljs | 8 -- .../app/main/data/workspace/interactions.cljs | 78 ++++++++++++------- .../app/main/data/workspace/transforms.cljs | 9 +-- frontend/src/app/main/ui/measurements.cljs | 16 ++-- frontend/src/app/main/ui/viewer.cljs | 40 +++++++--- frontend/src/app/main/ui/viewer/handoff.cljs | 4 +- .../app/main/ui/viewer/handoff/render.cljs | 37 ++++----- .../ui/viewer/handoff/selection_feedback.cljs | 19 +++-- frontend/src/app/main/ui/viewer/header.cljs | 1 + .../src/app/main/ui/viewer/interactions.cljs | 8 +- frontend/src/app/main/ui/viewer/shapes.cljs | 6 +- .../src/app/main/ui/viewer/thumbnails.cljs | 2 + .../sidebar/options/menus/measures.cljs | 2 +- .../src/app/main/ui/workspace/viewport.cljs | 19 +++-- 16 files changed, 148 insertions(+), 115 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 5107be901e..b132ccc1eb 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -278,6 +278,11 @@ (rest frames))] (or (:id top-frame) uuid/zero))) +(defn frame-by-position + [objects position] + (let [frame-id (frame-id-by-position objects position)] + (get objects frame-id))) + (declare indexed-shapes) (defn get-base-shape diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 7dbbe296cc..5f45b0173d 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -11,7 +11,6 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.spec.interactions :as cti] - [app.common.uuid :as uuid] [app.main.data.comments :as dcm] [app.main.data.fonts :as df] [app.main.repo :as rp] @@ -88,10 +87,10 @@ (defn select-frames [{:keys [objects] :as page}] - (let [root (get objects uuid/zero)] - (into [] (comp (map #(get objects %)) - (filter #(= :frame (:type %)))) - (reverse (:shapes root))))) + (into [] + (comp (map (d/getf objects)) + (remove :hide-in-viewer)) + (cph/sort-z-index objects (cph/get-frames-ids objects)))) ;; --- Data Fetching diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 5bc125ce1e..e5296d2101 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -8,7 +8,6 @@ (:require [app.common.data :as d] [app.common.geom.proportions :as gpr] - [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] @@ -59,13 +58,6 @@ ;; --- Common Helpers & Events -;; TODO: looks duplicate - -(defn get-frame-at-point - [objects point] - (let [frames (cph/get-frames objects)] - (d/seek #(gsh/has-point? % point) frames))) - (defn- extract-numeric-suffix [basename] (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index d44a55a776..200c359089 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -182,10 +182,17 @@ selected-shape (get objects selected-shape-id) selected-shape-frame-id (:frame-id selected-shape) start-frame (get objects selected-shape-frame-id) - end-frame (dwc/get-frame-at-point objects position)] - (cond-> state - (not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position) - (not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) + end-frame (cph/frame-by-position objects position) + + position (when (not= position initial-pos) position) + end-frame (when (and (not= (:id end-frame) uuid/zero ) + (not= (:id end-frame) (:id start-frame)) + (not= (:id end-frame) selected-shape-id) + (not (:hide-in-viewer end-frame))) + end-frame)] + (-> state + (assoc-in [:workspace-local :draw-interaction-to] position) + (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) (defn finish-edit-interaction [index initial-pos] @@ -199,32 +206,49 @@ ptk/WatchEvent (watch [_ state _] - (let [position @ms/mouse-position - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame (dwc/get-frame-at-point objects position) + (let [position @ms/mouse-position + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + target-frame (cph/frame-by-position objects position) - shape-id (-> state wsh/lookup-selected first) - shape (get objects shape-id)] + shape-id (-> state wsh/lookup-selected first) + shape (get objects shape-id) - (when (and shape (not (= position initial-pos))) - (if (nil? frame) - (when index - (rx/of (remove-interaction shape index))) - (let [frame (if (or (= (:id frame) (:id shape)) - (= (:id frame) (:frame-id shape))) - nil ;; Drop onto self frame -> set destination to none - frame)] - (if (nil? index) - (rx/of (add-new-interaction shape (:id frame))) - (rx/of (update-interaction shape index - (fn [interaction] - (cond-> interaction - (not (csi/has-destination interaction)) - (csi/set-action-type :navigate) + invalid-target? (or (nil? target-frame) + (= (:id target-frame) uuid/zero) + (= (:id target-frame) (:id shape)) + (= (:id target-frame) (:frame-id shape)) + (:hide-in-viewer target-frame)) + + change-interaction + (fn [interaction] + (cond-> interaction + (not (csi/has-destination interaction)) + (csi/set-action-type :navigate) + + :always + (csi/set-destination (:id target-frame))))] + + (cond + (or (nil? shape) + + ;; Didn't changed the position for the interaction + (= position initial-pos) + + ;; New interaction but invalid target + (and (nil? index) invalid-target?)) + nil + + ;; Dropped interaction in an invalid target. We remove it + (and (some? index) invalid-target?) + (rx/of (remove-interaction shape index)) + + (nil? index) + (rx/of (add-new-interaction shape (:id target-frame))) + + :else + (rx/of (update-interaction shape index change-interaction))))))) - :always - (csi/set-destination (:id frame)))))))))))))) ;; --- Overlays (declare move-overlay-pos) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 0ae4229f97..89bd283015 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -762,8 +762,8 @@ (rx/map (partial set-modifiers ids)) (rx/take-until stopper)) - (rx/of (apply-modifiers ids) - (calculate-frame-for-move ids) + (rx/of (calculate-frame-for-move ids) + (apply-modifiers ids) (finish-transform))))))))) (s/def ::direction #{:up :down :right :left}) @@ -854,9 +854,8 @@ moving-shapes (->> ids (cph/clean-loops objects) - (map #(get objects %)) - (remove #(or (nil? %) - (= (:frame-id %) frame-id)))) + (keep #(get objects %)) + (remove #(= (:frame-id %) frame-id))) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index f879e2345d..6571b3c58c 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -240,16 +240,20 @@ (when (seq selected-shapes) [:g.measurement-feedback {:pointer-events "none"} - [:& selection-guides {:selrect selected-selrect :bounds bounds :zoom zoom}] + [:& selection-guides {:selrect selected-selrect + :bounds bounds + :zoom zoom}] [:& size-display {:selrect selected-selrect :zoom zoom}] (if (or (not hover-shape) (not hover-selected-shape?)) (when (and frame (not= uuid/zero (:id frame))) - [:g.hover-shapes - [:& distance-display {:from (:selrect frame) - :to selected-selrect - :zoom zoom - :bounds bounds-selrect}]]) + (let [frame-bb (-> (:points frame) (gsh/points->selrect))] + [:g.hover-shapes + [:& selection-rect {:type :hover :selrect frame-bb :zoom zoom}] + [:& distance-display {:from frame-bb + :to selected-selrect + :zoom zoom + :bounds bounds-selrect}]])) [:g.hover-shapes [:& selection-rect {:type :hover :selrect hover-selrect :zoom zoom}] diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 4b84d5c032..45eb9b7c12 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -8,8 +8,10 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.main.data.comments :as dcm] @@ -37,18 +39,23 @@ [rumext.alpha :as mf])) (defn- calculate-size - [frame zoom] - (let [{:keys [_ _ width height]} (filters/get-filters-bounds frame) + [frame zoom bounds] + (let [frame-bounds (filters/get-filters-bounds frame) + {:keys [x y width height]} (if (:show-content frame) + (gsh/join-rects [bounds frame-bounds]) + frame-bounds) padding (filters/calculate-padding frame) - x (- (:horizontal padding)) - y (- (:vertical padding)) + x (- x (:horizontal padding)) + y (- y (:vertical padding)) width (+ width (* 2 (:horizontal padding))) height (+ height (* 2 (:vertical padding)))] {:base-width width :base-height height + :x x + :y y :width (* width zoom) :height (* height zoom) - :vbox (str x " " y " " width " " height)})) + :vbox (dm/fmt "% % % %" 0 0 width height)})) (defn- calculate-wrapper [size1 size2 zoom] @@ -128,7 +135,7 @@ :interactions-mode interactions-mode}] (for [overlay overlays] - (let [size-over (calculate-size (:frame overlay) zoom)] + (let [size-over (calculate-size (:frame overlay) zoom children-bounds)] [:* (when (or (:close-click-outside overlay) (:background-overlay overlay)) @@ -198,6 +205,13 @@ zoom (:zoom local) frames (:frames page) frame (get frames index) + + children-bounds + (mf/use-memo + (mf/deps page (:id frame)) + #(-> (cph/get-children (:objects page) (:id frame)) + (gsh/selection-rect))) + fullscreen? (mf/deref refs/viewer-fullscreen?) overlays (:overlays local) scroll (mf/use-state nil) @@ -207,12 +221,13 @@ (d/seek #(= (:id %) (:orig-frame-id current-animation)) frames)) size (mf/use-memo - (mf/deps frame zoom) - (fn [] (calculate-size frame zoom))) + (mf/deps frame zoom children-bounds) + (fn [] (calculate-size frame zoom children-bounds))) orig-size (mf/use-memo (mf/deps orig-frame zoom) - (fn [] (when orig-frame (calculate-size orig-frame zoom)))) + (fn [] (when orig-frame + (calculate-size orig-frame zoom children-bounds)))) wrapper-size (mf/use-memo (mf/deps size orig-size zoom) @@ -305,7 +320,7 @@ wrapper-size))))) (mf/use-layout-effect - (mf/deps current-animation) + (mf/deps current-animation children-bounds) (fn [] ;; Overlay animations may be started when needed. (when current-animation @@ -315,7 +330,7 @@ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation)))) overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation)) overlays) - overlay-size (calculate-size (:frame overlay) zoom) + overlay-size (calculate-size (:frame overlay) zoom children-bounds) overlay-position {:x (* (:x (:position overlay)) zoom) :y (* (:y (:position overlay)) zoom)}] (interactions/animate-open-overlay @@ -329,7 +344,7 @@ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation)))) overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation)) overlays) - overlay-size (calculate-size (:frame overlay) zoom) + overlay-size (calculate-size (:frame overlay) zoom children-bounds) overlay-position {:x (* (:x (:position overlay)) zoom) :y (* (:y (:position overlay)) zoom)}] (interactions/animate-close-overlay @@ -392,6 +407,7 @@ :file file :section section :local local + :size size} :index index :viewer-pagination viewer-pagination}] diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs index a17fecc13d..8a238fea67 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -25,7 +25,7 @@ (st/emit! (dv/select-shape (:id frame))))) (mf/defc viewport - [{:keys [local file page frame index viewer-pagination]}] + [{:keys [local file page frame index viewer-pagination size]}] (let [on-mouse-wheel (fn [event] (when (kbd/mod? event) @@ -60,7 +60,7 @@ [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} [:& viewer-pagination {:index index :num-frames (count (:frames page)) :left-bar true :right-bar true}] [:div.handoff-svg-container - [:& render-frame-svg {:frame frame :page page :local local}]]] + [:& render-frame-svg {:frame frame :page page :local local :size size}]]] [:& right-sidebar {:frame frame :selected (:selected local) diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 30df848d7c..6fb837fd52 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -13,7 +13,6 @@ [app.main.store :as st] [app.main.ui.shapes.bool :as bool] [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] @@ -154,6 +153,10 @@ (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") + frame-container + (mf/use-memo (mf/deps objects) + #(frame-container-factory objects)) + group-container (mf/use-memo (mf/deps objects) #(group-container-factory objects)) @@ -171,6 +174,7 @@ opts #js {:shape shape :frame frame}] (case (:type shape) + :frame [:> frame-container opts] :text [:> text-wrapper opts] :rect [:> rect-wrapper opts] :path [:> path-wrapper opts] @@ -181,46 +185,31 @@ :svg-raw [:> svg-raw-container opts]))))))) (mf/defc render-frame-svg - [{:keys [page frame local]}] + [{:keys [page frame local size]}] (let [objects (mf/use-memo (mf/deps page frame) - (prepare-objects page frame)) - + (prepare-objects page frame size)) ;; Retrieve frame again with correct modifier frame (get objects (:id frame)) - - zoom (:zoom local 1) - - {:keys [_ _ width height]} (filters/get-filters-bounds frame) - padding (filters/calculate-padding frame) - x (- (:horizontal padding)) - y (- (:vertical padding)) - width (+ width (* 2 (:horizontal padding))) - height (+ height (* 2 (:vertical padding))) - - vbox (str x " " y " " width " " height) - - width (* width zoom) - height (* height zoom) - render (mf/use-memo (mf/deps objects) #(frame-container-factory objects))] [:svg {:id "svg-frame" - :view-box vbox - :width width - :height height + :view-box (:vbox size) + :width (:width size) + :height (:height size) :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg" :fill "none"} - [:& render {:shape frame :view-box vbox}] + [:& render {:shape frame :view-box (:vbox size)}] [:& selection-feedback {:frame frame :objects objects - :local local}]])) + :local local + :size size}]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs index 4445eee30a..ba5f7b24d1 100644 --- a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.main.ui.measurements :refer [selection-guides size-display measurement]] + [app.main.ui.measurements :refer [size-display measurement]] [rumext.alpha :as mf])) ;; ------------------------------------------------ @@ -52,24 +52,23 @@ :stroke-width selection-rect-width}}]])) (mf/defc selection-feedback - [{:keys [frame local objects]}] + [{:keys [frame local objects size]}] (let [{:keys [hover selected zoom]} local - hover-shape (-> (or (first (resolve-shapes objects [hover])) frame) - (gsh/translate-to-frame frame)) - selected-shapes (->> (resolve-shapes objects selected)) - selrect (gsh/selection-rect selected-shapes) - bounds (frame->bounds frame)] + shapes (resolve-shapes objects [hover]) + hover-shape (or (first shapes) frame) + hover-shape (gsh/translate-to-frame hover-shape size) + selected-shapes (resolve-shapes objects selected) + selrect (gsh/selection-rect selected-shapes)] - (when (seq selected-shapes) + (when (d/not-empty? selected-shapes) [:g.selection-feedback {:pointer-events "none"} [:g.selected-shapes - [:& selection-guides {:bounds bounds :selrect selrect :zoom zoom}] [:& selection-rect {:selrect selrect :zoom zoom}] [:& size-display {:selrect selrect :zoom zoom}]] - [:& measurement {:bounds bounds + [:& measurement {:bounds (assoc size :x 0 :y 0) :selected-shapes selected-shapes :hover-shape hover-shape :zoom zoom}]]))) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 33d2df8009..c777ad9d5f 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -157,6 +157,7 @@ [:ul.dropdown (for [id (get-in file [:data :pages])] [:li {:id (str id) + :key (str id) :on-click (partial navigate-to id)} (get-in file [:data :pages-index id :name])])]]] diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index c7a8a7ce30..06a4f2b8b1 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -25,11 +25,11 @@ [rumext.alpha :as mf])) (defn prepare-objects - [page frame] + [page frame size] (fn [] (let [objects (:objects page) frame-id (:id frame) - modifier (-> (gpt/point (:x frame) (:y frame)) + modifier (-> (gpt/point (:x size) (:y size)) (gpt/negate) (gmt/translate-matrix)) @@ -43,8 +43,8 @@ {::mf/wrap [mf/memo]} [{:keys [page interactions-mode frame base-frame frame-offset size]}] (let [objects (mf/use-memo - (mf/deps page frame) - (prepare-objects page frame)) + (mf/deps page frame size) + (prepare-objects page frame size)) wrapper (mf/use-memo (mf/deps objects) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 3398a463e8..ca187f7b06 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -376,6 +376,10 @@ (mf/use-memo (mf/deps objects) #(group-container-factory objects)) + frame-container + (mf/use-memo (mf/deps objects) + #(frame-container-factory objects)) + bool-container (mf/use-memo (mf/deps objects) #(bool-container-factory objects)) @@ -391,7 +395,7 @@ opts #js {:shape shape :objects objects}] (case (:type shape) - :frame [:g.empty] + :frame [:> frame-container opts] :text [:> text-wrapper opts] :rect [:> rect-wrapper opts] :path [:> path-wrapper opts] diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 6473c5ac34..54c90d6364 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.viewer.thumbnails (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.viewer :as dv] [app.main.render :as render] [app.main.store :as st] @@ -114,6 +115,7 @@ :total (count frames)} (for [[i frame] (d/enumerate frames)] [:& thumbnail-item {:index i + :key (dm/str (:id frame) "-" i) :frame frame :objects objects :on-click on-item-click 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 4d05ca5843..7e194f5f92 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 @@ -38,7 +38,7 @@ (def ^:private type->options {:bool #{:size :position :rotation} :circle #{:size :position :rotation} - :frame #{:presets :size :position :radius :clip-content :show-in-viewer} + :frame #{:presets :size :position :rotation :radius :clip-content :show-in-viewer} :group #{:size :position :rotation} :image #{:size :position :rotation :radius} :path #{:size :position :rotation} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 54ecef54b0..1c2874d91f 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -287,6 +287,7 @@ {:objects base-objects :hover #{(->> @hover-ids (filter #(cph/frame-shape? base-objects %)) + (remove selected) (first))} :zoom zoom}]) @@ -400,14 +401,6 @@ [:& widgets/viewport-actions] - (when show-prototypes? - [:& interactions/interactions - {:selected selected - :zoom zoom - :objects objects-modified - :current-transform transform - :hover-disabled? hover-disabled?}]) - [:& scroll-bars/viewport-scrollbars {:objects base-objects :zoom zoom @@ -444,6 +437,12 @@ :shapes selected-shapes :zoom zoom :edition edition - :disable-handlers (or drawing-tool edition @space?)}]]) + :disable-handlers (or drawing-tool edition @space?)}] - ]]])) + (when show-prototypes? + [:& interactions/interactions + {:selected selected + :zoom zoom + :objects objects-modified + :current-transform transform + :hover-disabled? hover-disabled?}])])]]])) From 79a46efa35d1d5332f3d6e74e70f87ba2e1be9a6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 6 Jun 2022 21:45:14 +0200 Subject: [PATCH 21/86] :sparkles: Create nested frames from selection --- common/src/app/common/pages/helpers.cljc | 33 +++++++++--------- frontend/src/app/main/data/workspace.cljs | 14 ++++---- .../src/app/main/data/workspace/common.cljs | 5 ++- .../main/ui/workspace/viewport/actions.cljs | 34 +++++++++---------- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index b132ccc1eb..d7a86f7a48 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -227,17 +227,19 @@ (defn is-shape-over-shape? [objects base-shape-id over-shape-id] - (let [[base parent-a parent-b] (get-base objects base-shape-id over-shape-id)] + (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] (cond (= base base-shape-id) - ;; over-shape is a child of base-shape. Will be over if base is a root-frame - (= uuid/zero (get-in objects [base-shape-id :parent-id])) + + (and (frame-shape? objects over-shape-id) + (root-frame? objects over-shape-id)) (= base over-shape-id) - (not= uuid/zero (get-in objects [over-shape-id :parent-id])) + (or (not (frame-shape? objects over-shape-id)) + (not (root-frame? objects over-shape-id))) :else - (< parent-a parent-b)))) + (> index-a index-b)))) (defn sort-z-index ([objects ids] @@ -266,17 +268,11 @@ (defn frame-id-by-position [objects position] - (let [frames (->> (get-frames objects) - (filter #(and position (gsh/has-point? % position)))) - - top-frame - (reduce (fn [current-top frame] - (if (is-shape-over-shape? objects (:id current-top) (:id frame)) - frame - current-top)) - (first frames) - (rest frames))] - (or (:id top-frame) uuid/zero))) + (let [top-frame + (->> (get-frames-ids objects) + (sort-z-index objects) + (d/seek #(and position (gsh/has-point? (get objects %) position))))] + (or top-frame uuid/zero))) (defn frame-by-position [objects position] @@ -630,3 +626,8 @@ (-> (select-keys objects selected+parents) (d/update-vals remove-children)))) + +(defn is-child? + [objects parent-id candidate-child-id] + (let [parents (get-parents-seq objects candidate-child-id)] + (some? (d/seek #(= % parent-id) parents)))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 9f8f9deb5f..e05a4f9074 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1673,20 +1673,22 @@ (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - shapes (cph/get-immediate-children objects) selected (wsh/lookup-selected state) - selected-objs (map #(get objects %) selected) - has-frame? (some #(= (:type %) :frame) selected-objs)] - (when (not (or (empty? selected) has-frame?)) + selected-objs (map #(get objects %) selected)] + (when (d/not-empty? selected) (let [srect (gsh/selection-rect selected-objs) - frame-id (:frame-id (first shapes)) + frame-id (get-in objects [(first selected) :frame-id]) + parent-id (get-in objects [(first selected) :parent-id]) shape (-> (cp/make-minimal-shape :frame) (merge {:x (:x srect) :y (:y srect) :width (:width srect) :height (:height srect)}) - (assoc :frame-id frame-id) + (assoc :frame-id frame-id :parent-id parent-id) + (cond-> (not= frame-id uuid/zero) + (assoc :fills [] :hide-in-viewer true)) (cp/setup-rect-selrect))] (rx/of (dwu/start-undo-transaction) (dwc/add-shape shape) + (dwc/move-shapes-into-frame (:id shape) selected) (dwu/commit-undo-transaction)))))))) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index e5296d2101..0e5ff1fe96 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -313,7 +313,10 @@ selected) changes (-> (pcb/empty-changes it page-id) - (pcb/add-object shape #_{:index (when (= :frame (:type shape)) 0)}))] + (pcb/with-objects objects) + (pcb/add-object shape) + (cond-> (some? (:parent-id attrs)) + (pcb/change-parent (:parent-id attrs) [shape])))] (rx/concat (rx/of (dch/commit-changes changes) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index b02b804897..c7e8b1eb40 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -8,6 +8,7 @@ (:require [app.common.geom.point :as gpt] [app.common.math :as mth] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.workspace :as dw] @@ -165,6 +166,7 @@ (defn on-double-click [hover hover-ids drawing-path? objects edition] + (mf/use-callback (mf/deps @hover @hover-ids drawing-path? edition) (fn [event] @@ -174,30 +176,28 @@ alt? (kbd/alt? event) meta? (kbd/meta? event) - {:keys [id type] :as shape} @hover + {:keys [id type] :as shape} (or @hover (get objects (first @hover-ids))) - frame? (= :frame type) - group? (= :group type)] + editable? (contains? #{:text :rect :path :image :circle} type)] (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt? meta?)) ;; Emit asynchronously so the double click to exit shapes won't break (timers/schedule - #(when (and (not drawing-path?) shape) - (cond - frame? - (st/emit! (dw/select-shape id shift?)) + (fn [] + (when (and (not drawing-path?) shape) + (cond + (and editable? (not= id edition)) + (st/emit! (dw/select-shape id) + (dw/start-editing-selected)) - (and group? (> (count @hover-ids) 1)) - (let [selected (get objects (second @hover-ids))] - (reset! hover selected) - (reset! hover-ids (into [] (rest @hover-ids))) - - (st/emit! (dw/select-shape (:id selected)))) - - (not= id edition) - (st/emit! (dw/select-shape id) - (dw/start-editing-selected))))))))) + :else + (let [;; We only get inside childrens of the hovering shape + hover-ids (->> @hover-ids (filter (partial cph/is-child? objects id))) + selected (get objects (if (> (count hover-ids) 1) (second hover-ids) (first hover-ids)))] + (when (some? selected) + (reset! hover selected) + (st/emit! (dw/select-shape (:id selected))))))))))))) (defn on-context-menu [hover hover-ids] From 0bb0063be460269ac450b68d3ffd7d1da27709be Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 7 Jun 2022 15:49:49 +0200 Subject: [PATCH 22/86] :sparkles: Fix comments for nested frames --- CHANGES.md | 3 ++ .../app/common/geom/shapes/transforms.cljc | 11 ++--- .../src/app/main/data/workspace/common.cljs | 19 +++++---- .../main/data/workspace/drawing/common.cljs | 10 ++++- .../app/main/data/workspace/shortcuts.cljs | 4 +- frontend/src/app/main/ui/viewer/comments.cljs | 18 +++------ .../main/ui/workspace/viewport/widgets.cljs | 6 +-- frontend/translations/en.po | 40 +++++++++---------- 8 files changed, 60 insertions(+), 51 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8fd1bbad58..abac9d19d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,9 @@ ## :rocket: Next ### :sparkles: New features + +- Allow for nested boards inside other boards and groups + ### :bug: Bugs fixed ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index a334b33172..0f254b9a57 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -161,11 +161,12 @@ (transform-str shape nil)) ([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip]}] - (when (and (some? shape) - (or (some? transform) - (and (not no-flip) flip-x) - (and (not no-flip) flip-y))) - (dm/str (transform-matrix shape))))) + (if (and (some? shape) + (or (some? transform) + (and (not no-flip) flip-x) + (and (not no-flip) flip-y))) + (dm/str (transform-matrix shape)) + ""))) (defn inverse-transform-matrix ([shape] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 0e5ff1fe96..182e6de0a0 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -333,15 +333,20 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - to-move-shapes (into [] - (map (d/getf objects)) - (reverse (cph/sort-z-index objects shapes))) + to-move-shapes + (into [] + (map (d/getf objects)) + (reverse (cph/sort-z-index objects shapes))) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/change-parent frame-id to-move-shapes 0))] + changes + (when (d/not-empty? to-move-shapes) + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/change-parent frame-id to-move-shapes 0)))] - (rx/of (dch/commit-changes changes)))))) + (if (some? changes) + (rx/of (dch/commit-changes changes)) + (rx/empty)))))) (s/def ::set-of-uuid (s/every ::us/uuid :kind set?)) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 3bd78df3ff..1539b398c6 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -10,7 +10,9 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages :as cp] + [app.common.pages.helpers :as cph] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.worker :as uw] [beicon.core :as rx] @@ -29,7 +31,8 @@ ptk/WatchEvent (watch [_ state _] (let [tool (get-in state [:workspace-drawing :tool]) - shape (get-in state [:workspace-drawing :object])] + shape (get-in state [:workspace-drawing :object]) + objects (wsh/lookup-page-objects state)] (rx/concat (when (:initialized? shape) (let [page-id (:current-page-id state) @@ -68,7 +71,10 @@ (if (= :frame (:type shape)) (->> (uw/ask! {:cmd :selection/query :page-id page-id - :rect (:selrect shape)}) + :rect (:selrect shape) + :include-frames? true + :full-frame? true}) + (rx/map #(cph/clean-loops objects %)) (rx/map #(dwc/move-shapes-into-frame (:id shape) %))) (rx/empty))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 150cf4375f..25c82a8db4 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -208,8 +208,8 @@ ;; TOOLS - :draw-frame {:tooltip "A" - :command "a" + :draw-frame {:tooltip "B" + :command ["b" "a"] :subsections [:tools :basics] :fn #(st/emit! (dwd/select-for-drawing :frame))} diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index ad29c0e409..27bf9a08b0 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.viewer.comments (:require + [app.common.geom.shapes :as gsh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.main.data.comments :as dcm] @@ -75,13 +76,6 @@ [:span.label (tr "labels.show-comments-list")]]]]])) -(defn- frame-contains? - [{:keys [x y width height]} {px :x py :y}] - (let [x2 (+ x width) - y2 (+ y height)] - (and (<= x px x2) - (<= y py y2)))) - (def threads-ref (l/derived :comment-threads st/state)) @@ -93,11 +87,11 @@ (let [profile (mf/deref refs/profile) threads-map (mf/deref threads-ref) - modifier1 (-> (gpt/point (:x frame) (:y frame)) - (gpt/negate) - (gmt/translate-matrix)) + frame-corner (-> frame :points gsh/points->selrect gpt/point) + modifier1 (-> (gmt/matrix) + (gmt/translate (gpt/negate frame-corner))) - modifier2 (-> (gpt/point (:x frame) (:y frame)) + modifier2 (-> (gpt/point frame-corner) (gmt/translate-matrix)) cstate (mf/deref refs/comments-local) @@ -105,7 +99,7 @@ threads (->> (vals threads-map) (dcm/apply-filters cstate profile) (filter (fn [{:keys [position]}] - (frame-contains? frame position)))) + (gsh/has-point? frame position)))) on-bubble-click (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index fc840f671f..fa697e5104 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -140,9 +140,9 @@ (when (not (:hidden frame)) [:g {:id (dm/str "frame-title-" (:id frame)) - :transform frame-transform} + } (when (:use-for-thumbnail? frame) - [:g {:transform (dm/str (text-transform label-pos zoom))} + [:g {:transform (dm/str frame-transform " " (text-transform label-pos zoom))} [:svg {:x 0 :y -9 :width 12 @@ -156,7 +156,7 @@ :width width :height 20 :class "workspace-frame-label" - :transform (dm/str (text-transform label-pos zoom)) + :transform (dm/str frame-transform " " (text-transform label-pos zoom)) :style {:fill (when selected? "var(--color-primary-dark)")} :visibility (if show-artboard-names? "visible" "hidden") :on-mouse-down on-mouse-down diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 67d209c241..4cd45fb866 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -257,7 +257,7 @@ msgstr "" "[Libraries & templates](https://penpot.app/libraries-templates.html)" msgid "dashboard.export-frames" -msgstr "Export artboards to PDF..." +msgstr "Export boards to PDF..." #: src/app/main/ui/export.cljs msgid "dashboard.export-frames.title" @@ -976,7 +976,7 @@ msgid "handoff.tabs.code.selected.curve" msgstr "Curve" msgid "handoff.tabs.code.selected.frame" -msgstr "Artboard" +msgstr "Board" msgid "handoff.tabs.code.selected.group" msgstr "Group" @@ -1277,8 +1277,8 @@ msgstr[1] "%s files" msgid "labels.num-of-frames" msgid_plural "labels.num-of-frames" -msgstr[0] "1 artboard" -msgstr[1] "%s artboards" +msgstr[0] "1 board" +msgstr[1] "%s boards" #: src/app/main/ui/dashboard/team.cljs msgid "labels.num-of-projects" @@ -2021,7 +2021,7 @@ msgid "shortcuts.align-vcenter" msgstr "Align center vertically" msgid "shortcuts.artboard-selection" -msgstr "Create artboard from selection" +msgstr "Create board from selection" msgid "shortcuts.bool-difference" msgstr "Boolean difference" @@ -2081,7 +2081,7 @@ msgid "shortcuts.draw-ellipse" msgstr "Ellipse" msgid "shortcuts.draw-frame" -msgstr "Artboard" +msgstr "Board" msgid "shortcuts.draw-nodes" msgstr "Draw path" @@ -2183,7 +2183,7 @@ msgid "shortcuts.move-unit-up" msgstr "Move up" msgid "shortcuts.next-frame" -msgstr "Next artboard" +msgstr "Next board" msgid "shortcuts.opacity-0" msgstr "Set opacity to 100%" @@ -2240,7 +2240,7 @@ msgid "shortcuts.paste" msgstr "Paste" msgid "shortcuts.prev-frame" -msgstr "Previous artboard" +msgstr "Previous board" msgid "shortcuts.redo" msgstr "Redo" @@ -2425,11 +2425,11 @@ msgstr "Sorry!" #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.empty-state" -msgstr "No artboards found on the page." +msgstr "No boards found on the page." #: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs msgid "viewer.frame-not-found" -msgstr "Artboard not found." +msgstr "Board not found." msgid "viewer.header.comments-section" msgstr "Comments (%s)" @@ -2701,7 +2701,7 @@ msgstr "Enable snap to pixel" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-artboard-names" -msgstr "Hide artboard names" +msgstr "Hide board names" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.hide-grid" @@ -2748,7 +2748,7 @@ msgstr "Select all" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-artboard-names" -msgstr "Show artboards names" +msgstr "Show boards names" #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.menu.show-grid" @@ -3629,11 +3629,11 @@ msgstr "Search font" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.select-a-shape" -msgstr "Select a shape, artboard or group to drag a connection to other artboard." +msgstr "Select a shape, board or group to drag a connection to other board." #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.select-artboard" -msgstr "Select artboard" +msgstr "Select board" #: src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs msgid "workspace.options.selection-color" @@ -3940,7 +3940,7 @@ msgstr "Copy" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-artboard-from-selection" -msgstr "Selection to artboard" +msgstr "Selection to board" #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.create-component" @@ -4096,7 +4096,7 @@ msgid "workspace.sidebar.layers.components" msgstr "Components" msgid "workspace.sidebar.layers.frames" -msgstr "Artboards" +msgstr "Boards" msgid "workspace.sidebar.layers.groups" msgstr "Groups" @@ -4150,7 +4150,7 @@ msgstr "Ellipse (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.frame" -msgstr "Artboard (%s)" +msgstr "Board (%s)" #: src/app/main/ui/workspace/left_toolbar.cljs msgid "workspace.toolbar.image" @@ -4209,7 +4209,7 @@ msgid "workspace.undo.entry.multiple.curve" msgstr "curves" msgid "workspace.undo.entry.multiple.frame" -msgstr "artboard" +msgstr "board" msgid "workspace.undo.entry.multiple.group" msgstr "groups" @@ -4255,7 +4255,7 @@ msgid "workspace.undo.entry.single.curve" msgstr "curve" msgid "workspace.undo.entry.single.frame" -msgstr "artboard" +msgstr "board" msgid "workspace.undo.entry.single.group" msgstr "group" @@ -4317,4 +4317,4 @@ msgid "shortcuts.or" msgstr " or " msgid "shortcuts.not-found" -msgstr "No shortcuts found" \ No newline at end of file +msgstr "No shortcuts found" From a4cc57886b5737bcf6b51b98686abc5304d3b4b5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 8 Jun 2022 17:50:40 +0200 Subject: [PATCH 23/86] :sparkles: Thumbnails for clipped and nested artboards --- common/src/app/common/pages/helpers.cljc | 48 ++++++++++++++-- frontend/src/app/main/refs.cljs | 8 +++ frontend/src/app/main/ui/context.cljs | 3 +- frontend/src/app/main/ui/shapes/frame.cljs | 11 ++-- frontend/src/app/main/ui/viewer/comments.cljs | 2 +- .../src/app/main/ui/workspace/shapes.cljs | 14 ++++- .../shapes/frame/thumbnail_render.cljs | 29 +++++++--- .../app/main/ui/workspace/viewport/hooks.cljs | 57 +++++++++---------- 8 files changed, 119 insertions(+), 53 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index d7a86f7a48..1d5e6cc2fb 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -14,6 +14,8 @@ [app.common.uuid :as uuid] [cuerdas.core :as str])) +(declare reduce-objects) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; GENERIC SHAPE SELECTORS AND PREDICATES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -187,12 +189,21 @@ function of `get-immediate-children` for performance reasons. This function is executed in the render hot path." [objects] - (let [lookup (d/getf objects) - xform (comp (keep lookup) - (filter frame-shape?) - (map :id))] - (->> (:shapes (lookup uuid/zero)) - (into [] xform)))) + (let [add-frame + (fn [result shape] + (cond-> result + (frame-shape? shape) + (conj (:id shape))))] + (reduce-objects objects (complement frame-shape?) add-frame []))) + +(defn get-root-shapes-ids + [objects] + (let [add-shape + (fn [result shape] + (cond-> result + (not (frame-shape? shape)) + (conj (:id shape))))] + (reduce-objects objects (complement frame-shape?) add-shape []))) (defn get-root-frames "Retrieves all frame objects as vector. It is not implemented in @@ -631,3 +642,28 @@ [objects parent-id candidate-child-id] (let [parents (get-parents-seq objects candidate-child-id)] (some? (d/seek #(= % parent-id) parents)))) + +(defn reduce-objects + ([objects reducer-fn init-val] + (reduce-objects objects nil reducer-fn init-val)) + + ([objects check-children? reducer-fn init-val] + (let [root-children (get-in objects [uuid/zero :shapes])] + (if (empty? root-children) + init-val + + (loop [current-val init-val + current-id (first root-children) + pending-ids (rest root-children)] + + + (let [current-shape (get objects current-id) + next-val (reducer-fn current-val current-shape) + next-pending-ids + (if (or (nil? check-children?) (check-children? current-shape)) + (concat (or (:shapes current-shape) []) pending-ids) + pending-ids)] + + (if (empty? next-pending-ids) + next-val + (recur next-val (first next-pending-ids) (rest next-pending-ids))))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4e69988277..114ac67444 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -270,6 +270,14 @@ (into [] (keep (d/getf objects)) children-ids))) workspace-page-objects =)) +(defn all-children-objects + [id] + (l/derived + (fn [objects] + (let [children-ids (cph/get-children-ids objects id)] + (into [] (keep (d/getf objects)) children-ids))) + workspace-page-objects =)) + (def workspace-page-options (l/derived :options workspace-page)) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 634739944f..d430ea5f2f 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -21,4 +21,5 @@ (def current-project-id (mf/create-context nil)) (def current-page-id (mf/create-context nil)) (def current-file-id (mf/create-context nil)) -(def scroll-ctx (mf/create-context nil)) \ No newline at end of file +(def scroll-ctx (mf/create-context nil)) +(def active-frames-ctx (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 5eb258c544..8fb3772df5 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -45,7 +45,8 @@ (mf/defc frame-thumbnail {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape")] + (let [shape (obj/get props "shape") + bounds (or (obj/get props "bounds") (:selrect shape))] (when (:thumbnail shape) (let [{:keys [x y width height show-content]} shape transform (gsh/transform-str shape) @@ -72,10 +73,10 @@ [:image.frame-thumbnail {:id (dm/str "thumbnail-" (:id shape)) :href (:thumbnail shape) - :x (:x shape) - :y (:y shape) - :width (:width shape) - :height (:height shape) + :x (:x bounds) + :y (:y bounds) + :width (:width bounds) + :height (:height bounds) ;; DEBUG :style {:filter (when (debug? :thumbnails) "sepia(1)")}}]] diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 27bf9a08b0..8ce744c9ee 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -6,9 +6,9 @@ (ns app.main.ui.viewer.comments (:require - [app.common.geom.shapes :as gsh] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [app.main.data.comments :as dcm] [app.main.data.events :as ev] [app.main.refs :as refs] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 7905df9518..166aba242a 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -13,6 +13,7 @@ common." (:require [app.common.pages.helpers :as cph] + [app.main.ui.context :as ctx] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.rect :as rect] @@ -52,7 +53,8 @@ (mf/use-memo (mf/deps objects) #(cph/objects-by-frame objects))] - [:* + + [:& (mf/provider ctx/active-frames-ctx) {:value active-frames} ;; Render font faces only for shapes that are part of the root ;; frame but don't belongs to any other frame. (let [xform (comp @@ -75,7 +77,15 @@ ::mf/wrap-props false} [props] (let [shape (obj/get props "shape") - opts #js {:shape shape}] + + active-frames + (when (cph/root-frame? shape) (mf/use-ctx ctx/active-frames-ctx)) + + thumbnail? + (and (some? active-frames) + (not (contains? active-frames (:id shape)))) + + opts #js {:shape shape :thumbnail? thumbnail?}] (when (and (some? shape) (not (:hidden shape))) [:* (case (:type shape) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index c61ca1d84b..f7df70e1b7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] @@ -54,7 +55,7 @@ (defn use-render-thumbnail "Hook that will create the thumbnail thata" - [page-id {:keys [id x y width height] :as shape} node-ref rendered? disable? force-render] + [page-id {:keys [id] :as shape} node-ref rendered? disable? force-render] (let [frame-canvas-ref (mf/use-ref nil) frame-image-ref (mf/use-ref nil) @@ -63,13 +64,21 @@ regenerate-thumbnail (mf/use-var false) - fixed-width (mth/clamp (:width shape) 250 2000) - fixed-height (/ (* (:height shape) fixed-width) (:width shape)) + all-children-ref (mf/use-memo (mf/deps id) #(refs/all-children-objects id)) + all-children (mf/deref all-children-ref) + + {:keys [x y width height] :as shape-bb} + (if (:show-content shape) + (gsh/selection-rect all-children) + (-> shape :points gsh/points->selrect)) + + fixed-width (mth/clamp width 250 2000) + fixed-height (/ (* height fixed-width) width) image-url (mf/use-state nil) observer-ref (mf/use-var nil) - shape-ref (hooks/use-update-var shape) + shape-bb-ref (hooks/use-update-var shape-bb) updates-str (mf/use-memo #(rx/subject)) @@ -101,7 +110,8 @@ (fn [] (let [node @node-ref frame-html (dom/node->xml node) - {:keys [x y width height]} @shape-ref + + {:keys [x y width height]} @shape-bb-ref style-node (dom/query (dm/str "#frame-container-" (:id shape) " style")) style-str (or (-> style-node dom/node->xml) "") @@ -201,6 +211,7 @@ (mf/html [:* [:> frame/frame-thumbnail {:key (dm/str (:id shape)) + :bounds shape-bb :shape (cond-> shape (some? thumbnail-data) (assoc :thumbnail thumbnail-data))}] @@ -220,9 +231,9 @@ (when (some? @image-url) [:image {:ref frame-image-ref - :x (:x shape) - :y (:y shape) + :x x + :y y :href @image-url - :width (:width shape) - :height (:height shape) + :width width + :height height :on-load on-image-load}])])])) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 6dca71f82a..72683d8e72 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -10,7 +10,6 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -189,23 +188,28 @@ grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) + selected-with-parents + (into #{} (mapcat #(cph/get-parent-ids objects %)) selected) - remove-xfm (mapcat #(cph/get-parent-ids objects %)) - remove-id? (cond-> (into #{} remove-xfm selected) - (not mod?) - (into - (filter #(or (and (cph/root-frame? objects %) (d/not-empty? (get-in objects [% :shapes]))) - (group-empty-space? % objects ids))) - ids) + root-frame-with-data? #(and (cph/root-frame? objects %) (d/not-empty? (get-in objects [% :shapes]))) - mod? - (into (filter grouped?) ids)) + ;; Set with the elements to remove from the hover list + remove-id? + (cond-> selected-with-parents + (not mod?) + (into (filter #(or (root-frame-with-data? %) + (group-empty-space? % objects ids))) + ids) - hover-shape (->> ids - (remove remove-id?) - (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) - (first) - (get objects))] + mod? + (into (filter grouped?) ids)) + + hover-shape + (->> ids + (remove remove-id?) + (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) + (first) + (get objects))] (reset! hover hover-shape) (reset! hover-ids ids)))))) @@ -214,13 +218,7 @@ (let [root-frame-ids (mf/use-memo (mf/deps objects) - (fn [] - (let [frame? (into #{} (cph/get-frames-ids objects)) - ;; Removes from zero/shapes attribute all the frames so we can ask only for - ;; the non-frame children - objects (-> objects - (update-in [uuid/zero :shapes] #(filterv (comp not frame?) %)))] - (cph/get-children-ids objects uuid/zero)))) + #(cph/get-root-shapes-ids objects)) modifiers (select-keys modifiers root-frame-ids)] (sfd/use-dynamic-modifiers objects globals/document modifiers))) @@ -238,14 +236,13 @@ selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected)) active-selection (when (and (not= transform :move) (= (count selected-frames) 1)) (first selected-frames)) - hover-frame (last @hover-ids) - last-hover-frame (mf/use-var nil)] + last-hover-ids (mf/use-var nil)] (mf/use-effect - (mf/deps hover-frame) + (mf/deps @hover-ids) (fn [] - (when (some? hover-frame) - (reset! last-hover-frame hover-frame)))) + (when (d/not-empty? @hover-ids) + (reset! last-hover-ids (set @hover-ids))))) (mf/use-effect (mf/deps objects @hover-ids selected zoom transform vbox) @@ -258,7 +255,9 @@ ;; - If no hovering over any frames we keep the previous active one ;; - Check always that the active frames are inside the vbox - (let [is-active-frame? + (let [hover-ids? (set @hover-ids) + + is-active-frame? (fn [id] (or ;; Zoom > 130% shows every frame @@ -267,7 +266,7 @@ ;; Zoom >= 25% will show frames hovering (and (>= zoom 0.25) - (or (= id hover-frame) (= id @last-hover-frame))) + (or (contains? hover-ids? id) (contains? @last-hover-ids id))) ;; Otherwise, if it's a selected frame (= id active-selection) From 9bd382f833e989a764a038d74d3315716fe253d8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 9 Jun 2022 19:51:49 +0200 Subject: [PATCH 24/86] :sparkles: Fixed export/import for nested frames --- common/src/app/common/file_builder.cljc | 10 ++- common/src/app/common/pages/helpers.cljc | 21 +++-- frontend/src/app/main/render.cljs | 76 ++++++++++--------- .../app/main/ui/components/context_menu.cljs | 5 +- frontend/src/app/main/ui/shapes/export.cljs | 4 + frontend/src/app/util/import/parser.cljs | 12 ++- 6 files changed, 75 insertions(+), 53 deletions(-) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index eb73bde26c..06dd1975cd 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -222,9 +222,13 @@ (defn close-artboard [file] (assert (nil? (:current-component-id file))) - (-> file - (assoc :current-frame-id root-frame) - (update :parent-stack pop))) + + (let [parent-id (peek (get file :parent-stack)) + parent (lookup-shape file parent-id) + current-frame-id (or (:frame-id parent) root-frame)] + (-> file + (assoc :current-frame-id current-frame-id) + (update :parent-stack pop)))) (defn add-group [file data] (let [frame-id (:current-frame-id file) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 1d5e6cc2fb..d86fffc04e 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -105,9 +105,10 @@ "Returns a vector of parents of the specified shape." [objects shape-id] (loop [result [] id shape-id] - (if-let [parent-id (dm/get-in objects [id :parent-id])] - (recur (conj result parent-id) parent-id) - result))) + (let [parent-id (dm/get-in objects [id :parent-id])] + (if (and (some? parent-id) (not= parent-id id)) + (recur (conj result parent-id) parent-id) + result)))) (defn get-frame "Get the frame that contains the shape. If the shape is already a @@ -241,16 +242,15 @@ (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] (cond (= base base-shape-id) - - (and (frame-shape? objects over-shape-id) - (root-frame? objects over-shape-id)) + (and (frame-shape? objects base-shape-id) + (root-frame? objects base-shape-id)) (= base over-shape-id) (or (not (frame-shape? objects over-shape-id)) (not (root-frame? objects over-shape-id))) :else - (> index-a index-b)))) + (< index-a index-b)))) (defn sort-z-index ([objects ids] @@ -667,3 +667,10 @@ (if (empty? next-pending-ids) next-val (recur next-val (first next-pending-ids) (rest next-pending-ids))))))))) + +(defn selected-with-children + [objects selected] + + (into selected + (mapcat #(get-children-ids objects %)) + selected)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index bfb14c5d05..5c81e804bd 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -79,8 +79,7 @@ [{:keys [shape] :as props}] (let [childs (mapv #(get objects %) (:shapes shape)) shape (gsh/transform-shape shape)] - [:> shape-container {:shape shape} - [:& frame-shape {:shape shape :childs childs}]])))) + [:& frame-shape {:shape shape :childs childs}])))) (defn group-wrapper-factory [objects] @@ -255,8 +254,9 @@ [:& frame/frame-thumbnail {:shape item}]] frame? - [:& frame-wrapper {:shape item - :key (:id item)}] + [:> shape-container {:shape item} + [:& frame-wrapper {:shape item + :key (:id item)}]] :else [:& shape-wrapper {:shape item :key (:id item)}])))]]])) @@ -389,42 +389,44 @@ text-shapes (sequence (filter cph/text-shape?) (vals objects)) render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] - [:& (mf/provider embed/context) {:value render-embed?} - [:svg {:id (dm/str "screenshot-" object-id) - :view-box vbox - :width width - :height height - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - ;; Fix Chromium bug about color of html texts - ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 - :style {:-webkit-print-color-adjust :exact} - :fill "none"} + [:& (mf/provider export/include-metadata-ctx) {:value true} + [:& (mf/provider embed/context) {:value render-embed?} + [:svg {:id (dm/str "screenshot-" object-id) + :view-box vbox + :width width + :height height + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + ;; Fix Chromium bug about color of html texts + ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 + :style {:-webkit-print-color-adjust :exact} + :fill "none"} - (let [fonts (ff/shape->fonts object objects)] - [:& ff/fontfaces-style {:fonts fonts}]) + (let [fonts (ff/shape->fonts object objects)] + [:& ff/fontfaces-style {:fonts fonts}]) - (case (:type object) - :frame [:& frame-wrapper {:shape object :view-box vbox}] - :group [:> shape-container {:shape object} - [:& group-wrapper {:shape object}]] - [:& shape-wrapper {:shape object}])] + (case (:type object) + :frame [:> shape-container {:shape object} + [:& frame-wrapper {:shape object :view-box vbox}]] + :group [:> shape-container {:shape object} + [:& group-wrapper {:shape object}]] + [:& shape-wrapper {:shape object}])] - ;; Auxiliary SVG for rendering text-shapes - (when render-texts? - (for [object text-shapes] - [:& (mf/provider muc/text-plain-colors-ctx) {:value true} - [:svg - {:id (dm/str "screenshot-text-" (:id object)) - :view-box (dm/str "0 0 " (:width object) " " (:height object)) - :width (:width object) - :height (:height object) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :fill "none"} - [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))])) + ;; Auxiliary SVG for rendering text-shapes + (when render-texts? + (for [object text-shapes] + [:& (mf/provider muc/text-plain-colors-ctx) {:value true} + [:svg + {:id (dm/str "screenshot-text-" (:id object)) + :view-box (dm/str "0 0 " (:width object) " " (:height object)) + :width (:width object) + :height (:height object) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SPRITES (DEBUG) diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index 6054fd505b..498974a88f 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.context-menu (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.refs :as refs] [app.main.ui.components.dropdown :refer [dropdown']] [app.main.ui.icons :as i] @@ -110,10 +111,10 @@ (for [[index [option-name option-handler sub-options data-test]] (d/enumerate (:options level))] (when option-name (if (= option-name :separator) - [:li.separator] + [:li.separator {:key (dm/str "context-item-" index)}] [:li.context-menu-item {:class (dom/classnames :is-selected (and selected (= option-name selected))) - :key index} + :key (dm/str "context-item-" index)} (if-not sub-options [:a.context-menu-action {:on-click #(do (dom/stop-propagation %) (on-close) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index c59b46a6b9..5f8ceba502 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -95,6 +95,10 @@ (add! :constraints-v) (add! :fixed-scroll) + (cond-> frame? + (-> (add! :show-content) + (add! :hide-in-viewer))) + (cond-> (and (or rect? image? frame?) (some? (:r1 shape))) (-> (add! :r1) (add! :r2) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 292431093f..693735c8a2 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -789,10 +789,14 @@ :content node-content})))) (defn add-frame-data [props node] - (let [grids (parse-grids node)] - (cond-> props - (d/not-empty? grids) - (assoc :grids grids)))) + (let [grids (parse-grids node) + show-content (get-meta node :show-content str->bool) + hide-in-viewer (get-meta node :hide-in-viewer str->bool)] + (-> props + (assoc :show-content show-content) + (assoc :hide-in-viewer hide-in-viewer) + (cond-> (d/not-empty? grids) + (assoc :grids grids))))) (defn has-image? [node] From b4e218c13a56ce36702c4afb98a37165813e23e4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 9 Jun 2022 19:52:12 +0200 Subject: [PATCH 25/86] :sparkles: Fix copy/paste for multiple frames --- frontend/src/app/main/data/workspace.cljs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e05a4f9074..3e10ea51aa 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1241,8 +1241,8 @@ ;; Prepare the shape object. Mainly needed for image shapes ;; for retrieve the image data and convert it to the ;; data-url. - (prepare-object [objects selected {:keys [type] :as obj}] - (let [obj (maybe-translate obj objects selected)] + (prepare-object [objects selected+children {:keys [type] :as obj}] + (let [obj (maybe-translate obj objects selected+children)] (if (= type :image) (let [url (cfg/resolve-file-media (:metadata obj))] (->> (http/send! {:method :get @@ -1265,9 +1265,9 @@ (update res :images conj img-part)) res))) - (maybe-translate [shape objects selected] + (maybe-translate [shape objects selected+children] (if (and (not= (:type shape) :frame) - (not (contains? selected (:frame-id shape)))) + (not (contains? selected+children (:frame-id shape)))) ;; When the parent frame is not selected we change to relative ;; coordinates (let [frame (get objects (:frame-id shape))] @@ -1284,6 +1284,8 @@ (let [objects (wsh/lookup-page-objects state) selected (->> (wsh/lookup-selected state) (cph/clean-loops objects)) + + selected+children (cph/selected-with-children objects selected) pdata (reduce (partial collect-object-ids objects) {} selected) initial {:type :copied-shapes :file-id (:current-file-id state) @@ -1293,7 +1295,7 @@ (->> (rx/from (seq (vals pdata))) - (rx/merge-map (partial prepare-object objects selected)) + (rx/merge-map (partial prepare-object objects selected+children)) (rx/reduce collect-data initial) (rx/map (partial sort-selected state)) (rx/map t/encode-str) From a37233be1e8d28fdcc85ff3e7a61f753e592953e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 10 Jun 2022 09:49:40 +0200 Subject: [PATCH 26/86] :bug: Improved thumbnails rendering --- .../app/main/data/workspace/thumbnails.cljs | 2 +- .../app/main/ui/workspace/shapes/frame.cljs | 4 ++-- .../ui/workspace/shapes/frame/node_store.cljs | 2 +- .../shapes/frame/thumbnail_render.cljs | 20 ++++++++++++------- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 60e6a039c6..25791cd722 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -34,7 +34,7 @@ (fn [subs] ;; We look in the DOM a canvas that 1) matches the id and 2) that it's not empty ;; will be empty on first rendering before drawing the thumbnail and we don't want to store that - (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%']:not([data-empty])" object-id))] + (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'][data-empty='false']" object-id))] (if (some? node) (-> node (.toBlob (fn [blob] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index bef488434a..ae27666469 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -87,7 +87,7 @@ [on-load-frame-dom render-frame? thumbnail-renderer] (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render) - on-frame-load + [on-frame-load in-memory?] (fns/use-node-store thumbnail? node-ref rendered? render-frame?)] (fdm/use-dynamic-modifiers objects @node-ref modifiers) @@ -130,5 +130,5 @@ [:g.frame-thumbnail-wrapper {:id (dm/str "thumbnail-container-" (:id shape)) ;; Hide the thumbnail when not displaying - :opacity (when (and @rendered? (not thumbnail?) (not render-frame?)) 0)} + :opacity (when (and @rendered? (not thumbnail?) (not render-frame?) (not in-memory?)) 0)} thumbnail-renderer]]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs index 6d97305302..832795711d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs @@ -43,4 +43,4 @@ (.appendChild @parent-ref @node-ref) (reset! in-memory? false)))) - on-frame-load)) + [on-frame-load @in-memory?])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index f7df70e1b7..44b5c27366 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -33,7 +33,6 @@ (.clearRect canvas-context 0 0 canvas-width canvas-height) (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - (.removeAttribute canvas-node "data-empty") true)) (catch :default err (.error js/console err) @@ -87,8 +86,12 @@ prev-thumbnail-data (hooks/use-previous thumbnail-data) + ;; State to indicate to the parent that should render the frame render-frame? (mf/use-state (not thumbnail-data)) + ;; State variable to select whether we show the image thumbnail or the canvas thumbnail + show-frame-thumbnail (mf/use-state (some? thumbnail-data)) + on-image-load (mf/use-callback (fn [] @@ -98,6 +101,8 @@ (when (draw-thumbnail-canvas! canvas-node img-node) (reset! image-url nil) + (when @show-frame-thumbnail + (reset! show-frame-thumbnail false)) ;; If we don't have the thumbnail data saved (normaly the first load) we update the data ;; when available (when (not @thumbnail-data-ref) @@ -210,18 +215,19 @@ @render-frame? (mf/html [:* - [:> frame/frame-thumbnail {:key (dm/str (:id shape)) - :bounds shape-bb - :shape (cond-> shape - (some? thumbnail-data) - (assoc :thumbnail thumbnail-data))}] + (when @show-frame-thumbnail + [:> frame/frame-thumbnail {:key (dm/str (:id shape)) + :bounds shape-bb + :shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))}]) [:foreignObject {:x x :y y :width width :height height} [:canvas.thumbnail-canvas {:key (dm/str "thumbnail-canvas-" (:id shape)) :ref frame-canvas-ref :data-object-id (dm/str page-id (:id shape)) - :data-empty true + :data-empty @show-frame-thumbnail :width fixed-width :height fixed-height ;; DEBUG From cab2b8469e7f454ac8246184c00b02c7fee64484 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 10 Jun 2022 15:17:20 +0200 Subject: [PATCH 27/86] :sparkles: Fix nested frames with thumbnails --- common/src/app/common/pages/helpers.cljc | 9 ++ .../src/app/main/data/workspace/changes.cljs | 5 +- .../main/data/workspace/state_helpers.cljs | 8 ++ .../app/main/ui/workspace/shapes/frame.cljs | 125 +++++++++--------- 4 files changed, 85 insertions(+), 62 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index d86fffc04e..cd1f3f5616 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -674,3 +674,12 @@ (into selected (mapcat #(get-children-ids objects %)) selected)) + +(defn get-shape-id-root-frame + [objects shape-id] + (->> (get-parents-seq objects shape-id) + (map (d/getf objects)) + (d/seek #(and (= :frame (:type %)) + (= uuid/zero (:frame-id %)))) + + :id)) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index fa2235c1a1..9c43d99de9 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -10,6 +10,7 @@ [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] + [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.spec.change :as spec.change] [app.common.uuid :as uuid] @@ -126,9 +127,7 @@ []))] (into #{} (comp (mapcat change->ids) - (keep #(if (= :frame (get-in objects [% :type])) - % - (get-in objects [% :frame-id]))) + (keep #(cph/get-shape-id-root-frame objects %)) (remove #(= uuid/zero %))) changes))) diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index e0f60d196a..c0733644f2 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -70,6 +70,14 @@ selected (dm/get-in state [:workspace-local :selected])] (process-selected-shapes objects selected options)))) +(defn lookup-shape + ([state id] + (lookup-shape state (:current-page-id state) id)) + + ([state page-id id] + (let [objects (lookup-page-objects state page-id)] + (get objects id)))) + (defn lookup-shapes ([state ids] (lookup-shapes state (:current-page-id state) ids)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index ae27666469..deb213d491 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] @@ -58,77 +59,83 @@ ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - thumbnail? (unchecked-get props "thumbnail?") - objects (wsh/lookup-page-objects @st/state) - - render-id (mf/use-memo #(str (uuid/next))) - fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects)) - fonts (-> fonts (hooks/use-equal-memo)) - - force-render (mf/use-state false) - - ;; Thumbnail data + (let [shape (unchecked-get props "shape") frame-id (:id shape) - page-id (mf/use-ctx ctx/current-page-id) ;; References to the current rendered node and the its parentn node-ref (mf/use-var nil) - ;; when `true` we've called the mount for the frame - rendered? (mf/use-var false) + objects (wsh/lookup-page-objects @st/state) ;; Modifiers modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id)) - modifiers (mf/deref modifiers-ref) - - disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers])) - - [on-load-frame-dom render-frame? thumbnail-renderer] - (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render) - - [on-frame-load in-memory?] - (fns/use-node-store thumbnail? node-ref rendered? render-frame?)] + modifiers (mf/deref modifiers-ref)] (fdm/use-dynamic-modifiers objects @node-ref modifiers) - (mf/use-effect - (mf/deps fonts) - (fn [] - (->> (rx/from fonts) - (rx/merge-map fonts/fetch-font-css) - (rx/ignore)))) + (if-not (cph/root-frame? shape) + [:& frame-shape {:shape shape :ref node-ref}] - (mf/use-effect - (fn [] - ;; When a change in the data is received a "force-render" event is emited - ;; that will force the component to be mounted in memory - (let [sub - (->> (dwt/force-render-stream (:id shape)) - (rx/take-while #(not @rendered?)) - (rx/subs #(reset! force-render true)))] - #(when sub - (rx/dispose! sub))))) + ;; If the current shape is root we handle its thumbnail and the dynamic modifiers + (let [thumbnail? (unchecked-get props "thumbnail?") - (mf/use-effect - (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?) - (fn [] - (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?)) - (mf/mount - (mf/element frame-shape - #js {:ref on-load-frame-dom :shape shape :fonts fonts}) + render-id (mf/use-memo #(str (uuid/next))) + fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects)) + fonts (-> fonts (hooks/use-equal-memo)) - @node-ref) - (when (not @rendered?) (reset! rendered? true))))) + force-render (mf/use-state false) - [:& (mf/provider ctx/render-ctx) {:value render-id} - [:g.frame-container {:id (dm/str "frame-container-" (:id shape)) - :key "frame-container" - :ref on-frame-load - :opacity (when (:hidden shape) 0)} - [:& ff/fontfaces-style {:fonts fonts}] - [:g.frame-thumbnail-wrapper - {:id (dm/str "thumbnail-container-" (:id shape)) - ;; Hide the thumbnail when not displaying - :opacity (when (and @rendered? (not thumbnail?) (not render-frame?) (not in-memory?)) 0)} - thumbnail-renderer]]])))) + ;; Thumbnail data + page-id (mf/use-ctx ctx/current-page-id) + + ;; when `true` we've called the mount for the frame + rendered? (mf/use-var false) + + disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers])) + + [on-load-frame-dom render-frame? thumbnail-renderer] + (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render) + + [on-frame-load in-memory?] + (fns/use-node-store thumbnail? node-ref rendered? render-frame?)] + + (mf/use-effect + (mf/deps fonts) + (fn [] + (->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/ignore)))) + + (mf/use-effect + (fn [] + ;; When a change in the data is received a "force-render" event is emited + ;; that will force the component to be mounted in memory + (let [sub + (->> (dwt/force-render-stream (:id shape)) + (rx/take-while #(not @rendered?)) + (rx/subs #(reset! force-render true)))] + #(when sub + (rx/dispose! sub))))) + + (mf/use-effect + (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?) + (fn [] + (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?)) + (mf/mount + (mf/element frame-shape + #js {:ref on-load-frame-dom :shape shape :fonts fonts}) + + @node-ref) + (when (not @rendered?) (reset! rendered? true))))) + + [:& (mf/provider ctx/render-ctx) {:value render-id} + [:g.frame-container {:id (dm/str "frame-container-" (:id shape)) + :key "frame-container" + :ref on-frame-load + :opacity (when (:hidden shape) 0)} + [:& ff/fontfaces-style {:fonts fonts}] + [:g.frame-thumbnail-wrapper + {:id (dm/str "thumbnail-container-" (:id shape)) + ;; Hide the thumbnail when not displaying + :opacity (when (and @rendered? (not thumbnail?) (not render-frame?) (not in-memory?)) 0)} + thumbnail-renderer]]])))))) From 566dde21a5e7da4d81b8a4ea6347dcd8b8c320a9 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 13 Jun 2022 15:45:16 +0200 Subject: [PATCH 28/86] :sparkles: Fix viewer for new frames --- common/src/app/common/pages/helpers.cljc | 24 ++++++-- frontend/src/app/main/data/viewer.cljs | 15 ++--- frontend/src/app/main/render.cljs | 2 +- .../src/app/main/ui/viewer/interactions.cljs | 6 +- .../sidebar/options/menus/interactions.cljs | 21 ++++--- .../ui/workspace/viewport/interactions.cljs | 57 +++++++++---------- .../main/ui/workspace/viewport/widgets.cljs | 6 +- 7 files changed, 73 insertions(+), 58 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index cd1f3f5616..4841e47005 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -237,16 +237,18 @@ [base index-base-a index-base-b])) (defn is-shape-over-shape? - [objects base-shape-id over-shape-id] + [objects base-shape-id over-shape-id {:keys [top-frames?]}] (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] (cond (= base base-shape-id) - (and (frame-shape? objects base-shape-id) + (and (not top-frames?) + (frame-shape? objects base-shape-id) (root-frame? objects base-shape-id)) (= base over-shape-id) - (or (not (frame-shape? objects over-shape-id)) + (or top-frames? + (not (frame-shape? objects over-shape-id)) (not (root-frame? objects over-shape-id))) :else @@ -256,7 +258,7 @@ ([objects ids] (sort-z-index objects ids nil)) - ([objects ids {:keys [bottom-frames?]}] + ([objects ids {:keys [bottom-frames?] :as options}] (letfn [(comp [id-a id-b] (let [type-a (dm/get-in objects [id-a :type]) type-b (dm/get-in objects [id-b :type])] @@ -270,7 +272,7 @@ (= id-a id-b) 0 - (is-shape-over-shape? objects id-a id-b) + (is-shape-over-shape? objects id-a id-b options) 1 :else @@ -683,3 +685,15 @@ (= uuid/zero (:frame-id %)))) :id)) + +(defn get-viewer-frames + ([objects] + (get-viewer-frames objects nil)) + + ([objects {:keys [all-frames?]}] + (into [] + (comp (map (d/getf objects)) + (if all-frames? + identity + (remove :hide-in-viewer))) + (sort-z-index objects (get-frames-ids objects) {:top-frames? true})))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 5f45b0173d..52990d5da5 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -85,13 +85,6 @@ (update [_ state] (dissoc state :viewer)))) -(defn select-frames - [{:keys [objects] :as page}] - (into [] - (comp (map (d/getf objects)) - (remove :hide-in-viewer)) - (cph/sort-z-index objects (cph/get-frames-ids objects)))) - ;; --- Data Fetching (s/def ::fetch-bundle-params @@ -119,7 +112,9 @@ (let [pages (->> (get-in file [:data :pages]) (map (fn [page-id] (let [data (get-in file [:data :pages-index page-id])] - [page-id (assoc data :frames (select-frames data))]))) + [page-id (assoc data + :frames (cph/get-viewer-frames (:objects data)) + :all-frames (cph/get-viewer-frames (:objects data) {:all-frames? true}))]))) (into {}))] (ptk/reify ::bundle-fetched @@ -491,7 +486,7 @@ (let [route (:route state) qparams (:query-params route) page-id (:page-id qparams) - frames (get-in state [:viewer :pages page-id :frames]) + frames (get-in state [:viewer :pages page-id :all-frames]) frame (d/seek #(= (:id %) frame-id) frames) overlays (get-in state [:viewer-local :overlays])] (if-not (some #(= (:frame %) frame) overlays) @@ -516,7 +511,7 @@ (let [route (:route state) qparams (:query-params route) page-id (:page-id qparams) - frames (get-in state [:viewer :pages page-id :frames]) + frames (get-in state [:viewer :pages page-id :all-frames]) frame (d/seek #(= (:id %) frame-id) frames) overlays (get-in state [:viewer-local :overlays])] (if-not (some #(= (:frame %) frame) overlays) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 5c81e804bd..429e5b4d0d 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -389,7 +389,7 @@ text-shapes (sequence (filter cph/text-shape?) (vals objects)) render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] - [:& (mf/provider export/include-metadata-ctx) {:value true} + [:& (mf/provider export/include-metadata-ctx) {:value false} [:& (mf/provider embed/context) {:value render-embed?} [:svg {:id (dm/str "screenshot-" object-id) :view-box vbox diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 06a4f2b8b1..0878f911a5 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.viewer.interactions (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] @@ -127,8 +128,9 @@ [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} [:ul.dropdown.with-check - (for [flow flows] - [:li {:class (dom/classnames :selected (= (:id flow) (:id @current-flow))) + (for [[index flow] (d/enumerate flows)] + [:li {:key (dm/str "flow-" (:id flow) "-" index) + :class (dom/classnames :selected (= (:id flow) (:id @current-flow))) :on-click #(select-flow flow)} [:span.icon i/tick] [:span.label (:name flow)]])]]]))) 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 e8ba4e9b9a..5464fa633c 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 @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.interactions (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.spec.interactions :as csi] [app.common.spec.page :as csp] @@ -178,10 +179,10 @@ (mf/defc interaction-entry [{:keys [index shape interaction update-interaction remove-interaction]}] - (let [objects (deref refs/workspace-page-objects) - destination (get objects (:destination interaction)) - frames (mf/with-memo [objects] - (cph/get-frames objects)) + (let [objects (deref refs/workspace-page-objects) + destination (get objects (:destination interaction)) + + frames (mf/with-memo [objects] (cph/get-viewer-frames objects {:all-frames? (not= :navigate (:action-type interaction))})) overlay-pos-type (:overlay-pos-type interaction) close-click-outside? (:close-click-outside interaction false) @@ -313,7 +314,8 @@ (for [[value name] (event-type-names)] (when-not (and (= value :after-delay) (not= (:type shape) :frame)) - [:option {:value (str value)} name]))]] + [:option {:key (dm/str value) + :value (dm/str value)} name]))]] ; Delay (when (csi/has-delay interaction) @@ -334,7 +336,8 @@ {:value (str (:action-type interaction)) :on-change change-action-type} (for [[value name] (action-type-names)] - [:option {:value (str value)} name])]] + [:option {:key (dm/str "action-" value) + :value (str value)} name])]] ; Destination (when (csi/has-destination interaction) @@ -349,7 +352,8 @@ (for [frame frames] (when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself (not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame - [:option {:value (str (:id frame))} (:name frame)]))]]) + [:option {:key (dm/str "destination-" (:id frame)) + :value (str (:id frame))} (:name frame)]))]]) ; Preserve scroll (when (csi/has-preserve-scroll interaction) @@ -568,7 +572,8 @@ [:div.interactions-help (tr "workspace.options.use-play-button")]])] [:div.groups (for [[index interaction] (d/enumerate interactions)] - [:& interaction-entry {:index index + [:& interaction-entry {:key (dm/str (:id shape) "-" index) + :index index :shape shape :interaction interaction :update-interaction update-interaction diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 20a51b826c..b35eac1c7f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -280,7 +280,7 @@ selected? (contains? selected (:id shape)) level (calc-level index (:interactions shape))] (when-not selected? - [:& interaction-path {:key (dm/str (:id shape) "-" index) + [:& interaction-path {:key (dm/str "non-selected-" (:id shape) "-" index) :index index :level level :orig-shape shape @@ -307,35 +307,32 @@ (let [dest-shape (when (cti/destination? interaction) (get objects (:destination interaction))) level (calc-level index (:interactions shape))] - [:* - [:& interaction-path {:key (dm/str (:id shape) "-" index) - :index index - :level level - :orig-shape shape - :dest-shape dest-shape - :selected selected - :selected? true - :action-type (:action-type interaction) - :zoom zoom}] - (when (and (or (= (:action-type interaction) :open-overlay) - (= (:action-type interaction) :toggle-overlay)) - (= (:overlay-pos-type interaction) :manual)) - (if (and (some? move-overlay-to) - (= move-overlay-index index)) - [:& overlay-marker {:key (dm/str "pos" (:id shape) "-" index) - :index index - :orig-shape shape - :dest-shape dest-shape - :position move-overlay-to - :objects objects - :hover-disabled? hover-disabled?}] - [:& overlay-marker {:key (dm/str "pos" (:id shape) "-" index) - :index index - :orig-shape shape - :dest-shape dest-shape - :position (:overlay-position interaction) - :objects objects - :hover-disabled? hover-disabled?}]))]))) + [:g {:key (dm/str "interaction-path-" (:id shape) "-" index)} + [:& interaction-path {:index index + :level level + :orig-shape shape + :dest-shape dest-shape + :selected selected + :selected? true + :action-type (:action-type interaction) + :zoom zoom}] + (when (and (or (= (:action-type interaction) :open-overlay) + (= (:action-type interaction) :toggle-overlay)) + (= (:overlay-pos-type interaction) :manual)) + (if (and (some? move-overlay-to) + (= move-overlay-index index)) + [:& overlay-marker {:index index + :orig-shape shape + :dest-shape dest-shape + :position move-overlay-to + :objects objects + :hover-disabled? hover-disabled?}] + [:& overlay-marker {:index index + :orig-shape shape + :dest-shape dest-shape + :position (:overlay-position interaction) + :objects objects + :hover-disabled? hover-disabled?}]))]))) (when (and shape (not (cph/unframed-shape? shape)) (not (#{:move :rotate} current-transform))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index fa697e5104..68c8f61cff 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.widgets (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -253,9 +254,10 @@ on-frame-leave (unchecked-get props "on-frame-leave") on-frame-select (unchecked-get props "on-frame-select")] [:g.frame-flows - (for [flow flows] + (for [[index flow] (d/enumerate flows)] (let [frame (get objects (:starting-frame flow))] - [:& frame-flow {:flow flow + [:& frame-flow {:key (dm/str (:id frame) "-" index) + :flow flow :frame frame :selected? (contains? selected (:id frame)) :zoom zoom From ca326ac231918f7b3b32e48a814fc68d43b60aee Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 14 Jun 2022 12:24:40 +0200 Subject: [PATCH 29/86] :sparkles: Fix dashboard thumbnails for nested frames --- backend/src/app/rpc/queries/files.clj | 23 ++++++++++++--- common/src/app/common/geom/shapes.cljc | 1 + .../app/common/geom/shapes/transforms.cljc | 7 +++++ frontend/src/app/main/render.cljs | 29 ++++++++++++------- .../shapes/frame/dynamic_modifiers.cljs | 7 +++-- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 8efc073d32..40886f3e1f 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.pages.migrations :as pmg] [app.common.spec :as us] @@ -289,7 +290,7 @@ frame (-> page :objects cph/get-frames)] (assoc frame :page-id (:id page))))) - ;; function responsible to filter objects data strucuture of + ;; function responsible to filter objects data structure of ;; all unneded shapes if a concrete frame is provided. If no ;; frame, the objects is returned untouched. (filter-objects [objects frame-id] @@ -307,10 +308,24 @@ object-id (str page-id frame-id) frame (if-let [thumb (get thumbnails object-id)] (assoc frame :thumbnail thumb :shapes []) - (dissoc frame :thumbnail))] + (dissoc frame :thumbnail)) + + children-ids + (cph/get-children-ids objects frame-id) + + bounds + (when (:show-content frame) + (gsh/selection-rect (->> children-ids (map (d/getf objects))))) + + frame + (cond-> frame + (some? bounds) + (assoc :children-bounds bounds))] + (if (:thumbnail frame) - (recur (-> (assoc objects frame-id frame) - (d/without-keys (cph/get-children-ids objects frame-id))) + (recur (-> objects + (assoc frame-id frame) + (d/without-keys children-ids)) (rest frames)) (recur (assoc objects frame-id frame) (rest frames)))) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 90debd442c..6f054ab838 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -172,6 +172,7 @@ (dm/export gtr/merge-modifiers) (dm/export gtr/transform-shape) (dm/export gtr/transform-selrect) +(dm/export gtr/transform-selrect-matrix) (dm/export gtr/transform-bounds) (dm/export gtr/modifiers->transform) (dm/export gtr/empty-modifiers?) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 0f254b9a57..a727947c4c 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -645,6 +645,13 @@ (transform-bounds center modifiers) (gpr/points->selrect)))) +(defn transform-selrect-matrix + [selrect mtx] + (-> selrect + (gpr/rect->points) + (gco/transform-points mtx) + (gpr/points->selrect))) + (defn selection-rect "Returns a rect that contains all the shapes and is aware of the rotation of each shape. Mainly used for multiple selection." diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 429e5b4d0d..c9f62c7b8e 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -61,8 +61,12 @@ (defn- calculate-dimensions [objects] - (let [shapes (cph/get-immediate-children objects) - rect (gsh/selection-rect shapes)] + (let [rect + (->> (cph/get-immediate-children objects) + (map #(if (some? (:children-bounds %)) + (:children-bounds %) + (:selrect %))) + (gsh/join-selrects))] (-> rect (update :x mth/finite 0) (update :y mth/finite 0) @@ -77,9 +81,12 @@ frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-wrapper [{:keys [shape] :as props}] + (let [childs (mapv #(get objects %) (:shapes shape)) shape (gsh/transform-shape shape)] - [:& frame-shape {:shape shape :childs childs}])))) + (if (some? (:thumbnail shape)) + [:& frame/frame-thumbnail {:shape shape :bounds (:children-bounds shape)}] + [:& frame-shape {:shape shape :childs childs}]))))) (defn group-wrapper-factory [objects] @@ -251,7 +258,7 @@ (cond (and frame? thumbnails? (some? (:thumbnail item))) [:> shape-container {:shape item} - [:& frame/frame-thumbnail {:shape item}]] + [:& frame/frame-thumbnail {:shape item :bounds (:children-bounds item)}]] frame? [:> shape-container {:shape item} @@ -271,9 +278,11 @@ (let [frame-id (:id frame) include-metadata? (mf/use-ctx export/include-metadata-ctx) + bounds (or (:children-bounds frame) (:selrect frame)) + modifier - (mf/with-memo [(:x frame) (:y frame)] - (-> (gpt/point (:x frame) (:y frame)) + (mf/with-memo [(:x bounds) (:y bounds)] + (-> (gpt/point (:x bounds) (:y bounds)) (gpt/negate) (gmt/translate-matrix))) @@ -292,9 +301,9 @@ (mf/with-memo [objects] (frame-wrapper-factory objects)) - width (* (:width frame) zoom) - height (* (:height frame) zoom) - vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)})] + width (* (:width bounds) zoom) + height (* (:height bounds) zoom) + vbox (format-viewbox {:width (:width bounds 0) :height (:height bounds 0)})] [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) @@ -310,7 +319,7 @@ ;; Render the frame thumbnail (let [frame (gsh/transform-shape frame)] [:> shape-container {:shape frame} - [:& frame/frame-thumbnail {:shape frame}]]))])) + [:& frame/frame-thumbnail {:shape frame :bounds (assoc bounds :x 0 :y 0)}]]))])) ;; Component for rendering a thumbnail of a single componenent. Mainly diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 1722af34e7..0ce9b0a6a2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -77,14 +77,15 @@ (defn get-nodes "Retrieve the DOM nodes to apply the matrix transformation" - [base-node {:keys [id type masked-group?]}] - (let [shape-node (dom/query base-node (str "#shape-" id)) + [base-node {:keys [id type masked-group?] :as shape}] + (let [shape-node (if (= (.-id base-node) (dm/str "shape-" id)) + base-node + (dom/query base-node (dm/str "#shape-" id))) frame? (= :frame type) group? (= :group type) text? (= :text type) mask? (and group? masked-group?)] - (cond frame? [shape-node From 108291337d4aa03aaee8f92589c3321714ea2e0d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 14 Jun 2022 18:03:20 +0200 Subject: [PATCH 30/86] :sparkles: Improved frame indices --- CHANGES.md | 2 +- common/src/app/common/pages/helpers.cljc | 72 ++++++++++-------- frontend/src/app/main/data/workspace.cljs | 7 +- .../src/app/main/data/workspace/changes.cljs | 5 +- .../src/app/main/data/workspace/indices.cljs | 75 ------------------- .../data/workspace/indices/object_tree.cljs | 16 ---- .../app/main/data/workspace/thumbnails.cljs | 13 ++-- .../app/main/ui/workspace/shapes/path.cljs | 12 +-- .../workspace/shapes/text/viewport_texts.cljs | 4 +- 9 files changed, 62 insertions(+), 144 deletions(-) delete mode 100644 frontend/src/app/main/data/workspace/indices.cljs delete mode 100644 frontend/src/app/main/data/workspace/indices/object_tree.cljs diff --git a/CHANGES.md b/CHANGES.md index abac9d19d0..14e62ec6be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ ### :sparkles: New features -- Allow for nested boards inside other boards and groups +- Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982) ### :bug: Bugs fixed ### :arrow_up: Deps updates diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 4841e47005..442c43b478 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -153,30 +153,23 @@ (:shapes) (keep lookup))))) -(defn get-frames-ids - "Retrieves all frame objects as vector. It is not implemented in - function of `get-immediate-children` for performance reasons. This - function is executed in the render hot path." - [objects] - (let [lookup (d/getf objects) - xform (comp (remove #(= uuid/zero %)) - (keep lookup) - (filter frame-shape?) - (map :id))] - (->> (keys objects) - (into [] xform)))) - (defn get-frames - "Retrieves all frame objects as vector. It is not implemented in - function of `get-immediate-children` for performance reasons. This - function is executed in the render hot path." + "Retrieves all frame objects as vector" [objects] - (let [lookup (d/getf objects) - xform (comp (remove #(= uuid/zero %)) - (keep lookup) - (filter frame-shape?))] - (->> (keys objects) - (into [] xform)))) + (if (contains? (meta objects) ::index-frames) + (::index-frames (meta objects)) + (let [lookup (d/getf objects) + xform (comp (remove #(= uuid/zero %)) + (keep lookup) + (filter frame-shape?))] + (->> (keys objects) + (into [] xform))))) + +(defn get-frames-ids + "Retrieves all frame ids as vector" + [objects] + (->> (get-frames objects) + (mapv :id))) (defn get-nested-frames [objects frame-id] @@ -197,25 +190,19 @@ (conj (:id shape))))] (reduce-objects objects (complement frame-shape?) add-frame []))) -(defn get-root-shapes-ids +(defn get-root-shapes [objects] (let [add-shape (fn [result shape] (cond-> result (not (frame-shape? shape)) - (conj (:id shape))))] + (conj shape)))] (reduce-objects objects (complement frame-shape?) add-shape []))) -(defn get-root-frames - "Retrieves all frame objects as vector. It is not implemented in - function of `get-immediate-children` for performance reasons. This - function is executed in the render hot path." +(defn get-root-shapes-ids [objects] - (let [lookup (d/getf objects) - xform (comp (keep lookup) - (filter frame-shape?))] - (->> (:shapes (lookup uuid/zero)) - (into [] xform)))) + (->> (get-root-shapes objects) + (mapv :id))) (defn- get-base [objects id-a id-b] @@ -697,3 +684,22 @@ identity (remove :hide-in-viewer))) (sort-z-index objects (get-frames-ids objects) {:top-frames? true})))) + + +(defn start-page-index + [objects] + (with-meta objects {::index-frames (get-frames objects)})) + +(defn update-page-index + [objects] + (with-meta objects {::index-frames (get-frames objects)})) + +(defn start-object-indices + [file] + (letfn [(process-index [page-index page-id] + (update-in page-index [page-id :objects] start-page-index))] + (update file :pages-index #(reduce process-index % (keys %))))) + +(defn update-object-indices + [file page-id] + (update-in file [:pages-index page-id :objects] update-page-index)) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 3e10ea51aa..5bbd56046e 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -32,7 +32,6 @@ [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] - [app.main.data.workspace.indices :as dwidx] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.layout :as layout] @@ -128,9 +127,7 @@ team-id (dm/get-in bundle [:project :team-id])] (rx/merge (rx/of (dwn/initialize team-id file-id) - (dwp/initialize-file-persistence file-id) - (dwidx/start-indexing)) - + (dwp/initialize-file-persistence file-id)) (->> stream (rx/filter #(= ::dwc/index-initialized %)) (rx/take 1) @@ -153,6 +150,7 @@ :workspace-project project :workspace-file (assoc file :initialized true) :workspace-data (-> (:data file) + (cph/start-object-indices) ;; DEBUG: Uncomment this to try out migrations in local without changing ;; the version number #_(assoc :version 17) @@ -196,7 +194,6 @@ (watch [_ _ _] (rx/merge (rx/of (dwn/finalize file-id)) - (rx/of (dwidx/stop-indexing)) (->> (rx/of ::dwp/finalize) (rx/observe-on :async)))))) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 9c43d99de9..decc1b5f54 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -162,7 +162,10 @@ (us/assert ::spec.change/changes redo-changes) (us/assert ::spec.change/changes undo-changes) - (update-in state path cp/process-changes redo-changes false) + (update-in state path (fn [file] + (-> file + (cp/process-changes redo-changes false) + (cph/update-object-indices page-id)))) (catch :default err (log/error :js/error err) diff --git a/frontend/src/app/main/data/workspace/indices.cljs b/frontend/src/app/main/data/workspace/indices.cljs deleted file mode 100644 index 739a3f9e8b..0000000000 --- a/frontend/src/app/main/data/workspace/indices.cljs +++ /dev/null @@ -1,75 +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.workspace.indices - (:require - [app.main.data.workspace.changes :as dwc] - [app.main.data.workspace.indices.object-tree :as dwi-object-tree] - [app.main.data.workspace.state-helpers :as wsh] - [app.main.refs :as refs] - [beicon.core :as rx] - [potok.core :as ptk])) - -(def stop-indexing? (ptk/type? ::stop-indexing)) - -(def objects-changes #{:add-obj :mod-obj :del-obj :mov-objects}) - -(defn stop-indexing - [] - (ptk/reify ::stop-indexing - ptk/UpdateEvent - (update [_ state] - (-> state - (dissoc :index-object-tree))))) - -(defn process-changes - "Simplify changes so we have only the type of operation and the ids" - [changes] - (->> changes - (filter #(contains? objects-changes (:type %))) - (mapcat (fn [{:keys [type id shapes]}] - (if (some? shapes) - (->> shapes (map #(vector type %))) - [[type id]]))))) - -(defn update-indexing - [change-type shape-id old-objects new-objects] - (ptk/reify ::update-indexing - ptk/UpdateEvent - (update [_ state] - (-> state - (update :index-object-tree dwi-object-tree/update-index shape-id change-type old-objects new-objects))))) - -(defn start-indexing - [] - (ptk/reify ::start-indexing - ptk/UpdateEvent - (update [_ state] - (let [objects (wsh/lookup-page-objects state)] - (-> state - (assoc :index-object-tree (dwi-object-tree/init-index objects))))) - - ptk/WatchEvent - (watch [_ _ stream] - (let [stopper (->> stream (rx/filter stop-indexing?) (rx/take 1)) - objects-delta (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/buffer 2 1))] - (->> stream - (rx/filter dwc/commit-changes?) - (rx/flat-map #(->> % deref :changes process-changes)) - (rx/with-latest-from objects-delta) - (rx/map (fn [[[type id] [objects-old objects-new]]] - (update-indexing type id objects-old objects-new))) - #_(rx/tap (fn [[[type id] [objects-old objects-new]]] - (let [obj-old (get objects-old id) - obj-new (get objects-new id)] - (prn ">change" (or (:name obj-old) (:name obj-new))) - (prn " > " type) - (.log js/console " >" (clj->js obj-old)) - (.log js/console " >" (clj->js obj-new)) - - ))) - (rx/take-until stopper) - (rx/ignore)))))) diff --git a/frontend/src/app/main/data/workspace/indices/object_tree.cljs b/frontend/src/app/main/data/workspace/indices/object_tree.cljs deleted file mode 100644 index b7aaad28da..0000000000 --- a/frontend/src/app/main/data/workspace/indices/object_tree.cljs +++ /dev/null @@ -1,16 +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.workspace.indices.object-tree) - -(defn objects-tree - [_objects]) - -(defn init-index - [_objects]) - -(defn update-index - [_index _shape-id _change-type _old-objects _new-objects]) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 25791cd722..1a97606df4 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -73,12 +73,13 @@ (rx/merge-map (fn [data] - (let [params {:file-id file-id :object-id object-id :data data}] - (rx/merge - ;; Update the local copy of the thumbnails so we don't need to request it again - (rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data)) - (->> (rp/mutation! :upsert-file-object-thumbnail params) - (rx/ignore))))))))))) + (when (some? file-id) + (let [params {:file-id file-id :object-id object-id :data data}] + (rx/merge + ;; Update the local copy of the thumbnails so we don't need to request it again + (rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data)) + (->> (rp/mutation! :upsert-file-object-thumbnail params) + (rx/ignore)))))))))))) (defn- extract-frame-changes "Process a changes set in a commit to extract the frames that are changing" diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index dee228b390..0dd893faf0 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -14,6 +14,12 @@ [app.main.ui.workspace.shapes.path.common :as pc] [rumext.alpha :as mf])) +(defn apply-content-modifiers + [shape content-modifiers] + (let [shape (update shape :content upc/apply-content-modifiers content-modifiers) + [_ new-selrect] (helpers/content->points+selrect shape (:content shape))] + (assoc shape :selrect new-selrect))) + (mf/defc path-wrapper {::mf/wrap-props false} [props] @@ -22,11 +28,7 @@ content-modifiers (mf/deref content-modifiers-ref) editing-id (mf/deref refs/selected-edition) editing? (= editing-id (:id shape)) - shape (update shape :content upc/apply-content-modifiers content-modifiers) - - [_ new-selrect] - (helpers/content->points+selrect shape (:content shape)) - shape (assoc shape :selrect new-selrect)] + shape (mf/use-memo (mf/deps shape content-modifiers) #(apply-content-modifiers shape content-modifiers))] [:> shape-container {:shape shape :pointer-events (when editing? "none")} diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs index 3aca3072c9..5c69a3c484 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -32,8 +32,8 @@ (defn strip-modifier [modifier] - (if (or (some? (get-in modifier [:modifiers :resize-vector])) - (some? (get-in modifier [:modifiers :resize-vector-2]))) + (if (or (some? (dm/get-in modifier [:modifiers :resize-vector])) + (some? (dm/get-in modifier [:modifiers :resize-vector-2]))) modifier (d/update-when modifier :modifiers dissoc :displacement :rotation))) From e0a1da6bcaef68ed860e3f1f97c13d8c967190ca Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 15 Jun 2022 13:26:50 +0200 Subject: [PATCH 31/86] :bug: Fix problems with thumbnails --- common/src/app/common/pages/helpers.cljc | 13 +++- .../app/main/data/workspace/persistence.cljs | 2 +- .../app/main/data/workspace/thumbnails.cljs | 49 +++++++------ frontend/src/app/main/render.cljs | 70 ++++++++----------- frontend/src/app/main/ui/context.cljs | 1 + 5 files changed, 68 insertions(+), 67 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 442c43b478..fcbd882bbb 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -190,7 +190,16 @@ (conj (:id shape))))] (reduce-objects objects (complement frame-shape?) add-frame []))) +(defn get-root-objects + "Get all the objects under the root object" + [objects] + (let [add-shape + (fn [result shape] + (conj result shape))] + (reduce-objects objects (complement frame-shape?) add-shape []))) + (defn get-root-shapes + "Get all shapes that are not frames" [objects] (let [add-shape (fn [result shape] @@ -688,11 +697,11 @@ (defn start-page-index [objects] - (with-meta objects {::index-frames (get-frames objects)})) + (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) (defn update-page-index [objects] - (with-meta objects {::index-frames (get-frames objects)})) + (with-meta objects {::index-frames (get-frames (with-meta objects nil))})) (defn start-object-indices [file] diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 64c237b69d..8536e6074c 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -157,7 +157,7 @@ (->> (rx/from frame-updates) (rx/flat-map (fn [[page-id frames]] (->> frames (map #(vector page-id %))))) - (rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail page-id frame-id)))) + (rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail (:id file) page-id frame-id)))) (->> (rx/of lagged) (rx/mapcat seq) (rx/map #(shapes-changes-persisted file-id %))))))) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 1a97606df4..7ca84cda11 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -56,30 +56,35 @@ (defn update-thumbnail "Updates the thumbnail information for the given frame `id`" - [page-id frame-id] - (ptk/reify ::update-thumbnail - ptk/WatchEvent - (watch [_ state _] - (let [object-id (dm/str page-id frame-id) - file-id (:current-file-id state) - blob-result (thumbnail-stream object-id)] + ([page-id frame-id] + (update-thumbnail nil page-id frame-id)) - (->> blob-result - (rx/merge-map - (fn [blob] - (if (some? blob) - (wapi/read-file-as-data-url blob) - (rx/of nil)))) + ([file-id page-id frame-id] + (ptk/reify ::update-thumbnail + ptk/WatchEvent + (watch [_ state _] + (let [object-id (dm/str page-id frame-id) + file-id (or file-id (:current-file-id state)) + blob-result (thumbnail-stream object-id)] - (rx/merge-map - (fn [data] - (when (some? file-id) - (let [params {:file-id file-id :object-id object-id :data data}] - (rx/merge - ;; Update the local copy of the thumbnails so we don't need to request it again - (rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data)) - (->> (rp/mutation! :upsert-file-object-thumbnail params) - (rx/ignore)))))))))))) + (->> blob-result + (rx/merge-map + (fn [blob] + (if (some? blob) + (wapi/read-file-as-data-url blob) + (rx/of nil)))) + + (rx/merge-map + (fn [data] + (if (some? file-id) + (let [params {:file-id file-id :object-id object-id :data data}] + (rx/merge + ;; Update the local copy of the thumbnails so we don't need to request it again + (rx/of #(assoc-in % [:workspace-file :thumbnails object-id] data)) + (->> (rp/mutation! :upsert-file-object-thumbnail params) + (rx/ignore)))) + + (rx/empty)))))))))) (defn- extract-frame-changes "Process a changes set in a commit to extract the frames that are changing" diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index c9f62c7b8e..61a4fc00fc 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -62,10 +62,10 @@ (defn- calculate-dimensions [objects] (let [rect - (->> (cph/get-immediate-children objects) + (->> (cph/get-root-objects objects) (map #(if (some? (:children-bounds %)) (:children-bounds %) - (:selrect %))) + (gsh/points->selrect (:points %)))) (gsh/join-selrects))] (-> rect (update :x mth/finite 0) @@ -82,9 +82,10 @@ (mf/fnc frame-wrapper [{:keys [shape] :as props}] - (let [childs (mapv #(get objects %) (:shapes shape)) + (let [render-thumbnails? (mf/use-ctx muc/render-thumbnails) + childs (mapv #(get objects %) (:shapes shape)) shape (gsh/transform-shape shape)] - (if (some? (:thumbnail shape)) + (if (and render-thumbnails? (some? (:thumbnail shape))) [:& frame/frame-thumbnail {:shape shape :bounds (:children-bounds shape)}] [:& frame-shape {:shape shape :childs childs}]))))) @@ -221,52 +222,37 @@ vbox (format-viewbox dim) bgcolor (dm/get-in data [:options :background] default-color) - frame-wrapper - (mf/use-memo - (mf/deps objects) - #(frame-wrapper-factory objects)) - shape-wrapper (mf/use-memo (mf/deps objects) #(shape-wrapper-factory objects))] - [:& (mf/provider embed/context) {:value render-embed?} - [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} - [:svg {:view-box vbox - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") - :style {:width "100%" - :height "100%" - :background bgcolor} - :fill "none"} + [:& (mf/provider muc/render-thumbnails) {:value thumbnails?} + [:& (mf/provider embed/context) {:value render-embed?} + [:& (mf/provider export/include-metadata-ctx) {:value include-metadata?} + [:svg {:view-box vbox + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") + :style {:width "100%" + :height "100%" + :background bgcolor} + :fill "none"} - (when include-metadata? - [:& export/export-page {:options (:options data)}]) + (when include-metadata? + [:& export/export-page {:options (:options data)}]) - (let [shapes (->> shapes - (remove cph/frame-shape?) - (mapcat #(cph/get-children-with-self objects (:id %)))) - fonts (ff/shapes->fonts shapes)] - [:& ff/fontfaces-style {:fonts fonts}]) + (let [shapes (->> shapes + (remove cph/frame-shape?) + (mapcat #(cph/get-children-with-self objects (:id %)))) + fonts (ff/shapes->fonts shapes)] + [:& ff/fontfaces-style {:fonts fonts}]) - (for [item shapes] - (let [frame? (= (:type item) :frame)] - (cond - (and frame? thumbnails? (some? (:thumbnail item))) - [:> shape-container {:shape item} - [:& frame/frame-thumbnail {:shape item :bounds (:children-bounds item)}]] - - frame? - [:> shape-container {:shape item} - [:& frame-wrapper {:shape item - :key (:id item)}]] - :else - [:& shape-wrapper {:shape item - :key (:id item)}])))]]])) + (for [item shapes] + [:& shape-wrapper {:shape item + :key (:id item)}])]]]])) ;; Component that serves for render frame thumbnails, mainly used in @@ -278,7 +264,7 @@ (let [frame-id (:id frame) include-metadata? (mf/use-ctx export/include-metadata-ctx) - bounds (or (:children-bounds frame) (:selrect frame)) + bounds (or (:children-bounds frame) (gsh/points->rect (:points frame))) modifier (mf/with-memo [(:x bounds) (:y bounds)] diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index d430ea5f2f..34a3518c92 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -23,3 +23,4 @@ (def current-file-id (mf/create-context nil)) (def scroll-ctx (mf/create-context nil)) (def active-frames-ctx (mf/create-context nil)) +(def render-thumbnails (mf/create-context nil)) From 2e3f443758053cecf1ed097af562782eee775eba Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 16 Jun 2022 14:16:33 +0200 Subject: [PATCH 32/86] :sparkles: Fix problems with shadows and strokes for nested frames --- common/deps.edn | 2 +- common/src/app/common/file_builder.cljc | 2 +- common/src/app/common/pages/helpers.cljc | 8 +- frontend/src/app/main/data/workspace.cljs | 6 - frontend/src/app/main/ui/shapes/frame.cljs | 137 +++++++++--------- .../src/app/main/ui/workspace/shapes.cljs | 25 ++-- .../app/main/ui/workspace/shapes/frame.cljs | 12 +- .../shapes/frame/dynamic_modifiers.cljs | 59 ++++---- .../ui/workspace/shapes/frame/node_store.cljs | 6 +- .../shapes/frame/thumbnail_render.cljs | 18 ++- 10 files changed, 137 insertions(+), 138 deletions(-) diff --git a/common/deps.edn b/common/deps.edn index b267ccefcf..370e6d2d9c 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -22,7 +22,7 @@ java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/promesa {:mvn/version "8.0.450"} - funcool/cuerdas {:mvn/version "2022.06.13-401"} + funcool/cuerdas {:mvn/version "2022.06.16-403"} lambdaisland/uri {:mvn/version "1.13.95" :exclusions [org.clojure/data.json]} diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 06dd1975cd..787e17c74d 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -223,7 +223,7 @@ (defn close-artboard [file] (assert (nil? (:current-component-id file))) - (let [parent-id (peek (get file :parent-stack)) + (let [parent-id (-> file :parent-id peek) parent (lookup-shape file parent-id) current-frame-id (or (:frame-id parent) root-frame)] (-> file diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index fcbd882bbb..a3f48bf16a 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -223,8 +223,14 @@ (loop [parents-a (rest parents-a) parents-b (rest parents-b) base uuid/zero] - (if (not= (first parents-a) (first parents-b)) + (cond + (not= (first parents-a) (first parents-b)) [base (first parents-a) (first parents-b)] + + (or (empty? parents-a) (empty? parents-b)) + [uuid/zero (first parents-a) (first parents-b)] + + :else (recur (rest parents-a) (rest parents-b) (first parents-a)))) index-base-a (when base-child-a (get-position-on-parent objects base-child-a)) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5bbd56046e..c8ac153de1 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1214,12 +1214,6 @@ ;; selected and its parents objects (cph/selected-subtree objects selected) - ;;z-index (cp/calculate-z-index objects) - ;;z-values (->> selected - ;; (map #(vector % - ;; (+ (get z-index %) - ;; (get z-index (get-in objects [% :frame-id])))))) - selected (->> (cph/sort-z-index objects selected) (into (d/ordered-set)))] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 8fb3772df5..1aa7a7b5b7 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -42,86 +42,81 @@ [:> :path props] [:> :rect props])]))) +;; Wrapper around the frame that will handle things such as strokes and other properties +;; we wrap the proper frames and also the thumbnails +(mf/defc frame-container + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + children (obj/get props "children") + + {:keys [x y width height show-content]} shape + transform (gsh/transform-str shape) + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x x + :y y + :transform transform + :width width + :height height + :className "frame-background"})) + path? (some? (.-d props)) + render-id (mf/use-ctx muc/render-ctx)] + + [:* + [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id))} + (when (not show-content) + [:& frame-clip-def {:shape shape :render-id render-id}]) + + [:& shape-fills {:shape shape} + (if path? + [:> :path props] + [:> :rect props])] + + children] + + [:& shape-strokes {:shape shape} + (if path? + [:> :path props] + [:> :rect props])]])) + + +(mf/defc frame-thumbnail-image + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + bounds (or (obj/get props "bounds") (gsh/points->selrect (:points shape)))] + + (when (:thumbnail shape) + [:image.frame-thumbnail + {:id (dm/str "thumbnail-" (:id shape)) + :href (:thumbnail shape) + :x (:x bounds) + :y (:y bounds) + :width (:width bounds) + :height (:height bounds) + ;; DEBUG + :style {:filter (when (debug? :thumbnails) "sepia(1)")}}]))) + (mf/defc frame-thumbnail {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") - bounds (or (obj/get props "bounds") (:selrect shape))] + (let [shape (obj/get props "shape")] (when (:thumbnail shape) - (let [{:keys [x y width height show-content]} shape - transform (gsh/transform-str shape) - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props)) - render-id (mf/use-ctx muc/render-ctx)] - - [:* - [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id))} - (when (not show-content) - [:& frame-clip-def {:shape shape :render-id render-id}]) - [:& shape-fills {:shape shape} - (if path? - [:> :path props] - [:> :rect props])] - - [:image.frame-thumbnail - {:id (dm/str "thumbnail-" (:id shape)) - :href (:thumbnail shape) - :x (:x bounds) - :y (:y bounds) - :width (:width bounds) - :height (:height bounds) - ;; DEBUG - :style {:filter (when (debug? :thumbnails) "sepia(1)")}}]] - - [:& shape-strokes {:shape shape} - (if path? - [:> :path props] - [:> :rect props])]])))) + [:> frame-container props + [:> frame-thumbnail-image props]]))) (defn frame-shape [shape-wrapper] (mf/fnc frame-shape {::mf/wrap-props false} [props] - (let [childs (unchecked-get props "childs") - shape (unchecked-get props "shape") - {:keys [x y width height show-content]} shape - transform (gsh/transform-str shape) - - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props)) - render-id (mf/use-ctx muc/render-ctx)] - - [:* - [:g {:clip-path (when (not show-content) - (frame-clip-url shape render-id))} - [:& shape-fills {:shape shape} - (if path? - [:> :path props] - [:> :rect props])] - - [:g.frame-children - (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])]] - - [:& shape-strokes {:shape shape} - (if path? - [:> :path props] - [:> :rect props])]]))) + (let [childs (unchecked-get props "childs")] + [:> frame-container props + [:g.frame-children + (for [item childs] + [:& shape-wrapper {:key (dm/str (:id item)) :shape item}])]]))) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 166aba242a..293492c83e 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -87,21 +87,20 @@ opts #js {:shape shape :thumbnail? thumbnail?}] (when (and (some? shape) (not (:hidden shape))) - [:* - (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] - :bool [:> bool-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] + :bool [:> bool-wrapper opts] - ;; Only used when drawing a new frame. - :frame [:> frame-wrapper opts] + ;; Only used when drawing a new frame. + :frame [:> frame-wrapper opts] - nil)]))) + nil)))) (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index deb213d491..3e33494ac7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.fonts :as fonts] @@ -78,8 +77,6 @@ ;; If the current shape is root we handle its thumbnail and the dynamic modifiers (let [thumbnail? (unchecked-get props "thumbnail?") - - render-id (mf/use-memo #(str (uuid/next))) fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects)) fonts (-> fonts (hooks/use-equal-memo)) @@ -96,7 +93,7 @@ [on-load-frame-dom render-frame? thumbnail-renderer] (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render) - [on-frame-load in-memory?] + on-frame-load (fns/use-node-store thumbnail? node-ref rendered? render-frame?)] (mf/use-effect @@ -128,7 +125,7 @@ @node-ref) (when (not @rendered?) (reset! rendered? true))))) - [:& (mf/provider ctx/render-ctx) {:value render-id} + [:* [:g.frame-container {:id (dm/str "frame-container-" (:id shape)) :key "frame-container" :ref on-frame-load @@ -137,5 +134,6 @@ [:g.frame-thumbnail-wrapper {:id (dm/str "thumbnail-container-" (:id shape)) ;; Hide the thumbnail when not displaying - :opacity (when (and @rendered? (not thumbnail?) (not render-frame?) (not in-memory?)) 0)} - thumbnail-renderer]]])))))) + :opacity (when (and @rendered? (not thumbnail?) (not render-frame?)) 0)} + [:& shape-container {:shape shape} + thumbnail-renderer]]]])))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 0ce9b0a6a2..dc46bf2412 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -78,40 +78,41 @@ (defn get-nodes "Retrieve the DOM nodes to apply the matrix transformation" [base-node {:keys [id type masked-group?] :as shape}] - (let [shape-node (if (= (.-id base-node) (dm/str "shape-" id)) - base-node - (dom/query base-node (dm/str "#shape-" id))) + (when (some? base-node) + (let [shape-node (if (= (.-id base-node) (dm/str "shape-" id)) + base-node + (dom/query base-node (dm/str "#shape-" id))) - frame? (= :frame type) - group? (= :group type) - text? (= :text type) - mask? (and group? masked-group?)] - (cond - frame? - [shape-node - (dom/query shape-node ".frame-children") - (dom/query (dm/str "#thumbnail-container-" id)) - (dom/query (dm/str "#thumbnail-" id)) - (dom/query (dm/str "#frame-title-" id))] + frame? (= :frame type) + group? (= :group type) + text? (= :text type) + mask? (and group? masked-group?)] + (cond + frame? + [shape-node + (dom/query shape-node ".frame-children") + (dom/query (dm/str "#thumbnail-container-" id)) + (dom/query (dm/str "#thumbnail-" id)) + (dom/query (dm/str "#frame-title-" id))] - ;; For groups we don't want to transform the whole group but only - ;; its filters/masks - mask? - [(dom/query shape-node ".mask-clip-path") - (dom/query shape-node ".mask-shape")] + ;; For groups we don't want to transform the whole group but only + ;; its filters/masks + mask? + [(dom/query shape-node ".mask-clip-path") + (dom/query shape-node ".mask-shape")] - group? - (let [shape-defs (dom/query shape-node "defs")] - (d/concat-vec - (dom/query-all shape-defs ".svg-def") - (dom/query-all shape-defs ".svg-mask-wrapper"))) + group? + (let [shape-defs (dom/query shape-node "defs")] + (d/concat-vec + (dom/query-all shape-defs ".svg-def") + (dom/query-all shape-defs ".svg-mask-wrapper"))) - text? - [shape-node - (dom/query shape-node ".text-container")] + text? + [shape-node + (dom/query shape-node ".text-container")] - :else - [shape-node]))) + :else + [shape-node])))) (defn transform-region! [node modifiers] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs index 832795711d..8eef40f24a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs @@ -15,7 +15,7 @@ [thumbnail? node-ref rendered? render-frame?] (let [;; when `true` the node is in memory - in-memory? (mf/use-var true) + in-memory? (mf/use-state true) ;; State just for re-rendering re-render (mf/use-state 0) @@ -32,7 +32,7 @@ (reset! parent-ref node) (swap! re-render inc)))))] - (mf/use-effect + (mf/use-layout-effect (mf/deps thumbnail? render-frame?) (fn [] (when (and (some? @parent-ref) (some? @node-ref) @rendered? (and thumbnail? (not render-frame?))) @@ -43,4 +43,4 @@ (.appendChild @parent-ref @node-ref) (reset! in-memory? false)))) - [on-frame-load @in-memory?])) + on-frame-load)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index 44b5c27366..0358603438 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -214,13 +214,19 @@ [on-load-frame-dom @render-frame? (mf/html - [:* + [:& frame/frame-container {:bounds shape-bb + :shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))} + (when @show-frame-thumbnail - [:> frame/frame-thumbnail {:key (dm/str (:id shape)) - :bounds shape-bb - :shape (cond-> shape - (some? thumbnail-data) - (assoc :thumbnail thumbnail-data))}]) + [:> frame/frame-thumbnail-image + {:key (dm/str (:id shape)) + :bounds shape-bb + :shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))}]) + [:foreignObject {:x x :y y :width width :height height} [:canvas.thumbnail-canvas From a774f4d4fa2b12c28153b15f57a7b74f735b6c2f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 16 Jun 2022 17:48:50 +0200 Subject: [PATCH 33/86] :sparkles: Fix guides, grids and constraints for nested frames --- common/src/app/common/pages/helpers.cljc | 5 + .../app/main/data/workspace/transforms.cljs | 116 ++++++++++-------- .../app/main/ui/workspace/context_menu.cljs | 18 ++- .../sidebar/options/shapes/frame.cljs | 4 + .../ui/workspace/viewport/frame_grid.cljs | 13 +- .../main/ui/workspace/viewport/guides.cljs | 6 +- frontend/src/app/util/geom/snap_points.cljs | 18 ++- frontend/src/app/util/snap_data.cljs | 48 ++++---- 8 files changed, 135 insertions(+), 93 deletions(-) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index a3f48bf16a..111eb6194c 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.math :as mth] [app.common.spec :as us] [app.common.spec.page :as spec.page] [app.common.uuid :as uuid] @@ -718,3 +719,7 @@ (defn update-object-indices [file page-id] (update-in file [:pages-index page-id :objects] update-page-index)) + +(defn rotated-frame? + [frame] + (not (mth/almost-zero? (:rotation frame 0)))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 89bd283015..5b043d8b6e 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -181,50 +181,58 @@ (assoc :grow-type :fixed)))) (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) - ids-with-children (into (vec ids) (mapcat #(cph/get-children-ids objects %)) ids) - object-modifiers (get state :workspace-modifiers) - shapes (map (d/getf objects) ids) - ignore-tree (->> (map #(get-ignore-tree object-modifiers objects %) shapes) - (reduce merge {}))] + ([ids] + (apply-modifiers ids nil)) - (rx/of (dwu/start-undo-transaction) - (dwg/move-frame-guides ids-with-children) - (dch/update-shapes - ids-with-children - (fn [shape] - (let [modif (get object-modifiers (:id shape)) - text-shape? (cph/text-shape? shape)] - (-> shape - (merge modif) - (gsh/transform-shape) - (cond-> text-shape? - (update-grow-type 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 - :position-data - :flip-x - :flip-y - :grow-type]}) - (clear-local-transform) - (dwu/commit-undo-transaction)))))) + ([ids {:keys [undo-transation?] :or {undo-transation? true}}] + (us/verify (s/coll-of uuid?) ids) + (ptk/reify ::apply-modifiers + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + ids-with-children (into (vec ids) (mapcat #(cph/get-children-ids objects %)) ids) + object-modifiers (get state :workspace-modifiers) + shapes (map (d/getf objects) ids) + ignore-tree (->> (map #(get-ignore-tree object-modifiers objects %) shapes) + (reduce merge {}))] + + (rx/concat + (if undo-transation? + (rx/of (dwu/start-undo-transaction)) + (rx/empty)) + (rx/of (dwg/move-frame-guides ids-with-children) + (dch/update-shapes + ids-with-children + (fn [shape] + (let [modif (get object-modifiers (:id shape)) + text-shape? (cph/text-shape? shape)] + (-> shape + (merge modif) + (gsh/transform-shape) + (cond-> text-shape? + (update-grow-type 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 + :position-data + :flip-x + :flip-y + :grow-type]}) + (clear-local-transform)) + (if undo-transation? + (rx/of (dwu/commit-undo-transaction)) + (rx/empty)))))))) (defn- check-delta "If the shape is a component instance, check its relative position respect the @@ -762,9 +770,11 @@ (rx/map (partial set-modifiers ids)) (rx/take-until stopper)) - (rx/of (calculate-frame-for-move ids) - (apply-modifiers ids) - (finish-transform))))))))) + (rx/of (dwu/start-undo-transaction) + (calculate-frame-for-move ids) + (apply-modifiers ids {:undo-transation? false}) + (finish-transform) + (dwu/commit-undo-transaction))))))))) (s/def ::direction #{:up :down :right :left}) @@ -842,6 +852,14 @@ (rx/of (set-modifiers [id] {:displacement displ} false true) (apply-modifiers [id])))))) +(defn check-frame-move? + [target-frame-id objects position shape] + + (let [current-frame (get objects (:frame-id shape))] + ;; If the current frame contains the point and it's a child of the target + (and (gsh/has-point? current-frame position) + (cph/is-child? objects target-frame-id (:id current-frame))))) + (defn- calculate-frame-for-move [ids] (ptk/reify ::calculate-frame-for-move @@ -855,16 +873,14 @@ moving-shapes (->> ids (cph/clean-loops objects) (keep #(get objects %)) - (remove #(= (:frame-id %) frame-id))) + (remove (partial check-frame-move? frame-id objects position))) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (pcb/change-parent frame-id moving-shapes))] (when-not (empty? changes) - (rx/of dwu/pop-undo-into-transaction - (dch/commit-changes changes) - (dwu/commit-undo-transaction) + (rx/of (dch/commit-changes changes) (dwc/expand-collapse frame-id))))))) (defn- get-displacement diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 669e5cf91d..0cfc56436d 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -191,7 +191,6 @@ has-group? (->> shapes (d/seek #(= :group (:type %)))) has-bool? (->> shapes (d/seek #(= :bool (:type %)))) has-mask? (->> shapes (d/seek :masked-group?)) - has-frame? (->> shapes (d/seek #(= :frame (:type %)))) is-group? (and single? has-group?) is-bool? (and single? has-bool?) @@ -207,10 +206,9 @@ :shortcut (sc/get-tooltip :ungroup) :on-click do-remove-group}]) - (when (not has-frame?) - [:& menu-entry {:title (tr "workspace.shape.menu.group") - :shortcut (sc/get-tooltip :group) - :on-click do-create-group}]) + [:& menu-entry {:title (tr "workspace.shape.menu.group") + :shortcut (sc/get-tooltip :group) + :on-click do-create-group}] (when (or multiple? (and is-group? (not has-mask?)) is-bool?) [:& menu-entry {:title (tr "workspace.shape.menu.mask") @@ -222,12 +220,10 @@ :shortcut (sc/get-tooltip :unmask) :on-click do-unmask-group}]) - (when (not has-frame?) - [:* - [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection") - :shortcut (sc/get-tooltip :artboard-selection) - :on-click do-create-artboard-from-selection}] - [:& menu-separator]])])) + [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection") + :shortcut (sc/get-tooltip :artboard-selection) + :on-click do-create-artboard-from-selection}] + [:& menu-separator]])) (mf/defc context-focus-mode-menu [{:keys []}] 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 79e1bb2688..13aa47a60e 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 @@ -8,6 +8,7 @@ (:require [app.main.constants :refer [has-layout-item]] [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-shape 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]] @@ -25,6 +26,7 @@ stroke-values (select-keys shape stroke-attrs) layer-values (select-keys shape layer-attrs) measure-values (select-keys shape measure-attrs) + constraint-values (select-keys shape constraint-attrs) layout-values (select-keys shape layout-attrs) layout-item-values (select-keys shape layout-item-attrs)] [:* @@ -32,6 +34,8 @@ :values measure-values :type type :shape shape}] + [:& constraints-menu {:ids ids + :values constraint-values}] (when has-layout-item [:& layout-menu {:type type :ids [(:id shape)] :values layout-values}]) 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 013c1cee50..ece6b20b22 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.refs :as refs] [app.util.geom.grid :as gg] @@ -126,13 +127,15 @@ (mf/defc frame-grid {::mf/wrap [mf/memo]} [{:keys [zoom transform selected focus]}] - (let [frames (mf/deref refs/workspace-frames) - moving (when (= :move transform) selected) - is-moving? #(contains? moving (:id %))] + (let [frames (mf/deref refs/workspace-frames) + transforming (when (some? transform) selected) + is-transform? #(contains? transforming (:id %))] [:g.grid-display {:style {:pointer-events "none"}} - (for [frame (remove is-moving? frames)] - (when (or (empty? focus) (contains? focus (:id frame))) + (for [frame frames] + (when (and (not (is-transform? frame)) + (not (cph/rotated-frame? frame)) + (or (empty? focus) (contains? focus (:id frame)))) [:& grid-display-frame {:key (str "grid-" (:id frame)) :zoom zoom :frame (gsh/transform-shape frame)}]))])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 434458d31a..363feed7b1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.refs :as refs] @@ -286,8 +287,9 @@ guide-pill-corner-radius (/ guide-pill-corner-radius zoom)] (when (or (nil? frame) - (is-guide-inside-frame? (assoc guide :position pos) frame) - (:hover @state true)) + (and (is-guide-inside-frame? (assoc guide :position pos) frame) + (cph/root-frame? frame) + (not (cph/rotated-frame? frame)))) [:g.guide-area (when-not disabled-guides? (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index 17df6445a1..1940deaf79 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -7,7 +7,8 @@ (ns app.util.geom.snap-points (:require [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh])) + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph])) (defn selrect-snap-points [{:keys [x y width height] :as selrect}] #{(gpt/point x y) @@ -29,11 +30,20 @@ (when (and (not blocked) (not hidden)) (let [shape (gsh/transform-shape shape)] (case (:type shape) - :frame (-> shape :selrect frame-snap-points) + :frame (-> shape :points gsh/points->selrect frame-snap-points) (into #{(gsh/center-shape shape)} (:points shape)))))) (defn guide-snap-points - [guide] - (if (= :x (:axis guide)) + [guide frame] + + (cond + (and (some? frame) + (not (cph/rotated-frame? frame)) + (not (cph/root-frame? frame))) + #{} + + (= :x (:axis guide)) #{(gpt/point (:position guide) 0)} + + :else #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index 597e6ad4a1..81f60b9520 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -55,16 +55,18 @@ (defn get-grids-snap-points [frame coord] - (let [grid->snap (fn [[grid-type position]] - {:type :layout - :id (:id frame) - :grid grid-type - :pt position})] - (->> (:grids frame) - (mapcat (fn [grid] - (->> (gg/grid-snap-points frame grid coord) - (mapv #(vector (:type grid) %))))) - (mapv grid->snap)))) + (if (not (cph/rotated-frame? frame)) + [] + (let [grid->snap (fn [[grid-type position]] + {:type :layout + :id (:id frame) + :grid grid-type + :pt position})] + (->> (:grids frame) + (mapcat (fn [grid] + (->> (gg/grid-snap-points frame grid coord) + (mapv #(vector (:type grid) %))))) + (mapv grid->snap))))) (defn- add-frame [page-data frame] @@ -105,9 +107,10 @@ (defn- add-guide - [page-data guide] + [objects page-data guide] - (let [guide-data (->> (snap/guide-snap-points guide) + (let [frame (get objects (:frame-id guide)) + guide-data (->> (snap/guide-snap-points guide frame) (mapv #(array-map :type :guide :id (:id guide) @@ -178,10 +181,10 @@ (add-shape new-shape))) (defn- update-guide - [page-data [old-guide new-guide]] - (-> page-data - (remove-guide old-guide) - (add-guide new-guide))) + [objects page-data [old-guide new-guide]] + (as-> page-data $ + (remove-guide $ old-guide) + (add-guide objects $ new-guide))) ;; PUBLIC API @@ -203,7 +206,7 @@ (add-root-frame $) (reduce add-frame $ frames) (reduce add-shape $ shapes) - (reduce add-guide $ guides))] + (reduce (partial add-guide objects) $ guides))] (assoc snap-data (:id page) page-data))) (defn update-page @@ -214,7 +217,8 @@ ;; Update page (update snap-data (:id page) (fn [page-data] - (let [{:keys [change-frame-shapes + (let [{:keys [objects]} page + {:keys [change-frame-shapes change-frame-guides removed-frames removed-shapes @@ -235,10 +239,12 @@ (reduce update-shape $ updated-shapes) (reduce add-frame $ new-frames) (reduce add-shape $ new-shapes) - (reduce update-guide $ change-frame-guides) (reduce remove-guide $ removed-guides) - (reduce update-guide $ updated-guides) - (reduce add-guide $ new-guides))))) + + ;; Guides functions. Need objects to get its frame data + (reduce (partial update-guide objects) $ change-frame-guides) + (reduce (partial update-guide objects) $ updated-guides) + (reduce (partial add-guide objects) $ new-guides))))) ;; Page doesn't exist, we create a new entry (add-page snap-data page))) From 2799c09294ef5799f9bfc775bc52036ab42d6fb7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 16 Jun 2022 18:19:42 +0200 Subject: [PATCH 34/86] :sparkles: Fix interaction targets --- frontend/src/app/main/data/viewer.cljs | 7 +-- .../app/main/data/workspace/interactions.cljs | 52 +++++++++---------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 52990d5da5..de9e178eb9 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -384,7 +384,9 @@ (rx/of (rt/nav screen pparams (assoc qparams :index index))))))) (defn go-to-frame - ([frame-id] (go-to-frame frame-id nil)) + ([frame-id] + (go-to-frame frame-id nil)) + ([frame-id animation] (us/verify ::us/uuid frame-id) (us/verify (s/nilable ::cti/animation) animation) @@ -414,8 +416,7 @@ page-id (:page-id qparams) frames (get-in state [:viewer :pages page-id :frames]) index (d/index-of-pred frames #(= (:id %) frame-id))] - (when index - (rx/of (go-to-frame-by-index index)))))))) + (rx/of (go-to-frame-by-index (or index 0)))))))) (defn go-to-frame-auto [] diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 200c359089..d84b9769ed 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -171,25 +171,30 @@ (rx/map #(move-edit-interaction initial-pos %))) (rx/of (finish-edit-interaction index initial-pos)))))))) + +(defn get-target-frame + [state position] + + (let [objects (wsh/lookup-page-objects state) + from-id (-> state wsh/lookup-selected first) + from-shape (wsh/lookup-shape state from-id) + + from-frame-id (if (cph/frame-shape? from-shape) + from-id (:frame-id from-shape)) + + target-frame (cph/frame-by-position objects position)] + + (when (and (not= (:id target-frame) uuid/zero) + (not= (:id target-frame) from-frame-id) + (not (:hide-in-viewer target-frame))) + target-frame))) + (defn move-edit-interaction - [initial-pos position] + [_initial-pos position] (ptk/reify ::move-edit-interaction ptk/UpdateEvent (update [_ state] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected-shape-id (-> state wsh/lookup-selected first) - selected-shape (get objects selected-shape-id) - selected-shape-frame-id (:frame-id selected-shape) - start-frame (get objects selected-shape-frame-id) - end-frame (cph/frame-by-position objects position) - - position (when (not= position initial-pos) position) - end-frame (when (and (not= (:id end-frame) uuid/zero ) - (not= (:id end-frame) (:id start-frame)) - (not= (:id end-frame) selected-shape-id) - (not (:hide-in-viewer end-frame))) - end-frame)] + (let [end-frame (get-target-frame state position)] (-> state (assoc-in [:workspace-local :draw-interaction-to] position) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) @@ -207,18 +212,9 @@ ptk/WatchEvent (watch [_ state _] (let [position @ms/mouse-position - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - target-frame (cph/frame-by-position objects position) - + target-frame (get-target-frame state position) shape-id (-> state wsh/lookup-selected first) - shape (get objects shape-id) - - invalid-target? (or (nil? target-frame) - (= (:id target-frame) uuid/zero) - (= (:id target-frame) (:id shape)) - (= (:id target-frame) (:frame-id shape)) - (:hide-in-viewer target-frame)) + shape (wsh/lookup-shape state shape-id) change-interaction (fn [interaction] @@ -236,11 +232,11 @@ (= position initial-pos) ;; New interaction but invalid target - (and (nil? index) invalid-target?)) + (and (nil? index) (nil? target-frame))) nil ;; Dropped interaction in an invalid target. We remove it - (and (some? index) invalid-target?) + (and (some? index) (nil? target-frame)) (rx/of (remove-interaction shape index)) (nil? index) From 45b690ed05924b74ceee3f0564ceee4570da003b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 17 Jun 2022 12:50:58 +0200 Subject: [PATCH 35/86] :sparkles: Fix shadows and thumbnails --- backend/src/app/rpc/queries/files.clj | 2 +- common/src/app/common/pages/helpers.cljc | 4 ++++ frontend/src/app/main/render.cljs | 17 +++++++++------ frontend/src/app/main/ui/shapes/shape.cljs | 21 +++++++++++-------- frontend/src/app/main/ui/viewer.cljs | 9 ++++---- frontend/src/app/main/ui/viewer/comments.cljs | 3 +-- .../src/app/main/ui/viewer/thumbnails.cljs | 16 ++++++++------ .../app/main/ui/workspace/shapes/frame.cljs | 7 +++---- .../shapes/frame/thumbnail_render.cljs | 2 +- 9 files changed, 47 insertions(+), 34 deletions(-) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 40886f3e1f..946a7902f8 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -315,7 +315,7 @@ bounds (when (:show-content frame) - (gsh/selection-rect (->> children-ids (map (d/getf objects))))) + (gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects)))))) frame (cond-> frame diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 111eb6194c..3a7c418240 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -52,6 +52,10 @@ [{:keys [type]}] (= type :image)) +(defn svg-raw-shape? + [{:keys [type]}] + (= type :svg-raw)) + (defn unframed-shape? "Checks if it's a non-frame shape in the top level." [shape] diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 61a4fc00fc..ba5d938ccb 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -203,12 +203,17 @@ (update :width + (* 2 (:horizontal padding))) (update :height + (* 2 (:vertical padding))))] - (if (cph/group-shape? object) - (if (:masked-group? object) - (get-object-bounds objects (-> object :shapes first)) - (->> (:shapes object) - (into [bounds] (map (partial get-object-bounds objects))) - (gsh/join-rects))) + (cond + (and (cph/group-shape? object) (:masked-group? object)) + (get-object-bounds objects (-> object :shapes first)) + + (or (cph/group-shape? object) + (and (cph/frame-shape? object) (:show-content object))) + (->> (:shapes object) + (into [bounds] (map (partial get-object-bounds objects))) + (gsh/join-rects)) + + :else bounds))) (mf/defc page-svg diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index c22da29d89..902dc6727a 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.shape (:require [app.common.data :as d] + [app.common.pages.helpers :as cph] [app.common.data.macros :as dm] [app.common.uuid :as uuid] [app.main.ui.context :as muc] @@ -48,9 +49,10 @@ {::mf/forward-ref true ::mf/wrap-props false} [props ref] - (let [shape (obj/get props "shape") - children (obj/get props "children") - pointer-events (obj/get props "pointer-events") + (let [shape (obj/get props "shape") + children (obj/get props "children") + pointer-events (obj/get props "pointer-events") + disable-shadows? (obj/get props "disable-shadows?") type (:type shape) render-id (mf/use-memo #(str (uuid/next))) @@ -72,15 +74,16 @@ (obj/set! "id" (dm/fmt "shape-%" (:id shape))) (obj/set! "style" styles)) - wrapper-props - (cond-> wrapper-props - (some #(= (:type shape) %) [:group :svg-raw :frame]) - (obj/set! "filter" (filters/filter-str filter-id shape))) - wrapper-props (cond-> wrapper-props (= :group type) - (attrs/add-style-attrs shape render-id)) + (attrs/add-style-attrs shape render-id) + + (and (or (cph/group-shape? shape) + (cph/frame-shape? shape) + (cph/svg-raw-shape? shape)) + (not disable-shadows?)) + (obj/set! "filter" (filters/filter-str filter-id shape))) svg-group? (and (contains? shape :svg-attrs) (= :group type)) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 45eb9b7c12..cb4c97e058 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -86,7 +86,7 @@ (mf/defc viewer-wrapper [{:keys [wrapper-size scroll orig-frame orig-viewport-ref orig-size page file users current-viewport-ref - size frame interactions-mode overlays zoom close-overlay section index] :as props}] + size frame interactions-mode overlays zoom close-overlay section index children-bounds] :as props}] (let [{clist :list} (mf/deref refs/comments-local) show-comments-list (and (= section :comments) (= :show clist))] [:* @@ -173,8 +173,6 @@ :page page :zoom zoom}])]]]])) - - (mf/defc viewer [{:keys [params data]}] @@ -407,7 +405,7 @@ :file file :section section :local local - :size size} + :size size :index index :viewer-pagination viewer-pagination}] @@ -428,7 +426,8 @@ :overlays overlays :zoom zoom :section section - :index index}]))]]])) + :index index + :children-bounds children-bounds}]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 8ce744c9ee..bc69fd66f4 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -164,7 +164,6 @@ :on-submit on-draft-submit :zoom zoom}])]]])) - (mf/defc comments-sidebar [{:keys [users frame page]}] (let [profile (mf/deref refs/profile) @@ -173,7 +172,7 @@ threads (->> (vals threads-map) (dcm/apply-filters cstate profile) (filter (fn [{:keys [position]}] - (frame-contains? frame position))))] + (gsh/has-point? frame position))))] [:aside.settings-bar.settings-bar-right.comments-right-sidebar [:div.settings-bar-inside [:& wc/comments-sidebar {:users users :threads threads :page-id (:id page)}]]])) diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 54c90d6364..54fb855bc8 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.viewer.thumbnails (:require + [app.common.pages.helpers :as cph] + [app.common.geom.shapes :as gsh] [app.common.data :as d] [app.common.data.macros :as dm] [app.main.data.viewer :as dv] @@ -76,12 +78,14 @@ {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [{:keys [selected? frame on-click index objects]}] - [:div.thumbnail-item {:on-click #(on-click % index)} - [:div.thumbnail-preview - {:class (dom/classnames :selected selected?)} - [:& render/frame-svg {:frame frame :objects objects :show-thumbnails? true}]] - [:div.thumbnail-info - [:span.name {:title (:name frame)} (:name frame)]]]) + (let [children-ids (cph/get-children-ids objects (:id frame)) + children-bounds (gsh/selection-rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] + [:div.thumbnail-item {:on-click #(on-click % index)} + [:div.thumbnail-preview + {:class (dom/classnames :selected selected?)} + [:& render/frame-svg {:frame (assoc frame :children-bounds children-bounds) :objects objects :show-thumbnails? true}]] + [:div.thumbnail-info + [:span.name {:title (:name frame)} (:name frame)]]])) (mf/defc thumbnails-panel [{:keys [frames page index show?] :as props}] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 3e33494ac7..420eaaa442 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -40,7 +40,7 @@ childs (mf/deref childs-ref)] [:& (mf/provider embed/context) {:value true} - [:& shape-container {:shape shape :ref ref} + [:& shape-container {:shape shape :ref ref :disable-shadows? true} [:& frame-shape {:shape shape :childs childs} ]]])))) (defn check-props @@ -125,7 +125,7 @@ @node-ref) (when (not @rendered?) (reset! rendered? true))))) - [:* + [:& shape-container {:shape shape} [:g.frame-container {:id (dm/str "frame-container-" (:id shape)) :key "frame-container" :ref on-frame-load @@ -135,5 +135,4 @@ {:id (dm/str "thumbnail-container-" (:id shape)) ;; Hide the thumbnail when not displaying :opacity (when (and @rendered? (not thumbnail?) (not render-frame?)) 0)} - [:& shape-container {:shape shape} - thumbnail-renderer]]]])))))) + thumbnail-renderer]]])))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index 0358603438..9a3f0d0782 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -68,7 +68,7 @@ {:keys [x y width height] :as shape-bb} (if (:show-content shape) - (gsh/selection-rect all-children) + (gsh/selection-rect (concat [shape] all-children)) (-> shape :points gsh/points->selrect)) fixed-width (mth/clamp width 250 2000) From 1bde183c504d6bc655dffe2a9c3f0ff1baef30d5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 17 Jun 2022 14:24:37 +0200 Subject: [PATCH 36/86] :bug: Fix thumbnails in viewer thumbnails --- backend/src/app/rpc/queries/viewer.clj | 4 +++- frontend/src/app/main/refs.cljs | 15 +++++++++------ frontend/src/app/main/ui/viewer.cljs | 3 ++- frontend/src/app/main/ui/viewer/thumbnails.cljs | 15 ++++++++++----- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 27b040a768..b98de0a381 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -53,8 +53,10 @@ [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] (p/let [slink (slnk/retrieve-share-link pool file-id share-id) perms (files/get-permissions pool profile-id file-id share-id) + thumbs (files/retrieve-object-thumbnails cfg file-id) bundle (p/-> (retrieve-bundle cfg file-id) - (assoc :permissions perms))] + (assoc :permissions perms) + (assoc-in [:file :thumbnails] thumbs))] ;; When we have neither profile nor share, we just return a not ;; found response to the user. diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 114ac67444..3e709aea15 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -360,15 +360,18 @@ ;; ---- Viewer refs -(def viewer-file - (l/derived :viewer-file st/state)) - -(def viewer-project - (l/derived :viewer-file st/state)) - (def viewer-data (l/derived :viewer st/state)) +(def viewer-file + (l/derived :file viewer-data)) + +(def viewer-thumbnails + (l/derived :thumbnails viewer-file)) + +(def viewer-project + (l/derived :project viewer-data)) + (def viewer-state (l/derived :viewer st/state)) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index cb4c97e058..0fde2d1986 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -383,7 +383,8 @@ [:& thumbnails-panel {:frames frames :show? (:show-thumbnails local false) :page page - :index index}] + :index index + :thumbnail-data (:thumbnails file)}] [:section.viewer-section {:id "viewer-section" :ref viewer-section-ref :class (if fullscreen? "fullscreen" "")} diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 54fb855bc8..b0b6828ba0 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -77,23 +77,26 @@ (mf/defc thumbnail-item {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} - [{:keys [selected? frame on-click index objects]}] + [{:keys [selected? frame on-click index objects page-id thumbnail-data]}] + (let [children-ids (cph/get-children-ids objects (:id frame)) children-bounds (gsh/selection-rect (concat [frame] (->> children-ids (keep (d/getf objects)))))] [:div.thumbnail-item {:on-click #(on-click % index)} [:div.thumbnail-preview {:class (dom/classnames :selected selected?)} - [:& render/frame-svg {:frame (assoc frame :children-bounds children-bounds) :objects objects :show-thumbnails? true}]] + [:& render/frame-svg {:frame (-> frame + (assoc :thumbnail (get thumbnail-data (dm/str page-id (:id frame))))) + :objects objects + :show-thumbnails? true}]] [:div.thumbnail-info [:span.name {:title (:name frame)} (:name frame)]]])) (mf/defc thumbnails-panel - [{:keys [frames page index show?] :as props}] + [{:keys [frames page index show? thumbnail-data] :as props}] (let [expanded? (mf/use-state false) container (mf/use-ref) objects (:objects page) - on-close #(st/emit! dv/toggle-thumbnails-panel) selected (mf/use-var false) @@ -121,6 +124,8 @@ [:& thumbnail-item {:index i :key (dm/str (:id frame) "-" i) :frame frame + :page-id (:id page) :objects objects :on-click on-item-click - :selected? (= i index)}])]])) + :selected? (= i index) + :thumbnail-data thumbnail-data}])]])) From e638475a67c1c0209ba58a0254c769d1b4609b4d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 17 Jun 2022 14:24:54 +0200 Subject: [PATCH 37/86] :sparkles: Handoff handling nested frames --- frontend/src/app/main/render.cljs | 46 ++++++++++++------- frontend/src/app/main/ui/shapes/shape.cljs | 2 +- .../app/main/ui/viewer/handoff/render.cljs | 16 ++++--- .../src/app/main/ui/viewer/thumbnails.cljs | 7 +-- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index ba5d938ccb..d53c44ba67 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -277,18 +277,33 @@ (gpt/negate) (gmt/translate-matrix))) + children-ids + (cph/get-children-ids objects frame-id) + objects (mf/with-memo [frame-id objects modifier] (let [update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)] - (->> (cph/get-children-ids objects frame-id) + (->> children-ids (into [frame-id]) (reduce update-fn objects)))) frame (mf/with-memo [modifier] - (assoc-in frame [:modifiers :displacement] modifier)) + (-> frame + (assoc-in [:modifiers :displacement] modifier) + (gsh/transform-shape))) - wrapper + bounds + (if (:show-content frame) + (gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects))))) + (-> frame :points gsh/points->rect)) + + frame + (cond-> frame + (and (some? bounds) (nil? (:children-bounds bounds))) + (assoc :children-bounds bounds)) + + frame-wrapper (mf/with-memo [objects] (frame-wrapper-factory objects)) @@ -296,21 +311,18 @@ height (* (:height bounds) zoom) vbox (format-viewbox {:width (:width bounds 0) :height (:height bounds 0)})] - [:svg {:view-box vbox - :width (ust/format-precision width viewbox-decimal-precision) - :height (ust/format-precision height viewbox-decimal-precision) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") - :fill "none"} - (if (or (not show-thumbnails?) (nil? (:thumbnail frame))) - [:& wrapper {:shape frame :view-box vbox}] + [:& (mf/provider muc/render-thumbnails) {:value show-thumbnails?} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") + :fill "none"} - ;; Render the frame thumbnail - (let [frame (gsh/transform-shape frame)] - [:> shape-container {:shape frame} - [:& frame/frame-thumbnail {:shape frame :bounds (assoc bounds :x 0 :y 0)}]]))])) + [:> shape-container {:shape frame} + [:& frame-wrapper {:shape frame :view-box vbox}]]]])) ;; Component for rendering a thumbnail of a single componenent. Mainly diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 902dc6727a..820b627466 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -7,8 +7,8 @@ (ns app.main.ui.shapes.shape (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] [app.common.data.macros :as dm] + [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 6fb837fd52..3cfb179bf3 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -30,24 +30,26 @@ (declare shape-container-factory) (defn handle-hover-shape - [{:keys [type id]} hover?] + [shape hover?] (fn [event] - (when-not (#{:group :frame} type) + (when-not (or (cph/group-shape? shape) + (cph/root-frame? shape)) (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (dv/hover-shape id hover?))))) + (st/emit! (dv/hover-shape (:id shape) hover?))))) -(defn select-shape [{:keys [type id]}] +(defn select-shape [shape] (fn [event] - (when-not (#{:group :frame} type) + (when-not (or (cph/group-shape? shape) + (cph/root-frame? shape)) (dom/stop-propagation event) (dom/prevent-default event) (cond (.-shiftKey ^js event) - (st/emit! (dv/toggle-selection id)) + (st/emit! (dv/toggle-selection (:id shape))) :else - (st/emit! (dv/select-shape id)))))) + (st/emit! (dv/select-shape (:id shape))))))) (defn shape-wrapper-factory [component] diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index b0b6828ba0..afd76945f7 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -6,10 +6,10 @@ (ns app.main.ui.viewer.thumbnails (:require - [app.common.pages.helpers :as cph] - [app.common.geom.shapes :as gsh] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] [app.main.data.viewer :as dv] [app.main.render :as render] [app.main.store :as st] @@ -85,7 +85,8 @@ [:div.thumbnail-preview {:class (dom/classnames :selected selected?)} [:& render/frame-svg {:frame (-> frame - (assoc :thumbnail (get thumbnail-data (dm/str page-id (:id frame))))) + (assoc :thumbnail (get thumbnail-data (dm/str page-id (:id frame)))) + (assoc :children-bounds children-bounds)) :objects objects :show-thumbnails? true}]] [:div.thumbnail-info From 3a55f07f4564a6495c6243f698c837193fa32179 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 20 Jun 2022 14:17:31 +0200 Subject: [PATCH 38/86] :bug: Remove duplicate work on storing already existing files in storage --- backend/src/app/storage.clj | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 10e289b7e4..e96cd7f7ff 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -84,13 +84,14 @@ " and backend = ?" " and deleted_at is null" " limit 1")] - (db/exec-one! conn [sql hash bucket (name backend)]))) + (some-> (db/exec-one! conn [sql hash bucket (name backend)]) + (update :metadata db/decode-transit-pgobject)))) (defn- create-database-object [{:keys [conn backend executor]} {:keys [::content ::expired-at ::touched-at] :as params}] (us/assert ::storage-content content) (px/with-dispatch executor - (let [id (uuid/random) + (let [id (uuid/next) mdata (cond-> (get-metadata params) (satisfies? impl/IContentHash content) @@ -106,13 +107,15 @@ (get-database-object-by-hash conn backend (:bucket mdata) (:hash mdata))) result (or result - (db/insert! conn :storage-object - {:id id - :size (count content) - :backend (name backend) - :metadata (db/tjson mdata) - :deleted-at expired-at - :touched-at touched-at}))] + (-> (db/insert! conn :storage-object + {:id id + :size (count content) + :backend (name backend) + :metadata (db/tjson mdata) + :deleted-at expired-at + :touched-at touched-at}) + (update :metadata db/decode-transit-pgobject) + (update :metadata assoc ::created? true)))] (StorageObject. (:id result) (:size result) @@ -120,7 +123,7 @@ (:deleted-at result) (:touched-at result) backend - mdata + (:metadata result) nil)))) (def ^:private sql:retrieve-storage-object @@ -173,9 +176,10 @@ (p/let [storage (assoc storage :conn (or conn pool)) object (create-database-object storage params)] - ;; Store the data finally on the underlying storage subsystem. - (-> (impl/resolve-backend storage backend) - (impl/put-object object content)) + (when (::created? (meta object)) + ;; Store the data finally on the underlying storage subsystem. + (-> (impl/resolve-backend storage backend) + (impl/put-object object content))) object)) From 53df0f7585d388311c2de05b317eb9935c51e030 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 21 Jun 2022 11:10:05 +0200 Subject: [PATCH 39/86] On view mode only show arrows on hover --- frontend/resources/styles/main/partials/viewer.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index cc4b727301..0a774646f6 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -27,9 +27,10 @@ height: 100%; display: flex; align-items: center; + width: 53px; .arrow { - display: flex; + display: none; align-items: center; justify-content: center; border-radius: 12px; @@ -49,6 +50,10 @@ fill: $color-black; } } + + &:hover .arrow { + display: flex; + } } & .viewer-go-next { From 8e60834292474735a689ccece5b578a8536ed110 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 21 Jun 2022 22:18:18 +0200 Subject: [PATCH 40/86] :sparkles: Fix shadows in frames for dashboard and viewer --- common/src/app/common/geom/shapes.cljc | 7 - common/src/app/common/geom/shapes/bounds.cljc | 151 ++++++++++++++++++ common/src/app/common/pages/helpers.cljc | 5 +- exporter/src/app/renderer/svg.cljs | 1 - .../main/partials/viewer-thumbnails.scss | 1 + frontend/src/app/main/render.cljs | 123 ++++---------- .../src/app/main/ui/shapes/custom_stroke.cljs | 3 +- frontend/src/app/main/ui/shapes/filters.cljs | 115 +------------ frontend/src/app/main/ui/shapes/svg_defs.cljs | 4 +- frontend/src/app/main/ui/viewer.cljs | 41 ++--- frontend/src/app/render.cljs | 22 +-- 11 files changed, 215 insertions(+), 258 deletions(-) create mode 100644 common/src/app/common/geom/shapes/bounds.cljc diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 6f054ab838..28bc5dcef0 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -98,13 +98,6 @@ (defn distance-shapes [shape other] (distance-selrect (:selrect shape) (:selrect other))) -(defn shape-stroke-margin - [shape stroke-width] - (if (= (:type shape) :path) - ;; TODO: Calculate with the stroke offset (not implemented yet - (mth/sqrt (* 2 stroke-width stroke-width)) - (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) - (defn close-attrs? "Compares two shapes attributes to see if they are equal or almost equal (in case of numeric). Takes into account attributes that are diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc new file mode 100644 index 0000000000..012183e9ba --- /dev/null +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -0,0 +1,151 @@ +(ns app.common.geom.shapes.bounds + (:require + [app.common.data :as d] + [app.common.geom.shapes.rect :as gsr] + [app.common.math :as mth] + [app.common.pages.helpers :as cph])) + +(defn shape-stroke-margin + [shape stroke-width] + (if (= (:type shape) :path) + ;; TODO: Calculate with the stroke offset (not implemented yet + (mth/sqrt (* 2 stroke-width stroke-width)) + (- (mth/sqrt (* 2 stroke-width stroke-width)) stroke-width))) + +(defn blur-filters [type value] + (->> [value] + (remove :hidden) + (filter #(= (:type %) type)) + (map #(hash-map :id (str "filter_" (:id %)) + :type (:type %) + :params %)))) + +(defn shadow-filters [type filters] + (->> filters + (remove :hidden) + (filter #(= (:style %) type)) + (map #(hash-map :id (str "filter_" (:id %)) + :type (:style %) + :params %)))) + +(defn shape->filters + [shape] + (d/concat-vec + [{:id "BackgroundImageFix" :type :image-fix}] + + ;; Background blur won't work in current SVG specification + ;; We can revisit this in the future + #_(->> shape :blur (blur-filters :background-blur)) + + (->> shape :shadow (shadow-filters :drop-shadow)) + [{:id "shape" :type :blend-filters}] + (->> shape :shadow (shadow-filters :inner-shadow)) + (->> shape :blur (blur-filters :layer-blur)))) + +(defn calculate-filter-bounds [{:keys [x y width height]} filter-entry] + (let [{:keys [offset-x offset-y blur spread] :or {offset-x 0 offset-y 0 blur 0 spread 0}} (:params filter-entry) + filter-x (min x (+ x offset-x (- spread) (- blur) -5)) + filter-y (min y (+ y offset-y (- spread) (- blur) -5)) + filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) + filter-height (+ height (mth/abs offset-y) (* spread 2) (* blur 2) 10)] + (gsr/make-selrect filter-x filter-y filter-width filter-height))) + +(defn get-rect-filter-bounds + [selrect filters blur-value] + (let [filter-bounds (->> filters + (filter #(= :drop-shadow (:type %))) + (map (partial calculate-filter-bounds selrect)) + (concat [selrect]) + (gsr/join-selrects)) + delta-blur (* blur-value 2) + + result + (-> filter-bounds + (update :x - delta-blur) + (update :y - delta-blur) + (update :x1 - delta-blur) + (update :x1 - delta-blur) + (update :x2 + delta-blur) + (update :y2 + delta-blur) + (update :width + (* delta-blur 2)) + (update :height + (* delta-blur 2)))] + + result)) + +(defn get-shape-filter-bounds + ([shape] + (let [svg-root? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag])))] + (if svg-root? + (:selrect shape) + + (let [filters (shape->filters shape) + blur-value (or (-> shape :blur :value) 0)] + (get-rect-filter-bounds (-> shape :points gsr/points->selrect) filters blur-value)))))) + +(defn calculate-padding + ([shape] + (calculate-padding shape false)) + + ([shape ignore-margin?] + (let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) + :center (/ (:stroke-width % 0) 2) + :outer (:stroke-width % 0) + 0) (:strokes shape))) + + margin (if ignore-margin? + 0 + (apply max 0 (map #(shape-stroke-margin % stroke-width) (:strokes shape)))) + + shadow-width (apply max 0 (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0) (:shadow shape))) + + shadow-height (apply max 0 (map #(case (:style % :drop-shadow) + :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) + 0) (:shadow shape)))] + + {:horizontal (+ stroke-width margin shadow-width) + :vertical (+ stroke-width margin shadow-height)}))) + +(defn- add-padding + [bounds padding] + (-> bounds + (update :x - (:horizontal padding)) + (update :y - (:vertical padding)) + (update :width + (* 2 (:horizontal padding))) + (update :height + (* 2 (:vertical padding))))) + +(defn get-object-bounds + [objects shape] + + (let [calculate-base-bounds + (fn [shape] + (-> (get-shape-filter-bounds shape) + (add-padding (calculate-padding shape true)))) + + bounds + (cph/reduce-objects + objects + (fn [shape] + (and (d/not-empty? (:shapes shape)) + (or (not (cph/frame-shape? shape)) + (:show-content shape)) + + (or (not (cph/group-shape? shape)) + (not (:masked-group? shape))))) + + (:id shape) + + (fn [result shape] + (conj result (get-object-bounds objects shape))) + + [(calculate-base-bounds shape)]) + + + children-bounds (or (:children-bounds shape) (gsr/join-selrects bounds)) + + filters (shape->filters shape) + blur-value (or (-> shape :blur :value) 0)] + + (get-rect-filter-bounds children-bounds filters blur-value))) + diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 3a7c418240..1a6d12f2c4 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -657,7 +657,10 @@ (reduce-objects objects nil reducer-fn init-val)) ([objects check-children? reducer-fn init-val] - (let [root-children (get-in objects [uuid/zero :shapes])] + (reduce-objects objects check-children? uuid/zero reducer-fn init-val)) + + ([objects check-children? root-id reducer-fn init-val] + (let [root-children (get-in objects [root-id :shapes])] (if (empty? root-children) init-val diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 36923bab99..825c51ad91 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -359,7 +359,6 @@ (p/let [params {:file-id file-id :page-id page-id - :render-texts true :render-embed true :object-id (mapv :id objects) :route "objects"} diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss index 03b60ae12a..8d9067ed07 100644 --- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -147,6 +147,7 @@ height: 120px; border: 1px solid $color-gray-20; border-radius: 2px; + padding: 4px; display: flex; justify-content: center; diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index d53c44ba67..2914629274 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -14,11 +14,11 @@ (:require ["react-dom/server" :as rds] [app.common.colors :as clr] - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bounds :as gsb] [app.common.math :as mth] [app.common.pages.helpers :as cph] [app.config :as cfg] @@ -28,7 +28,6 @@ [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as export] - [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] @@ -61,13 +60,11 @@ (defn- calculate-dimensions [objects] - (let [rect + (let [bounds (->> (cph/get-root-objects objects) - (map #(if (some? (:children-bounds %)) - (:children-bounds %) - (gsh/points->selrect (:points %)))) - (gsh/join-selrects))] - (-> rect + (map (partial gsb/get-object-bounds objects)) + (gsh/join-rects))] + (-> bounds (update :x mth/finite 0) (update :y mth/finite 0) (update :width mth/finite 100000) @@ -193,36 +190,12 @@ (reduce updt-fn objects mod-ids))) -(defn get-object-bounds - [objects object-id] - (let [object (get objects object-id) - padding (filters/calculate-padding object true) - bounds (-> (filters/get-filters-bounds object) - (update :x - (:horizontal padding)) - (update :y - (:vertical padding)) - (update :width + (* 2 (:horizontal padding))) - (update :height + (* 2 (:vertical padding))))] - - (cond - (and (cph/group-shape? object) (:masked-group? object)) - (get-object-bounds objects (-> object :shapes first)) - - (or (cph/group-shape? object) - (and (cph/frame-shape? object) (:show-content object))) - (->> (:shapes object) - (into [bounds] (map (partial get-object-bounds objects))) - (gsh/join-rects)) - - :else - bounds))) - (mf/defc page-svg {::mf/wrap [mf/memo]} [{:keys [data thumbnails? render-embed? include-metadata?] :as props :or {render-embed? false include-metadata? false}}] (let [objects (:objects data) shapes (cph/get-immediate-children objects) - dim (calculate-dimensions objects) vbox (format-viewbox dim) bgcolor (dm/get-in data [:options :background] default-color) @@ -248,7 +221,6 @@ (when include-metadata? [:& export/export-page {:options (:options data)}]) - (let [shapes (->> shapes (remove cph/frame-shape?) (mapcat #(cph/get-children-with-self objects (:id %)))) @@ -262,20 +234,20 @@ ;; Component that serves for render frame thumbnails, mainly used in ;; the viewer and handoff - (mf/defc frame-svg {::mf/wrap [mf/memo]} [{:keys [objects frame zoom show-thumbnails?] :or {zoom 1} :as props}] (let [frame-id (:id frame) include-metadata? (mf/use-ctx export/include-metadata-ctx) - bounds (or (:children-bounds frame) (gsh/points->rect (:points frame))) + bounds (gsb/get-object-bounds objects frame) - modifier - (mf/with-memo [(:x bounds) (:y bounds)] - (-> (gpt/point (:x bounds) (:y bounds)) - (gpt/negate) - (gmt/translate-matrix))) + ;; Bounds without shadows/blur will be the bounds of the thumbnail + bounds2 (gsb/get-object-bounds objects (dissoc frame :shadow :blur)) + + delta-bounds (gpt/point (:x bounds) (:y bounds)) + + modifier (gmt/translate-matrix (gpt/negate delta-bounds)) children-ids (cph/get-children-ids objects frame-id) @@ -293,19 +265,19 @@ (assoc-in [:modifiers :displacement] modifier) (gsh/transform-shape))) - bounds - (if (:show-content frame) - (gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects))))) - (-> frame :points gsh/points->rect)) - frame (cond-> frame (and (some? bounds) (nil? (:children-bounds bounds))) - (assoc :children-bounds bounds)) + (assoc :children-bounds bounds2)) - frame-wrapper - (mf/with-memo [objects] - (frame-wrapper-factory objects)) + frame (-> frame + (update-in [:children-bounds :x] - (:x delta-bounds)) + (update-in [:children-bounds :y] - (:y delta-bounds))) + + shape-wrapper + (mf/use-memo + (mf/deps objects) + #(shape-wrapper-factory objects)) width (* (:width bounds) zoom) height (* (:height bounds) zoom) @@ -321,13 +293,11 @@ :xmlns:penpot (when include-metadata? "https://penpot.app/xmlns") :fill "none"} - [:> shape-container {:shape frame} - [:& frame-wrapper {:shape frame :view-box vbox}]]]])) + [:& shape-wrapper {:shape frame}]]])) ;; Component for rendering a thumbnail of a single componenent. Mainly ;; used to render thumbnails on assets panel. - (mf/defc component-svg {::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [{:keys [objects group zoom] :or {zoom 1} :as props}] @@ -375,7 +345,7 @@ (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object-id render-texts? render-embed?] + [{:keys [objects object-id render-embed?] :or {render-embed? false} :as props}] (let [object (get objects object-id) @@ -383,30 +353,21 @@ (:hide-fill-on-export object) (assoc :fills [])) - {:keys [x y width height]} (get-object-bounds objects object-id) - vbox (dm/str x " " y " " width " " height) - frame-wrapper - (mf/with-memo [objects] - (frame-wrapper-factory objects)) - - group-wrapper - (mf/with-memo [objects] - (group-wrapper-factory objects)) + bounds (gsb/get-object-bounds objects object) + vbox (format-viewbox bounds) + fonts (ff/shape->fonts object objects) shape-wrapper (mf/with-memo [objects] - (shape-wrapper-factory objects)) - - text-shapes (sequence (filter cph/text-shape?) (vals objects)) - render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] + (shape-wrapper-factory objects))] [:& (mf/provider export/include-metadata-ctx) {:value false} [:& (mf/provider embed/context) {:value render-embed?} [:svg {:id (dm/str "screenshot-" object-id) :view-box vbox - :width width - :height height + :width (:width bounds) + :height (:height bounds) :version "1.1" :xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" @@ -415,30 +376,8 @@ :style {:-webkit-print-color-adjust :exact} :fill "none"} - (let [fonts (ff/shape->fonts object objects)] - [:& ff/fontfaces-style {:fonts fonts}]) - - (case (:type object) - :frame [:> shape-container {:shape object} - [:& frame-wrapper {:shape object :view-box vbox}]] - :group [:> shape-container {:shape object} - [:& group-wrapper {:shape object}]] - [:& shape-wrapper {:shape object}])] - - ;; Auxiliary SVG for rendering text-shapes - (when render-texts? - (for [object text-shapes] - [:& (mf/provider muc/text-plain-colors-ctx) {:value true} - [:svg - {:id (dm/str "screenshot-text-" (:id object)) - :view-box (dm/str "0 0 " (:width object) " " (:height object)) - :width (:width object) - :height (:height object) - :version "1.1" - :xmlns "http://www.w3.org/2000/svg" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :fill "none"} - [:& shape-wrapper {:shape (assoc object :x 0 :y 0)}]]]))]])) + [:& ff/fontfaces-style {:fonts fonts}] + [:& shape-wrapper {:shape object}]]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SPRITES (DEBUG) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 775a2eb6d6..c0eb8ddf40 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bounds :as gsb] [app.common.pages.helpers :as cph] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] @@ -45,7 +46,7 @@ :center (/ (:stroke-width shape 0) 2) :outer (:stroke-width shape 0) 0) - margin (gsh/shape-stroke-margin shape stroke-width) + margin (gsb/shape-stroke-margin shape stroke-width) bounding-box (-> (gsh/points->selrect (:points shape)) (update :x - (+ stroke-width margin)) (update :y - (+ stroke-width margin)) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index c3b9574019..a9cc7a264f 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -8,8 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.geom.shapes :as gsh] - [app.common.math :as mth] + [app.common.geom.shapes.bounds :as gsb] [app.common.uuid :as uuid] [app.util.color :as color] [cuerdas.core :as str] @@ -108,34 +107,6 @@ :in2 filter-in :result filter-id}]) -(defn filter-bounds [shape filter-entry] - (let [{:keys [x y width height]} (:selrect shape) - {:keys [offset-x offset-y blur spread] :or {offset-x 0 offset-y 0 blur 0 spread 0}} (:params filter-entry) - filter-x (min x (+ x offset-x (- spread) (- blur) -5)) - filter-y (min y (+ y offset-y (- spread) (- blur) -5)) - filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10) - filter-height (+ height (mth/abs offset-y) (* spread 2) (* blur 2) 10)] - {:x1 filter-x - :y1 filter-y - :x2 (+ filter-x filter-width) - :y2 (+ filter-y filter-height)})) - -(defn blur-filters [type value] - (->> [value] - (remove :hidden) - (filter #(= (:type %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:type %) - :params %)))) - -(defn shadow-filters [type filters] - (->> filters - (remove :hidden) - (filter #(= (:style %) type)) - (map #(hash-map :id (str "filter_" (:id %)) - :type (:style %) - :params %)))) - (mf/defc filter-entry [{:keys [entry]}] (let [props #js {:filter-id (:id entry) :filter-in (:filter-in entry) @@ -148,84 +119,6 @@ :image-fix [:> image-fix-filter props] :blend-filters [:> blend-filters props]))) -(defn shape->filters - [shape] - (d/concat-vec - [{:id "BackgroundImageFix" :type :image-fix}] - - ;; Background blur won't work in current SVG specification - ;; We can revisit this in the future - #_(->> shape :blur (blur-filters :background-blur)) - - (->> shape :shadow (shadow-filters :drop-shadow)) - [{:id "shape" :type :blend-filters}] - (->> shape :shadow (shadow-filters :inner-shadow)) - (->> shape :blur (blur-filters :layer-blur)))) - -(defn get-filters-bounds - ([shape] - (let [filters (shape->filters shape) - blur-value (or (-> shape :blur :value) 0)] - (get-filters-bounds shape filters blur-value))) - - ([shape filters blur-value] - - (let [svg-root? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag]))) - {:keys [x y width height]} (:selrect shape)] - (if svg-root? - ;; When is a raw-svg but not the root we use the whole svg as bound for the filter. Is the maximum - ;; we're allowed to display - {:x x :y y :width width :height height} - - ;; Otherwise we calculate the bound - (let [filter-bounds (->> filters - (filter #(= :drop-shadow (:type %))) - (map (partial filter-bounds shape))) - ;; We add the selrect so the minimum size will be the selrect - filter-bounds (conj filter-bounds (-> shape :points gsh/points->selrect)) - - x1 (apply min (map :x1 filter-bounds)) - y1 (apply min (map :y1 filter-bounds)) - x2 (apply max (map :x2 filter-bounds)) - y2 (apply max (map :y2 filter-bounds)) - - x1 (- x1 (* blur-value 2)) - x2 (+ x2 (* blur-value 2)) - y1 (- y1 (* blur-value 2)) - y2 (+ y2 (* blur-value 2))] - - ;; We should move the frame filter coordinates because they should be - ;; relative with the frame. By default they come as absolute - {:x x1 - :y y1 - :width (- x2 x1) - :height (- y2 y1)}))))) - -(defn calculate-padding - ([shape] - (calculate-padding shape false)) - - ([shape ignore-margin?] - (let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) - :center (/ (:stroke-width % 0) 2) - :outer (:stroke-width % 0) - 0) (:strokes shape))) - - margin (if ignore-margin? - 0 - (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape)))) - - shadow-width (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-x %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape))) - - shadow-height (apply max 0 (map #(case (:style % :drop-shadow) - :drop-shadow (+ (mth/abs (:offset-y %)) (* (:spread %) 2) (* (:blur %) 2) 10) - 0) (:shadow shape)))] - - {:horizontal (+ stroke-width margin shadow-width) - :vertical (+ stroke-width margin shadow-height)}))) - (defn change-filter-in "Adds the previous filter as `filter-in` parameter" [filters] @@ -234,9 +127,9 @@ (mf/defc filters [{:keys [filter-id shape]}] - (let [filters (-> shape shape->filters change-filter-in) - bounds (get-filters-bounds shape filters (or (-> shape :blur :value) 0)) - padding (calculate-padding shape) + (let [filters (-> shape gsb/shape->filters change-filter-in) + bounds (gsb/get-rect-filter-bounds (:selrect shape) filters (or (-> shape :blur :value) 0)) + padding (gsb/calculate-padding shape) selrect (:selrect shape) filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect)) filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect)) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index d46007c9f2..7f69b9b262 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -10,7 +10,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] - [app.main.ui.shapes.filters :as f] + [app.common.geom.shapes.bounds :as gsb] [app.util.svg :as usvg] [rumext.alpha :as mf])) @@ -87,7 +87,7 @@ (d/parse-double (get-in svg-def [:attrs :width])) (d/parse-double (get-in svg-def [:attrs :height]))) (gsh/transform-rect transform)) - (f/get-filters-bounds shape)))) + (gsb/get-shape-filter-bounds shape)))) (mf/defc svg-defs [{:keys [shape render-id]}] (let [svg-defs (:svg-defs shape) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 0fde2d1986..7849c7bc45 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -11,7 +11,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.bounds :as gsb] [app.common.pages.helpers :as cph] [app.common.text :as txt] [app.main.data.comments :as dcm] @@ -23,7 +23,6 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] - [app.main.ui.shapes.filters :as filters] [app.main.ui.share-link] [app.main.ui.static :as static] [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] @@ -39,16 +38,8 @@ [rumext.alpha :as mf])) (defn- calculate-size - [frame zoom bounds] - (let [frame-bounds (filters/get-filters-bounds frame) - {:keys [x y width height]} (if (:show-content frame) - (gsh/join-rects [bounds frame-bounds]) - frame-bounds) - padding (filters/calculate-padding frame) - x (- x (:horizontal padding)) - y (- y (:vertical padding)) - width (+ width (* 2 (:horizontal padding))) - height (+ height (* 2 (:vertical padding)))] + [objects frame zoom] + (let [{:keys [x y width height]} (gsb/get-object-bounds objects frame)] {:base-width width :base-height height :x x @@ -83,10 +74,9 @@ [:div.counter (str/join " / " [(+ index 1) num-frames])] [:span]]]) - (mf/defc viewer-wrapper [{:keys [wrapper-size scroll orig-frame orig-viewport-ref orig-size page file users current-viewport-ref - size frame interactions-mode overlays zoom close-overlay section index children-bounds] :as props}] + size frame interactions-mode overlays zoom close-overlay section index] :as props}] (let [{clist :list} (mf/deref refs/comments-local) show-comments-list (and (= section :comments) (= :show clist))] [:* @@ -135,7 +125,7 @@ :interactions-mode interactions-mode}] (for [overlay overlays] - (let [size-over (calculate-size (:frame overlay) zoom children-bounds)] + (let [size-over (calculate-size (:objects page) (:frame overlay) zoom)] [:* (when (or (:close-click-outside overlay) (:background-overlay overlay)) @@ -204,12 +194,6 @@ frames (:frames page) frame (get frames index) - children-bounds - (mf/use-memo - (mf/deps page (:id frame)) - #(-> (cph/get-children (:objects page) (:id frame)) - (gsh/selection-rect))) - fullscreen? (mf/deref refs/viewer-fullscreen?) overlays (:overlays local) scroll (mf/use-state nil) @@ -219,13 +203,13 @@ (d/seek #(= (:id %) (:orig-frame-id current-animation)) frames)) size (mf/use-memo - (mf/deps frame zoom children-bounds) - (fn [] (calculate-size frame zoom children-bounds))) + (mf/deps frame zoom) + (fn [] (calculate-size (:objects page) frame zoom))) orig-size (mf/use-memo (mf/deps orig-frame zoom) (fn [] (when orig-frame - (calculate-size orig-frame zoom children-bounds)))) + (calculate-size (:objects page) orig-frame zoom)))) wrapper-size (mf/use-memo (mf/deps size orig-size zoom) @@ -318,7 +302,7 @@ wrapper-size))))) (mf/use-layout-effect - (mf/deps current-animation children-bounds) + (mf/deps current-animation) (fn [] ;; Overlay animations may be started when needed. (when current-animation @@ -328,7 +312,7 @@ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation)))) overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation)) overlays) - overlay-size (calculate-size (:frame overlay) zoom children-bounds) + overlay-size (calculate-size (:objects page) (:frame overlay) zoom) overlay-position {:x (* (:x (:position overlay)) zoom) :y (* (:y (:position overlay)) zoom)}] (interactions/animate-open-overlay @@ -342,7 +326,7 @@ (let [overlay-viewport (dom/get-element (str "overlay-" (str (:overlay-id current-animation)))) overlay (d/seek #(= (:id (:frame %)) (:overlay-id current-animation)) overlays) - overlay-size (calculate-size (:frame overlay) zoom children-bounds) + overlay-size (calculate-size (:objects page) (:frame overlay) zoom) overlay-position {:x (* (:x (:position overlay)) zoom) :y (* (:y (:position overlay)) zoom)}] (interactions/animate-close-overlay @@ -427,8 +411,7 @@ :overlays overlays :zoom zoom :section section - :index index - :children-bounds children-bounds}]))]]])) + :index index}]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 5d921f0a72..872e0f2476 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -98,7 +98,7 @@ state)) (mf/defc object-svg - [{:keys [page-id file-id object-id render-embed? render-texts?]}] + [{:keys [page-id file-id object-id render-embed?]}] (let [fetch-state (mf/use-fn (mf/deps file-id page-id object-id) (fn [] @@ -131,11 +131,10 @@ [:& render/object-svg {:objects objects :object-id object-id - :render-embed? render-embed? - :render-texts? render-texts?}]))) + :render-embed? render-embed?}]))) (mf/defc objects-svg - [{:keys [page-id file-id object-ids render-embed? render-texts?]}] + [{:keys [page-id file-id object-ids render-embed?]}] (let [fetch-state (mf/use-fn (mf/deps file-id page-id) (fn [] @@ -157,27 +156,24 @@ {:objects objects :key (str object-id) :object-id object-id - :render-embed? render-embed? - :render-texts? render-texts?}]))))) + :render-embed? render-embed?}]))))) (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::object-id (s/or :single ::us/uuid :multiple (s/coll-of ::us/uuid))) -(s/def ::render-text ::us/boolean) (s/def ::embed ::us/boolean) (s/def ::render-objects (s/keys :req-un [::file-id ::page-id ::object-id] - :opt-un [::render-text ::render-embed])) + :opt-un [::render-embed])) (defn- render-objects [params] (let [{:keys [file-id page-id - render-embed - render-texts] + render-embed] :as params} (us/conform ::render-objects params) @@ -190,8 +186,7 @@ {:file-id file-id :page-id page-id :object-id object-id - :render-embed? render-embed - :render-texts? render-texts}]) + :render-embed? render-embed}]) :multiple (mf/html @@ -199,8 +194,7 @@ {:file-id file-id :page-id page-id :object-ids (into #{} object-id) - :render-embed? render-embed - :render-texts? render-texts}])))) + :render-embed? render-embed}])))) ;; ---- COMPONENTS SPRITE From ebcb385593a1710a71ea34fa846ede4a8d5e5f94 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Jun 2022 11:34:36 +0200 Subject: [PATCH 41/86] :recycle: Minor refactor on storages Fix many issues on FS & S3 backend; removes the unused and broken DB backend. Normalize operations on bytes and byte streams on a separated namespace: app.util.bytes --- backend/deps.edn | 3 +- backend/src/app/main.clj | 22 ++---- backend/src/app/storage.clj | 10 ++- backend/src/app/storage/db.clj | 67 ----------------- backend/src/app/storage/fs.clj | 9 ++- backend/src/app/storage/impl.clj | 119 +++++++++++++------------------ backend/src/app/storage/s3.clj | 58 +++++++++++---- backend/src/app/storage/tmp.clj | 83 +++++++++++++++++++++ backend/src/app/util/bytes.clj | 110 ++++++++++++++++++++++++++++ 9 files changed, 302 insertions(+), 179 deletions(-) delete mode 100644 backend/src/app/storage/db.clj create mode 100644 backend/src/app/storage/tmp.clj create mode 100644 backend/src/app/util/bytes.clj diff --git a/backend/deps.edn b/backend/deps.edn index 1ceba4b524..77e325ba4f 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -28,7 +28,8 @@ metosin/reitit-core {:mvn/version "0.5.18"} org.postgresql/postgresql {:mvn/version "42.4.0"} com.zaxxer/HikariCP {:mvn/version "5.0.1"} - funcool/datoteka {:mvn/version "2.0.0"} + + funcool/datoteka {:mvn/version "3.0.64"} buddy/buddy-hashers {:mvn/version "1.8.158"} buddy/buddy-sign {:mvn/version "3.4.333"} diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 9fe00155ed..7d547976aa 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -71,6 +71,10 @@ :app.tokens/tokens {:keys (ig/ref :app.setup/keys)} + :app.storage.tmp/cleaner + {:executor (ig/ref [::worker :app.worker/executor]) + :scheduler (ig/ref :app.worker/scheduler)} + :app.storage/gc-deleted-task {:pool (ig/ref :app.db/pool) :storage (ig/ref :app.storage/storage) @@ -336,23 +340,12 @@ :backends {:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) - :assets-db (ig/ref [::assets :app.storage.db/backend]) :assets-fs (ig/ref [::assets :app.storage.fs/backend]) - :tmp (ig/ref [::tmp :app.storage.fs/backend]) - :fdata-s3 (ig/ref [::fdata :app.storage.s3/backend]) - ;; keep this for backward compatibility :s3 (ig/ref [::assets :app.storage.s3/backend]) :fs (ig/ref [::assets :app.storage.fs/backend])}} - [::fdata :app.storage.s3/backend] - {:region (cf/get :storage-fdata-s3-region) - :bucket (cf/get :storage-fdata-s3-bucket) - :endpoint (cf/get :storage-fdata-s3-endpoint) - :prefix (cf/get :storage-fdata-s3-prefix) - :executor (ig/ref [::default :app.worker/executor])} - [::assets :app.storage.s3/backend] {:region (cf/get :storage-assets-s3-region) :endpoint (cf/get :storage-assets-s3-endpoint) @@ -361,12 +354,7 @@ [::assets :app.storage.fs/backend] {:directory (cf/get :storage-assets-fs-directory)} - - [::tmp :app.storage.fs/backend] - {:directory "/tmp/penpot"} - - [::assets :app.storage.db/backend] - {:pool (ig/ref :app.db/pool)}}) + }) (def system nil) diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index e96cd7f7ff..1a54a08b76 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -14,7 +14,6 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] - [app.storage.db :as sdb] [app.storage.fs :as sfs] [app.storage.impl :as impl] [app.storage.s3 :as ss3] @@ -32,14 +31,12 @@ (s/def ::s3 ::ss3/backend) (s/def ::fs ::sfs/backend) -(s/def ::db ::sdb/backend) (s/def ::backends (s/map-of ::us/keyword (s/nilable (s/or :s3 ::ss3/backend - :fs ::sfs/backend - :db ::sdb/backend)))) + :fs ::sfs/backend)))) (defmethod ig/pre-init-spec ::storage [_] (s/keys :req-un [::db/pool ::wrk/executor ::backends])) @@ -109,7 +106,7 @@ result (or result (-> (db/insert! conn :storage-object {:id id - :size (count content) + :size (impl/get-size content) :backend (name backend) :metadata (db/tjson mdata) :deleted-at expired-at @@ -263,7 +260,8 @@ ;; A task responsible to permanently delete already marked as deleted ;; storage files. The storage objects are practically never marked to ;; be deleted directly by the api call. The touched-gc is responsible -;; of collecting the usage of the object and mark it as deleted. +;; of collecting the usage of the object and mark it as deleted. Only +;; the TMP files are are created with expiration date in future. (declare sql:retrieve-deleted-objects-chunk) diff --git a/backend/src/app/storage/db.clj b/backend/src/app/storage/db.clj deleted file mode 100644 index 4ccbf74800..0000000000 --- a/backend/src/app/storage/db.clj +++ /dev/null @@ -1,67 +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.storage.db - (:require - [app.common.spec :as us] - [app.db :as db] - [app.storage.impl :as impl] - [clojure.spec.alpha :as s] - [integrant.core :as ig] - [promesa.exec :as px]) - (:import - java.io.ByteArrayInputStream)) - -;; --- BACKEND INIT - -(defmethod ig/pre-init-spec ::backend [_] - (s/keys :opt-un [::db/pool])) - -(defmethod ig/init-key ::backend - [_ cfg] - (assoc cfg :type :db)) - -(s/def ::type ::us/keyword) -(s/def ::backend - (s/keys :req-un [::type ::db/pool])) - -;; --- API IMPL - -(defmethod impl/put-object :db - [{:keys [conn executor] :as storage} {:keys [id] :as object} content] - (px/with-dispatch executor - (let [data (impl/slurp-bytes content)] - (db/insert! conn :storage-data {:id id :data data}) - object))) - -(defmethod impl/get-object-data :db - [{:keys [conn executor] :as backend} {:keys [id] :as object}] - (px/with-dispatch executor - (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 executor] :as backend} {:keys [id] :as object}] - (px/with-dispatch executor - (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 2b56549a79..4feaaf6242 100644 --- a/backend/src/app/storage/fs.clj +++ b/backend/src/app/storage/fs.clj @@ -10,11 +10,13 @@ [app.common.spec :as us] [app.common.uri :as u] [app.storage.impl :as impl] + [app.util.bytes :as bs] [clojure.java.io :as io] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs] [integrant.core :as ig] + [promesa.core :as p] [promesa.exec :as px]) (:import java.io.InputStream @@ -72,9 +74,10 @@ (io/input-stream full)))) (defmethod impl/get-object-bytes :fs - [{:keys [executor] :as backend} object] - (px/with-dispatch executor - (fs/slurp-bytes (impl/get-object-data backend object)))) + [backend object] + (p/let [input (impl/get-object-data backend object)] + (ex/with-always (bs/close! input) + (bs/read-as-bytes input)))) (defmethod impl/get-object-url :fs [{:keys [uri executor] :as backend} {:keys [id] :as object} _] diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index c5623dd5a8..bca9b5e200 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -9,18 +9,15 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.uuid :as uuid] + [app.util.bytes :as bs] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] [clojure.java.io :as io]) (:import java.nio.ByteBuffer - java.util.UUID - java.io.ByteArrayInputStream - java.io.InputStream java.nio.file.Files - org.apache.commons.io.input.BoundedInputStream - )) + java.nio.file.Path + java.util.UUID)) ;; --- API Definition @@ -95,23 +92,23 @@ (defn coerce-id [id] (cond - (string? id) (uuid/uuid id) - (uuid? id) id - :else (ex/raise :type :internal - :code :invalid-id-type - :hint "id should be string or uuid"))) + (string? id) (parse-uuid id) + (uuid? id) id + :else (ex/raise :type :internal + :code :invalid-id-type + :hint "id should be string or uuid"))) (defprotocol IContentObject - (size [_] "get object size")) + (get-size [_] "get object size")) (defprotocol IContentHash (get-hash [_] "get precalculated hash")) -(defn- make-content - [^InputStream is ^long size] +(defn- path->content + [^Path path ^long size] (reify IContentObject - (size [_] size) + (get-size [_] size) io/IOFactory (make-reader [this opts] @@ -119,47 +116,53 @@ (make-writer [_ _] (throw (UnsupportedOperationException. "not implemented"))) (make-input-stream [_ _] - (doto (BoundedInputStream. is size) - (.setPropagateClose false))) + (-> (io/input-stream path) + (bs/bounded-input-stream size))) (make-output-stream [_ _] + (throw (UnsupportedOperationException. "not implemented"))))) + +(defn- bytes->content + [^bytes data ^long size] + (reify + IContentObject + (get-size [_] size) + + io/IOFactory + (make-reader [this opts] + (io/make-reader this opts)) + (make-writer [_ _] (throw (UnsupportedOperationException. "not implemented"))) - - clojure.lang.Counted - (count [_] size) - - java.lang.AutoCloseable - (close [_] - (.close is)))) + (make-input-stream [_ _] + (-> (bs/bytes-input-stream data) + (bs/bounded-input-stream size))) + (make-output-stream [_ _] + (throw (UnsupportedOperationException. "not implemented"))))) (defn content ([data] (content data nil)) ([data size] (cond (instance? java.nio.file.Path data) - (make-content (io/input-stream data) - (Files/size data)) + (path->content data (or size (Files/size data))) (instance? java.io.File data) - (content (.toPath ^java.io.File data) nil) + (content (.toPath ^java.io.File data) size) (instance? String data) - (let [data (.getBytes data "UTF-8") - bais (ByteArrayInputStream. ^bytes data)] - (make-content bais (alength data))) + (let [data (.getBytes data "UTF-8")] + (bytes->content data (alength data))) (bytes? data) - (let [size (alength ^bytes data) - bais (ByteArrayInputStream. ^bytes data)] - (make-content bais size)) + (bytes->content data (or size (alength ^bytes data))) - (instance? InputStream data) - (do - (when-not size - (throw (UnsupportedOperationException. "size should be provided on InputStream"))) - (make-content data size)) + ;; (instance? InputStream data) + ;; (do + ;; (when-not size + ;; (throw (UnsupportedOperationException. "size should be provided on InputStream"))) + ;; (make-content data size)) :else - (throw (UnsupportedOperationException. "type not supported"))))) + (throw (IllegalArgumentException. "invalid argument type"))))) (defn wrap-with-hash [content ^String hash] @@ -171,7 +174,7 @@ (reify IContentObject - (size [_] (size content)) + (get-size [_] (get-size content)) IContentHash (get-hash [_] hash) @@ -184,43 +187,17 @@ (make-input-stream [_ opts] (io/make-input-stream content opts)) (make-output-stream [_ opts] - (io/make-output-stream content opts)) - - clojure.lang.Counted - (count [_] (count content)) - - java.lang.AutoCloseable - (close [_] - (.close ^java.lang.AutoCloseable content)))) + (io/make-output-stream content opts)))) (defn content? [v] (satisfies? IContentObject v)) -(defn slurp-bytes - [content] - (with-open [input (io/input-stream content) - output (java.io.ByteArrayOutputStream. (count content))] - (io/copy input output) - (.toByteArray output))) - (defn calculate-hash - [path-or-stream] - (let [result (cond - (instance? InputStream path-or-stream) - (let [result (-> (bh/blake2b-256 path-or-stream) - (bc/bytes->hex))] - (.reset path-or-stream) - result) - - (string? path-or-stream) - (-> (bh/blake2b-256 path-or-stream) - (bc/bytes->hex)) - - :else - (with-open [is (io/input-stream path-or-stream)] - (-> (bh/blake2b-256 is) - (bc/bytes->hex))))] + [resource] + (let [result (with-open [input (io/input-stream resource)] + (-> (bh/blake2b-256 input) + (bc/bytes->hex)))] (str "blake2b:" result))) (defn resolve-backend diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index c5c4a68196..72480dd539 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -12,14 +12,17 @@ [app.common.spec :as us] [app.common.uri :as u] [app.storage.impl :as impl] + [app.storage.tmp :as tmp] [app.util.time :as dt] [app.worker :as wrk] [clojure.java.io :as io] [clojure.spec.alpha :as s] + [datoteka.core :as fs] [integrant.core :as ig] [promesa.core :as p] [promesa.exec :as px]) (:import + java.io.FilterInputStream java.io.InputStream java.nio.ByteBuffer java.time.Duration @@ -30,6 +33,7 @@ org.reactivestreams.Subscription software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.core.async.AsyncRequestBody + software.amazon.awssdk.core.async.AsyncResponseTransformer software.amazon.awssdk.core.client.config.ClientAsyncConfiguration software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient @@ -107,7 +111,16 @@ (defmethod impl/get-object-data :s3 [backend object] - (get-object-data backend object)) + (letfn [(no-such-key? [cause] + (instance? software.amazon.awssdk.services.s3.model.NoSuchKeyException cause)) + (handle-not-found [cause] + (ex/raise :type :not-found + :code :object-not-found + :hint "s3 object not found" + :cause cause))] + + (-> (get-object-data backend object) + (p/catch no-such-key? handle-not-found)))) (defmethod impl/get-object-bytes :s3 [backend object] @@ -204,7 +217,7 @@ (reify AsyncRequestBody (contentLength [_] - (Optional/of (long (count content)))) + (Optional/of (long (impl/get-size content)))) (^void subscribe [_ ^Subscriber s] (let [thread (Thread. #(writer-fn s))] @@ -216,7 +229,6 @@ (cancel [_] (.interrupt thread) (.release sem 1)) - (request [_ n] (.release sem (int n)))))))))) @@ -238,16 +250,31 @@ ^AsyncRequestBody content)))) (defn get-object-data - [{:keys [client bucket prefix]} {:keys [id]}] - (p/let [gor (.. (GetObjectRequest/builder) - (bucket bucket) - (key (str prefix (impl/id->path id))) - (build)) - obj (.getObject ^S3AsyncClient client ^GetObjectRequest gor) - ;; rsp (.response ^ResponseInputStream obj) - ;; len (.contentLength ^GetObjectResponse rsp) - ] - (io/input-stream obj))) + [{:keys [client bucket prefix]} {:keys [id size]}] + (let [gor (.. (GetObjectRequest/builder) + (bucket bucket) + (key (str prefix (impl/id->path id))) + (build))] + + ;; If the file size is greater than 2MiB then stream the content + ;; to the filesystem and then read with buffered inputstream; if + ;; not, read the contento into memory using bytearrays. + (if (> size (* 1024 1024 2)) + (p/let [path (tmp/tempfile :prefix "penpot.storage.s3.") + rxf (AsyncResponseTransformer/toFile path) + _ (.getObject ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf)] + (proxy [FilterInputStream] [(io/input-stream path)] + (close [] + (fs/delete path) + (proxy-super close)))) + + (p/let [rxf (AsyncResponseTransformer/toBytes) + obj (.getObject ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf)] + (.asInputStream ^ResponseBytes obj))))) (defn get-object-bytes [{:keys [client bucket prefix]} {:keys [id]}] @@ -255,7 +282,10 @@ (bucket bucket) (key (str prefix (impl/id->path id))) (build)) - obj (.getObjectAsBytes ^S3AsyncClient client ^GetObjectRequest gor)] + rxf (AsyncResponseTransformer/toBytes) + obj (.getObjectAsBytes ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf)] (.asByteArray ^ResponseBytes obj))) (def default-max-age diff --git a/backend/src/app/storage/tmp.clj b/backend/src/app/storage/tmp.clj new file mode 100644 index 0000000000..cdb1b0cc71 --- /dev/null +++ b/backend/src/app/storage/tmp.clj @@ -0,0 +1,83 @@ +;; 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.storage.tmp + "Temporal files service all created files will be tried to clean after + 1 hour afrer creation. This is a best effort, if this process fails, + the operating system cleaning task should be responsible of + permanently delete these files (look at systemd-tempfiles)." + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.util.time :as dt] + [app.worker :as wrk] + [clojure.core.async :as a] + [clojure.spec.alpha :as s] + [datoteka.core :as fs] + [integrant.core :as ig] + [promesa.exec :as px])) + +(declare remove-temp-file) +(defonce queue (a/chan 128)) + +(s/def ::min-age ::dt/duration) + +(defmethod ig/pre-init-spec ::cleaner [_] + (s/keys :req-un [::min-age ::wrk/scheduler ::wrk/executor])) + +(defmethod ig/prep-key ::cleaner + [_ cfg] + (merge {:min-age (dt/duration {:minutes 30})} + (d/without-nils cfg))) + +(defmethod ig/init-key ::cleaner + [_ {:keys [scheduler executor min-age] :as cfg}] + (l/info :hint "starting tempfile cleaner service") + (let [cch (a/chan)] + (a/go-loop [] + (let [[path port] (a/alts! [queue cch])] + (when (not= port cch) + (l/trace :hint "schedule tempfile deletion" :path path + :expires-at (dt/plus (dt/now) min-age)) + (px/schedule! scheduler + (inst-ms min-age) + (partial remove-temp-file executor path)) + (recur)))) + cch)) + +(defmethod ig/halt-key! ::cleaner + [_ close-ch] + (l/info :hint "stoping tempfile cleaner service") + (some-> close-ch a/close!)) + +(defn- remove-temp-file + "Permanently delete tempfile" + [executor path] + (px/with-dispatch executor + (l/trace :hint "permanently delete tempfile" :path path) + (when (fs/exists? path) + (fs/delete path)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn tempfile + "Returns a tmpfile candidate (without creating it)" + [& {:keys [suffix prefix] + :or {prefix "penpot." + suffix ".tmp"}}] + (let [candidate (fs/tempfile :suffix suffix :prefix prefix)] + (a/offer! queue candidate) + candidate)) + +(defn create-tempfile + [& {:keys [suffix prefix] + :or {prefix "penpot." + suffix ".tmp"}}] + (let [path (fs/create-tempfile :suffix suffix :prefix prefix)] + (a/offer! queue path) + path)) diff --git a/backend/src/app/util/bytes.clj b/backend/src/app/util/bytes.clj new file mode 100644 index 0000000000..5be58f4054 --- /dev/null +++ b/backend/src/app/util/bytes.clj @@ -0,0 +1,110 @@ +;; 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.bytes + "Bytes & Byte Streams helpers" + (:require + [clojure.java.io :as io] + [datoteka.core :as fs] + [yetti.adapter :as yt]) + (:import + com.github.luben.zstd.ZstdInputStream + com.github.luben.zstd.ZstdOutputStream + java.io.ByteArrayInputStream + java.io.ByteArrayOutputStream + java.io.DataInputStream + java.io.DataOutputStream + java.io.OutputStream + java.io.InputStream + java.lang.AutoCloseable + org.apache.commons.io.IOUtils + org.apache.commons.io.input.BoundedInputStream)) + +(set! *warn-on-reflection* true) + +(def ^:const default-buffer-size + (:xnio/buffer-size yt/defaults)) + +(defn copy! + [src dst & {:keys [offset size buffer-size] + :or {offset 0 buffer-size default-buffer-size}}] + (let [^bytes buff (byte-array buffer-size)] + (if size + (IOUtils/copyLarge ^InputStream src ^OutputStream dst (long offset) (long size) buff) + (IOUtils/copyLarge ^InputStream src ^OutputStream dst buff)))) + +(defn write-to-file! + [src dst & {:keys [size]}] + (with-open [^OutputStream output (io/output-stream dst)] + (cond + (bytes? src) + (if size + (with-open [^InputStream input (ByteArrayInputStream. ^bytes src)] + (with-open [^InputStream input (BoundedInputStream. input (or size (alength ^bytes src)))] + (copy! input output :size size))) + + (do + (IOUtils/writeChunked ^bytes src output) + (.flush ^OutputStream output) + (alength ^bytes src))) + + (instance? InputStream src) + (copy! src output :size size) + + :else + (throw (IllegalArgumentException. "invalid arguments"))))) + +(defn read-as-bytes + "Read input stream as byte array." + [input & {:keys [size]}] + (cond + (instance? InputStream input) + (with-open [output (ByteArrayOutputStream. (or size (.available ^InputStream input)))] + (copy! input output :size size) + (.toByteArray output)) + + (fs/path? input) + (with-open [input (io/input-stream input) + output (ByteArrayOutputStream. (or size (.available input)))] + (copy! input output :size size) + (.toByteArray output)) + + :else + (throw (IllegalArgumentException. "invalid arguments")))) + +(defn bytes-input-stream + "Creates an instance of ByteArrayInputStream." + [^bytes data] + (ByteArrayInputStream. data)) + +(defn bounded-input-stream + [input size & {:keys [close?] :or {close? true}}] + (doto (BoundedInputStream. ^InputStream input ^long size) + (.setPropagateClose close?))) + +(defn zstd-input-stream + ^InputStream + [input] + (ZstdInputStream. ^InputStream input)) + +(defn zstd-output-stream + ^OutputStream + [output & {:keys [level] :or {level 0}}] + (ZstdOutputStream. ^OutputStream output (int level))) + +(defn data-input-stream + ^DataInputStream + [input] + (DataInputStream. ^InputStream input)) + +(defn data-output-stream + ^DataOutputStream + [output] + (DataOutputStream. ^OutputStream output)) + +(defn close! + [^AutoCloseable stream] + (.close stream)) From 46d075611d85b83139a9156a032472c438249d79 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Jun 2022 11:39:57 +0200 Subject: [PATCH 42/86] :recycle: Adapt media & fonts handling to new tmp service And storage backend changes --- backend/src/app/media.clj | 97 +++++++++++------------- backend/src/app/rpc/mutations/fonts.clj | 6 +- backend/src/app/rpc/mutations/media.clj | 37 ++++----- backend/src/app/tasks/file_gc.clj | 2 +- backend/test/app/services_fonts_test.clj | 7 +- backend/test/app/storage_test.clj | 4 +- common/src/app/common/exceptions.cljc | 6 ++ common/src/app/common/uuid.cljc | 3 + 8 files changed, 85 insertions(+), 77 deletions(-) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 3814d94732..99cbe15cdd 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -12,18 +12,16 @@ [app.common.media :as cm] [app.common.spec :as us] [app.config :as cf] + [app.storage.tmp :as tmp] + [app.util.bytes :as bs] [app.util.svg :as svg] [buddy.core.bytes :as bb] [buddy.core.codecs :as bc] - [clojure.java.io :as io] [clojure.java.shell :as sh] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs]) (:import - java.io.ByteArrayInputStream - java.io.OutputStream - org.apache.commons.io.IOUtils org.im4java.core.ConvertCmd org.im4java.core.IMOperation org.im4java.core.Info)) @@ -93,18 +91,16 @@ (let [{:keys [path mtype]} input format (or (cm/mtype->format mtype) format) ext (cm/format->extension format) - tmp (fs/create-tempfile :suffix ext)] + tmp (tmp/tempfile :prefix "penpot.media." :suffix ext)] (doto (ConvertCmd.) (.run operation (into-array (map str [path tmp])))) - (let [thumbnail-data (fs/slurp-bytes tmp)] - (fs/delete tmp) - (assoc params - :format format - :mtype (cm/format->mtype format) - :size (alength ^bytes thumbnail-data) - :data (ByteArrayInputStream. thumbnail-data))))) + (assoc params + :format format + :mtype (cm/format->mtype format) + :size (fs/size tmp) + :data tmp))) (defmethod process :generic-thumbnail [{:keys [quality width height] :as params}] @@ -201,59 +197,54 @@ (defmethod process :generate-fonts [{:keys [input] :as params}] (letfn [(ttf->otf [data] - (let [input-file (fs/create-tempfile :prefix "penpot") - output-file (fs/path (str input-file ".otf")) - _ (with-open [out (io/output-stream input-file)] - (IOUtils/writeChunked ^bytes data ^OutputStream out) - (.flush ^OutputStream out)) - res (sh/sh "fontforge" "-lang=ff" "-c" - (str/fmt "Open('%s'); Generate('%s')" - (str input-file) - (str output-file)))] + (let [finput (tmp/tempfile :prefix "penpot.font." :suffix "") + foutput (fs/path (str finput ".otf")) + _ (bs/write-to-file! data finput) + res (sh/sh "fontforge" "-lang=ff" "-c" + (str/fmt "Open('%s'); Generate('%s')" + (str finput) + (str foutput)))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file)))) - + foutput))) (otf->ttf [data] - (let [input-file (fs/create-tempfile :prefix "penpot") - output-file (fs/path (str input-file ".ttf")) - _ (with-open [out (io/output-stream input-file)] - (IOUtils/writeChunked ^bytes data ^OutputStream out) - (.flush ^OutputStream out)) - res (sh/sh "fontforge" "-lang=ff" "-c" - (str/fmt "Open('%s'); Generate('%s')" - (str input-file) - (str output-file)))] + (let [finput (tmp/tempfile :prefix "penpot.font." :suffix "") + foutput (fs/path (str finput ".ttf")) + _ (bs/write-to-file! data finput) + res (sh/sh "fontforge" "-lang=ff" "-c" + (str/fmt "Open('%s'); Generate('%s')" + (str finput) + (str foutput)))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file)))) + foutput))) (ttf-or-otf->woff [data] - (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") - output-file (fs/path (str input-file ".woff")) - _ (with-open [out (io/output-stream input-file)] - (IOUtils/writeChunked ^bytes data ^OutputStream out) - (.flush ^OutputStream out)) - res (sh/sh "sfnt2woff" (str input-file))] + ;; NOTE: foutput is not used directly, it represents the + ;; default output of the exection of the underlying + ;; command. + (let [finput (tmp/tempfile :prefix "penpot.font." :suffix "") + foutput (fs/path (str finput ".woff")) + _ (bs/write-to-file! data finput) + res (sh/sh "sfnt2woff" (str finput))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file)))) + foutput))) (ttf-or-otf->woff2 [data] - (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") - output-file (fs/path (str input-file ".woff2")) - _ (with-open [out (io/output-stream input-file)] - (IOUtils/writeChunked ^bytes data ^OutputStream out) - (.flush ^OutputStream out)) - res (sh/sh "woff2_compress" (str input-file))] + ;; NOTE: foutput is not used directly, it represents the + ;; default output of the exection of the underlying + ;; command. + (let [finput (tmp/tempfile :prefix "penpot.font." :suffix ".tmp") + foutput (fs/path (str (fs/base finput) ".woff2")) + _ (bs/write-to-file! data finput) + res (sh/sh "woff2_compress" (str finput))] (when (zero? (:exit res)) - (fs/slurp-bytes output-file)))) + foutput))) (woff->sfnt [data] - (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "") - _ (with-open [out (io/output-stream input-file)] - (IOUtils/writeChunked ^bytes data ^OutputStream out) - (.flush ^OutputStream out)) - res (sh/sh "woff2sfnt" (str input-file) - :out-enc :bytes)] + (let [finput (tmp/tempfile :prefix "penpot" :suffix "") + _ (bs/write-to-file! data finput) + res (sh/sh "woff2sfnt" (str finput) + :out-enc :bytes)] (when (zero? (:exit res)) (:out res)))) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index 91b8024d88..a405c279c0 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -71,9 +71,9 @@ data) (persist-font-object [data mtype] - (when-let [fdata (get data mtype)] - (p/let [hash (calculate-hash fdata) - content (-> (sto/content fdata) + (when-let [resource (get data mtype)] + (p/let [hash (calculate-hash resource) + content (-> (sto/content resource) (sto/wrap-with-hash hash))] (sto/put-object! storage {::sto/content content ::sto/touched-at (dt/now) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 70c8c20ff9..66ef41da63 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -17,6 +17,8 @@ [app.rpc.queries.teams :as teams] [app.rpc.rlimit :as rlimit] [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.util.bytes :as bs] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] @@ -179,11 +181,12 @@ (* 1024 1024 100)) ; 100MiB (defn- create-file-media-object-from-url - [{:keys [storage http-client] :as cfg} {:keys [url name] :as params}] + [{:keys [http-client] :as cfg} {:keys [url name] :as params}] (letfn [(parse-and-validate-size [headers] (let [size (some-> (get headers "content-length") d/parse-integer) mtype (get headers "content-type") format (cm/mtype->format mtype)] + (when-not size (ex/raise :type :validation :code :unknown-size @@ -203,24 +206,24 @@ :mtype mtype :format format})) - (get-upload-object [sobj] - (p/let [path (sto/get-object-path storage sobj) - mdata (meta sobj)] - {:filename "tempfile" - :size (:size sobj) - :path path - :mtype (:content-type mdata)})) - (download-media [uri] - (p/let [{:keys [body headers]} (http-client {:method :get :uri uri} {:response-type :input-stream}) - {:keys [size mtype]} (parse-and-validate-size headers)] + (-> (http-client {:method :get :uri uri} {:response-type :input-stream}) + (p/then process-response))) - (-> (assoc storage :backend :tmp) - (sto/put-object! {::sto/content (sto/content body size) - ::sto/expired-at (dt/in-future {:minutes 30}) - :content-type mtype - :bucket "file-media-object"}) - (p/then get-upload-object))))] + (process-response [{:keys [body headers] :as response}] + (let [{:keys [size mtype]} (parse-and-validate-size headers) + path (tmp/tempfile :prefix "penpot.media.download.") + written (bs/write-to-file! body path :size size)] + + (when (not= written size) + (ex/raise :type :internal + :code :mismatch-write-size + :hint "unexpected state: unable to write to file")) + + {:filename "tempfile" + :size size + :path path + :mtype mtype}))] (p/let [content (download-media url)] (->> (merge params {:content content :name (or name (:filename content))}) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 029f0b7fe1..29ad2eeb84 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -82,7 +82,7 @@ :kf first :initk (dt/now))))) -(defn- collect-used-media +(defn collect-used-media [data] (let [xform (comp (map :objects) diff --git a/backend/test/app/services_fonts_test.clj b/backend/test/app/services_fonts_test.clj index 71f217b6aa..dfe87e5698 100644 --- a/backend/test/app/services_fonts_test.clj +++ b/backend/test/app/services_fonts_test.clj @@ -11,6 +11,7 @@ [app.http :as http] [app.storage :as sto] [app.test-helpers :as th] + [app.util.bytes :as bs] [clojure.java.io :as io] [clojure.test :as t] [datoteka.core :as fs])) @@ -25,7 +26,8 @@ font-id (uuid/custom 10 1) ttfdata (-> (io/resource "app/test_files/font-1.ttf") - (fs/slurp-bytes)) + io/input-stream + bs/read-as-bytes) params {::th/type :create-font-variant :profile-id (:id prof) @@ -60,7 +62,8 @@ font-id (uuid/custom 10 1) data (-> (io/resource "app/test_files/font-1.woff") - (fs/slurp-bytes)) + io/input-stream + bs/read-as-bytes) params {::th/type :create-font-variant :profile-id (:id prof) diff --git a/backend/test/app/storage_test.clj b/backend/test/app/storage_test.clj index cab9e01d8a..832fbdc6fb 100644 --- a/backend/test/app/storage_test.clj +++ b/backend/test/app/storage_test.clj @@ -12,6 +12,7 @@ [app.storage :as sto] [app.test-helpers :as th] [app.util.time :as dt] + [app.util.bytes :as bs] [clojure.java.io :as io] [clojure.test :as t] [cuerdas.core :as str] @@ -197,7 +198,8 @@ :is-shared false}) ttfdata (-> (io/resource "app/test_files/font-1.ttf") - (fs/slurp-bytes)) + io/input-stream + bs/read-as-bytes) mfile {:filename "sample.jpg" :path (th/tempfile "app/test_files/sample.jpg") diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index d76f943e54..424a594a85 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -50,6 +50,12 @@ [& exprs] `(try* (^:once fn* [] ~@exprs) identity)) +(defn with-always + "A helper that evaluates an exptession independently if the body + raises exception or not." + [always-expr & body] + `(try ~@body (finally ~always-expr))) + (defn ex-info? [v] (instance? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) v)) diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 6045029007..1a71256ef6 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -48,3 +48,6 @@ #?(:clj (dm/export impl/get-word-high)) + +#?(:clj + (dm/export impl/get-word-low)) From ce14acac2c3c2929f785e51c3faafc33b3b53cac Mon Sep 17 00:00:00 2001 From: Michael Wood Date: Fri, 24 Jun 2022 08:41:29 +0200 Subject: [PATCH 43/86] :books: Fix spelling of 'sneak peek' https://theoatmeal.com/comics/sneak_peek --- frontend/src/app/main/ui/releases/v1_11.cljs | 2 +- frontend/src/app/main/ui/releases/v1_12.cljs | 2 +- frontend/src/app/main/ui/releases/v1_13.cljs | 2 +- frontend/src/app/main/ui/releases/v1_14.cljs | 2 +- frontend/src/app/main/ui/releases/v1_4.cljs | 2 +- frontend/src/app/main/ui/releases/v1_5.cljs | 2 +- frontend/src/app/main/ui/releases/v1_6.cljs | 2 +- frontend/src/app/main/ui/releases/v1_7.cljs | 2 +- frontend/src/app/main/ui/releases/v1_8.cljs | 2 +- frontend/src/app/main/ui/releases/v1_9.cljs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/releases/v1_11.cljs b/frontend/src/app/main/ui/releases/v1_11.cljs index 56e8dacef8..451005ab0e 100644 --- a/frontend/src/app/main/ui/releases/v1_11.cljs +++ b/frontend/src/app/main/ui/releases/v1_11.cljs @@ -25,7 +25,7 @@ [:span.release "Beta 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 Beta 1.11 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Beta 1.11 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_12.cljs b/frontend/src/app/main/ui/releases/v1_12.cljs index b494cb568a..d3bc515cc1 100644 --- a/frontend/src/app/main/ui/releases/v1_12.cljs +++ b/frontend/src/app/main/ui/releases/v1_12.cljs @@ -25,7 +25,7 @@ [:span.release "Beta 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 Beta 1.12 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Beta 1.12 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_13.cljs b/frontend/src/app/main/ui/releases/v1_13.cljs index 9f73b57298..6cf873b858 100644 --- a/frontend/src/app/main/ui/releases/v1_13.cljs +++ b/frontend/src/app/main/ui/releases/v1_13.cljs @@ -25,7 +25,7 @@ [:span.release "Beta version " version] [:div.modal-content [:p "Penpot continues to grow 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 Beta 1.13 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Beta 1.13 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_14.cljs b/frontend/src/app/main/ui/releases/v1_14.cljs index f0e4e459ef..77a714a5a7 100644 --- a/frontend/src/app/main/ui/releases/v1_14.cljs +++ b/frontend/src/app/main/ui/releases/v1_14.cljs @@ -25,7 +25,7 @@ [:span.release "Beta version " version] [:div.modal-content [:p "Penpot continues to grow 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 Beta 1.14 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Beta 1.14 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_4.cljs b/frontend/src/app/main/ui/releases/v1_4.cljs index da5bb480b4..42032d87b9 100644 --- a/frontend/src/app/main/ui/releases/v1_4.cljs +++ b/frontend/src/app/main/ui/releases/v1_4.cljs @@ -25,7 +25,7 @@ [: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.4.0 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Alpha 1.4.0 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_5.cljs b/frontend/src/app/main/ui/releases/v1_5.cljs index 64f83de777..3fc9dd01e1 100644 --- a/frontend/src/app/main/ui/releases/v1_5.cljs +++ b/frontend/src/app/main/ui/releases/v1_5.cljs @@ -25,7 +25,7 @@ [: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.5.0 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Alpha 1.5.0 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_6.cljs b/frontend/src/app/main/ui/releases/v1_6.cljs index 6402875d58..19b539363c 100644 --- a/frontend/src/app/main/ui/releases/v1_6.cljs +++ b/frontend/src/app/main/ui/releases/v1_6.cljs @@ -25,7 +25,7 @@ [: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.6.0 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Alpha 1.6.0 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_7.cljs b/frontend/src/app/main/ui/releases/v1_7.cljs index 4a61adc388..9024266e07 100644 --- a/frontend/src/app/main/ui/releases/v1_7.cljs +++ b/frontend/src/app/main/ui/releases/v1_7.cljs @@ -25,7 +25,7 @@ [: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."]] + [:p "We are happy to show you a sneak peek 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"}] diff --git a/frontend/src/app/main/ui/releases/v1_8.cljs b/frontend/src/app/main/ui/releases/v1_8.cljs index 7d88f71c1c..aba61525d6 100644 --- a/frontend/src/app/main/ui/releases/v1_8.cljs +++ b/frontend/src/app/main/ui/releases/v1_8.cljs @@ -25,7 +25,7 @@ [: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.8 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Alpha 1.8 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] diff --git a/frontend/src/app/main/ui/releases/v1_9.cljs b/frontend/src/app/main/ui/releases/v1_9.cljs index 7bf95fadbc..febb043ad1 100644 --- a/frontend/src/app/main/ui/releases/v1_9.cljs +++ b/frontend/src/app/main/ui/releases/v1_9.cljs @@ -25,7 +25,7 @@ [: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.9 version brings."]] + [:p "We are happy to show you a sneak peek of the most important stuff that the Alpha 1.9 version brings."]] [:div.modal-navigation [:button.btn-secondary {:on-click next} "Continue"]]] [:img.deco {:src "images/deco-left.png" :border "0"}] From 639eaa24586a4528d60a585fb5314492664aa83e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 23 Jun 2022 09:22:59 +0200 Subject: [PATCH 44/86] :sparkles: Improved nested boards thumbnail handling --- frontend/src/app/main/ui/shapes/shape.cljs | 2 +- frontend/src/app/main/ui/workspace/viewport/hooks.cljs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 820b627466..c7ea0ebf32 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -69,7 +69,7 @@ wrapper-props (-> (obj/clone props) - (obj/without ["shape" "children"]) + (obj/without ["shape" "children" "disable-shadows?"]) (obj/set! "ref" ref) (obj/set! "id" (dm/fmt "shape-%" (:id shape))) (obj/set! "style" styles)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 72683d8e72..a6f4881490 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -229,10 +229,12 @@ (defn setup-active-frames [objects hover-ids selected active-frames zoom transform vbox] - (let [frame? #(= :frame (get-in objects [% :type])) - all-frames (mf/use-memo (mf/deps objects) #(cph/get-root-frames-ids objects)) + (let [all-frames (mf/use-memo (mf/deps objects) #(cph/get-root-frames-ids objects)) selected-frames (mf/use-memo (mf/deps selected) #(->> all-frames (filter selected))) - xf-selected-frame (comp (remove frame?) (map #(get-in objects [% :frame-id]))) + + xf-selected-frame (comp (remove cph/root-frame?) + (map #(cph/get-shape-id-root-frame objects %))) + selected-shapes-frames (mf/use-memo (mf/deps selected) #(into #{} xf-selected-frame selected)) active-selection (when (and (not= transform :move) (= (count selected-frames) 1)) (first selected-frames)) @@ -255,7 +257,7 @@ ;; - If no hovering over any frames we keep the previous active one ;; - Check always that the active frames are inside the vbox - (let [hover-ids? (set @hover-ids) + (let [hover-ids? (set (->> @hover-ids (map #(cph/get-shape-id-root-frame objects %)))) is-active-frame? (fn [id] From b944d977bb42847a5f1451af3e1417936ecb9c6c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Jun 2022 11:42:23 +0200 Subject: [PATCH 45/86] :tada: Add binfile import/export internal functionality --- backend/resources/api-doc.tmpl | 27 + backend/resources/log4j2-devenv.xml | 10 +- backend/resources/log4j2.xml | 9 +- backend/resources/templates/debug.tmpl | 117 ++- backend/resources/templates/styles.css | 26 +- backend/src/app/http.clj | 19 +- backend/src/app/http/debug.clj | 323 +++++--- backend/src/app/http/doc.clj | 7 +- backend/src/app/http/middleware.clj | 19 + backend/src/app/http/session.clj | 25 +- backend/src/app/main.clj | 10 +- backend/src/app/migrations.clj | 6 + ...0073-mod-file-media-object-constraints.sql | 11 + .../0074-mod-file-library-rel-constraints.sql | 5 + backend/src/app/rpc.clj | 37 +- backend/src/app/rpc/commands/binfile.clj | 716 ++++++++++++++++++ backend/src/app/rpc/mutations/files.clj | 25 +- backend/test/app/storage_test.clj | 6 +- common/deps.edn | 2 +- common/src/app/common/pprint.cljc | 19 +- 20 files changed, 1238 insertions(+), 181 deletions(-) create mode 100644 backend/src/app/migrations/sql/0073-mod-file-media-object-constraints.sql create mode 100644 backend/src/app/migrations/sql/0074-mod-file-library-rel-constraints.sql create mode 100644 backend/src/app/rpc/commands/binfile.clj diff --git a/backend/resources/api-doc.tmpl b/backend/resources/api-doc.tmpl index f319a46925..35d3a700d0 100644 --- a/backend/resources/api-doc.tmpl +++ b/backend/resources/api-doc.tmpl @@ -20,6 +20,33 @@
+

RPC COMMAND METHODS:

+
    + {% for item in command-methods %} +
  • +
    + {#
    {{item.type}}
    #} +
    {{item.name}}
    +
    + + Auth: + {% if item.auth %}YES{% else %}NO{% endif %} + +
    +
    + +
  • + {% endfor %} +
+

RPC QUERY METHODS:

    {% for item in query-methods %} diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index d07a33d7dd..6305bccef8 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -25,6 +25,11 @@ + + + + + @@ -38,11 +43,6 @@ - - - - - diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index d2a045c36c..e9feb9e001 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -10,11 +10,12 @@ - - - + + + + - + diff --git a/backend/resources/templates/debug.tmpl b/backend/resources/templates/debug.tmpl index a3044dba54..40f2e3c5bb 100644 --- a/backend/resources/templates/debug.tmpl +++ b/backend/resources/templates/debug.tmpl @@ -10,23 +10,110 @@ Debug Main Page
    -
    -

    Download file data:

    - Given an FILE-ID, downloads the file data as file. The file data is encoded using transit. -
    - - - -
    +
    +
    + Download file data: + Given an FILE-ID, downloads the file data as file. The file data is encoded using transit. +
    +
    + +
    +
    + + +
    +
    +
    + +
    + Upload File Data: + Create a new file on your draft projects using the file downloaded from the previous section. +
    +
    + +
    +
    + + +
    + + +
    +
    -
    -

    Upload File Data:

    - Create a new file on your draft projects using the file downloaded from the previous section. -
    - - -
    +
    +
    + Export binfile: + Given an FILE-ID, downloads the file and optionally all + the related libraries in a single custom formatted binary + file. + +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    +
    +
    + Import binfile: + Import penpot file in binary + format. If overwrite is checked, all files will + be overwriten using the same ids found in the file instead of + generating a new ones. + +
    +
    + +
    + +
    + + +
    + + Instead of creating a new file with all relations remaped, + reuses all ids and updates/overwrites the objects that are + already exists on the database. + Warning, this operation should be used with caution. + +
    + +
    + + +
    + + Applies the file migrations on the importation process. + +
    + +
    + + +
    + + Do not break on index lookup erros (remap operation). + Useful when importing a broken file that has broken + relations or missing pieces. + +
    + +
    + +
    +
    +
    {% endblock %} diff --git a/backend/resources/templates/styles.css b/backend/resources/templates/styles.css index 60db4b548e..74b19390a3 100644 --- a/backend/resources/templates/styles.css +++ b/backend/resources/templates/styles.css @@ -14,7 +14,6 @@ pre { } desc { - display: flex; margin-bottom: 10px; font-size: 10px; color: #666; @@ -28,6 +27,15 @@ main { margin: 20px; } +small { + font-size: 9px; + color: #888; +} + +small > strong { + font-size: 9px; +} + nav { position: fixed; width: 100vw; @@ -95,17 +103,25 @@ nav > div:not(:last-child) { .index { margin-top: 40px; + display: flex; } .index > section { padding: 10px; background-color: #e3e3e3; + max-width: 400px; + margin: 5px; + height: fit-content; } -.index > section:not(:last-child) { - margin-bottom: 10px; +.index fieldset:not(:first-child) { + margin-top: 15px; } +/* .index > section:not(:last-child) { */ +/* margin-bottom: 10px; */ +/* } */ + .index > section > h2 { margin-top: 0px; @@ -148,3 +164,7 @@ nav > div:not(:last-child) { color: inherit; } +form .row { + padding: 5px 0; +} + diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 95631738dd..56984d8c55 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -120,16 +120,17 @@ (s/def ::feedback fn?) (s/def ::ws fn?) (s/def ::audit-handler fn?) -(s/def ::debug map?) (s/def ::awsns-handler fn?) (s/def ::session map?) +(s/def ::debug-routes vector?) (defmethod ig/pre-init-spec ::router [_] (s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets - ::session ::feedback ::awsns-handler ::debug ::audit-handler])) + ::session ::feedback ::awsns-handler ::debug-routes + ::audit-handler])) (defmethod ig/init-key ::router - [_ {:keys [ws session rpc oauth metrics assets feedback debug] :as cfg}] + [_ {:keys [ws session rpc oauth metrics assets feedback debug-routes] :as cfg}] (rr/router [["" {:middleware [[middleware/server-timing] [middleware/format-response] @@ -137,20 +138,14 @@ [middleware/parse-request] [middleware/errors errors/handle] [middleware/restrict-methods]]} + ["/metrics" {:handler (:handler metrics)}] ["/assets" {:middleware [(:middleware session)]} ["/by-id/:id" {:handler (:objects-handler assets)}] ["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}] ["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]] - ["/dbg" {:middleware [(:middleware session)]} - ["" {:handler (:index debug)}] - ["/changelog" {:handler (:changelog debug)}] - ["/error-by-id/:id" {:handler (:retrieve-error debug)}] - ["/error/:id" {:handler (:retrieve-error debug)}] - ["/error" {:handler (:retrieve-error-list debug)}] - ["/file/data" {:handler (:file-data debug)}] - ["/file/changes" {:handler (:retrieve-file-changes debug)}]] + debug-routes ["/webhooks" ["/sns" {:handler (:awsns-handler cfg) @@ -162,7 +157,6 @@ ["/api" {:middleware [[middleware/cors] (:middleware session)]} - ["/health" {:handler (:health-check debug)}] ["/_doc" {:handler (doc/handler rpc) :allowed-methods #{:get}}] ["/feedback" {:handler feedback @@ -177,6 +171,7 @@ :allowed-methods #{:post}}] ["/rpc" + ["/command/:command" {:handler (:command-handler rpc)}] ["/query/:type" {:handler (:query-handler rpc)}] ["/mutation/:type" {:handler (:mutation-handler rpc) :allowed-methods #{:post}}]]]]])) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 35e9ed27f9..03c2a090d2 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -5,36 +5,39 @@ ;; Copyright (c) UXBOX Labs SL (ns app.http.debug + (:refer-clojure :exclude [error-handler]) (:require - [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.pprint :as pp] [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.db.sql :as sql] - [app.rpc.mutations.files :as m.files] + [app.http.middleware :as mw] + [app.rpc.commands.binfile :as binf] + [app.rpc.mutations.files :refer [create-file]] [app.rpc.queries.profile :as profile] [app.util.blob :as blob] + [app.util.bytes :as bs] [app.util.template :as tmpl] [app.util.time :as dt] [app.worker :as wrk] [clojure.java.io :as io] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [datoteka.core :as fs] [emoji.core :as emj] - [fipp.edn :as fpp] [integrant.core :as ig] [markdown.core :as md] [markdown.transformers :as mdt] - [promesa.core :as p] - [promesa.exec :as px] [yetti.request :as yrq] [yetti.response :as yrs])) ;; (selmer.parser/cache-off!) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn authorized? [pool {:keys [profile-id]}] (or (= "devenv" (cf/get :host)) @@ -42,7 +45,22 @@ admins (or (cf/get :admins) #{})] (contains? admins (:email profile))))) -(defn index +(defn prepare-response + [body] + (let [headers {"content-type" "application/transit+json"}] + (yrs/response :status 200 :body body :headers headers))) + +(defn prepare-download-response + [body filename] + (let [headers {"content-disposition" (str "attachment; filename=" filename) + "content-type" "application/octet-stream"}] + (yrs/response :status 200 :body body :headers headers))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; INDEX +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn index-handler [{:keys [pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication @@ -52,6 +70,9 @@ :body (-> (io/resource "templates/debug.tmpl") (tmpl/render {})))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FILE CHANGES +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def sql:retrieve-range-of-changes "select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") @@ -59,28 +80,16 @@ (def sql:retrieve-single-change "select revn, changes, data from file_change where file_id=? and revn = ?") -(defn prepare-response - [{:keys [params] :as request} body filename] - (when-not body - (ex/raise :type :not-found - :code :enpty-data - :hint "empty response")) - - (cond-> (yrs/response :status 200 - :body body - :headers {"content-type" "application/transit+json"}) - (contains? params :download) - (update :headers assoc "content-disposition" (str "attachment; filename=" filename)))) - (defn- retrieve-file-data - [{:keys [pool]} {:keys [params] :as request}] + [{:keys [pool]} {:keys [params profile-id] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) - (let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid) - revn (some-> (get-in request [:params :revn]) d/parse-integer) + (let [file-id (some-> params :file-id parse-uuid) + revn (some-> params :revn parse-long) filename (str file-id)] + (when-not file-id (ex/raise :type :validation :code :missing-arguments)) @@ -88,35 +97,63 @@ (let [data (if (integer? revn) (some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data) (some-> (db/get-by-id pool :file file-id) :data))] - (if (contains? params :download) - (-> (prepare-response request data filename) - (update :headers assoc "content-type" "application/octet-stream")) - (prepare-response request (some-> data blob/decode) filename))))) + + (when-not data + (ex/raise :type :not-found + :code :enpty-data + :hint "empty response")) + (cond + (contains? params :download) + (prepare-download-response data filename) + + (contains? params :clone) + (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + data (some-> data blob/decode)] + (create-file pool {:id (uuid/next) + :name (str "Cloned file: " filename) + :project-id project-id + :profile-id profile-id + :data data}) + (yrs/response 201 "OK CREATED")) + + :else + (prepare-response (some-> data blob/decode)))))) + +(defn- is-file-exists? + [pool id] + (let [sql "select exists (select 1 from file where id=?) as exists;"] + (-> (db/exec-one! pool [sql id]) :exists))) (defn- upload-file-data [{:keys [pool]} {:keys [profile-id params] :as request}] (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) - data (some-> params :file :path fs/slurp-bytes blob/decode)] + data (some-> params :file :path bs/read-as-bytes blob/decode)] (if (and data project-id) - (let [fname (str "imported-file-" (dt/now)) - file-id (try - (uuid/uuid (-> params :file :filename)) - (catch Exception _ (uuid/next))) - file (db/exec-one! pool (sql/select :file {:id file-id}))] - (if file - (db/update! pool :file - {:data (blob/encode data)} - {:id file-id}) - (m.files/create-file pool {:id file-id - :name fname - :project-id project-id - :profile-id profile-id - :data data})) - (yrs/response 200 "OK")) + (let [fname (str "Imported file *: " (dt/now)) + overwrite? (contains? params :overwrite?) + file-id (or (and overwrite? (ex/ignoring (-> params :file :filename parse-uuid))) + (uuid/next))] + + (if (and overwrite? file-id + (is-file-exists? pool file-id)) + (do + (db/update! pool :file + {:data (blob/encode data)} + {:id file-id}) + (yrs/response 200 "OK UPDATED")) + + (do + (create-file pool {:id file-id + :name fname + :project-id project-id + :profile-id profile-id + :data data}) + (yrs/response 201 "OK CREATED")))) + (yrs/response 500 "ERROR")))) -(defn file-data +(defn file-data-handler [cfg request] (case (yrq/method request) :get (retrieve-file-data cfg request) @@ -124,43 +161,47 @@ (ex/raise :type :http :code :method-not-found))) -(defn retrieve-file-changes - [{:keys [pool]} request] +(defn file-changes-handler + [{:keys [pool]} {:keys [params] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) - (let [file-id (some-> (get-in request [:params :id]) uuid/uuid) - revn (or (get-in request [:params :revn]) "latest") - filename (str file-id)] + (letfn [(retrieve-changes [file-id revn] + (if (str/includes? revn ":") + (let [[start end] (->> (str/split revn #":") + (map str/trim) + (map parse-long))] + (some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end]) + (map :changes) + (map blob/decode) + (mapcat identity) + (vec))) - (when (or (not file-id) (not revn)) - (ex/raise :type :validation - :code :invalid-arguments - :hint "missing arguments")) + (if-let [revn (parse-long revn)] + (let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])] + (some-> item :changes blob/decode vec)) + (ex/raise :type :validation :code :invalid-arguments))))] - (cond - (d/num-string? revn) - (let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])] - (prepare-response request (some-> item :changes blob/decode vec) filename)) + (let [file-id (some-> params :id parse-uuid) + revn (or (some-> params :revn parse-long) "latest") + filename (str file-id)] - (str/includes? revn ":") - (let [[start end] (->> (str/split revn #":") - (map str/trim) - (map d/parse-integer)) - items (db/exec! pool [sql:retrieve-range-of-changes file-id start end])] - (prepare-response request - (some->> items - (map :changes) - (map blob/decode) - (mapcat identity) - (vec)) - filename)) - :else - (ex/raise :type :validation :code :invalid-arguments)))) + (when (or (not file-id) (not revn)) + (ex/raise :type :validation + :code :invalid-arguments + :hint "missing arguments")) + (let [data (retrieve-changes file-id revn)] + (if (contains? params :download) + (prepare-download-response data filename) + (prepare-response data)))))) -(defn retrieve-error +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ERROR BROWSER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn error-handler [{:keys [pool]} request] (letfn [(parse-id [request] (let [id (get-in request [:path-params :id]) @@ -176,9 +217,8 @@ (let [context (dissoc report :trace :cause :params :data :spec-problems :spec-explain :spec-value :error :explain :hint) - params {:context (with-out-str - (fpp/pprint context {:width 200})) - :hint (:hint report) + params {:context (pp/pprint-str context :width 200) + :hint (:hint report) :spec-explain (:spec-explain report) :spec-problems (:spec-problems report) :spec-value (:spec-value report) @@ -206,7 +246,7 @@ (def sql:error-reports "select id, created_at from server_error_report order by created_at desc limit 100") -(defn retrieve-error-list +(defn error-list-handler [{:keys [pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication @@ -219,14 +259,88 @@ :headers {"content-type" "text/html; charset=utf-8" "x-robots-tag" "noindex"}))) -(defn health-check +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; EXPORT/IMPORT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn export-handler + [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + (let [file-id (some-> params :file-id parse-uuid) + libs? (contains? params :includelibs) + clone? (contains? params :clone)] + + (when-not file-id + (ex/raise :type :validation + :code :missing-arguments)) + + (let [path (-> cfg + (assoc ::binf/file-id file-id) + (assoc ::binf/include-libraries? libs?) + (binf/export!))] + (if clone? + (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] + (binf/import! + (assoc cfg + ::binf/input path + ::binf/overwrite? false + ::binf/profile-id profile-id + ::binf/project-id project-id)) + + (yrs/response + :status 200 + :headers {"content-type" "text/plain"} + :body "OK CLONED")) + + (yrs/response + :status 200 + :headers {"content-type" "application/octet-stream" + "content-disposition" (str "attachmen; filename=" file-id ".penpot")} + :body (io/input-stream path)))))) + + +(defn import-handler + [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + (when-not (contains? params :file) + (ex/raise :type :validation + :code :missing-upload-file + :hint "missing upload file")) + + (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + overwrite? (contains? params :overwrite) + migrate? (contains? params :migrate) + ignore-index-errors? (contains? params :ignore-index-errors)] + + (when-not project-id + (ex/raise :type :validation + :code :missing-project + :hint "project not found")) + + (binf/import! + (assoc cfg + ::binf/input (-> params :file :path) + ::binf/overwrite? overwrite? + ::binf/migrate? migrate? + ::binf/ignore-index-errors? ignore-index-errors? + ::binf/profile-id profile-id + ::binf/project-id project-id)) + + (yrs/response + :status 200 + :headers {"content-type" "text/plain"} + :body "OK"))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; OTHER SMALL VIEWS/HANDLERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn health-handler "Mainly a task that performs a health check." [{:keys [pool]} _] (db/with-atomic [conn pool] (db/exec-one! conn ["select count(*) as count from server_prop;"]) (yrs/response 200 "OK"))) -(defn changelog +(defn changelog-handler [_ _] (letfn [(transform-emoji [text state] [(emj/emojify text) state]) @@ -238,22 +352,39 @@ :body (-> clog slurp md->html)) (yrs/response :status 404 :body "NOT FOUND")))) -(defn- wrap-async - [{:keys [executor] :as cfg} f] - (fn [request respond raise] - (-> (px/submit! executor #(f cfg request)) - (p/then respond) - (p/catch raise)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; INIT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defmethod ig/pre-init-spec ::handlers [_] - (s/keys :req-un [::db/pool ::wrk/executor])) +(def with-authorization + {:compile + (fn [& _] + (fn [handler pool] + (fn [request respond raise] + (if (authorized? pool request) + (handler request respond raise) + (raise (ex/error :type :authentication + :code :only-admins-allowed))))))}) -(defmethod ig/init-key ::handlers - [_ cfg] - {:index (wrap-async cfg index) - :health-check (wrap-async cfg health-check) - :retrieve-file-changes (wrap-async cfg retrieve-file-changes) - :retrieve-error (wrap-async cfg retrieve-error) - :retrieve-error-list (wrap-async cfg retrieve-error-list) - :file-data (wrap-async cfg file-data) - :changelog (wrap-async cfg changelog)}) + +(s/def ::session map?) + +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req-un [::db/pool ::wrk/executor ::session])) + +(defmethod ig/init-key ::routes + [_ {:keys [session pool executor] :as cfg}] + ["/dbg" {:middleware [[(:middleware session)] + [with-authorization pool] + [mw/with-promise-async executor] + [mw/with-config cfg]]} + ["" {:handler index-handler}] + ["/health" {:handler health-handler}] + ["/changelog" {:handler changelog-handler}] + ;; ["/error-by-id/:id" {:handler error-handler}] + ["/error/:id" {:handler error-handler}] + ["/error" {:handler error-list-handler}] + ["/file/export" {:handler export-handler}] + ["/file/import" {:handler import-handler}] + ["/file/data" {:handler file-data-handler}] + ["/file/changes" {:handler file-changes-handler}]]) diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj index a6e88458b8..d079d89303 100644 --- a/backend/src/app/http/doc.clj +++ b/backend/src/app/http/doc.clj @@ -34,7 +34,12 @@ :auth (:auth mdata true) :docs (::sv/docs mdata) :spec (get-spec-str (::sv/spec mdata))}))] - {:query-methods + + {:command-methods + (into [] + (map (partial gen-doc :command)) + (->> rpc :methods :command (sort-by first))) + :query-methods (into [] (map (partial gen-doc :query)) (->> rpc :methods :query (sort-by first))) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index af7f140a86..f0be700a85 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -12,6 +12,8 @@ [app.config :as cf] [app.util.json :as json] [cuerdas.core :as str] + [promesa.core :as p] + [promesa.exec :as px] [yetti.adapter :as yt] [yetti.middleware :as ymw] [yetti.request :as yrq] @@ -192,3 +194,20 @@ (def restrict-methods {:name ::restrict-methods :compile compile-restrict-methods}) + +(def with-promise-async + {:compile + (fn [& _] + (fn [handler executor] + (fn [request respond raise] + (-> (px/submit! executor #(handler request)) + (p/then respond) + (p/catch raise)))))}) + +(def with-config + {:compile + (fn [& _] + (fn [handler config] + (fn + ([request] (handler config request)) + ([request respond raise] (handler config request respond raise)))))}) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index b680929409..65bb3bccec 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -162,21 +162,22 @@ (defn- make-middleware [{:keys [::events-ch store] :as cfg}] - {:name :session-middleware - :wrap (fn [handler] - (fn [request respond raise] - (try - (-> (retrieve-session store request) - (p/then' #(merge request %)) - (p/finally (fn [request cause] - (if cause - (raise cause) - (do + {:name :session + :compile (fn [& _] + (fn [handler] + (fn [request respond raise] + (try + (-> (retrieve-session store request) + (p/then' #(merge request %)) + (p/finally (fn [request cause] + (if cause + (raise cause) + (do (when-let [session-id (:session-id request)] (a/offer! events-ch session-id)) (handler request respond raise)))))) - (catch Throwable cause - (raise cause)))))}) + (catch Throwable cause + (raise cause))))))}) ;; --- STATE INIT: SESSION diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 7d547976aa..529bfc6606 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -129,7 +129,7 @@ :session (ig/ref :app.http/session) :awsns-handler (ig/ref :app.http.awsns/handler) :oauth (ig/ref :app.http.oauth/handler) - :debug (ig/ref :app.http.debug/handlers) + :debug-routes (ig/ref :app.http.debug/routes) :ws (ig/ref :app.http.websocket/handler) :metrics (ig/ref :app.metrics/metrics) :public-uri (cf/get :public-uri) @@ -139,9 +139,11 @@ :rpc (ig/ref :app.rpc/rpc) :executor (ig/ref [::default :app.worker/executor])} - :app.http.debug/handlers - {:pool (ig/ref :app.db/pool) - :executor (ig/ref [::worker :app.worker/executor])} + :app.http.debug/routes + {:pool (ig/ref :app.db/pool) + :executor (ig/ref [::worker :app.worker/executor]) + :storage (ig/ref :app.storage/storage) + :session (ig/ref :app.http/session)} :app.http.websocket/handler {:pool (ig/ref :app.db/pool) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index bc9f6c94f8..b8657d010a 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -226,6 +226,12 @@ {:name "0072-mod-file-object-thumbnail-table" :fn (mg/resource "app/migrations/sql/0072-mod-file-object-thumbnail-table.sql")} + + {:name "0073-mod-file-media-object-constraints" + :fn (mg/resource "app/migrations/sql/0073-mod-file-media-object-constraints.sql")} + + {:name "0074-mod-file-library-rel-constraints" + :fn (mg/resource "app/migrations/sql/0074-mod-file-library-rel-constraints.sql")} ]) diff --git a/backend/src/app/migrations/sql/0073-mod-file-media-object-constraints.sql b/backend/src/app/migrations/sql/0073-mod-file-media-object-constraints.sql new file mode 100644 index 0000000000..fd17ab4dd2 --- /dev/null +++ b/backend/src/app/migrations/sql/0073-mod-file-media-object-constraints.sql @@ -0,0 +1,11 @@ +ALTER TABLE file_media_object +ALTER CONSTRAINT file_media_object_media_id_fkey DEFERRABLE INITIALLY IMMEDIATE; + +ALTER TABLE file_media_object +ALTER CONSTRAINT file_media_object_thumbnail_id_fkey DEFERRABLE INITIALLY IMMEDIATE; + +ALTER TABLE file_media_object +RENAME CONSTRAINT media_object_file_id_fkey TO file_media_object_file_id_fkey; + +ALTER TABLE file_media_object +ALTER CONSTRAINT file_media_object_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE; diff --git a/backend/src/app/migrations/sql/0074-mod-file-library-rel-constraints.sql b/backend/src/app/migrations/sql/0074-mod-file-library-rel-constraints.sql new file mode 100644 index 0000000000..f7ed7eb856 --- /dev/null +++ b/backend/src/app/migrations/sql/0074-mod-file-library-rel-constraints.sql @@ -0,0 +1,5 @@ +ALTER TABLE file_library_rel +ALTER CONSTRAINT file_library_rel_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE; + +ALTER TABLE file_library_rel +ALTER CONSTRAINT file_library_rel_library_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE; diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 9e53af92eb..b5219df9b6 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -87,6 +87,30 @@ (let [context {:profile-id profile-id}] (raise (ex/wrap-with-context cause context))))))))) +(defn- rpc-command-handler + "Ring handler that dispatches cmd requests and convert between + internal async flow into ring async flow." + [methods {:keys [profile-id session-id params] :as request} respond raise] + (letfn [(handle-response [result] + (let [mdata (meta result)] + (p/-> (yrs/response 200 result) + (handle-response-transformation request mdata) + (handle-before-comple-hook mdata))))] + + (let [cmd (keyword (:command params)) + data (into {::request request} params) + data (if profile-id + (assoc data :profile-id profile-id ::session-id session-id) + (dissoc data :profile-id)) + + method (get methods cmd default-handler)] + (-> (method data) + (p/then handle-response) + (p/then respond) + (p/catch (fn [cause] + (let [context {:profile-id profile-id}] + (raise (ex/wrap-with-context cause context))))))))) + (defn- wrap-metrics "Wrap service method with metrics measurement." [{:keys [metrics ::metrics-id]} f mdata] @@ -212,6 +236,13 @@ (map (partial process-method cfg)) (into {})))) +(defn- resolve-command-methods + [cfg] + (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] + (->> (sv/scan-ns 'app.rpc.commands.binfile) + (map (partial process-method cfg)) + (into {})))) + (s/def ::storage some?) (s/def ::session map?) (s/def ::tokens fn?) @@ -225,7 +256,9 @@ (defmethod ig/init-key ::rpc [_ cfg] (let [mq (resolve-query-methods cfg) - mm (resolve-mutation-methods cfg)] - {:methods {:query mq :mutation mm} + mm (resolve-mutation-methods cfg) + cm (resolve-command-methods cfg)] + {:methods {:query mq :mutation mm :command cm} + :command-handler (partial rpc-command-handler cm) :query-handler (partial rpc-query-handler mq) :mutation-handler (partial rpc-mutation-handler mm)})) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj new file mode 100644 index 0000000000..dc46177e39 --- /dev/null +++ b/backend/src/app/rpc/commands/binfile.clj @@ -0,0 +1,716 @@ +;; 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.rpc.commands.binfile + (:refer-clojure :exclude [assert]) + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [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.media :as media] + [app.rpc.queries.files :refer [decode-row]] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.tasks.file-gc] + [app.util.blob :as blob] + [app.util.bytes :as bs] + [app.util.fressian :as fres] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.java.io :as io] + [clojure.spec.alpha :as s] + [clojure.walk :as walk] + [cuerdas.core :as str] + [yetti.adapter :as yt]) + (:import + java.io.DataInputStream + java.io.DataOutputStream + java.io.InputStream + java.io.OutputStream + java.lang.AutoCloseable)) + +(set! *warn-on-reflection* true) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; LOW LEVEL STREAM IO +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) +(def ^:const penpot-magic-number 800099563638710213) +(def ^:const max-object-size (* 1024 1024 100)) ; Only allow 100MiB max file size. + +(def ^:dynamic *position* nil) + +(defn get-mark + [id] + (case id + :header 1 + :stream 2 + :uuid 3 + :label 4 + :obj 5 + (ex/raise :type :validation + :code :invalid-mark-id + :hint (format "invalid mark id %s" id)))) + +;; (defn buffered-output-stream +;; "Returns a buffered output stream that ignores flush calls. This is +;; needed because transit-java calls flush very aggresivelly on each +;; object write." +;; [^java.io.OutputStream os ^long chunk-size] +;; (proxy [java.io.BufferedOutputStream] [os (int chunk-size)] +;; ;; Explicitly do not forward flush +;; (flush []) +;; (close [] +;; (proxy-super flush) +;; (proxy-super close))) + +(defmacro assert + [expr hint] + `(when-not ~expr + (ex/raise :type :validation + :code :unexpected-condition + :hint ~hint))) + +(defmacro assert-mark + [v type] + `(let [expected# (get-mark ~type) + val# (long ~v)] + (when (not= val# expected#) + (ex/raise :type :validation + :code :unexpected-mark + :hint (format "received mark %s, expected %s" val# expected#))))) + +(defmacro assert-label + [expr label] + `(let [v# ~expr] + (when (not= v# ~label) + (ex/raise :type :assertion + :code :unexpected-label + :hint (format "received label %s, expected %s" v# ~label))))) + +;; --- PRIMITIVE + +(defn write-byte! + [^DataOutputStream output data] + (l/trace :fn "write-byte!" :data data :position @*position* ::l/async false) + (.writeByte output (byte data)) + (swap! *position* inc)) + +(defn read-byte! + [^DataInputStream input] + (let [v (.readByte input)] + (l/trace :fn "read-byte!" :val v :position @*position* ::l/async false) + (swap! *position* inc) + v)) + +(defn write-long! + [^DataOutputStream output data] + (l/trace :fn "write-long!" :data data :position @*position* ::l/async false) + (.writeLong output (long data)) + (swap! *position* + 8)) + + +(defn read-long! + [^DataInputStream input] + (let [v (.readLong input)] + (l/trace :fn "read-long!" :val v :position @*position* ::l/async false) + (swap! *position* + 8) + v)) + +(defn write-bytes! + [^DataOutputStream output ^bytes data] + (let [size (alength data)] + (l/trace :fn "write-bytes!" :size size :position @*position* ::l/async false) + (.write output data 0 size) + (swap! *position* + size))) + +(defn read-bytes! + [^InputStream input ^bytes buff] + (let [size (alength buff) + readed (.readNBytes input buff 0 size)] + (l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/async false) + (swap! *position* + readed) + readed)) + +;; --- COMPOSITE + +(defn write-uuid! + [^DataOutputStream output id] + (l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/async false) + + (doto output + (write-byte! (get-mark :uuid)) + (write-long! (uuid/get-word-high id)) + (write-long! (uuid/get-word-low id)))) + +(defn read-uuid! + [^DataInputStream input] + (l/trace :fn "read-uuid!" :position @*position* ::l/async false) + (let [m (read-byte! input)] + (assert-mark m :uuid) + (let [a (read-long! input) + b (read-long! input)] + (uuid/custom a b)))) + +(defn write-obj! + [^DataOutputStream output data] + (l/trace :fn "write-obj!" :position @*position* ::l/async false) + (let [^bytes data (fres/encode data)] + (doto output + (write-byte! (get-mark :obj)) + (write-long! (alength data)) + (write-bytes! data)))) + +(defn read-obj! + [^DataInputStream input] + (l/trace :fn "read-obj!" :position @*position* ::l/async false) + (let [m (read-byte! input)] + (assert-mark m :obj) + (let [size (read-long! input)] + (assert (pos? size) "incorrect header size found on reading header") + (let [buff (byte-array size)] + (read-bytes! input buff) + (fres/decode buff))))) + +(defn write-label! + [^DataOutputStream output label] + (l/trace :fn "write-label!" :label label :position @*position* ::l/async false) + (doto output + (write-byte! (get-mark :label)) + (write-obj! label))) + +(defn read-label! + [^DataInputStream input] + (l/trace :fn "read-label!" :position @*position* ::l/async false) + (let [m (read-byte! input)] + (assert-mark m :label) + (read-obj! input))) + +(defn write-header! + [^DataOutputStream output & {:keys [version metadata]}] + (l/trace :fn "write-header!" + :version version + :metadata metadata + :position @*position* + ::l/async false) + + (doto output + (write-byte! (get-mark :header)) + (write-long! penpot-magic-number) + (write-long! version) + (write-obj! metadata))) + +(defn read-header! + [^DataInputStream input] + (l/trace :fn "read-header!" :position @*position* ::l/async false) + (let [mark (read-byte! input) + mnum (read-long! input) + vers (read-long! input)] + + (when (or (not= mark (get-mark :header)) + (not= mnum penpot-magic-number)) + (ex/raise :type :validation + :code :invalid-penpot-file)) + + (-> (read-obj! input) + (assoc ::version vers)))) + +(defn copy-stream! + [^OutputStream output ^InputStream input ^long size] + (let [written (bs/copy! input output :size size)] + (l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/async false) + (swap! *position* + written) + written)) + +(defn write-stream! + [^DataOutputStream output stream size] + (l/trace :fn "write-stream!" :position @*position* ::l/async false :size size) + (doto output + (write-byte! (get-mark :stream)) + (write-long! size)) + + (copy-stream! output stream size)) + +(def size-2mib + (* 1024 1024 2)) + +(defn read-stream! + [^DataInputStream input] + (l/trace :fn "read-stream!" :position @*position* ::l/async false) + (let [m (read-byte! input) + s (read-long! input) + p (tmp/tempfile :prefix "penpot.binfile.")] + (assert-mark m :stream) + + (when (> s max-object-size) + (ex/raise :type :validation + :code :max-file-size-reached + :hint (str/ffmt "unable to import storage object with size % bytes" s))) + + (if (> s size-2mib) + ;; If size is more than 2MiB, use a temporal file. + (with-open [^OutputStream output (io/output-stream p)] + (let [readed (bs/copy! input output :offset 0 :size s)] + (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/async false) + (swap! *position* + readed) + [s p])) + + ;; If not, use an in-memory byte-array. + [s (bs/read-as-bytes input :size s)]))) + +(defmacro assert-read-label! + [input expected-label] + `(let [readed# (read-label! ~input) + expected# ~expected-label] + (when (not= readed# expected#) + (ex/raise :type :validation + :code :unexpected-label + :hint (format "unxpected label found: %s, expected: %s" readed# expected#))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HIGH LEVEL IMPL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- retrieve-file + [pool file-id] + (->> (db/query pool :file {:id file-id}) + (map decode-row) + (first))) + +(def ^:private sql:file-media-objects + "SELECT * FROM file_media_object WHERE id = ANY(?)") + +(defn- retrieve-file-media + [pool {:keys [data] :as file}] + (with-open [^AutoCloseable conn (db/open pool)] + (let [ids (app.tasks.file-gc/collect-used-media data) + ids (db/create-array conn "uuid" ids)] + (db/exec! conn [sql:file-media-objects ids])))) + +(def ^:private storage-object-id-xf + (comp + (mapcat (juxt :media-id :thumbnail-id)) + (filter uuid?))) + +(def ^:private sql:file-libraries + "WITH RECURSIVE libs AS ( + SELECT fl.id, fl.deleted_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.id, fl.deleted_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 DISTINCT l.id + FROM libs AS l + WHERE l.deleted_at IS NULL OR l.deleted_at > now();") + +(defn- retrieve-libraries + [pool file-id] + (map :id (db/exec! pool [sql:file-libraries file-id]))) + +(def ^:private sql:file-library-rels + "SELECT * FROM file_library_rel + WHERE file_id = ANY(?)") + +(defn- retrieve-library-relations + [pool ids] + (with-open [^AutoCloseable conn (db/open pool)] + (db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)]))) + +(defn write-export! + "Do the exportation of a speficied file in custom penpot binary + format. There are some options available for customize the output: + + `::include-libraries?`: additionaly to the specified file, all the + linked libraries also will be included (including transitive + dependencies). + " + + [{:keys [pool storage ::output ::file-id ::include-libraries?]}] + (let [libs (when include-libraries? + (retrieve-libraries pool file-id)) + rels (when include-libraries? + (retrieve-library-relations pool (cons file-id libs))) + files (into [file-id] libs) + sids (atom #{})] + + ;; Write header with metadata + (l/debug :hint "exportation summary" + :files (count files) + :rels (count rels) + :include-libs? include-libraries? + ::l/async false) + + (let [sections [:v1/files :v1/rels :v1/sobjects] + mdata {:penpot-version (:full cf/version) + :sections sections + :files files}] + (write-header! output :version 1 :metadata mdata)) + + (l/debug :hint "write section" :section :v1/files :total (count files) ::l/async false) + (write-label! output :v1/files) + (doseq [file-id files] + (let [file (retrieve-file pool file-id) + media (retrieve-file-media pool file)] + + ;; Collect all storage ids for later write them all under + ;; specific storage objects section. + (swap! sids into (sequence storage-object-id-xf media)) + + (l/trace :hint "write penpot file" + :id file-id + :media (count media) + ::l/async false) + + (doto output + (write-obj! file) + (write-obj! media)))) + + (l/debug :hint "write section" :section :v1/rels :total (count rels) ::l/async false) + (doto output + (write-label! :v1/rels) + (write-obj! rels)) + + (let [sids (into [] @sids)] + (l/debug :hint "write section" + :section :v1/sobjects + :items (count sids) + ::l/async false) + + ;; Write all collected storage objects + (doto output + (write-label! :v1/sobjects) + (write-obj! sids)) + + (let [storage (media/configure-assets-storage storage)] + (doseq [id sids] + (let [{:keys [size] :as obj} @(sto/get-object storage id)] + (l/trace :hint "write sobject" :id id ::l/async false) + + (doto output + (write-uuid! id) + (write-obj! (meta obj))) + + (with-open [^InputStream stream @(sto/get-object-data storage obj)] + (let [written (write-stream! output stream size)] + (when (not= written size) + (ex/raise :type :validation + :code :mismatch-readed-size + :hint (str/ffmt "found unexpected object size; size=% written=%" size written))))))))))) + + +;; Dynamic variables for importation process. + +(def ^:dynamic *files*) +(def ^:dynamic *media*) +(def ^:dynamic *index*) +(def ^:dynamic *conn*) + +(defn read-import! + "Do the importation of the specified resource in penpot custom binary + format. There are some options for customize the importation + behavior: + + `::overwrite?`: if true, instead of creating new files and remaping id references, + it reuses all ids and updates existing objects; defaults to `false`. + + `::migrate?`: if true, applies the migration before persisting the + file data; defaults to `false`. + + `::ignore-index-errors?`: if true, do not fail on index lookup errors, can + happen with broken files; defaults to: `false`. + " + + [{:keys [pool storage ::project-id ::ts ::input ::overwrite? ::migrate? ::ignore-index-errors?] + :or {overwrite? false migrate? false ts (dt/now)} + :as cfg}] + + (letfn [(lookup-index [id] + (if ignore-index-errors? + (or (get @*index* id) id) + (let [val (get @*index* id)] + (l/trace :fn "lookup-index" :id id :val val ::l/async false) + (when-not val + (ex/raise :type :validation + :code :incomplete-index + :hint "looks like index has missing data")) + val))) + + (update-index [index coll] + (loop [items (seq coll) + index index] + (if-let [id (first items)] + (let [new-id (if overwrite? id (uuid/next))] + (l/trace :fn "update-index" :id id :new-id new-id ::l/async false) + (recur (rest items) + (assoc index id new-id))) + index))) + + (process-map-form [form] + (cond-> form + ;; Relink Image Shapes + (and (map? (:metadata form)) + (= :image (:type form))) + (update-in [:metadata :id] lookup-index) + + ;; This covers old shapes and the new :fills. + (uuid? (:fill-color-ref-file form)) + (update :fill-color-ref-file lookup-index) + + ;; This covers the old shapes and the new :strokes + (uuid? (:storage-color-ref-file form)) + (update :stroke-color-ref-file lookup-index) + + ;; This covers all text shapes that have typography referenced + (uuid? (:typography-ref-file form)) + (update :typography-ref-file lookup-index) + + ;; This covers the shadows and grids (they have directly + ;; the :file-id prop) + (uuid? (:file-id form)) + (update :file-id lookup-index))) + + ;; a function responsible to analyze all file data and + ;; replace the old :component-file reference with the new + ;; ones, using the provided file-index + (relink-shapes [data] + (walk/postwalk (fn [form] + (if (map? form) + (try + (process-map-form form) + (catch Throwable cause + (l/trace :hint "failed form" :form (pr-str form) ::l/async false) + (throw cause))) + form)) + data)) + + ;; A function responsible of process the :media attr of file + ;; data and remap the old ids with the new ones. + (relink-media [media] + (reduce-kv (fn [res k v] + (let [id (lookup-index k)] + (if (uuid? id) + (-> res + (assoc id (assoc v :id id)) + (dissoc k)) + res))) + media + media)) + + (create-or-update-file [params] + (let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT (id) DO UPDATE SET data=?")] + (db/exec-one! *conn* [sql + (:id params) + (:project-id params) + (:name params) + (:revn params) + (:is-shared params) + (:data params) + (:created-at params) + (:modified-at params) + (:data params)]))) + + (read-files-section! [input] + (l/debug :hint "reading section" :section :v1/files ::l/async false) + (assert-read-label! input :v1/files) + + ;; Process/Read all file + (doseq [expected-file-id *files*] + (let [file (read-obj! input) + media' (read-obj! input) + file-id (:id file)] + + (when (not= file-id expected-file-id) + (ex/raise :type :validation + :code :inconsistent-penpot-file + :hint "the penpot file seems corrupt, found unexpected uuid (file-id)")) + + + ;; Update index using with media + (l/trace :hint "update index with media" ::l/async false) + (vswap! *index* update-index (map :id media')) + + ;; Store file media for later insertion + (l/trace :hint "update media references" ::l/async false) + (vswap! *media* into (map #(update % :id lookup-index)) media') + + (l/trace :hint "procesing file" :file-id file-id ::l/async false) + + (let [file-id' (lookup-index file-id) + data (-> (:data file) + (assoc :id file-id') + (cond-> migrate? (pmg/migrate-data)) + (update :pages-index relink-shapes) + (update :components relink-shapes) + (update :media relink-media)) + + params {:id file-id' + :project-id project-id + :name (str "Imported: " (:name file)) + :revn (:revn file) + :is-shared (:is-shared file) + :data (blob/encode data) + :created-at ts + :modified-at ts}] + + (l/trace :hint "create file" :id file-id' ::l/async false) + + (if overwrite? + (create-or-update-file params) + (db/insert! *conn* :file params)) + + (when overwrite? + (db/delete! *conn* :file-thumbnail {:file-id file-id'})))))) + + (read-rels-section! [input] + (l/debug :hint "reading section" :section :v1/rels ::l/async false) + (assert-read-label! input :v1/rels) + + (let [rels (read-obj! input)] + ;; Insert all file relations + (doseq [rel rels] + (let [rel (-> rel + (assoc :synced-at ts) + (update :file-id lookup-index) + (update :library-file-id lookup-index))] + (l/trace :hint "create file library link" + :file-id (:file-id rel) + :lib-id (:library-file-id rel) + ::l/async false) + (db/insert! *conn* :file-library-rel rel))))) + + (read-sobjects-section! [input] + (l/debug :hint "reading section" :section :v1/sobjects ::l/async false) + (assert-read-label! input :v1/sobjects) + + (let [storage (media/configure-assets-storage storage) + ids (read-obj! input)] + + ;; Step 1: process all storage objects + (doseq [expected-storage-id ids] + (let [id (read-uuid! input) + mdata (read-obj! input)] + + (when (not= id expected-storage-id) + (ex/raise :type :validation + :code :inconsistent-penpot-file + :hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)")) + + (l/trace :hint "readed storage object" :id id ::l/async false) + + (let [[size resource] (read-stream! input) + hash (sto/calculate-hash resource) + content (-> (sto/content resource size) + (sto/wrap-with-hash hash)) + params (-> mdata + (assoc ::sto/deduplicate? true) + (assoc ::sto/content content) + (assoc ::sto/touched-at (dt/now))) + sobject @(sto/put-object! storage params)] + (l/trace :hint "persisted storage object" :id id :new-id (:id sobject) ::l/async false) + (vswap! *index* assoc id (:id sobject))))) + + ;; Step 2: insert all file-media-object rows with correct + ;; storage-id reference. + (doseq [item @*media*] + (l/trace :hint "inserting file media objects" :id (:id item) ::l/async false) + (db/insert! *conn* :file-media-object + (-> item + (update :file-id lookup-index) + (d/update-when :media-id lookup-index) + (d/update-when :thumbnail-id lookup-index)) + {:on-conflict-do-nothing overwrite?})))) + + (read-section! [section input] + (case section + :v1/rels (read-rels-section! input) + :v1/files (read-files-section! input) + :v1/sobjects (read-sobjects-section! input)))] + + (with-open [input (bs/zstd-input-stream input)] + (with-open [input (bs/data-input-stream input)] + (db/with-atomic [conn pool] + (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"]) + + ;; Verify that we received a proper .penpot file + (let [{:keys [sections files]} (read-header! input)] + (l/debug :hint "import verified" :files files :overwrite? overwrite?) + (binding [*index* (volatile! (update-index {} files)) + *media* (volatile! []) + *files* files + *conn* conn] + (run! #(read-section! % input) sections)))))))) + +(defn export! + [cfg] + (let [path (tmp/tempfile :prefix "penpot.export.") + id (uuid/next) + ts (dt/now) + cs (volatile! nil)] + (try + (l/info :hint "start exportation" :export-id id) + (with-open [output (io/output-stream path)] + (with-open [output (bs/zstd-output-stream output :level 12)] + (with-open [output (bs/data-output-stream output)] + (binding [*position* (atom 0)] + (write-export! (assoc cfg ::output output)) + path)))) + + (catch Throwable cause + (vreset! cs cause) + (throw cause)) + + (finally + (l/info :hint "exportation finished" :export-id id + :elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms") + :cause @cs))))) + +(defn import! + [{:keys [::input] :as cfg}] + (let [id (uuid/next) + ts (dt/now) + cs (volatile! nil)] + (try + (l/info :hint "start importation" :import-id id) + (binding [*position* (atom 0)] + (with-open [input (io/input-stream input)] + (read-import! (assoc cfg ::input input)))) + + (catch Throwable cause + (vreset! cs cause) + (throw cause)) + + (finally + (l/info :hint "importation finished" :import-id id + :elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms") + :error? (some? @cs) + :cause @cs))))) + +;; --- Command: export-binfile + +(s/def ::file-id ::us/uuid) +(s/def ::profile-id ::us/uuid) + +(s/def ::export-binfile + (s/keys :req-un [::profile-id ::file-id])) + +#_:clj-kondo/ignore +(sv/defmethod ::export-binfile + "Export a penpot file in a binary format." + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + {:hello "world"}) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index f52d4fcfa2..dba09a40a2 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -6,6 +6,7 @@ (ns app.rpc.mutations.files (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages :as cp] [app.common.pages.migrations :as pmg] @@ -63,21 +64,23 @@ (db/insert! conn :file-profile-rel)))) (defn create-file - [conn {:keys [id name project-id is-shared data deleted-at revn] - :or {is-shared false - revn 0 - deleted-at nil} + [conn {:keys [id name project-id is-shared data revn + modified-at deleted-at ignore-sync-until] + :or {is-shared false revn 0} :as params}] (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 - :revn revn - :is-shared is-shared - :data (blob/encode data) - :deleted-at deleted-at})] + (d/without-nils + {:id id + :project-id project-id + :name name + :revn revn + :is-shared is-shared + :data (blob/encode data) + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at}))] (->> (assoc params :file-id id :role :owner) (create-file-role conn)) diff --git a/backend/test/app/storage_test.clj b/backend/test/app/storage_test.clj index 832fbdc6fb..3a921c8aae 100644 --- a/backend/test/app/storage_test.clj +++ b/backend/test/app/storage_test.clj @@ -28,11 +28,11 @@ "Given storage map, returns a storage configured with the appropriate backend for assets." ([storage] - (assoc storage :backend :tmp)) + (assoc storage :backend :assets-fs)) ([storage conn] (-> storage (assoc :conn conn) - (assoc :backend :tmp)))) + (assoc :backend :assets-fs)))) (t/deftest put-and-retrieve-object (let [storage (-> (:app.storage/storage th/*system*) @@ -44,7 +44,7 @@ (t/is (sto/storage-object? object)) (t/is (fs/path? @(sto/get-object-path storage object))) (t/is (nil? (:expired-at object))) - (t/is (= :tmp (:backend object))) + (t/is (= :assets-fs (:backend object))) (t/is (= "data" (:other (meta object)))) (t/is (= "text/plain" (:content-type (meta object)))) (t/is (= "content" (slurp @(sto/get-object-data storage object)))) diff --git a/common/deps.edn b/common/deps.edn index b267ccefcf..370e6d2d9c 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -22,7 +22,7 @@ java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/promesa {:mvn/version "8.0.450"} - funcool/cuerdas {:mvn/version "2022.06.13-401"} + funcool/cuerdas {:mvn/version "2022.06.16-403"} lambdaisland/uri {:mvn/version "1.13.95" :exclusions [org.clojure/data.json]} diff --git a/common/src/app/common/pprint.cljc b/common/src/app/common/pprint.cljc index e95ad84f6f..78fc78b712 100644 --- a/common/src/app/common/pprint.cljc +++ b/common/src/app/common/pprint.cljc @@ -7,21 +7,16 @@ (ns app.common.pprint (:refer-clojure :exclude [prn]) (:require - [cuerdas.core :as str] [fipp.edn :as fpp])) (defn pprint-str - [expr] - (binding [*print-level* 8 - *print-length* 25] + [expr & {:keys [width level length] + :or {width 110 level 8 length 25}}] + (binding [*print-level* level + *print-length* length] (with-out-str - (fpp/pprint expr {:width 110})))) + (fpp/pprint expr {:width width})))) (defn pprint - ([expr] - (println (pprint-str expr))) - ([label expr] - (println (str/concat "============ " label "============")) - (pprint expr))) - - + [expr & {:as opts}] + (println (pprint-str expr opts))) From da2214379c305d8d80440efdfea417180a9ab5d5 Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 28 Jun 2022 07:31:27 +0200 Subject: [PATCH 46/86] :sparkles: Show shape name in right toolbar --- .../sidebar/options/menus/component.cljs | 31 ++++++------------- .../sidebar/options/shapes/group.cljs | 2 +- 2 files changed, 11 insertions(+), 22 deletions(-) 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 fdd9da5369..0a28635ced 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,11 +6,9 @@ (ns app.main.ui.workspace.sidebar.options.menus.component (:require - [app.common.pages.helpers :as cph] [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.components.context-menu :refer [context-menu]] [app.main.ui.context :as ctx] @@ -22,7 +20,7 @@ (def component-attrs [:component-id :component-file :shape-ref]) (mf/defc component-menu - [{:keys [ids values] :as props}] + [{:keys [ids values shape-name] :as props}] (let [current-file-id (mf/use-ctx ctx/current-file-id) id (first ids) @@ -30,15 +28,6 @@ component-id (:component-id values) library-id (:component-file values) - - local-file (deref refs/workspace-local-library) - libraries (deref refs/workspace-libraries) - - ;; NOTE: this is necessary because the `cph/get-component` - ;; expects a map of all libraries, including the local one. - libraries (assoc libraries (:id local-file) {:data local-file}) - - component (cph/get-component libraries library-id component-id) show? (some? component-id) on-menu-click @@ -63,14 +52,14 @@ do-update-remote-component #(st/emit! (modal/show - {:type :confirm - :message "" - :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 do-update-component})) + {:type :confirm + :message "" + :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 do-update-component})) do-show-component #(st/emit! (dw/go-to-component component-id)) do-navigate-component-file #(st/emit! (dwl/nav-to-component-file library-id))] @@ -81,7 +70,7 @@ [:div.element-set-content [:div.row-flex.component-row i/component - (:name component) + shape-name [:div.row-actions {:on-click on-menu-click} i/actions 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 3f27507e26..705f6c48fe 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 @@ -49,7 +49,7 @@ [:div.options [:& measures-menu {:type type :ids measure-ids :values measure-values :shape shape}] - [:& component-menu {:ids comp-ids :values comp-values}] + [:& component-menu {:ids comp-ids :values comp-values :shape-name (:name shape)}] (when-not (empty? layout-ids) [:& layout-menu {:type type :ids layout-ids :values layout-values}]) (when has-layout-item From 6f94745aedab9ca9e2e85d6a9663da1b1d13cab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 9 Jun 2022 12:00:32 +0200 Subject: [PATCH 47/86] :recycle: Rename specs -> types NO FUNCTIONALITY IS CHANGED in this commit, only moving things around --- backend/src/app/srepl/main.clj | 1 - common/src/app/common/file_builder.cljc | 6 +- common/src/app/common/pages/changes.cljc | 8 +- .../change.cljc => pages/changes_spec.cljc} | 36 +-- common/src/app/common/pages/helpers.cljc | 4 +- .../src/app/common/path/shapes_to_path.cljc | 4 +- .../src/app/common/{spec => types}/color.cljc | 2 +- .../src/app/common/{spec => types}/file.cljc | 17 +- .../src/app/common/{spec => types}/page.cljc | 6 +- .../src/app/common/{spec => types}/shape.cljc | 38 +-- .../common/{spec => types/shape}/blur.cljc | 2 +- .../common/{spec => types/shape}/export.cljc | 4 +- .../{spec => types/shape}/interactions.cljc | 2 +- .../common/{spec => types/shape}/radius.cljc | 2 +- .../common/{spec => types/shape}/shadow.cljc | 6 +- .../common/{spec => types}/typography.cljc | 2 +- .../app/common/spec_interactions_test.cljc | 290 +++++++++--------- frontend/src/app/main/data/viewer.cljs | 12 +- frontend/src/app/main/data/workspace.cljs | 4 +- .../src/app/main/data/workspace/changes.cljs | 6 +- .../src/app/main/data/workspace/common.cljs | 14 +- .../src/app/main/data/workspace/guides.cljs | 6 +- .../app/main/data/workspace/interactions.cljs | 36 +-- .../app/main/data/workspace/libraries.cljs | 22 +- .../data/workspace/libraries_helpers.cljs | 6 +- .../main/data/workspace/notifications.cljs | 4 +- .../app/main/data/workspace/persistence.cljs | 8 +- .../app/main/data/workspace/selection.cljs | 8 +- .../src/app/main/data/workspace/undo.cljs | 6 +- frontend/src/app/main/ui/shapes/attrs.cljs | 6 +- .../ui/viewer/handoff/attributes/layout.cljs | 6 +- .../src/app/main/ui/viewer/interactions.cljs | 4 +- frontend/src/app/main/ui/viewer/shapes.cljs | 20 +- .../app/main/ui/workspace/context_menu.cljs | 4 +- .../sidebar/options/menus/interactions.cljs | 62 ++-- .../sidebar/options/menus/measures.cljs | 20 +- .../ui/workspace/viewport/interactions.cljs | 6 +- frontend/src/app/util/import/parser.cljs | 10 +- 38 files changed, 348 insertions(+), 352 deletions(-) rename common/src/app/common/{spec/change.cljc => pages/changes_spec.cljc} (86%) rename common/src/app/common/{spec => types}/color.cljc (99%) rename common/src/app/common/{spec => types}/file.cljc (80%) rename common/src/app/common/{spec => types}/page.cljc (96%) rename common/src/app/common/{spec => types}/shape.cljc (93%) rename common/src/app/common/{spec => types/shape}/blur.cljc (93%) rename common/src/app/common/{spec => types/shape}/export.cljc (93%) rename common/src/app/common/{spec => types/shape}/interactions.cljc (99%) rename common/src/app/common/{spec => types/shape}/radius.cljc (98%) rename common/src/app/common/{spec => types/shape}/shadow.cljc (91%) rename common/src/app/common/{spec => types}/typography.cljc (97%) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index e5455aab6d..47245e57f2 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -7,7 +7,6 @@ [app.common.logging :as l] [app.common.pages :as cp] [app.common.pages.migrations :as pmg] - [app.common.spec.file :as spec.file] [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 787e17c74d..882991a1f0 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -11,9 +11,9 @@ [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] [app.common.pages.changes :as ch] + [app.common.pages.changes-spec :as pcs] [app.common.pages.init :as init] [app.common.spec :as us] - [app.common.spec.change :as spec.change] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -44,9 +44,9 @@ :frame-id (:current-frame-id file)))] (when fail-on-spec? - (us/verify ::spec.change/change change)) + (us/verify ::pcs/change change)) - (let [valid? (us/valid? ::spec.change/change change)] + (let [valid? (us/valid? ::pcs/change change)] #?(:cljs (when-not valid? (.warn js/console "Invalid shape" (clj->js change)))) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 393791cd45..36a8a576ca 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -16,8 +16,8 @@ [app.common.pages.helpers :as cph] [app.common.pages.init :as init] [app.common.spec :as us] - [app.common.spec.change :as spec.change] - [app.common.spec.shape :as spec.shape])) + [app.common.pages.changes-spec :as pcs] + [app.common.types.shape :as cts])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Specific helpers @@ -49,7 +49,7 @@ ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice (when verify? - (us/assert ::spec.change/changes items)) + (us/assert ::pcs/changes items)) (let [result (reduce #(or (process-change %1 %2) %1) data items)] ;; Validate result shapes (only on the backend) @@ -59,7 +59,7 @@ (doseq [[id shape] (:objects page)] (when-not (= shape (get-in data [:pages-index page-id :objects id])) ;; If object has change verify is correct - (us/verify ::spec.shape/shape shape)))))) + (us/verify ::cts/shape shape)))))) result))) diff --git a/common/src/app/common/spec/change.cljc b/common/src/app/common/pages/changes_spec.cljc similarity index 86% rename from common/src/app/common/spec/change.cljc rename to common/src/app/common/pages/changes_spec.cljc index db9847b753..d53249e06b 100644 --- a/common/src/app/common/spec/change.cljc +++ b/common/src/app/common/pages/changes_spec.cljc @@ -4,14 +4,14 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.change +(ns app.common.pages.changes-spec (:require [app.common.spec :as us] - [app.common.spec.color :as color] - [app.common.spec.file :as file] - [app.common.spec.page :as page] - [app.common.spec.shape :as shape] - [app.common.spec.typography :as typg] + [app.common.types.color :as ctc] + [app.common.types.file :as ctf] + [app.common.types.page :as ctp] + [app.common.types.shape :as cts] + [app.common.types.typography :as ctt] [clojure.spec.alpha :as s])) (s/def ::index integer?) @@ -52,7 +52,7 @@ (s/keys :req-un [:internal.changes.set-option/option :internal.changes.set-option/value])) -(s/def :internal.changes.add-obj/obj ::shape/shape) +(s/def :internal.changes.add-obj/obj ::cts/shape) (defn- valid-container-id-frame? [o] @@ -89,18 +89,18 @@ valid-container-id?)) (defmethod change-spec :reg-objects [_] - (s/and (s/keys :req-un [::shape/shapes] + (s/and (s/keys :req-un [::cts/shapes] :opt-un [::page-id ::component-id]) valid-container-id?)) (defmethod change-spec :mov-objects [_] - (s/and (s/keys :req-un [::parent-id ::shape/shapes] + (s/and (s/keys :req-un [::parent-id ::cts/shapes] :opt-un [::page-id ::component-id ::index]) valid-container-id?)) (defmethod change-spec :add-page [_] (s/or :empty (s/keys :req-un [::id ::name]) - :complete (s/keys :req-un [::page/page]))) + :complete (s/keys :req-un [::ctp/page]))) (defmethod change-spec :mod-page [_] (s/keys :req-un [::id ::name])) @@ -112,21 +112,21 @@ (s/keys :req-un [::id ::index])) (defmethod change-spec :add-color [_] - (s/keys :req-un [::color/color])) + (s/keys :req-un [::ctc/color])) (defmethod change-spec :mod-color [_] - (s/keys :req-un [::color/color])) + (s/keys :req-un [::ctc/color])) (defmethod change-spec :del-color [_] (s/keys :req-un [::id])) -(s/def :internal.changes.add-recent-color/color ::color/recent-color) +(s/def :internal.changes.add-recent-color/color ::ctc/recent-color) (defmethod change-spec :add-recent-color [_] (s/keys :req-un [:internal.changes.add-recent-color/color])) -(s/def :internal.changes.add-media/object ::file/media-object) +(s/def :internal.changes.add-media/object ::ctf/media-object) (defmethod change-spec :add-media [_] (s/keys :req-un [:internal.changes.add-media/object])) @@ -149,7 +149,7 @@ (s/keys :req-un [::id])) (s/def :internal.changes.add-component/shapes - (s/coll-of ::shape/shape)) + (s/coll-of ::cts/shape)) (defmethod change-spec :add-component [_] (s/keys :req-un [::id ::name :internal.changes.add-component/shapes] @@ -163,13 +163,13 @@ (s/keys :req-un [::id])) (defmethod change-spec :add-typography [_] - (s/keys :req-un [::typg/typography])) + (s/keys :req-un [::ctt/typography])) (defmethod change-spec :mod-typography [_] - (s/keys :req-un [::typg/typography])) + (s/keys :req-un [::ctt/typography])) (defmethod change-spec :del-typography [_] - (s/keys :req-un [::typg/id])) + (s/keys :req-un [::ctt/id])) (s/def ::change (s/multi-spec change-spec :type)) (s/def ::changes (s/coll-of ::change)) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 1a6d12f2c4..043f6a3a79 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -11,7 +11,7 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.spec :as us] - [app.common.spec.page :as spec.page] + [app.common.types.page :as ctp] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -64,7 +64,7 @@ (defn get-shape [container shape-id] - (us/assert ::spec.page/container container) + (us/assert ::ctp/container container) (us/assert ::us/uuid shape-id) (-> container (get :objects) diff --git a/common/src/app/common/path/shapes_to_path.cljc b/common/src/app/common/path/shapes_to_path.cljc index 37bc286a27..d4adbccdb8 100644 --- a/common/src/app/common/path/shapes_to_path.cljc +++ b/common/src/app/common/path/shapes_to_path.cljc @@ -15,7 +15,7 @@ [app.common.geom.shapes.path :as gsp] [app.common.path.bool :as pb] [app.common.path.commands :as pc] - [app.common.spec.radius :as ctr])) + [app.common.types.shape.radius :as ctsr])) (def ^:const bezier-circle-c 0.551915024494) @@ -152,7 +152,7 @@ (defn rect->path "Creates a bezier curve that approximates a rounded corner rectangle" [{:keys [x y width height] :as shape}] - (case (ctr/radius-mode shape) + (case (ctsr/radius-mode shape) :radius-1 (let [radius (gso/shape-corners-1 shape)] (draw-rounded-rect-path x y width height radius)) diff --git a/common/src/app/common/spec/color.cljc b/common/src/app/common/types/color.cljc similarity index 99% rename from common/src/app/common/spec/color.cljc rename to common/src/app/common/types/color.cljc index 8fbedb11b0..674de1cc2c 100644 --- a/common/src/app/common/spec/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.color +(ns app.common.types.color (:require [app.common.data :as d] [app.common.spec :as us] diff --git a/common/src/app/common/spec/file.cljc b/common/src/app/common/types/file.cljc similarity index 80% rename from common/src/app/common/spec/file.cljc rename to common/src/app/common/types/file.cljc index 2affdafbda..5c7475d609 100644 --- a/common/src/app/common/spec/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -4,12 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.file +(ns app.common.types.file (:require [app.common.spec :as us] - [app.common.spec.color :as color] - [app.common.spec.page :as page] - [app.common.spec.typography] + [app.common.types.color :as ctc] + [app.common.types.page :as ctp] [clojure.spec.alpha :as s])) (s/def :internal.media-object/name string?) @@ -31,13 +30,13 @@ :opt-un [:internal.media-object/path])) (s/def ::colors - (s/map-of uuid? ::color/color)) + (s/map-of uuid? ::ctc/color)) (s/def ::recent-colors - (s/coll-of ::color/recent-color :kind vector?)) + (s/coll-of ::ctc/recent-color :kind vector?)) (s/def ::typographies - (s/map-of uuid? :app.common.spec.typography/typography)) + (s/map-of uuid? :ctst/typography)) (s/def ::pages (s/coll-of uuid? :kind vector?)) @@ -46,10 +45,10 @@ (s/map-of uuid? ::media-object)) (s/def ::pages-index - (s/map-of uuid? ::page/page)) + (s/map-of uuid? ::ctp/page)) (s/def ::components - (s/map-of uuid? ::page/container)) + (s/map-of uuid? ::ctp/container)) (s/def ::data (s/keys :req-un [::pages-index diff --git a/common/src/app/common/spec/page.cljc b/common/src/app/common/types/page.cljc similarity index 96% rename from common/src/app/common/spec/page.cljc rename to common/src/app/common/types/page.cljc index 135eadb1eb..0ed513dc0d 100644 --- a/common/src/app/common/spec/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -4,11 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.page +(ns app.common.types.page (:require [app.common.data :as d] [app.common.spec :as us] - [app.common.spec.shape :as shape] + [app.common.types.shape :as cts] [clojure.spec.alpha :as s])) ;; --- Grid options @@ -90,7 +90,7 @@ (s/def ::id uuid?) (s/def ::name string?) -(s/def ::objects (s/map-of uuid? ::shape/shape)) +(s/def ::objects (s/map-of uuid? ::cts/shape)) (s/def ::page (s/keys :req-un [::id ::name ::objects ::options])) diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/types/shape.cljc similarity index 93% rename from common/src/app/common/spec/shape.cljc rename to common/src/app/common/types/shape.cljc index 0c1deb156b..317e4af786 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -4,17 +4,17 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.shape +(ns app.common.types.shape (:require [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.spec :as us] - [app.common.spec.blur :as blur] - [app.common.spec.color :as color] - [app.common.spec.export :as export] - [app.common.spec.interactions :as cti] - [app.common.spec.radius :as radius] - [app.common.spec.shadow :as shadow] + [app.common.types.color :as ctc] + [app.common.types.shape.blur :as ctsb] + [app.common.types.shape.export :as ctse] + [app.common.types.shape.interactions :as ctsi] + [app.common.types.shape.radius :as ctsr] + [app.common.types.shape.shadow :as ctss] [clojure.set :as set] [clojure.spec.alpha :as s])) @@ -47,7 +47,7 @@ (s/def ::fill-color string?) (s/def ::fill-opacity ::us/safe-number) -(s/def ::fill-color-gradient (s/nilable ::color/gradient)) +(s/def ::fill-color-gradient (s/nilable ::ctc/gradient)) (s/def ::fill-color-ref-file (s/nilable uuid?)) (s/def ::fill-color-ref-id (s/nilable uuid?)) @@ -69,7 +69,7 @@ (s/def ::proportion ::us/safe-number) (s/def ::proportion-lock boolean?) (s/def ::stroke-color string?) -(s/def ::stroke-color-gradient (s/nilable ::color/gradient)) +(s/def ::stroke-color-gradient (s/nilable ::ctc/gradient)) (s/def ::stroke-color-ref-file (s/nilable uuid?)) (s/def ::stroke-color-ref-id (s/nilable uuid?)) (s/def ::stroke-opacity ::us/safe-number) @@ -102,7 +102,7 @@ (s/keys :req-un [::x ::y ::x1 ::y1 ::x2 ::y2 ::width ::height])) (s/def ::exports - (s/coll-of ::export/export :kind vector?)) + (s/coll-of ::ctse/export :kind vector?)) (s/def ::points (s/every ::gpt/point :kind vector?)) @@ -187,12 +187,12 @@ ::constraints-h ::constraints-v ::fixed-scroll - ::radius/rx - ::radius/ry - ::radius/r1 - ::radius/r2 - ::radius/r3 - ::radius/r4 + ::ctsr/rx + ::ctsr/ry + ::ctsr/r1 + ::ctsr/r2 + ::ctsr/r3 + ::ctsr/r4 ::x ::y ::exports @@ -213,9 +213,9 @@ ::width ::height ::masked-group? - ::cti/interactions - ::shadow/shadow - ::blur/blur + ::ctsi/interactions + ::ctss/shadow + ::ctsb/blur ::opacity ::blend-mode])) diff --git a/common/src/app/common/spec/blur.cljc b/common/src/app/common/types/shape/blur.cljc similarity index 93% rename from common/src/app/common/spec/blur.cljc rename to common/src/app/common/types/shape/blur.cljc index 04b643e896..03257d8f9c 100644 --- a/common/src/app/common/spec/blur.cljc +++ b/common/src/app/common/types/shape/blur.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.blur +(ns app.common.types.shape.blur (:require [app.common.spec :as us] [clojure.spec.alpha :as s])) diff --git a/common/src/app/common/spec/export.cljc b/common/src/app/common/types/shape/export.cljc similarity index 93% rename from common/src/app/common/spec/export.cljc rename to common/src/app/common/types/shape/export.cljc index acfe7b2791..057d575e09 100644 --- a/common/src/app/common/spec/export.cljc +++ b/common/src/app/common/types/shape/export.cljc @@ -4,12 +4,11 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.export +(ns app.common.types.shape.export (:require [app.common.spec :as us] [clojure.spec.alpha :as s])) - (s/def ::suffix string?) (s/def ::scale ::us/safe-number) (s/def ::type keyword?) @@ -19,4 +18,3 @@ ::suffix ::scale])) - diff --git a/common/src/app/common/spec/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc similarity index 99% rename from common/src/app/common/spec/interactions.cljc rename to common/src/app/common/types/shape/interactions.cljc index eb3c2171dc..c28f40d89f 100644 --- a/common/src/app/common/spec/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.interactions +(ns app.common.types.shape.interactions (:require [app.common.data :as d] [app.common.geom.point :as gpt] diff --git a/common/src/app/common/spec/radius.cljc b/common/src/app/common/types/shape/radius.cljc similarity index 98% rename from common/src/app/common/spec/radius.cljc rename to common/src/app/common/types/shape/radius.cljc index bbc077deae..c93189e948 100644 --- a/common/src/app/common/spec/radius.cljc +++ b/common/src/app/common/types/shape/radius.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.radius +(ns app.common.types.shape.radius (:require [app.common.pages.common :refer [editable-attrs]] [app.common.spec :as us] diff --git a/common/src/app/common/spec/shadow.cljc b/common/src/app/common/types/shape/shadow.cljc similarity index 91% rename from common/src/app/common/spec/shadow.cljc rename to common/src/app/common/types/shape/shadow.cljc index 5b4bd553af..e2e92e5868 100644 --- a/common/src/app/common/spec/shadow.cljc +++ b/common/src/app/common/types/shape/shadow.cljc @@ -4,10 +4,10 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.shadow +(ns app.common.types.shape.shadow (:require [app.common.spec :as us] - [app.common.spec.color :as color] + [app.common.types.color :as ctc] [clojure.spec.alpha :as s])) @@ -24,7 +24,7 @@ (s/def ::color string?) (s/def ::opacity ::us/safe-number) -(s/def ::gradient (s/nilable ::color/gradient)) +(s/def ::gradient (s/nilable ::ctc/gradient)) (s/def ::file-id (s/nilable uuid?)) (s/def ::ref-id (s/nilable uuid?)) diff --git a/common/src/app/common/spec/typography.cljc b/common/src/app/common/types/typography.cljc similarity index 97% rename from common/src/app/common/spec/typography.cljc rename to common/src/app/common/types/typography.cljc index 51c54a5171..ff63bf14be 100644 --- a/common/src/app/common/spec/typography.cljc +++ b/common/src/app/common/types/typography.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.common.spec.typography +(ns app.common.types.typography (:require [clojure.spec.alpha :as s])) diff --git a/common/test/app/common/spec_interactions_test.cljc b/common/test/app/common/spec_interactions_test.cljc index d874268955..7c90b625c8 100644 --- a/common/test/app/common/spec_interactions_test.cljc +++ b/common/test/app/common/spec_interactions_test.cljc @@ -10,62 +10,62 @@ [clojure.pprint :refer [pprint]] [app.common.exceptions :as ex] [app.common.pages.init :as cpi] - [app.common.spec.interactions :as csi] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.common.geom.point :as gpt])) (t/deftest set-event-type - (let [interaction csi/default-interaction + (let [interaction ctsi/default-interaction shape (cpi/make-minimal-shape :rect) frame (cpi/make-minimal-shape :frame)] (t/testing "Set event type unchanged" (let [new-interaction - (csi/set-event-type interaction :click shape)] + (ctsi/set-event-type interaction :click shape)] (t/is (= :click (:event-type new-interaction))))) (t/testing "Set event type changed" (let [new-interaction - (csi/set-event-type interaction :mouse-press shape)] + (ctsi/set-event-type interaction :mouse-press shape)] (t/is (= :mouse-press (:event-type new-interaction))))) (t/testing "Set after delay on non-frame" (let [result (ex/try - (csi/set-event-type interaction :after-delay shape))] + (ctsi/set-event-type interaction :after-delay shape))] (t/is (ex/exception? result)))) (t/testing "Set after delay on frame" (let [new-interaction - (csi/set-event-type interaction :after-delay frame)] + (ctsi/set-event-type interaction :after-delay frame)] (t/is (= :after-delay (:event-type new-interaction))) (t/is (= 600 (:delay new-interaction))))) (t/testing "Set after delay with previous data" (let [interaction (assoc interaction :delay 300) new-interaction - (csi/set-event-type interaction :after-delay frame)] + (ctsi/set-event-type interaction :after-delay frame)] (t/is (= :after-delay (:event-type new-interaction))) (t/is (= 300 (:delay new-interaction))))))) (t/deftest set-action-type - (let [interaction csi/default-interaction] + (let [interaction ctsi/default-interaction] (t/testing "Set action type unchanged" (let [new-interaction - (csi/set-action-type interaction :navigate)] + (ctsi/set-action-type interaction :navigate)] (t/is (= :navigate (:action-type new-interaction))))) (t/testing "Set action type changed" (let [new-interaction - (csi/set-action-type interaction :prev-screen)] + (ctsi/set-action-type interaction :prev-screen)] (t/is (= :prev-screen (:action-type new-interaction))))) (t/testing "Set action type navigate" (let [interaction {:event-type :click :action-type :prev-screen} new-interaction - (csi/set-action-type interaction :navigate)] + (ctsi/set-action-type interaction :navigate)] (t/is (= :navigate (:action-type new-interaction))) (t/is (nil? (:destination new-interaction))) (t/is (= false (:preserve-scroll new-interaction))))) @@ -77,14 +77,14 @@ :destination destination :preserve-scroll true} new-interaction - (csi/set-action-type interaction :navigate)] + (ctsi/set-action-type interaction :navigate)] (t/is (= :navigate (:action-type new-interaction))) (t/is (= destination (:destination new-interaction))) (t/is (= true (:preserve-scroll new-interaction))))) (t/testing "Set action type open-overlay" (let [new-interaction - (csi/set-action-type interaction :open-overlay)] + (ctsi/set-action-type interaction :open-overlay)] (t/is (= :open-overlay (:action-type new-interaction))) (t/is (= :center (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) @@ -93,14 +93,14 @@ (let [interaction (assoc interaction :overlay-pos-type :top-left :overlay-position (gpt/point 100 200)) new-interaction - (csi/set-action-type interaction :open-overlay)] + (ctsi/set-action-type interaction :open-overlay)] (t/is (= :open-overlay (:action-type new-interaction))) (t/is (= :top-left (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 100 200) (:overlay-position new-interaction))))) (t/testing "Set action type toggle-overlay" (let [new-interaction - (csi/set-action-type interaction :toggle-overlay)] + (ctsi/set-action-type interaction :toggle-overlay)] (t/is (= :toggle-overlay (:action-type new-interaction))) (t/is (= :center (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) @@ -109,14 +109,14 @@ (let [interaction (assoc interaction :overlay-pos-type :top-left :overlay-position (gpt/point 100 200)) new-interaction - (csi/set-action-type interaction :toggle-overlay)] + (ctsi/set-action-type interaction :toggle-overlay)] (t/is (= :toggle-overlay (:action-type new-interaction))) (t/is (= :top-left (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 100 200) (:overlay-position new-interaction))))) (t/testing "Set action type close-overlay" (let [new-interaction - (csi/set-action-type interaction :close-overlay)] + (ctsi/set-action-type interaction :close-overlay)] (t/is (= :close-overlay (:action-type new-interaction))) (t/is (nil? (:destination new-interaction))))) @@ -124,89 +124,89 @@ (let [destination (uuid/next) interaction (assoc interaction :destination destination) new-interaction - (csi/set-action-type interaction :close-overlay)] + (ctsi/set-action-type interaction :close-overlay)] (t/is (= :close-overlay (:action-type new-interaction))) (t/is (= destination (:destination new-interaction))))) (t/testing "Set action type prev-screen" (let [new-interaction - (csi/set-action-type interaction :prev-screen)] + (ctsi/set-action-type interaction :prev-screen)] (t/is (= :prev-screen (:action-type new-interaction))))) (t/testing "Set action type open-url" (let [new-interaction - (csi/set-action-type interaction :open-url)] + (ctsi/set-action-type interaction :open-url)] (t/is (= :open-url (:action-type new-interaction))) (t/is (= "" (:url new-interaction))))) (t/testing "Set action type open-url with previous data" (let [interaction (assoc interaction :url "https://example.com") new-interaction - (csi/set-action-type interaction :open-url)] + (ctsi/set-action-type interaction :open-url)] (t/is (= :open-url (:action-type new-interaction))) (t/is (= "https://example.com" (:url new-interaction))))))) (t/deftest option-delay (let [frame (cpi/make-minimal-shape :frame) - i1 csi/default-interaction - i2 (csi/set-event-type i1 :after-delay frame)] + i1 ctsi/default-interaction + i2 (ctsi/set-event-type i1 :after-delay frame)] (t/testing "Has delay" - (t/is (not (csi/has-delay i1))) - (t/is (csi/has-delay i2))) + (t/is (not (ctsi/has-delay i1))) + (t/is (ctsi/has-delay i2))) (t/testing "Set delay" - (let [new-interaction (csi/set-delay i2 1000)] + (let [new-interaction (ctsi/set-delay i2 1000)] (t/is (= 1000 (:delay new-interaction))))))) (t/deftest option-destination (let [destination (uuid/next) - i1 csi/default-interaction - i2 (csi/set-action-type i1 :prev-screen) - i3 (csi/set-action-type i1 :open-overlay)] + i1 ctsi/default-interaction + i2 (ctsi/set-action-type i1 :prev-screen) + i3 (ctsi/set-action-type i1 :open-overlay)] (t/testing "Has destination" - (t/is (csi/has-destination i1)) - (t/is (not (csi/has-destination i2)))) + (t/is (ctsi/has-destination i1)) + (t/is (not (ctsi/has-destination i2)))) (t/testing "Set destination" - (let [new-interaction (csi/set-destination i1 destination)] + (let [new-interaction (ctsi/set-destination i1 destination)] (t/is (= destination (:destination new-interaction))) (t/is (nil? (:overlay-pos-type new-interaction))) (t/is (nil? (:overlay-position new-interaction))))) (t/testing "Set destination of overlay" - (let [new-interaction (csi/set-destination i3 destination)] + (let [new-interaction (ctsi/set-destination i3 destination)] (t/is (= destination (:destination new-interaction))) (t/is (= :center (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))))) (t/deftest option-preserve-scroll - (let [i1 csi/default-interaction - i2 (csi/set-action-type i1 :prev-screen)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-action-type i1 :prev-screen)] (t/testing "Has preserve-scroll" - (t/is (csi/has-preserve-scroll i1)) - (t/is (not (csi/has-preserve-scroll i2)))) + (t/is (ctsi/has-preserve-scroll i1)) + (t/is (not (ctsi/has-preserve-scroll i2)))) (t/testing "Set preserve-scroll" - (let [new-interaction (csi/set-preserve-scroll i1 true)] + (let [new-interaction (ctsi/set-preserve-scroll i1 true)] (t/is (= true (:preserve-scroll new-interaction))))))) (t/deftest option-url - (let [i1 csi/default-interaction - i2 (csi/set-action-type i1 :open-url)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-action-type i1 :open-url)] (t/testing "Has url" - (t/is (not (csi/has-url i1))) - (t/is (csi/has-url i2))) + (t/is (not (ctsi/has-url i1))) + (t/is (ctsi/has-url i2))) (t/testing "Set url" - (let [new-interaction (csi/set-url i2 "https://example.com")] + (let [new-interaction (ctsi/set-url i2 "https://example.com")] (t/is (= "https://example.com" (:url new-interaction))))))) @@ -220,35 +220,35 @@ objects {(:id base-frame) base-frame (:id overlay-frame) overlay-frame} - i1 csi/default-interaction - i2 (csi/set-action-type i1 :open-overlay) + i1 ctsi/default-interaction + i2 (ctsi/set-action-type i1 :open-overlay) i3 (-> i1 - (csi/set-action-type :open-overlay) - (csi/set-destination (:id overlay-frame)))] + (ctsi/set-action-type :open-overlay) + (ctsi/set-destination (:id overlay-frame)))] (t/testing "Has overlay options" - (t/is (not (csi/has-overlay-opts i1))) - (t/is (csi/has-overlay-opts i2))) + (t/is (not (ctsi/has-overlay-opts i1))) + (t/is (ctsi/has-overlay-opts i2))) (t/testing "Set overlay-pos-type without destination" - (let [new-interaction (csi/set-overlay-pos-type i2 :top-right base-frame objects)] + (let [new-interaction (ctsi/set-overlay-pos-type i2 :top-right base-frame objects)] (t/is (= :top-right (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) (t/testing "Set overlay-pos-type with destination and auto" - (let [new-interaction (csi/set-overlay-pos-type i3 :bottom-right base-frame objects)] + (let [new-interaction (ctsi/set-overlay-pos-type i3 :bottom-right base-frame objects)] (t/is (= :bottom-right (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) (t/testing "Set overlay-pos-type with destination and manual" - (let [new-interaction (csi/set-overlay-pos-type i3 :manual base-frame objects)] + (let [new-interaction (ctsi/set-overlay-pos-type i3 :manual base-frame objects)] (t/is (= :manual (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 35 40) (:overlay-position new-interaction))))) (t/testing "Toggle overlay-pos-type" - (let [new-interaction (csi/toggle-overlay-pos-type i3 :center base-frame objects) - new-interaction-2 (csi/toggle-overlay-pos-type new-interaction :center base-frame objects) - new-interaction-3 (csi/toggle-overlay-pos-type new-interaction-2 :top-right base-frame objects)] + (let [new-interaction (ctsi/toggle-overlay-pos-type i3 :center base-frame objects) + new-interaction-2 (ctsi/toggle-overlay-pos-type new-interaction :center base-frame objects) + new-interaction-3 (ctsi/toggle-overlay-pos-type new-interaction-2 :top-right base-frame objects)] (t/is (= :manual (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 35 40) (:overlay-position new-interaction))) (t/is (= :center (:overlay-pos-type new-interaction-2))) @@ -257,73 +257,73 @@ (t/is (= (gpt/point 0 0) (:overlay-position new-interaction-3))))) (t/testing "Set overlay-position" - (let [new-interaction (csi/set-overlay-position i3 (gpt/point 50 60))] + (let [new-interaction (ctsi/set-overlay-position i3 (gpt/point 50 60))] (t/is (= :manual (:overlay-pos-type new-interaction))) (t/is (= (gpt/point 50 60) (:overlay-position new-interaction))))) (t/testing "Set close-click-outside" - (let [new-interaction (csi/set-close-click-outside i3 true)] + (let [new-interaction (ctsi/set-close-click-outside i3 true)] (t/is (not (:close-click-outside i3))) (t/is (:close-click-outside new-interaction)))) (t/testing "Set background-overlay" - (let [new-interaction (csi/set-background-overlay i3 true)] + (let [new-interaction (ctsi/set-background-overlay i3 true)] (t/is (not (:background-overlay i3))) (t/is (:background-overlay new-interaction)))))) (t/deftest animation-checks - (let [i1 csi/default-interaction - i2 (csi/set-action-type i1 :open-overlay) - i3 (csi/set-action-type i1 :toggle-overlay) - i4 (csi/set-action-type i1 :close-overlay) - i5 (csi/set-action-type i1 :prev-screen) - i6 (csi/set-action-type i1 :open-url)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-action-type i1 :open-overlay) + i3 (ctsi/set-action-type i1 :toggle-overlay) + i4 (ctsi/set-action-type i1 :close-overlay) + i5 (ctsi/set-action-type i1 :prev-screen) + i6 (ctsi/set-action-type i1 :open-url)] (t/testing "Has animation?" - (t/is (csi/has-animation? i1)) - (t/is (csi/has-animation? i2)) - (t/is (csi/has-animation? i3)) - (t/is (csi/has-animation? i4)) - (t/is (not (csi/has-animation? i5))) - (t/is (not (csi/has-animation? i6)))) + (t/is (ctsi/has-animation? i1)) + (t/is (ctsi/has-animation? i2)) + (t/is (ctsi/has-animation? i3)) + (t/is (ctsi/has-animation? i4)) + (t/is (not (ctsi/has-animation? i5))) + (t/is (not (ctsi/has-animation? i6)))) (t/testing "Valid push?" - (t/is (csi/allow-push? (:action-type i1))) - (t/is (not (csi/allow-push? (:action-type i2)))) - (t/is (not (csi/allow-push? (:action-type i3)))) - (t/is (not (csi/allow-push? (:action-type i4)))) - (t/is (not (csi/allow-push? (:action-type i5)))) - (t/is (not (csi/allow-push? (:action-type i6))))))) + (t/is (ctsi/allow-push? (:action-type i1))) + (t/is (not (ctsi/allow-push? (:action-type i2)))) + (t/is (not (ctsi/allow-push? (:action-type i3)))) + (t/is (not (ctsi/allow-push? (:action-type i4)))) + (t/is (not (ctsi/allow-push? (:action-type i5)))) + (t/is (not (ctsi/allow-push? (:action-type i6))))))) (t/deftest set-animation-type - (let [i1 csi/default-interaction - i2 (csi/set-animation-type i1 :dissolve)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type i1 :dissolve)] (t/testing "Set animation type nil" (let [new-interaction - (csi/set-animation-type i1 nil)] + (ctsi/set-animation-type i1 nil)] (t/is (nil? (-> new-interaction :animation :animation-type))))) (t/testing "Set animation type unchanged" (let [new-interaction - (csi/set-animation-type i2 :dissolve)] + (ctsi/set-animation-type i2 :dissolve)] (t/is (= :dissolve (-> new-interaction :animation :animation-type))))) (t/testing "Set animation type changed" (let [new-interaction - (csi/set-animation-type i2 :slide)] + (ctsi/set-animation-type i2 :slide)] (t/is (= :slide (-> new-interaction :animation :animation-type))))) (t/testing "Set animation type reset" (let [new-interaction - (csi/set-animation-type i2 nil)] + (ctsi/set-animation-type i2 nil)] (t/is (nil? (-> new-interaction :animation))))) (t/testing "Set animation type dissolve" (let [new-interaction - (csi/set-animation-type i1 :dissolve)] + (ctsi/set-animation-type i1 :dissolve)] (t/is (= :dissolve (-> new-interaction :animation :animation-type))) (t/is (= 300 (-> new-interaction :animation :duration))) (t/is (= :linear (-> new-interaction :animation :easing))))) @@ -336,14 +336,14 @@ :direction :left :offset-effect true}) new-interaction - (csi/set-animation-type interaction :dissolve)] + (ctsi/set-animation-type interaction :dissolve)] (t/is (= :dissolve (-> new-interaction :animation :animation-type))) (t/is (= 1000 (-> new-interaction :animation :duration))) (t/is (= :ease-out (-> new-interaction :animation :easing))))) (t/testing "Set animation type slide" (let [new-interaction - (csi/set-animation-type i1 :slide)] + (ctsi/set-animation-type i1 :slide)] (t/is (= :slide (-> new-interaction :animation :animation-type))) (t/is (= 300 (-> new-interaction :animation :duration))) (t/is (= :linear (-> new-interaction :animation :easing))) @@ -359,7 +359,7 @@ :direction :left :offset-effect true}) new-interaction - (csi/set-animation-type interaction :slide)] + (ctsi/set-animation-type interaction :slide)] (t/is (= :slide (-> new-interaction :animation :animation-type))) (t/is (= 1000 (-> new-interaction :animation :duration))) (t/is (= :ease-out (-> new-interaction :animation :easing))) @@ -369,7 +369,7 @@ (t/testing "Set animation type push" (let [new-interaction - (csi/set-animation-type i1 :push)] + (ctsi/set-animation-type i1 :push)] (t/is (= :push (-> new-interaction :animation :animation-type))) (t/is (= 300 (-> new-interaction :animation :duration))) (t/is (= :linear (-> new-interaction :animation :easing))) @@ -383,7 +383,7 @@ :direction :left :offset-effect true}) new-interaction - (csi/set-animation-type interaction :push)] + (ctsi/set-animation-type interaction :push)] (t/is (= :push (-> new-interaction :animation :animation-type))) (t/is (= 1000 (-> new-interaction :animation :duration))) (t/is (= :ease-out (-> new-interaction :animation :easing))) @@ -391,9 +391,9 @@ (t/deftest allowed-animation - (let [i1 (csi/set-action-type csi/default-interaction :open-overlay) - i2 (csi/set-action-type csi/default-interaction :close-overlay) - i3 (csi/set-action-type csi/default-interaction :toggle-overlay)] + (let [i1 (ctsi/set-action-type ctsi/default-interaction :open-overlay) + i2 (ctsi/set-action-type ctsi/default-interaction :close-overlay) + i3 (ctsi/set-action-type ctsi/default-interaction :toggle-overlay)] (t/testing "Cannot use animation push for an overlay action" (let [bad-interaction-1 (assoc i1 :animation {:animation-type :push @@ -408,72 +408,72 @@ :duration 1000 :easing :ease-out :direction :left})] - (t/is (not (csi/allowed-animation? (:action-type bad-interaction-1) + (t/is (not (ctsi/allowed-animation? (:action-type bad-interaction-1) (-> bad-interaction-1 :animation :animation-type)))) - (t/is (not (csi/allowed-animation? (:action-type bad-interaction-2) + (t/is (not (ctsi/allowed-animation? (:action-type bad-interaction-2) (-> bad-interaction-1 :animation :animation-type)))) - (t/is (not (csi/allowed-animation? (:action-type bad-interaction-3) + (t/is (not (ctsi/allowed-animation? (:action-type bad-interaction-3) (-> bad-interaction-1 :animation :animation-type)))))) (t/testing "Remove animation if moving to an forbidden state" - (let [interaction (csi/set-animation-type csi/default-interaction :push) - new-interaction (csi/set-action-type interaction :open-overlay)] + (let [interaction (ctsi/set-animation-type ctsi/default-interaction :push) + new-interaction (ctsi/set-action-type interaction :open-overlay)] (t/is (nil? (:animation new-interaction))))))) (t/deftest option-duration - (let [i1 csi/default-interaction - i2 (csi/set-animation-type csi/default-interaction :dissolve)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type ctsi/default-interaction :dissolve)] (t/testing "Has duration?" - (t/is (not (csi/has-duration? i1))) - (t/is (csi/has-duration? i2))) + (t/is (not (ctsi/has-duration? i1))) + (t/is (ctsi/has-duration? i2))) (t/testing "Set duration" - (let [new-interaction (csi/set-duration i2 1000)] + (let [new-interaction (ctsi/set-duration i2 1000)] (t/is (= 1000 (-> new-interaction :animation :duration))))))) (t/deftest option-easing - (let [i1 csi/default-interaction - i2 (csi/set-animation-type csi/default-interaction :dissolve)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type ctsi/default-interaction :dissolve)] (t/testing "Has easing?" - (t/is (not (csi/has-easing? i1))) - (t/is (csi/has-easing? i2))) + (t/is (not (ctsi/has-easing? i1))) + (t/is (ctsi/has-easing? i2))) (t/testing "Set easing" - (let [new-interaction (csi/set-easing i2 :ease-in)] + (let [new-interaction (ctsi/set-easing i2 :ease-in)] (t/is (= :ease-in (-> new-interaction :animation :easing))))))) (t/deftest option-way - (let [i1 csi/default-interaction - i2 (csi/set-animation-type csi/default-interaction :slide) - i3 (csi/set-action-type i2 :open-overlay)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type ctsi/default-interaction :slide) + i3 (ctsi/set-action-type i2 :open-overlay)] (t/testing "Has way?" - (t/is (not (csi/has-way? i1))) - (t/is (csi/has-way? i2)) - (t/is (not (csi/has-way? i3))) + (t/is (not (ctsi/has-way? i1))) + (t/is (ctsi/has-way? i2)) + (t/is (not (ctsi/has-way? i3))) (t/is (some? (-> i3 :animation :way)))) ; <- it exists but is ignored (t/testing "Set way" - (let [new-interaction (csi/set-way i2 :out)] + (let [new-interaction (ctsi/set-way i2 :out)] (t/is (= :out (-> new-interaction :animation :way))))))) (t/deftest option-direction - (let [i1 csi/default-interaction - i2 (csi/set-animation-type csi/default-interaction :push) - i3 (csi/set-animation-type csi/default-interaction :dissolve)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type ctsi/default-interaction :push) + i3 (ctsi/set-animation-type ctsi/default-interaction :dissolve)] (t/testing "Has direction?" - (t/is (not (csi/has-direction? i1))) - (t/is (csi/has-direction? i2))) + (t/is (not (ctsi/has-direction? i1))) + (t/is (ctsi/has-direction? i2))) (t/testing "Set direction" - (let [new-interaction (csi/set-direction i2 :left)] + (let [new-interaction (ctsi/set-direction i2 :left)] (t/is (= :left (-> new-interaction :animation :direction))))) (t/testing "Invert direction" @@ -483,12 +483,12 @@ a-up (assoc a-right :direction :up) a-down (assoc a-right :direction :down) - a-nil' (csi/invert-direction nil) - a-none' (csi/invert-direction a-none) - a-right' (csi/invert-direction a-right) - a-left' (csi/invert-direction a-left) - a-up' (csi/invert-direction a-up) - a-down' (csi/invert-direction a-down)] + a-nil' (ctsi/invert-direction nil) + a-none' (ctsi/invert-direction a-none) + a-right' (ctsi/invert-direction a-right) + a-left' (ctsi/invert-direction a-left) + a-up' (ctsi/invert-direction a-up) + a-down' (ctsi/invert-direction a-down)] (t/is (nil? a-nil')) (t/is (nil? (:direction a-none'))) @@ -499,44 +499,44 @@ (t/deftest option-offset-effect - (let [i1 csi/default-interaction - i2 (csi/set-animation-type csi/default-interaction :slide) - i3 (csi/set-action-type i2 :open-overlay)] + (let [i1 ctsi/default-interaction + i2 (ctsi/set-animation-type ctsi/default-interaction :slide) + i3 (ctsi/set-action-type i2 :open-overlay)] (t/testing "Has offset-effect" - (t/is (not (csi/has-offset-effect? i1))) - (t/is (csi/has-offset-effect? i2)) - (t/is (not (csi/has-offset-effect? i3))) + (t/is (not (ctsi/has-offset-effect? i1))) + (t/is (ctsi/has-offset-effect? i2)) + (t/is (not (ctsi/has-offset-effect? i3))) (t/is (some? (-> i3 :animation :offset-effect)))) ; <- it exists but is ignored (t/testing "Set offset-effect" - (let [new-interaction (csi/set-offset-effect i2 true)] + (let [new-interaction (ctsi/set-offset-effect i2 true)] (t/is (= true (-> new-interaction :animation :offset-effect))))))) (t/deftest modify-interactions - (let [i1 (csi/set-action-type csi/default-interaction :open-overlay) - i2 (csi/set-action-type csi/default-interaction :close-overlay) - i3 (csi/set-action-type csi/default-interaction :prev-screen) + (let [i1 (ctsi/set-action-type ctsi/default-interaction :open-overlay) + i2 (ctsi/set-action-type ctsi/default-interaction :close-overlay) + i3 (ctsi/set-action-type ctsi/default-interaction :prev-screen) interactions [i1 i2]] (t/testing "Add interaction to nil" - (let [new-interactions (csi/add-interaction nil i3)] + (let [new-interactions (ctsi/add-interaction nil i3)] (t/is (= (count new-interactions) 1)) (t/is (= (:action-type (last new-interactions)) :prev-screen)))) (t/testing "Add interaction to normal" - (let [new-interactions (csi/add-interaction interactions i3)] + (let [new-interactions (ctsi/add-interaction interactions i3)] (t/is (= (count new-interactions) 3)) (t/is (= (:action-type (last new-interactions)) :prev-screen)))) (t/testing "Remove interaction" - (let [new-interactions (csi/remove-interaction interactions 0)] + (let [new-interactions (ctsi/remove-interaction interactions 0)] (t/is (= (count new-interactions) 1)) (t/is (= (:action-type (last new-interactions)) :close-overlay)))) (t/testing "Update interaction" - (let [new-interactions (csi/update-interaction interactions 1 #(csi/set-action-type % :open-url))] + (let [new-interactions (ctsi/update-interaction interactions 1 #(ctsi/set-action-type % :open-url))] (t/is (= (count new-interactions) 2)) (t/is (= (:action-type (last new-interactions)) :open-url)))))) @@ -556,16 +556,16 @@ ids-map {(:id frame1) (:id frame4) (:id frame2) (:id frame5)} - i1 (csi/set-destination csi/default-interaction (:id frame1)) - i2 (csi/set-destination csi/default-interaction (:id frame2)) - i3 (csi/set-destination csi/default-interaction (:id frame3)) - i4 (csi/set-destination csi/default-interaction nil) - i5 (csi/set-destination csi/default-interaction (:id frame6)) + i1 (ctsi/set-destination ctsi/default-interaction (:id frame1)) + i2 (ctsi/set-destination ctsi/default-interaction (:id frame2)) + i3 (ctsi/set-destination ctsi/default-interaction (:id frame3)) + i4 (ctsi/set-destination ctsi/default-interaction nil) + i5 (ctsi/set-destination ctsi/default-interaction (:id frame6)) interactions [i1 i2 i3 i4 i5]] (t/testing "Remap interactions" - (let [new-interactions (csi/remap-interactions interactions ids-map objects)] + (let [new-interactions (ctsi/remap-interactions interactions ids-map objects)] (t/is (= (count new-interactions) 4)) (t/is (= (:id frame4) (:destination (get new-interactions 0)))) (t/is (= (:id frame5) (:destination (get new-interactions 1)))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index de9e178eb9..98f576c940 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -10,7 +10,7 @@ [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.interactions :as cti] + [app.common.types.shape.interactions :as ctsi] [app.main.data.comments :as dcm] [app.main.data.fonts :as df] [app.main.repo :as rp] @@ -389,7 +389,7 @@ ([frame-id animation] (us/verify ::us/uuid frame-id) - (us/verify (s/nilable ::cti/animation) animation) + (us/verify (s/nilable ::ctsi/animation) animation) (ptk/reify ::go-to-frame ptk/UpdateEvent (update [_ state] @@ -480,7 +480,7 @@ (us/verify ::gpt/point position) (us/verify (s/nilable ::us/boolean) close-click-outside) (us/verify (s/nilable ::us/boolean) background-overlay) - (us/verify (s/nilable ::cti/animation) animation) + (us/verify (s/nilable ::ctsi/animation) animation) (ptk/reify ::open-overlay ptk/UpdateEvent (update [_ state] @@ -505,7 +505,7 @@ (us/verify ::gpt/point position) (us/verify (s/nilable ::us/boolean) close-click-outside) (us/verify (s/nilable ::us/boolean) background-overlay) - (us/verify (s/nilable ::cti/animation) animation) + (us/verify (s/nilable ::ctsi/animation) animation) (ptk/reify ::toggle-overlay ptk/UpdateEvent (update [_ state] @@ -524,13 +524,13 @@ animation) (do-close-overlay state (:id frame) - (cti/invert-direction animation))))))) + (ctsi/invert-direction animation))))))) (defn close-overlay ([frame-id] (close-overlay frame-id nil)) ([frame-id animation] (us/verify ::us/uuid frame-id) - (us/verify (s/nilable ::cti/animation) animation) + (us/verify (s/nilable ::ctsi/animation) animation) (ptk/reify ::close-overlay ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 26fca15259..7885fbc921 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -18,9 +18,9 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.shape :as spec.shape] [app.common.text :as txt] [app.common.transit :as t] + [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.events :as ev] @@ -62,7 +62,7 @@ [cuerdas.core :as str] [potok.core :as ptk])) -(s/def ::shape-attrs ::spec.shape/shape-attrs) +(s/def ::shape-attrs ::cts/shape-attrs) (s/def ::set-of-string (s/every string? :kind set?)) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index decc1b5f54..209c8f5492 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -10,9 +10,9 @@ [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] + [app.common.pages.changes-spec :as pcs] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.change :as spec.change] [app.common.uuid :as uuid] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -159,8 +159,8 @@ [:workspace-data] [:workspace-libraries file-id :data])] (try - (us/assert ::spec.change/changes redo-changes) - (us/assert ::spec.change/changes undo-changes) + (us/assert ::pcs/changes redo-changes) + (us/assert ::pcs/changes undo-changes) (update-in state path (fn [file] (-> file diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 182e6de0a0..8eddc4758c 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -13,9 +13,9 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.interactions :as csi] - [app.common.spec.page :as csp] - [app.common.spec.shape :as spec.shape] + [app.common.types.page :as ctp] + [app.common.types.shape :as cts] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] @@ -29,7 +29,7 @@ ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) -(s/def ::shape-attrs ::spec.shape/shape-attrs) +(s/def ::shape-attrs ::cts/shape-attrs) (s/def ::set-of-string (s/every string? :kind set?)) (s/def ::ordered-set-of-uuid (s/every uuid? :kind d/ordered-set?)) @@ -382,7 +382,7 @@ ;; If any of the deleted shapes is the destination of ;; some interaction, this must be deleted, too. (let [interactions (:interactions shape)] - (some #(and (csi/has-destination %) + (some #(and (ctsi/has-destination %) (contains? ids (:destination %))) interactions))) (vals objects)) @@ -451,13 +451,13 @@ (update shape :interactions (fn [interactions] (when interactions - (d/removev #(and (csi/has-destination %) + (d/removev #(and (ctsi/has-destination %) (contains? ids (:destination %))) interactions)))))) (cond-> (seq starting-flows) (pcb/update-page-option :flows (fn [flows] - (reduce #(csp/remove-flow %1 (:id %2)) + (reduce #(ctp/remove-flow %1 (:id %2)) flows starting-flows)))))] diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index e9d16710ce..c6060daa5f 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -10,7 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages.changes-builder :as pcb] [app.common.spec :as us] - [app.common.spec.page :as csp] + [app.common.types.page :as ctp] [app.main.data.workspace.changes :as dwc] [app.main.data.workspace.state-helpers :as wsh] [beicon.core :as rx] @@ -24,7 +24,7 @@ (merge guide)))) (defn update-guides [guide] - (us/verify ::csp/guide guide) + (us/verify ::ctp/guide guide) (ptk/reify ::update-guides ptk/WatchEvent (watch [it state _] @@ -36,7 +36,7 @@ (rx/of (dwc/commit-changes changes)))))) (defn remove-guide [guide] - (us/verify ::csp/guide guide) + (us/verify ::ctp/guide guide) (ptk/reify ::remove-guide ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index d84b9769ed..5ea0d8b830 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -11,8 +11,8 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.interactions :as csi] - [app.common.spec.page :as csp] + [app.common.types.page :as ctp] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] @@ -41,7 +41,7 @@ (rx/of (dch/commit-changes (-> (pcb/empty-changes it) (pcb/with-page page) - (pcb/update-page-option :flows csp/add-flow new-flow)))))))) + (pcb/update-page-option :flows ctp/add-flow new-flow)))))))) (defn add-flow-selected-frame [] @@ -61,7 +61,7 @@ (rx/of (dch/commit-changes (-> (pcb/empty-changes it) (pcb/with-page page) - (pcb/update-page-option :flows csp/remove-flow flow-id)))))))) + (pcb/update-page-option :flows ctp/remove-flow flow-id)))))))) (defn rename-flow [flow-id name] @@ -74,8 +74,8 @@ (rx/of (dch/commit-changes (-> (pcb/empty-changes it) (pcb/with-page page) - (pcb/update-page-option :flows csp/update-flow flow-id - #(csp/rename-flow % name))))))))) + (pcb/update-page-option :flows ctp/update-flow flow-id + #(ctp/rename-flow % name))))))))) (defn start-rename-flow [id] @@ -99,8 +99,8 @@ in the page" [objects frame-id] (let [children (cph/get-children-with-self objects frame-id)] - (or (some csi/flow-origin? (map :interactions children)) - (some #(csi/flow-to? % frame-id) (map :interactions (vals objects)))))) + (or (some ctsi/flow-origin? (map :interactions children)) + (some #(ctsi/flow-to? % frame-id) (map :interactions (vals objects)))))) (defn add-new-interaction ([shape] (add-new-interaction shape nil)) @@ -116,15 +116,15 @@ page-id :options :flows] []) - flow (csp/get-frame-flow flows (:id frame))] + flow (ctp/get-frame-flow flows (:id frame))] (rx/concat (rx/of (dch/update-shapes [(:id shape)] (fn [shape] - (let [new-interaction (csi/set-destination - csi/default-interaction + (let [new-interaction (ctsi/set-destination + ctsi/default-interaction destination)] (update shape :interactions - csi/add-interaction new-interaction))))) + ctsi/add-interaction new-interaction))))) (when (and (not (connected-frame? objects (:id frame))) (nil? flow)) (rx/of (add-flow (:id frame)))))))))) @@ -137,7 +137,7 @@ (rx/of (dch/update-shapes [(:id shape)] (fn [shape] (update shape :interactions - csi/remove-interaction index))))))) + ctsi/remove-interaction index))))))) (defn update-interaction [shape index update-fn] @@ -147,7 +147,7 @@ (rx/of (dch/update-shapes [(:id shape)] (fn [shape] (update shape :interactions - csi/update-interaction index update-fn))))))) + ctsi/update-interaction index update-fn))))))) (declare move-edit-interaction) (declare finish-edit-interaction) @@ -219,11 +219,11 @@ change-interaction (fn [interaction] (cond-> interaction - (not (csi/has-destination interaction)) - (csi/set-action-type :navigate) + (not (ctsi/has-destination interaction)) + (ctsi/set-action-type :navigate) :always - (csi/set-destination (:id target-frame))))] + (ctsi/set-destination (:id target-frame))))] (cond (or (nil? shape) @@ -322,7 +322,7 @@ new-interactions (update interactions index - #(csi/set-overlay-position % overlay-pos))] + #(ctsi/set-overlay-position % overlay-pos))] (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 90a0c7a846..47c2aa7036 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -11,12 +11,12 @@ [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] + [app.common.pages.changes-spec :as pcs] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.change :as spec.change] - [app.common.spec.color :as spec.color] - [app.common.spec.file :as spec.file] - [app.common.spec.typography :as spec.typography] + [app.common.types.color :as ctc] + [app.common.types.file :as ctf] + [app.common.types.typography :as ctt] [app.common.uuid :as uuid] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] @@ -98,7 +98,7 @@ color (-> color (assoc :id id) (assoc :name (default-color-name color)))] - (us/assert ::spec.color/color color) + (us/assert ::ctc/color color) (ptk/reify ::add-color IDeref (-deref [_] color) @@ -112,7 +112,7 @@ (defn add-recent-color [color] - (us/assert ::spec.color/recent-color color) + (us/assert ::ctc/recent-color color) (ptk/reify ::add-recent-color ptk/WatchEvent (watch [it _ _] @@ -141,7 +141,7 @@ (defn update-color [color file-id] - (us/assert ::spec.color/color color) + (us/assert ::ctc/color color) (us/assert ::us/uuid file-id) (ptk/reify ::update-color ptk/WatchEvent @@ -175,7 +175,7 @@ (defn add-media [media] - (us/assert ::spec.file/media-object media) + (us/assert ::ctf/media-object media) (ptk/reify ::add-media ptk/WatchEvent (watch [it _ _] @@ -217,7 +217,7 @@ ([typography] (add-typography typography true)) ([typography edit?] (let [typography (update typography :id #(or % (uuid/next)))] - (us/assert ::spec.typography/typography typography) + (us/assert ::ctt/typography typography) (ptk/reify ::add-typography IDeref (-deref [_] typography) @@ -244,7 +244,7 @@ (defn update-typography [typography file-id] - (us/assert ::spec.typography/typography typography) + (us/assert ::ctt/typography typography) (us/assert ::us/uuid file-id) (ptk/reify ::update-typography ptk/WatchEvent @@ -462,7 +462,7 @@ (defn ext-library-changed [file-id modified-at revn changes] (us/assert ::us/uuid file-id) - (us/assert ::spec.change/changes changes) + (us/assert ::pcs/changes changes) (ptk/reify ::ext-library-changed ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 7fcb2cefa3..c95763d982 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -14,8 +14,8 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.color :as color] [app.common.text :as txt] + [app.common.types.color :as ctc] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.state-helpers :as wsh] @@ -299,7 +299,7 @@ (defmethod uses-assets? :colors [_ shape library-id _] - (color/uses-library-colors? shape library-id)) + (ctc/uses-library-colors? shape library-id)) (defmethod uses-assets? :typographies [_ shape library-id _] @@ -331,7 +331,7 @@ (let [library-colors (get-assets library-id :colors state)] (pcb/update-shapes changes [(:id shape)] - #(color/sync-shape-colors % library-id library-colors)))) + #(ctc/sync-shape-colors % library-id library-colors)))) (defmethod generate-sync-shape :typographies [_ changes library-id state container shape] diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index c87ed12999..6d1527b7c5 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -7,8 +7,8 @@ (ns app.main.data.workspace.notifications (:require [app.common.data :as d] + [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] - [app.common.spec.change :as spec.change] [app.common.uuid :as uuid] [app.main.data.websocket :as dws] [app.main.data.workspace.changes :as dch] @@ -184,7 +184,7 @@ (s/def ::file-id uuid?) (s/def ::session-id uuid?) (s/def ::revn integer?) -(s/def ::changes ::spec.change/changes) +(s/def ::changes ::pcs/changes) (s/def ::file-change-event (s/keys :req-un [::type ::profile-id ::file-id ::session-id ::revn ::changes])) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 8536e6074c..d01df8f382 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -9,9 +9,9 @@ [app.common.data :as d] [app.common.logging :as log] [app.common.pages :as cp] + [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] - [app.common.spec.change :as spec.change] - [app.common.spec.file :as spec.file] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.dashboard :as dd] @@ -199,7 +199,7 @@ :updated-at (dt/now))))))) (s/def ::shapes-changes-persisted - (s/keys :req-un [::revn ::spec.change/changes])) + (s/keys :req-un [::revn ::pcs/changes])) (defn shapes-persisted-event? [event] (= (ptk/type event) ::changes-persisted)) @@ -237,7 +237,7 @@ (s/def ::version ::us/integer) (s/def ::revn ::us/integer) (s/def ::ordering ::us/integer) -(s/def ::data ::spec.file/data) +(s/def ::data ::ctf/data) (s/def ::file ::dd/file) (s/def ::project ::dd/project) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index eff690a40b..d65dfcb428 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -14,8 +14,8 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] - [app.common.spec.interactions :as cti] - [app.common.spec.page :as ctp] + [app.common.types.page :as ctp] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] @@ -326,7 +326,7 @@ :shapes []) (dissoc :use-for-thumbnail?) (gsh/move delta) - (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects))) changes (-> (pcb/add-object changes new-frame) (pcb/amend-last-change #(assoc % :old-id (:id obj)))) @@ -361,7 +361,7 @@ :frame-id frame-id) (dissoc :shapes) (gsh/move delta) - (d/update-when :interactions #(cti/remap-interactions % ids-map objects))) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects))) changes (-> (pcb/add-object changes new-obj {:ignore-touched true}) (pcb/amend-last-change #(assoc % :old-id (:id obj))))] diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index b4a93cd53a..ac30d5ab15 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -6,8 +6,8 @@ (ns app.main.data.workspace.undo (:require + [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] - [app.common.spec.change :as spec.change] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -15,8 +15,8 @@ ;; Undo / Redo ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::undo-changes ::spec.change/changes) -(s/def ::redo-changes ::spec.change/changes) +(s/def ::undo-changes ::pcs/changes) +(s/def ::redo-changes ::pcs/changes) (s/def ::undo-entry (s/keys :req-un [::undo-changes ::redo-changes])) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 32353e9f7f..4b95e43501 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -10,8 +10,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] - [app.common.spec.radius :as ctr] - [app.common.spec.shape :refer [stroke-caps-line stroke-caps-marker]] + [app.common.types.shape :refer [stroke-caps-line stroke-caps-marker]] + [app.common.types.shape.radius :as ctsr] [app.main.ui.context :as muc] [app.util.object :as obj] [app.util.svg :as usvg] @@ -31,7 +31,7 @@ (defn add-border-radius [attrs {:keys [x y width height] :as shape}] - (case (ctr/radius-mode shape) + (case (ctsr/radius-mode shape) :radius-1 (let [radius (gsh/shape-corners-1 shape)] (obj/merge! attrs #js {:rx radius :ry radius})) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs index f2fca42b8c..ccf89070f0 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs @@ -6,7 +6,7 @@ (ns app.main.ui.viewer.handoff.attributes.layout (:require - [app.common.spec.radius :as ctr] + [app.common.types.shape.radius :as ctsr] [app.main.ui.components.copy-button :refer [copy-button]] [app.main.ui.formats :as fmt] [app.util.code-gen :as cg] @@ -59,13 +59,13 @@ [:div.attributes-value (fmt/format-pixels y)] [:& copy-button {:data (copy-data selrect :y)}]]) - (when (ctr/radius-1? shape) + (when (ctsr/radius-1? shape) [:div.attributes-unit-row [:div.attributes-label (tr "handoff.attributes.layout.radius")] [:div.attributes-value (fmt/format-pixels (:rx shape 0))] [:& copy-button {:data (copy-data shape :rx)}]]) - (when (ctr/radius-4? shape) + (when (ctsr/radius-4? shape) [:div.attributes-unit-row [:div.attributes-label (tr "handoff.attributes.layout.radius")] [:div.attributes-value diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 0878f911a5..32c2a0c0d9 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -11,7 +11,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] - [app.common.spec.page :as csp] + [app.common.types.page :as ctp] [app.main.data.comments :as dcm] [app.main.data.viewer :as dv] [app.main.refs :as refs] @@ -108,7 +108,7 @@ frames (:frames page) frame (get frames index) current-flow (mf/use-state - (csp/get-frame-flow flows (:id frame))) + (ctp/get-frame-flow flows (:id frame))) show-dropdown? (mf/use-state false) toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index ca187f7b06..2ffe4f1866 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -10,7 +10,7 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] - [app.common.spec.interactions :as cti] + [app.common.types.shape.interactions :as ctsi] [app.main.data.viewer :as dv] [app.main.refs :as refs] [app.main.store :as st] @@ -56,10 +56,10 @@ background-overlay (:background-overlay interaction) dest-frame (get objects dest-frame-id) - position (cti/calc-overlay-position interaction - base-frame - dest-frame - frame-offset)] + position (ctsi/calc-overlay-position interaction + base-frame + dest-frame + frame-offset)] (when dest-frame-id (st/emit! (dv/open-overlay dest-frame-id position @@ -123,10 +123,10 @@ background-overlay (:background-overlay interaction) dest-frame (get objects dest-frame-id) - position (cti/calc-overlay-position interaction - base-frame - dest-frame - frame-offset)] + position (ctsi/calc-overlay-position interaction + base-frame + dest-frame + frame-offset)] (when dest-frame-id (st/emit! (dv/open-overlay dest-frame-id position @@ -235,7 +235,7 @@ (if-not svg-element? [:> shape-container {:shape shape - :cursor (when (cti/actionable? interactions) "pointer") + :cursor (when (ctsi/actionable? interactions) "pointer") :on-mouse-down #(on-mouse-down % shape base-frame frame-offset objects) :on-mouse-up #(on-mouse-up % shape base-frame frame-offset objects) :on-mouse-enter #(on-mouse-enter % shape base-frame frame-offset objects) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 0cfc56436d..11003df3a6 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -9,7 +9,7 @@ (:require [app.common.data :as d] [app.common.pages.helpers :as cph] - [app.common.spec.page :as csp] + [app.common.types.page :as ctp] [app.main.data.events :as ev] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] @@ -329,7 +329,7 @@ is-frame? (and single? has-frame?)] (when (and prototype? is-frame?) - (let [flow (csp/get-frame-flow flows (-> shapes first :id))] + (let [flow (ctp/get-frame-flow flows (-> shapes first :id))] (if (some? flow) [:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start") :on-click (do-remove-flow flow)}] 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 5464fa633c..18035e283e 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 @@ -9,8 +9,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] - [app.common.spec.interactions :as csi] - [app.common.spec.page :as csp] + [app.common.types.page :as ctp] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.interactions :as dwi] @@ -80,7 +80,7 @@ {:dissolve (tr "workspace.options.interaction-animation-dissolve") :slide (tr "workspace.options.interaction-animation-slide")} - (csi/allow-push? (:action-type interaction)) + (ctsi/allow-push? (:action-type interaction)) (assoc :push (tr "workspace.options.interaction-animation-push")))) (defn- easing-names @@ -166,7 +166,7 @@ (mf/defc shape-flows [{:keys [flows shape]}] (when (= (:type shape) :frame) - (let [flow (csp/get-frame-flow flows (:id shape))] + (let [flow (ctp/get-frame-flow flows (:id shape))] [:div.element-set.interactions-options [:div.element-set-title [:span (tr "workspace.options.flows.flow-start")]] @@ -202,27 +202,27 @@ change-event-type (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-event-type % value shape)))) + (update-interaction index #(ctsi/set-event-type % value shape)))) change-action-type (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-action-type % value)))) + (update-interaction index #(ctsi/set-action-type % value)))) change-delay (fn [value] - (update-interaction index #(csi/set-delay % value))) + (update-interaction index #(ctsi/set-delay % value))) change-destination (fn [event] (let [value (-> event dom/get-target dom/get-value) value (when (not= value "") (uuid/uuid value))] - (update-interaction index #(csi/set-destination % value)))) + (update-interaction index #(ctsi/set-destination % value)))) change-preserve-scroll (fn [event] (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(csi/set-preserve-scroll % value)))) + (update-interaction index #(ctsi/set-preserve-scroll % value)))) change-url (fn [event] @@ -238,55 +238,55 @@ (if (dom/valid? target) (do (dom/remove-class! target "error") - (update-interaction index #(csi/set-url % value))) + (update-interaction index #(ctsi/set-url % value))) (dom/add-class! target "error")))) change-overlay-pos-type (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-overlay-pos-type % value shape objects)))) + (update-interaction index #(ctsi/set-overlay-pos-type % value shape objects)))) toggle-overlay-pos-type (fn [pos-type] - (update-interaction index #(csi/toggle-overlay-pos-type % pos-type shape objects))) + (update-interaction index #(ctsi/toggle-overlay-pos-type % pos-type shape objects))) change-close-click-outside (fn [event] (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(csi/set-close-click-outside % value)))) + (update-interaction index #(ctsi/set-close-click-outside % value)))) change-background-overlay (fn [event] (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(csi/set-background-overlay % value)))) + (update-interaction index #(ctsi/set-background-overlay % value)))) change-animation-type (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-animation-type % value)))) + (update-interaction index #(ctsi/set-animation-type % value)))) change-duration (fn [value] - (update-interaction index #(csi/set-duration % value))) + (update-interaction index #(ctsi/set-duration % value))) change-easing (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-easing % value)))) + (update-interaction index #(ctsi/set-easing % value)))) change-way (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (update-interaction index #(csi/set-way % value)))) + (update-interaction index #(ctsi/set-way % value)))) change-direction (fn [value] - (update-interaction index #(csi/set-direction % value))) + (update-interaction index #(ctsi/set-direction % value))) change-offset-effect (fn [event] (let [value (-> event dom/get-target dom/checked?)] - (update-interaction index #(csi/set-offset-effect % value)))) + (update-interaction index #(ctsi/set-offset-effect % value)))) ] [:* @@ -318,7 +318,7 @@ :value (dm/str value)} name]))]] ; Delay - (when (csi/has-delay interaction) + (when (ctsi/has-delay interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} @@ -340,7 +340,7 @@ :value (str value)} name])]] ; Destination - (when (csi/has-destination interaction) + (when (ctsi/has-destination interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")] [:select.input-select @@ -356,7 +356,7 @@ :value (str (:id frame))} (:name frame)]))]]) ; Preserve scroll - (when (csi/has-preserve-scroll interaction) + (when (ctsi/has-preserve-scroll interaction) [:div.interactions-element [:div.input-checkbox [:input {:type "checkbox" @@ -367,7 +367,7 @@ (tr "workspace.options.interaction-preserve-scroll")]]]) ; URL - (when (csi/has-url interaction) + (when (ctsi/has-url interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")] [:input.input-text {:type "url" @@ -375,7 +375,7 @@ :default-value (str (:url interaction)) :on-blur change-url}]]) - (when (csi/has-overlay-opts interaction) + (when (ctsi/has-overlay-opts interaction) [:* ; Overlay position (select) [:div.interactions-element @@ -437,7 +437,7 @@ [:label {:for (str "background-" index)} (tr "workspace.options.interaction-background")]]]]) - (when (csi/has-animation? interaction) + (when (ctsi/has-animation? interaction) [:* ; Animation select [:div.interactions-element.separator @@ -450,7 +450,7 @@ [:option {:value (str value)} name])]] ; Direction - (when (csi/has-way? interaction) + (when (ctsi/has-way? interaction) [:div.interactions-element.interactions-way-buttons [:div.input-radio [:input {:type "radio" @@ -470,7 +470,7 @@ [:label {:for "way-out"} (tr "workspace.options.interaction-out")]]]) ; Direction - (when (csi/has-direction? interaction) + (when (ctsi/has-direction? interaction) [:div.interactions-element.interactions-direction-buttons [:div.element-set-actions-button {:class (dom/classnames :active (= direction :right)) @@ -490,7 +490,7 @@ i/animate-up]]) ; Duration - (when (csi/has-duration? interaction) + (when (ctsi/has-duration? interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} @@ -502,7 +502,7 @@ [:span.after (tr "workspace.options.interaction-ms")]]]) ; Easing - (when (csi/has-easing? interaction) + (when (ctsi/has-easing? interaction) [:div.interactions-element [:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")] [:select.input-select @@ -519,7 +519,7 @@ :ease-in-out i/easing-ease-in-out)]]) ; Offset effect - (when (csi/has-offset-effect? interaction) + (when (ctsi/has-offset-effect? interaction) [:div.interactions-element [:div.input-checkbox [:input {:type "checkbox" 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 7e194f5f92..7b2f613a56 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 @@ -8,7 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.spec.radius :as ctr] + [app.common.types.shape.radius :as ctsr] [app.main.constants :refer [size-presets]] [app.main.data.workspace :as udw] [app.main.data.workspace.changes :as dch] @@ -99,8 +99,8 @@ show-presets-dropdown? (mf/use-state false) - radius-mode (ctr/radius-mode values) - all-equal? (ctr/all-equal? values) + radius-mode (ctsr/radius-mode values) + all-equal? (ctsr/all-equal? values) radius-multi? (mf/use-state nil) radius-input-ref (mf/use-ref nil) @@ -154,7 +154,7 @@ (fn [update-fn] (dch/update-shapes ids-with-children (fn [shape] - (if (ctr/has-radius? shape) + (if (ctsr/has-radius? shape) (update-fn shape) shape))))) @@ -163,21 +163,21 @@ (mf/deps ids) (fn [_value] (if all-equal? - (st/emit! (change-radius ctr/switch-to-radius-1)) + (st/emit! (change-radius ctsr/switch-to-radius-1)) (reset! radius-multi? true)))) on-switch-to-radius-4 (mf/use-callback (mf/deps ids) (fn [_value] - (st/emit! (change-radius ctr/switch-to-radius-4)) + (st/emit! (change-radius ctsr/switch-to-radius-4)) (reset! radius-multi? false))) on-radius-1-change (mf/use-callback (mf/deps ids) (fn [value] - (st/emit! (change-radius #(ctr/set-radius-1 % value))))) + (st/emit! (change-radius #(ctsr/set-radius-1 % value))))) on-radius-multi-change (mf/use-callback @@ -185,15 +185,15 @@ (fn [event] (let [value (-> event dom/get-target dom/get-value d/parse-integer)] (when (some? value) - (st/emit! (change-radius ctr/switch-to-radius-1) - (change-radius #(ctr/set-radius-1 % value))) + (st/emit! (change-radius ctsr/switch-to-radius-1) + (change-radius #(ctsr/set-radius-1 % value))) (reset! radius-multi? false))))) on-radius-4-change (mf/use-callback (mf/deps ids) (fn [value attr] - (st/emit! (change-radius #(ctr/set-radius-4 % attr value))))) + (st/emit! (change-radius #(ctsr/set-radius-4 % attr value))))) on-width-change #(on-size-change % :width) on-height-change #(on-size-change % :height) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index b35eac1c7f..1236859e1a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -11,7 +11,7 @@ [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] - [app.common.spec.interactions :as cti] + [app.common.types.shape.interactions :as ctsi] [app.main.data.workspace :as dw] [app.main.refs :as refs] [app.main.store :as st] @@ -275,7 +275,7 @@ [:g.non-selected (for [shape active-shapes] (for [[index interaction] (d/enumerate (:interactions shape))] - (let [dest-shape (when (cti/destination? interaction) + (let [dest-shape (when (ctsi/destination? interaction) (get objects (:destination interaction))) selected? (contains? selected (:id shape)) level (calc-level index (:interactions shape))] @@ -304,7 +304,7 @@ (if (seq (:interactions shape)) (for [[index interaction] (d/enumerate (:interactions shape))] (when-not (= index editing-interaction-index) - (let [dest-shape (when (cti/destination? interaction) + (let [dest-shape (when (ctsi/destination? interaction) (get objects (:destination interaction))) level (calc-level index (:interactions shape))] [:g {:key (dm/str "interaction-path-" (:id shape) "-" index)} diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 693735c8a2..f04ba60c72 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -10,7 +10,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] - [app.common.spec.interactions :as cti] + [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.util.color :as uc] [app.util.json :as json] @@ -939,17 +939,17 @@ (let [interaction {:event-type (get-meta node :event-type keyword) :action-type (get-meta node :action-type keyword)}] (cond-> interaction - (cti/has-delay interaction) + (ctsi/has-delay interaction) (assoc :delay (get-meta node :delay d/parse-double)) - (cti/has-destination interaction) + (ctsi/has-destination interaction) (assoc :destination (get-meta node :destination uuid/uuid) :preserve-scroll (get-meta node :preserve-scroll str->bool)) - (cti/has-url interaction) + (ctsi/has-url interaction) (assoc :url (get-meta node :url str)) - (cti/has-overlay-opts interaction) + (ctsi/has-overlay-opts interaction) (assoc :overlay-pos-type (get-meta node :overlay-pos-type keyword) :overlay-position (gpt/point (get-meta node :overlay-position-x d/parse-double) From 0f04398e61759bea5d78b858c83b0b2ce7cca015 Mon Sep 17 00:00:00 2001 From: Eva Date: Thu, 16 Jun 2022 12:56:22 +0200 Subject: [PATCH 48/86] :lipstick: Improve shared link modal --- backend/src/app/migrations.clj | 3 + .../sql/0073-mod-share-link-table.sql | 4 + backend/src/app/rpc/mutations/share_link.clj | 20 +- backend/src/app/rpc/queries/share_link.clj | 1 - .../styles/main/partials/share-link.scss | 337 ++++++++++++------ frontend/src/app/main/ui/modal.cljs | 1 + frontend/src/app/main/ui/share_link.cljs | 303 ++++++++-------- frontend/src/app/main/ui/viewer/header.cljs | 3 +- frontend/translations/ca.po | 18 - frontend/translations/de.po | 18 - frontend/translations/en.po | 38 +- frontend/translations/es.po | 36 +- frontend/translations/fa.po | 18 - frontend/translations/fr.po | 18 - frontend/translations/he.po | 18 - frontend/translations/id.po | 10 - frontend/translations/lt.po | 18 - frontend/translations/ml.po | 18 - frontend/translations/pl.po | 18 - frontend/translations/ru.po | 19 - frontend/translations/tr.po | 18 - frontend/translations/zh_CN.po | 19 - frontend/translations/zh_Hant.po | 18 - 23 files changed, 453 insertions(+), 521 deletions(-) create mode 100644 backend/src/app/migrations/sql/0073-mod-share-link-table.sql diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index b8657d010a..068ff52023 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -229,6 +229,9 @@ {:name "0073-mod-file-media-object-constraints" :fn (mg/resource "app/migrations/sql/0073-mod-file-media-object-constraints.sql")} + + {:name "0073-mod-share-link-table" + :fn (mg/resource "app/migrations/sql/0073-mod-share-link-table.sql")} {:name "0074-mod-file-library-rel-constraints" :fn (mg/resource "app/migrations/sql/0074-mod-file-library-rel-constraints.sql")} diff --git a/backend/src/app/migrations/sql/0073-mod-share-link-table.sql b/backend/src/app/migrations/sql/0073-mod-share-link-table.sql new file mode 100644 index 0000000000..326091cda5 --- /dev/null +++ b/backend/src/app/migrations/sql/0073-mod-share-link-table.sql @@ -0,0 +1,4 @@ +ALTER TABLE share_link + ADD COLUMN who_comment text NOT NULL DEFAULT('team'), + ADD COLUMN who_inspect text NOT NULL DEFAULT('team'), + DROP COLUMN flags; diff --git a/backend/src/app/rpc/mutations/share_link.clj b/backend/src/app/rpc/mutations/share_link.clj index 6079ecf7de..e9d9efc6c1 100644 --- a/backend/src/app/rpc/mutations/share_link.clj +++ b/backend/src/app/rpc/mutations/share_link.clj @@ -19,7 +19,8 @@ (s/def ::id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::file-id ::us/uuid) -(s/def ::flags (s/every ::us/string :kind set?)) +(s/def ::who-comment ::us/string) +(s/def ::who-inspect ::us/string) (s/def ::pages (s/every ::us/uuid :kind set?)) ;; --- Mutation: Create Share Link @@ -27,14 +28,13 @@ (declare create-share-link) (s/def ::create-share-link - (s/keys :req-un [::profile-id ::file-id ::flags] - :opt-un [::pages])) + (s/keys :req-un [::profile-id ::file-id ::who-comment ::who-inspect ::pages])) (sv/defmethod ::create-share-link "Creates a share-link object. - Share links are resources that allows external users access to - specific files with specific permissions (flags)." + Share links are resources that allows external users access to specific + pages of a file with specific permissions (who-comment and who-inspect)." [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] @@ -42,19 +42,17 @@ (create-share-link conn params))) (defn create-share-link - [conn {:keys [profile-id file-id pages flags]}] + [conn {:keys [profile-id file-id pages who-comment who-inspect]}] (let [pages (db/create-array conn "uuid" pages) - flags (->> (map name flags) - (db/create-array conn "text")) slink (db/insert! conn :share-link {:id (uuid/next) :file-id file-id - :flags flags + :who-comment who-comment + :who-inspect who-inspect :pages pages :owner-id profile-id})] (-> slink - (update :pages db/decode-pgarray #{}) - (update :flags db/decode-pgarray #{})))) + (update :pages db/decode-pgarray #{})))) ;; --- Mutation: Delete Share Link diff --git a/backend/src/app/rpc/queries/share_link.clj b/backend/src/app/rpc/queries/share_link.clj index 0bc5673649..96ec658c88 100644 --- a/backend/src/app/rpc/queries/share_link.clj +++ b/backend/src/app/rpc/queries/share_link.clj @@ -11,7 +11,6 @@ (defn decode-share-link-row [row] (-> row - (update :flags db/decode-pgarray #{}) (update :pages db/decode-pgarray #{}))) (defn retrieve-share-link diff --git a/frontend/resources/styles/main/partials/share-link.scss b/frontend/resources/styles/main/partials/share-link.scss index 1dd9a5415a..0a9f4c5bd3 100644 --- a/frontend/resources/styles/main/partials/share-link.scss +++ b/frontend/resources/styles/main/partials/share-link.scss @@ -1,139 +1,240 @@ -.share-link-dialog { - width: 475px; - background-color: $color-white; +.share-modal { + display: block; + top: 50px; + left: calc(100vw - 500px); - .modal-footer { - display: flex; - align-items: center; - justify-content: flex-end; - height: unset; - padding: 16px 26px; + .share-link-dialog { + width: 480px; + background-color: $color-white; - .btn-primary, - .btn-secondary, - .btn-warning { - width: 126px; - margin-bottom: 0px; - - &:not(:last-child) { - margin-right: 10px; - } - } - - .confirm-dialog { - display: flex; - flex-direction: column; - background-color: unset; - - .description { - font-size: $fs14; - - margin-bottom: 16px; - } - .actions { + .modal-content { + padding: 16px 32px; + &:first-child { + border-top: 0px; + padding: 0; + height: 50px; display: flex; - justify-content: flex-end; + justify-content: center; } - } - } - - .modal-content { - padding: 26px; - - &:first-child { - border-top: 0px; - } - - .title { - display: flex; - justify-content: space-between; - - h2 { - font-size: $fs18; - color: $color-black; - } - - .modal-close-button { - margin-right: 0px; - } - } - - .share-link-section { - margin-top: 12px; - label { - font-size: $fs12; - color: $color-black; - } - - .hint { - padding-top: 10px; - font-size: $fs14; - color: $color-gray-40; - } - - .help-icon { - cursor: pointer; - } - } - - .view-mode, - .access-mode { - display: flex; - flex-direction: column; .title { - color: $color-black; - font-weight: 400; + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; + margin-left: 32px; + h2 { + font-size: $fs18; + color: $color-black; + } + + .modal-close-button { + margin-right: 16px; + } } - .items { - padding-left: 20px; - display: flex; - - > .input-checkbox, - > .input-radio { + .share-link-section { + .custom-input { display: flex; - user-select: none; - - /* input { */ - /* appearance: checkbox; */ - /* } */ - - label { + flex-direction: row; + margin-bottom: 15px; + border: 1px solid $color-gray-20; + input { + padding: 0 0 0 15px; + border: none; + } + } + .hint-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + .hint { + font-size: $fs12; + color: $color-gray-40; + } + .confirm-dialog { display: flex; - align-items: center; - color: $color-black; + flex-direction: column; + background-color: unset; + .actions { + display: flex; + justify-content: flex-end; + gap: 16px; + } + .description { + font-size: $fs12; + margin-bottom: 16px; + color: $color-black; + } + .btn-primary, + .btn-secondary, + .btn-warning { + width: 126px; + margin-bottom: 0px; - .hint { - margin-left: 5px; + &:not(:last-child) { + margin-right: 10px; + } + } + } + } + + label { + font-size: $fs12; + color: $color-black; + } + + .help-icon { + height: 40px; + width: 40px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + position: relative; + right: 0; + top: 0; + background-color: $color-gray-10; + border-left: 1px solid $color-gray-20; + svg { + fill: $color-gray-30; + } + &:hover { + background-color: $color-primary; + svg { + fill: $color-gray-60; + } + } + } + input { + margin: 0; + } + } + + &.ops-section { + .manage-permissions { + display: flex; + color: $color-primary-dark; + font-size: $fs12; + cursor: pointer; + .icon { + svg { + height: 16px; + width: 16px; + fill: $color-primary-dark; + } + } + .title { + margin-left: 8px; + } + } + .view-mode { + min-height: 34px; + .subtitle { + height: 32px; + } + .row { + display: flex; + justify-content: space-between; + align-items: center; + .count-pages { + font-size: $fs12; color: $color-gray-30; } } - - &.disabled { - label { - color: $color-gray-30; + .current-tag { + font-size: $fs12; + color: $color-gray-30; + } + label { + color: $color-black; + } + } + .access-mode, + .inspect-mode { + display: grid; + grid-template-columns: auto 1fr; + .items { + display: flex; + justify-content: flex-end; + align-items: center; + } + } + .view-mode, + .access-mode, + .inspect-mode { + margin: 8px 0; + .subtitle { + display: flex; + justify-content: flex-start; + align-items: center; + color: $color-black; + font-size: $fs16; + .icon { + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + svg { + height: 16px; + width: 16px; + } } } + .items { + .input-select { + background-image: url("/images/icons/arrow-down.svg"); + margin: 0; + padding-right: 28px; + border: 1px solid $color-gray-10; + max-width: 227px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + > .input-radio { + display: flex; + user-select: none; + margin-top: 0; + margin-bottom: 0; + label { + display: flex; + align-items: center; + color: $color-black; + max-width: 115px; + + &::before { + height: 16px; + width: 16px; + } + .hint { + margin-left: 5px; + color: $color-gray-30; + } + } + + &.disabled { + label { + color: $color-gray-30; + } + } + } + } + } + + .pages-selection { + border-top: 1px solid $color-gray-10; + border-bottom: 1px solid $color-gray-10; + padding-left: 20px; + max-height: 200px; + overflow-y: scroll; + user-select: none; + + label { + color: $color-black; + } } } } - - .pages-selection { - padding-left: 20px; - max-height: 200px; - overflow-y: scroll; - user-select: none; - - label { - color: $color-black; - } - } - - .custom-input { - input { - padding: 0 40px 0 15px; - } - } } } diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index e4ae3e4bc2..9152ac4a14 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -84,5 +84,6 @@ [] (let [modal (mf/deref modal-ref)] (when modal + (.log js/console "modal"(clj->js modal)) [:& modal-wrapper {:data modal :key (:id modal)}]))) diff --git a/frontend/src/app/main/ui/share_link.cljs b/frontend/src/app/main/ui/share_link.cljs index 52438b3de7..467988c430 100644 --- a/frontend/src/app/main/ui/share_link.cljs +++ b/frontend/src/app/main/ui/share_link.cljs @@ -24,53 +24,68 @@ (log/set-level! :warn) (defn prepare-params - [{:keys [sections pages pages-mode]}] - {:pages pages - :flags (-> #{} - (into (map #(str "section-" %)) sections) - (into (map #(str "pages-" %)) [pages-mode]))}) + [{:keys [pages who-comment who-inspect]}] + + {:pages pages + :who-comment who-comment + :who-inspect who-inspect}) + + (mf/defc share-link-dialog {::mf/register modal/components ::mf/register-as :share-link} [{:keys [file page]}] - (let [slinks (mf/deref refs/share-links) - router (mf/deref refs/router) - route (mf/deref refs/route) + (let [current-page page + slinks (mf/deref refs/share-links) + router (mf/deref refs/router) + route (mf/deref refs/route) - link (mf/use-state nil) - confirm (mf/use-state false) + link (mf/use-state nil) + confirm (mf/use-state false) + open-ops (mf/use-state false) + + opts (mf/use-state + {:pages-mode "current" + :all-pages false + :pages #{(:id page)} + :who-comment "team" + :who-inspect "team"}) - opts (mf/use-state - {:sections #{"viewer"} - :pages-mode "current" - :pages #{(:id page)}}) close (fn [event] (dom/prevent-default event) - (st/emit! (modal/hide))) + (st/emit! (modal/hide)) + (modal/disallow-click-outside!)) - select-pages-mode - (fn [mode] + toggle-all + (fn [] (reset! confirm false) (swap! opts (fn [state] - (-> state - (assoc :pages-mode mode) - (cond-> (= mode "current") (assoc :pages #{(:id page)})) - (cond-> (= mode "all") (assoc :pages (into #{} (get-in file [:data :pages])))))))) + (if (= true (:all-pages state)) + (-> state + (assoc :all-pages false) + (assoc :pages #{(:id page)})) + (-> state + (assoc :all-pages true) + (assoc :pages (into #{} (get-in file [:data :pages])))))))) mark-checked-page (fn [event id] (let [target (dom/get-target event) - checked? (.-checked ^js target)] - (reset! confirm false) - (swap! opts update :pages - (fn [pages] - (if checked? - (conj pages id) - (disj pages id)))))) + checked? (.-checked ^js target) + dif-pages? (not= id (first (:pages @opts))) + no-one-page (< 1 (count (:pages @opts))) + should-change (or no-one-page dif-pages?)] + (when should-change + (reset! confirm false) + (swap! opts update :pages + (fn [pages] + (if checked? + (conj pages id) + (disj pages id))))))) create-link (fn [_] @@ -83,7 +98,7 @@ (wapi/write-to-clipboard @link) (st/emit! (dm/show {:type :info :content (tr "common.share-link.link-copied-success") - :timeout 3000}))) + :timeout 1000}))) try-delete-link (fn [_] @@ -94,17 +109,27 @@ (let [params (prepare-params @opts) slink (d/seek #(= (:flags %) (:flags params)) slinks)] (reset! confirm false) - (st/emit! (dc/delete-share-link slink) - (dm/show {:type :info - :content (tr "common.share-link.link-deleted-success") - :timeout 3000})))) - ] + (st/emit! (dc/delete-share-link slink)))) + + manage-open-ops + (fn [_] + (swap! open-ops not)) + + on-who-change + (fn [type event] + (let [target (dom/get-target event) + value (dom/get-value target) + value (keyword value)] + (reset! confirm false) + (if (= type :comment) + (swap! opts assoc :who-comment (d/name value)) + (swap! opts assoc :who-inspect (d/name value)))))] (mf/use-effect (mf/deps file slinks @opts) (fn [] - (let [{:keys [flags pages] :as params} (prepare-params @opts) - slink (d/seek #(and (= (:flags %) flags) (= (:pages %) pages)) slinks) + (let [{:keys [pages who-comment who-inspect] :as params} (prepare-params @opts) + slink (d/seek #(and (= (:who-inspect %) who-inspect) (= (:who-comment %) who-comment) (= (:pages %) pages)) slinks) href (when slink (let [pparams (:path-params route) qparams (-> (:query-params route) @@ -114,123 +139,123 @@ (assoc cf/public-uri :fragment href)))] (reset! link (some-> href str))))) - [:div.modal-overlay + [:div.modal-overlay.share-modal [:div.modal-container.share-link-dialog - [:div.modal-content + [:div.modal-content.initial [:div.title [:h2 (tr "common.share-link.title")] [:div.modal-close-button {:on-click close :title (tr "labels.close")} - i/close]] - - [:div.share-link-section - [:label (tr "labels.link")] - [:div.custom-input.with-icon - [:input {:type "text" - :value (or @link "") - :placeholder (tr "common.share-link.placeholder") - :read-only true}] - (when (some? @link) - [:div.help-icon {:title (tr "labels.copy") - :on-click copy-link} - i/copy])] - - [:div.hint (tr "common.share-link.permissions-hint")]]] - + i/close]]] [:div.modal-content - (let [sections (:sections @opts)] - [:div.access-mode - [:div.title (tr "common.share-link.permissions-can-access")] - [:div.items - [:div.input-checkbox.check-primary.disabled - [:input.check-primary.input-checkbox {:type "checkbox" :disabled true}] - [:label (tr "labels.workspace")]] + [:div.share-link-section + (when (and (not @confirm) (some? @link)) + [:div.custom-input.with-icon + [:input {:type "text" + :value (or @link "") + :placeholder (tr "common.share-link.placeholder") + :read-only true}] + [:div.help-icon {:title (tr "viewer.header.share.copy-link") + :on-click copy-link} + i/copy]]) + [:div.hint-wrapper + (when (not @confirm) [:div.hint (tr "common.share-link.permissions-hint")]) + (cond + (true? @confirm) + [:div.confirm-dialog + [:div.description (tr "common.share-link.confirm-deletion-link-description")] + [:div.actions + [:input.btn-secondary + {:type "button" + :on-click #(reset! confirm false) + :value (tr "labels.cancel")}] + [:input.btn-warning + {:type "button" + :on-click delete-link + :value (tr "common.share-link.destroy-link")}]]] - [:div.input-checkbox.check-primary - [:input {:type "checkbox" - :default-checked (contains? sections "viewer")}] - [:label (tr "labels.viewer") - [:span.hint "(" (tr "labels.default") ")"]]] - - ;; [:div.input-checkbox.check-primary - ;; [:input.check-primary.input-checkbox {:type "checkbox"}] - ;; [:label "Handoff" ]] - ]]) - - (let [mode (:pages-mode @opts)] - [:* - [:div.view-mode - [:div.title (tr "common.share-link.permissions-can-view")] - [:div.items - [:div.input-radio.radio-primary - [:input {:type "radio" - :id "view-all" - :checked (= "all" mode) - :name "pages-mode" - :on-change #(select-pages-mode "all")}] - [:label {:for "view-all"} (tr "common.share-link.view-all-pages")]] - - [:div.input-radio.radio-primary - [:input {:type "radio" - :id "view-current" - :name "pages-mode" - :checked (= "current" mode) - :on-change #(select-pages-mode "current")}] - [:label {:for "view-current"} (tr "common.share-link.view-current-page")]] - - [:div.input-radio.radio-primary - [:input {:type "radio" - :id "view-selected" - :name "pages-mode" - :checked (= "selected" mode) - :on-change #(select-pages-mode "selected")}] - [:label {:for "view-selected"} (tr "common.share-link.view-selected-pages")]]]] - - (when (= "selected" mode) - (let [pages (->> (get-in file [:data :pages]) - (map #(get-in file [:data :pages-index %]))) - selected (:pages @opts)] - [:ul.pages-selection - (for [page pages] - [:li.input-checkbox.check-primary {:key (str (:id page))} - [:input {:type "checkbox" - :id (str "page-" (:id page)) - :on-change #(mark-checked-page % (:id page)) - :checked (contains? selected (:id page))}] - [:label {:for (str "page-" (:id page))} (:name page)]])]))])] - - [:div.modal-footer - (cond - (true? @confirm) - [:div.confirm-dialog - [:div.description (tr "common.share-link.confirm-deletion-link-description")] - [:div.actions + (some? @link) [:input.btn-secondary {:type "button" - :on-click #(reset! confirm false) - :value (tr "labels.cancel")}] - [:input.btn-warning + :class "primary" + :on-click try-delete-link + :value (tr "common.share-link.destroy-link")}] + + :else + [:input.btn-primary {:type "button" - :on-click delete-link - :value (tr "common.share-link.remove-link") - }]]] + :class "primary" + :on-click create-link + :value (tr "common.share-link.get-link")}])]]] + [:div.modal-content.ops-section + [:div.manage-permissions + {:on-click manage-open-ops} + [:span.icon i/picker-hsv] + [:div.title (tr "common.share-link.manage-ops")]] + (when @open-ops + [:* + (let [all-selected? (:all-pages @opts) + pages (->> (get-in file [:data :pages]) + (map #(get-in file [:data :pages-index %]))) + selected (:pages @opts)] - (some? @link) - [:input.btn-secondary - {:type "button" - :class "primary" - :on-click try-delete-link - :value (tr "common.share-link.remove-link")}] + [:* + [:div.view-mode + [:div.subtitle + [:span.icon i/play] + (tr "common.share-link.permissions-pages")] + [:div.items + (if (= 1 (count pages)) + [:div.input-checkbox.check-primary + [:input {:type "checkbox" + :id (str "page-" (:id current-page)) + :on-change #(mark-checked-page % (:id current-page)) + :checked true}] + [:label {:for (str "page-" (:id current-page))} (:name current-page)] + [:span (str " " (tr "common.share-link.current-tag"))]] - :else - [:input.btn-primary - {:type "button" - :class "primary" - :on-click create-link - :value (tr "common.share-link.get-link")}])] + [:* + [:div.row + [:div.input-checkbox.check-primary + [:input {:type "checkbox" + :id "view-all" + :checked all-selected? + :name "pages-mode" + :on-change toggle-all}] + [:label {:for "view-all"} (tr "common.share-link.view-all")]] + [:span.count-pages (tr "common.share-link.page-shared" (i18n/c (count selected)))]] - ]])) + [:ul.pages-selection + (for [page pages] + [:li.input-checkbox.check-primary {:key (str (:id page))} + [:input {:type "checkbox" + :id (str "page-" (:id page)) + :on-change #(mark-checked-page % (:id page)) + :checked (contains? selected (:id page))}] + (if (= (:id current-page) (:id page)) + [:* + [:label {:for (str "page-" (:id page))} (:name page)] + [:span.current-tag (str " " (tr "common.share-link.current-tag"))]] + [:label {:for (str "page-" (:id page))} (:name page)])])]])]]]) + [:div.access-mode + [:div.subtitle + [:span.icon i/chat] + (tr "common.share-link.permissions-can-comment")] + [:div.items + [:select.input-select {:on-change (partial on-who-change :comment) + :value (:who-comment @opts)} + [:option {:value "team"} (tr "common.share-link.team-members")] + [:option {:value "all"} (tr "common.share-link.all-users")]]]] + [:div.inspect-mode + [:div.subtitle + [:span.icon i/code] + (tr "common.share-link.permissions-can-inspect")] + [:div.items + [:select.input-select {:on-change (partial on-who-change :inspect) + :value (:who-inspect @opts)} + [:option {:value "team"} (tr "common.share-link.team-members")] + [:option {:value "all"} (tr "common.share-link.all-users")]]]]])]]])) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index c777ad9d5f..da39c63b70 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -77,7 +77,8 @@ (mf/use-callback (mf/deps page) (fn [] - (modal/show! :share-link {:page page :file file})))] + (modal/show! :share-link {:page page :file file}) + (modal/allow-click-outside!)))] [:div.options-zone (case section diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 2d0eff1421..bd43175e7d 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -185,33 +185,15 @@ msgstr "S'ha copiat l'enllaç correctament" msgid "common.share-link.link-deleted-success" msgstr "S'ha eliminat l'enllaç correctament" -msgid "common.share-link.permissions-can-access" -msgstr "Pot accedir" - -msgid "common.share-link.permissions-can-view" -msgstr "Lector" - msgid "common.share-link.permissions-hint" msgstr "Qualsevol persona amb l'enllaç hi tindrà accés" msgid "common.share-link.placeholder" msgstr "L'enllaç per a compartir apareixerà aquí" -msgid "common.share-link.remove-link" -msgstr "Elimina l'enllaç" - msgid "common.share-link.title" msgstr "Compartiu prototips" -msgid "common.share-link.view-all-pages" -msgstr "Totes les pàgines" - -msgid "common.share-link.view-current-page" -msgstr "Només aquesta pàgina" - -msgid "common.share-link.view-selected-pages" -msgstr "Pàgines seleccionades" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Afegeix a la biblioteca compartida" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index b6486cbd6b..1884b139ff 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -188,33 +188,15 @@ msgstr "Link wurde erfolgreich kopiert" msgid "common.share-link.link-deleted-success" msgstr "Link wurde erfolgreich gelöscht" -msgid "common.share-link.permissions-can-access" -msgstr "Freigabe für" - -msgid "common.share-link.permissions-can-view" -msgstr "Sichtbar" - msgid "common.share-link.permissions-hint" msgstr "Jeder mit dem Link kann auf die Datei zugreifen" msgid "common.share-link.placeholder" msgstr "Link zum Teilen wird hier erscheinen" -msgid "common.share-link.remove-link" -msgstr "Link entfernen" - msgid "common.share-link.title" msgstr "Prototypen teilen" -msgid "common.share-link.view-all-pages" -msgstr "Alle Seiten" - -msgid "common.share-link.view-current-page" -msgstr "Nur diese Seite" - -msgid "common.share-link.view-selected-pages" -msgstr "Ausgewählte Seiten" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4cd45fb866..ccaf94b024 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -183,11 +183,14 @@ msgstr "Link copied successfully" msgid "common.share-link.link-deleted-success" msgstr "Link deleted successfully" -msgid "common.share-link.permissions-can-access" -msgstr "Can access" +msgid "common.share-link.permissions-can-comment" +msgstr "Can comment" -msgid "common.share-link.permissions-can-view" -msgstr "Can view" +msgid "common.share-link.permissions-can-inspect" +msgstr "Can inspect code" + +msgid "common.share-link.permissions-pages" +msgstr "Pages shared" msgid "common.share-link.permissions-hint" msgstr "Anyone with link will have access" @@ -195,20 +198,31 @@ msgstr "Anyone with link will have access" msgid "common.share-link.placeholder" msgstr "Shareable link will appear here" -msgid "common.share-link.remove-link" -msgstr "Remove link" +msgid "common.share-link.destroy-link" +msgstr "Destroy link" msgid "common.share-link.title" msgstr "Share prototypes" -msgid "common.share-link.view-all-pages" -msgstr "All pages" +msgid "common.share-link.view-all" +msgstr "Select All" -msgid "common.share-link.view-current-page" -msgstr "Only this page" +msgid "common.share-link.current-tag" +msgstr "(current)" -msgid "common.share-link.view-selected-pages" -msgstr "Selected pages" +msgid "common.share-link.manage-ops" +msgstr "Manage permissions" + +msgid "common.share-link.team-members" +msgstr "Only team members" + +msgid "common.share-link.all-users" +msgstr "All Penpot users" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "1 page shared" +msgstr[1] "%s pages shared" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b6ec5136c3..ad6977a9c6 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -188,11 +188,14 @@ msgstr "Enlace copiado satisfactoriamente" msgid "common.share-link.link-deleted-success" msgstr "Enlace eliminado correctamente" -msgid "common.share-link.permissions-can-access" -msgstr "Puede acceder a" +msgid "common.share-link.permissions-can-comment" +msgstr "Pueden comentar" -msgid "common.share-link.permissions-can-view" -msgstr "Puede ver" +msgid "common.share-link.permissions-can-inspect" +msgstr "Pueden ver código" + +msgid "common.share-link.permissions-pages" +msgstr "Páginas compartidas" msgid "common.share-link.permissions-hint" msgstr "Cualquiera con el enlace puede acceder" @@ -200,20 +203,31 @@ msgstr "Cualquiera con el enlace puede acceder" msgid "common.share-link.placeholder" msgstr "El enlace para compartir aparecerá aquí" -msgid "common.share-link.remove-link" +msgid "common.share-link.destroy-link" msgstr "Eliminar enlace" msgid "common.share-link.title" msgstr "Compartir prototipos" -msgid "common.share-link.view-all-pages" -msgstr "Todas las paginas" +msgid "common.share-link.view-all" +msgstr "Selecctionar todas" -msgid "common.share-link.view-current-page" -msgstr "Solo esta pagina" +msgid "common.share-link.current-tag" +msgstr "(actual)" -msgid "common.share-link.view-selected-pages" -msgstr "Paginas seleccionadas" +msgid "common.share-link.manage-ops" +msgstr "Gestionar permisos" + +msgid "common.share-link.team-members" +msgstr "Sólo integrantes del equipo" + +msgid "common.share-link.all-users" +msgstr "Todo usario de Penpot" + +msgid "common.share-link.page-shared" +msgid_plural "common.share-link.page-shared" +msgstr[0] "1 página compartida" +msgstr[1] "%s páginas compartidas" #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs diff --git a/frontend/translations/fa.po b/frontend/translations/fa.po index 597fada276..21aaf336b5 100644 --- a/frontend/translations/fa.po +++ b/frontend/translations/fa.po @@ -189,30 +189,12 @@ msgstr "لینک با موفقیت کپی شد" msgid "common.share-link.link-deleted-success" msgstr "لینک با موفقیت حذف شد" -msgid "common.share-link.permissions-can-access" -msgstr "می‌تواند دسترسی داشته باشد" - -msgid "common.share-link.permissions-can-view" -msgstr "می‌تواند مشاهده کند" - msgid "common.share-link.permissions-hint" msgstr "هر کسی که لینک داشته باشد دسترسی خواهد داشت" -msgid "common.share-link.remove-link" -msgstr "حذف لینک" - msgid "common.share-link.title" msgstr "اشتراک‌گذاری پروتوتایپ‌ها" -msgid "common.share-link.view-all-pages" -msgstr "تمام صفحات" - -msgid "common.share-link.view-current-page" -msgstr "فقط این صفحه" - -msgid "common.share-link.view-selected-pages" -msgstr "صفحات انتخاب‌شده" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "افزودن به‌عنوان کتابخانه مشترک" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 72a3fc1bc8..a7fc116dc0 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -186,33 +186,15 @@ msgstr "Lien copié avec succès" msgid "common.share-link.link-deleted-success" msgstr "Lien supprimé avec succès" -msgid "common.share-link.permissions-can-access" -msgstr "Peut y accéder" - -msgid "common.share-link.permissions-can-view" -msgstr "Peut le visionner" - msgid "common.share-link.permissions-hint" msgstr "N'importe qui possédant ce lien peut y accéder" msgid "common.share-link.placeholder" msgstr "Le lien à partager apparaîtra ici" -msgid "common.share-link.remove-link" -msgstr "Supprimer le lien" - msgid "common.share-link.title" msgstr "Partager les prototypes" -msgid "common.share-link.view-all-pages" -msgstr "Toutes les pages" - -msgid "common.share-link.view-current-page" -msgstr "Seulement cette page" - -msgid "common.share-link.view-selected-pages" -msgstr "Pages sélectionnées" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 7129f0cd26..64417c503e 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -179,33 +179,15 @@ msgstr "הקישור הועתק בהצלחה" msgid "common.share-link.link-deleted-success" msgstr "הקישור נמחק בהצלחה" -msgid "common.share-link.permissions-can-access" -msgstr "יש גישה" - -msgid "common.share-link.permissions-can-view" -msgstr "אפשר לצפות" - msgid "common.share-link.permissions-hint" msgstr "כל מי שיש לו את הקישור יכול לגשת" msgid "common.share-link.placeholder" msgstr "הקישור לשיתוף יופיע כאן" -msgid "common.share-link.remove-link" -msgstr "הסרת קישור" - msgid "common.share-link.title" msgstr "שיתוף אבות טיפוס" -msgid "common.share-link.view-all-pages" -msgstr "כל העמודים" - -msgid "common.share-link.view-current-page" -msgstr "רק העמוד הזה" - -msgid "common.share-link.view-selected-pages" -msgstr "עמודים נבחרים" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index adc798da59..035806315a 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -189,20 +189,10 @@ msgstr "Tautan berhasil disalin" msgid "common.share-link.link-deleted-success" msgstr "Tautan berhasil dihapus" -msgid "common.share-link.permissions-can-access" -msgstr "Dapat mengakses" - -msgid "common.share-link.permissions-can-view" -msgstr "Dapat melihat" msgid "common.share-link.permissions-hint" msgstr "Siapapun yang memiliki tautan dapat mengakses" -msgid "common.share-link.view-current-page" -msgstr "Hanya halaman ini" - -msgid "common.share-link.view-selected-pages" -msgstr "Halaman yang dipilih" #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/lt.po b/frontend/translations/lt.po index 016e16ab9b..8638740d8f 100644 --- a/frontend/translations/lt.po +++ b/frontend/translations/lt.po @@ -188,33 +188,15 @@ msgstr "Nuoroda sėkmingai nukopijuota" msgid "common.share-link.link-deleted-success" msgstr "Nuoroda sėkmingai ištrinta" -msgid "common.share-link.permissions-can-access" -msgstr "Gali pasiekti" - -msgid "common.share-link.permissions-can-view" -msgstr "Galima peržiūrėti" - msgid "common.share-link.permissions-hint" msgstr "Kiekvienas, turintis nuorodą, turės prieigą" msgid "common.share-link.placeholder" msgstr "Bendrinama nuoroda bus rodoma čia" -msgid "common.share-link.remove-link" -msgstr "Pašalinti nuorodą" - msgid "common.share-link.title" msgstr "Dalinkitės prototipais" -msgid "common.share-link.view-all-pages" -msgstr "Visi puslapiai" - -msgid "common.share-link.view-current-page" -msgstr "Tik šis puslapis" - -msgid "common.share-link.view-selected-pages" -msgstr "Parinkti puslapiai" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Pridėti kaip bendrinamą biblioteką" diff --git a/frontend/translations/ml.po b/frontend/translations/ml.po index 80e7216828..71beaaa818 100644 --- a/frontend/translations/ml.po +++ b/frontend/translations/ml.po @@ -189,33 +189,15 @@ msgstr "കണ്ണി വിജയകരമായി പകർത്തി" msgid "common.share-link.link-deleted-success" msgstr "കണ്ണി വിജയകരമായി മായിച്ചു" -msgid "common.share-link.permissions-can-access" -msgstr "പ്രാപ്യമാണ്" - -msgid "common.share-link.permissions-can-view" -msgstr "കാണാവുന്നതാണ്" - msgid "common.share-link.permissions-hint" msgstr "കണ്ണിയുള്ള ആർക്കും പ്രാപ്യമാകും" msgid "common.share-link.placeholder" msgstr "പങ്കുവെക്കാവുന്ന കണ്ണി ഇവിടെ ലഭ്യമാകും" -msgid "common.share-link.remove-link" -msgstr "കണ്ണി നീക്കുക" - msgid "common.share-link.title" msgstr "പ്രോട്ടോടൈപ്പുകൾ പങ്കുവെയ്ക്കുക" -msgid "common.share-link.view-all-pages" -msgstr "എല്ലാ താളുകളും" - -msgid "common.share-link.view-current-page" -msgstr "ഈ താൾ മാത്രം" - -msgid "common.share-link.view-selected-pages" -msgstr "തിരഞ്ഞെടുത്ത താളുകൾ" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "പങ്കിട്ട ലൈബ്രറിയായി ചേർക്കുക" diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po index 70a4ed3798..0e11e03a73 100644 --- a/frontend/translations/pl.po +++ b/frontend/translations/pl.po @@ -186,33 +186,15 @@ msgstr "Link skopiowano pomyślnie" msgid "common.share-link.link-deleted-success" msgstr "Link usunięto pomyślnie" -msgid "common.share-link.permissions-can-access" -msgstr "Można uzyskać dostęp" - -msgid "common.share-link.permissions-can-view" -msgstr "Można zobaczyć" - msgid "common.share-link.permissions-hint" msgstr "Każdy, kto ma link, będzie miał dostęp" msgid "common.share-link.placeholder" msgstr "Tutaj pojawi się link do udostępniania" -msgid "common.share-link.remove-link" -msgstr "Usuń link" - msgid "common.share-link.title" msgstr "Udostępnij prototypy" -msgid "common.share-link.view-all-pages" -msgstr "Wszystkie strony" - -msgid "common.share-link.view-current-page" -msgstr "Tylko ta strona" - -msgid "common.share-link.view-selected-pages" -msgstr "Wybrane strony" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Dodaj jako Udostępnioną Bibliotekę" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index 50cd08e0a6..8dbd536260 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -186,35 +186,16 @@ msgstr "Ссылка скопирована" msgid "common.share-link.link-deleted-success" msgstr "Ссылка удалена" -#, fuzzy -msgid "common.share-link.permissions-can-access" -msgstr "Могут зайти" - -msgid "common.share-link.permissions-can-view" -msgstr "Могут видеть" - msgid "common.share-link.permissions-hint" msgstr "Доступ открыт для получателей ссылки" msgid "common.share-link.placeholder" msgstr "Ссылка появится здесь" -msgid "common.share-link.remove-link" -msgstr "Удалить ссылку" - #, fuzzy msgid "common.share-link.title" msgstr "Поделиться прототипами" -msgid "common.share-link.view-all-pages" -msgstr "Все страницы" - -msgid "common.share-link.view-current-page" -msgstr "Только эту страницу" - -msgid "common.share-link.view-selected-pages" -msgstr "Выбранные страницы" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "Добавить как общую библиотеку" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 130cf57b6f..f92cf5004d 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -186,33 +186,15 @@ msgstr "Bağlantı başarıyla kopyalandı" msgid "common.share-link.link-deleted-success" msgstr "Bağlantı başarıyla silindi" -msgid "common.share-link.permissions-can-access" -msgstr "Erişebilir" - -msgid "common.share-link.permissions-can-view" -msgstr "Görüntüleyebilir" - msgid "common.share-link.permissions-hint" msgstr "Bağlantıya sahip olan herkes erişebilir" msgid "common.share-link.placeholder" msgstr "Paylaşılabilir bağlantı burada görünecek" -msgid "common.share-link.remove-link" -msgstr "Bağlantıyı kaldır" - msgid "common.share-link.title" msgstr "Prototipleri paylaş" -msgid "common.share-link.view-all-pages" -msgstr "Tüm sayfalar" - -msgid "common.share-link.view-current-page" -msgstr "Yalnızca bu sayfa" - -msgid "common.share-link.view-selected-pages" -msgstr "Seçili sayfalar" - #: src/app/main/ui/workspace/header.cljs, #: src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index d6b44316a0..76baa1a9bd 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -177,13 +177,6 @@ msgstr "链接已复制" msgid "common.share-link.link-deleted-success" msgstr "链接已移除" -#, fuzzy -msgid "common.share-link.permissions-can-access" -msgstr "可访问" - -msgid "common.share-link.permissions-can-view" -msgstr "可浏览" - #, fuzzy msgid "common.share-link.permissions-hint" msgstr "任何人通过此链接都可访问" @@ -191,21 +184,9 @@ msgstr "任何人通过此链接都可访问" msgid "common.share-link.placeholder" msgstr "可分享的链接会在此处显示" -msgid "common.share-link.remove-link" -msgstr "移除链接" - msgid "common.share-link.title" msgstr "分享原型" -msgid "common.share-link.view-all-pages" -msgstr "全部页面" - -msgid "common.share-link.view-current-page" -msgstr "仅此页面" - -msgid "common.share-link.view-selected-pages" -msgstr "选中的页面" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "dashboard.add-shared" msgstr "添加为共享库" diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index eade57f69f..104ee54225 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -181,33 +181,15 @@ msgstr "成功複製連結" msgid "common.share-link.link-deleted-success" msgstr "成功刪除連結" -msgid "common.share-link.permissions-can-access" -msgstr "能夠存取" - -msgid "common.share-link.permissions-can-view" -msgstr "能夠檢視" - msgid "common.share-link.permissions-hint" msgstr "任何有連結的人皆能存取" msgid "common.share-link.placeholder" msgstr "可分享的連結將會在此顯示" -msgid "common.share-link.remove-link" -msgstr "移除連結" - msgid "common.share-link.title" msgstr "分享原型" -msgid "common.share-link.view-all-pages" -msgstr "所有頁面" - -msgid "common.share-link.view-current-page" -msgstr "僅此頁面" - -msgid "common.share-link.view-selected-pages" -msgstr "選擇的頁面" - #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs #, fuzzy msgid "dashboard.add-shared" From 115314e97cf95091d428dc8455e744564027c460 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 22 Jun 2022 18:16:34 +0200 Subject: [PATCH 49/86] :sparkles: In view mode allow comment/inspect to non-team users (by shared link permissions) --- CHANGES.md | 1 + backend/src/app/migrations.clj | 8 +- ...able.sql => 0075-mod-share-link-table.sql} | 0 backend/src/app/rpc/mutations/comments.clj | 37 +++++--- backend/src/app/rpc/permissions.clj | 10 ++ backend/src/app/rpc/queries/comments.clj | 63 +++++++++--- backend/src/app/rpc/queries/files.clj | 18 +++- backend/src/app/rpc/queries/viewer.clj | 10 +- backend/test/app/services_viewer_test.clj | 3 +- frontend/src/app/main/data/comments.cljs | 95 +++++++++++-------- frontend/src/app/main/data/users.cljs | 15 ++- frontend/src/app/main/data/viewer.cljs | 13 ++- frontend/src/app/main/data/workspace.cljs | 5 +- .../app/main/data/workspace/persistence.cljs | 31 +++--- frontend/src/app/main/refs.cljs | 3 + frontend/src/app/main/ui/comments.cljs | 1 - .../src/app/main/ui/dashboard/comments.cljs | 2 +- frontend/src/app/main/ui/modal.cljs | 1 - frontend/src/app/main/ui/share_link.cljs | 2 - frontend/src/app/main/ui/viewer.cljs | 55 +++++++---- frontend/src/app/main/ui/viewer/header.cljs | 9 +- .../src/app/main/ui/workspace/comments.cljs | 2 +- .../main/ui/workspace/viewport/comments.cljs | 2 +- 23 files changed, 254 insertions(+), 132 deletions(-) rename backend/src/app/migrations/sql/{0073-mod-share-link-table.sql => 0075-mod-share-link-table.sql} (100%) diff --git a/CHANGES.md b/CHANGES.md index 966de9ca51..310170e310 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982) - View mode improvements to enable access and use in different conditions [Taiga #3023](https://tree.taiga.io/project/penpot/us/3023) +- Improved share link options. Now you can allow non-team members to comment and/or inspect [Taiga #3056] (https://tree.taiga.io/project/penpot/us/3056) ### :bug: Bugs fixed ### :arrow_up: Deps updates diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 068ff52023..9b55fb4ca7 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -228,13 +228,13 @@ :fn (mg/resource "app/migrations/sql/0072-mod-file-object-thumbnail-table.sql")} {:name "0073-mod-file-media-object-constraints" - :fn (mg/resource "app/migrations/sql/0073-mod-file-media-object-constraints.sql")} - - {:name "0073-mod-share-link-table" - :fn (mg/resource "app/migrations/sql/0073-mod-share-link-table.sql")} + :fn (mg/resource "app/migrations/sql/0073-mod-file-media-object-constraints.sql")} {:name "0074-mod-file-library-rel-constraints" :fn (mg/resource "app/migrations/sql/0074-mod-file-library-rel-constraints.sql")} + + {:name "0075-mod-share-link-table" + :fn (mg/resource "app/migrations/sql/0075-mod-share-link-table.sql")} ]) diff --git a/backend/src/app/migrations/sql/0073-mod-share-link-table.sql b/backend/src/app/migrations/sql/0075-mod-share-link-table.sql similarity index 100% rename from backend/src/app/migrations/sql/0073-mod-share-link-table.sql rename to backend/src/app/migrations/sql/0075-mod-share-link-table.sql diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj index 438cfdebd9..45e75a626f 100644 --- a/backend/src/app/rpc/mutations/comments.clj +++ b/backend/src/app/rpc/mutations/comments.clj @@ -26,19 +26,21 @@ (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::profile-id ::us/uuid) (s/def ::position ::gpt/point) (s/def ::content ::us/string) (s/def ::create-comment-thread - (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id])) + (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id] + :opt-un [::share-id])) (sv/defmethod ::create-comment-thread {::retry/max-retries 3 ::retry/matches retry/conflict-db-insert?} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] (db/with-atomic [conn pool] - (files/check-read-permissions! conn profile-id file-id) + (files/check-comment-permissions! conn profile-id file-id share-id) (create-comment-thread conn params))) (defn- retrieve-next-seqn @@ -92,18 +94,20 @@ ;; --- Mutation: Update Comment Thread Status (s/def ::id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::update-comment-thread-status - (s/keys :req-un [::profile-id ::id])) + (s/keys :req-un [::profile-id ::id] + :opt-un [::share-id])) (sv/defmethod ::update-comment-thread-status - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}] (db/with-atomic [conn pool] (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})] (when-not cthr (ex/raise :type :not-found)) - (files/check-read-permissions! conn profile-id (:file-id cthr)) + (files/check-comment-permissions! conn profile-id (:file-id cthr) share-id) (upsert-comment-thread-status! conn profile-id (:id cthr))))) (def sql:upsert-comment-thread-status @@ -122,16 +126,17 @@ (s/def ::is-resolved ::us/boolean) (s/def ::update-comment-thread - (s/keys :req-un [::profile-id ::id ::is-resolved])) + (s/keys :req-un [::profile-id ::id ::is-resolved] + :opt-un [::share-id])) (sv/defmethod ::update-comment-thread - [{:keys [pool] :as cfg} {:keys [profile-id id is-resolved] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}] (db/with-atomic [conn pool] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (when-not thread (ex/raise :type :not-found)) - (files/check-read-permissions! conn profile-id (:file-id thread)) + (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) (db/update! conn :comment-thread {:is-resolved is-resolved} @@ -142,10 +147,11 @@ ;; --- Mutation: Add Comment (s/def ::add-comment - (s/keys :req-un [::profile-id ::thread-id ::content])) + (s/keys :req-un [::profile-id ::thread-id ::content] + :opt-un [::share-id])) (sv/defmethod ::add-comment - [{:keys [pool] :as cfg} {:keys [profile-id thread-id content] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id thread-id content share-id] :as params}] (db/with-atomic [conn pool] (let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true}) (comments/decode-row)) @@ -155,7 +161,7 @@ (when-not thread (ex/raise :type :not-found)) ;; Permission Checks - (files/check-read-permissions! conn profile-id (:file-id thread)) + (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) ;; Update the page-name cachedattribute on comment thread table. (when (not= pname (:page-name thread)) @@ -199,10 +205,11 @@ ;; --- Mutation: Update Comment (s/def ::update-comment - (s/keys :req-un [::profile-id ::id ::content])) + (s/keys :req-un [::profile-id ::id ::content] + :opt-un [::share-id])) (sv/defmethod ::update-comment - [{:keys [pool] :as cfg} {:keys [profile-id id content] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id id content share-id] :as params}] (db/with-atomic [conn pool] (let [comment (db/get-by-id conn :comment id {:for-update true}) _ (when-not comment (ex/raise :type :not-found)) @@ -210,7 +217,7 @@ _ (when-not thread (ex/raise :type :not-found)) pname (retrieve-page-name conn thread)] - (files/check-read-permissions! conn profile-id (:file-id thread)) + (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) ;; Don't allow edit comments to not owners (when-not (= (:owner-id thread) profile-id) diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index 363f967e65..773038253b 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -53,6 +53,16 @@ ([perms] (:can-read perms)) ([conn & args] (check (apply qfn conn args))))) +(defn make-comment-predicate-fn + "A simple factory for comment permission predicate functions." + [qfn] + (us/assert fn? qfn) + (fn check + ([perms] + (and (:is-logged perms) (= (:who-comment perms) "all"))) + ([conn & args] + (check (apply qfn conn args))))) + (defn make-check-fn "Helper that converts a predicate permission function to a check function (function that raises an exception)." diff --git a/backend/src/app/rpc/queries/comments.clj b/backend/src/app/rpc/queries/comments.clj index 1894022f91..3815ef31c9 100644 --- a/backend/src/app/rpc/queries/comments.clj +++ b/backend/src/app/rpc/queries/comments.clj @@ -25,16 +25,16 @@ (s/def ::team-id ::us/uuid) (s/def ::file-id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::comment-threads (s/and (s/keys :req-un [::profile-id] - :opt-un [::file-id ::team-id]) + :opt-un [::file-id ::share-id ::team-id]) #(or (:file-id %) (:team-id %)))) (sv/defmethod ::comment-threads - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [pool] :as cfg} params] (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) (retrieve-comment-threads conn params))) (def sql:comment-threads @@ -60,8 +60,8 @@ window w as (partition by c.thread_id order by c.created_at asc)") (defn- retrieve-comment-threads - [conn {:keys [profile-id file-id]}] - (files/check-read-permissions! conn profile-id file-id) + [conn {:keys [profile-id file-id share-id]}] + (files/check-comment-permissions! conn profile-id file-id share-id) (->> (db/exec! conn [sql:comment-threads profile-id file-id]) (into [] (map decode-row)))) @@ -116,13 +116,15 @@ ;; --- Query: Single Comment Thread (s/def ::id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::comment-thread - (s/keys :req-un [::profile-id ::file-id ::id])) + (s/keys :req-un [::profile-id ::file-id ::id] + :opt-un [::share-id])) (sv/defmethod ::comment-thread - [{:keys [pool] :as cfg} {:keys [profile-id file-id id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}] (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) + (files/check-comment-permissions! conn profile-id file-id share-id) (let [sql (str "with threads as (" sql:comment-threads ")" "select * from threads where id = ?")] (-> (db/exec-one! conn [sql profile-id file-id id]) @@ -133,15 +135,17 @@ (declare retrieve-comments) (s/def ::file-id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::thread-id ::us/uuid) (s/def ::comments - (s/keys :req-un [::profile-id ::thread-id])) + (s/keys :req-un [::profile-id ::thread-id] + :opt-un [::share-id])) (sv/defmethod ::comments - [{:keys [pool] :as cfg} {:keys [profile-id thread-id] :as params}] + [{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}] (with-open [conn (db/open pool)] (let [thread (db/get-by-id conn :comment-thread thread-id)] - (files/check-read-permissions! conn profile-id (:file-id thread)) + (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) (retrieve-comments conn thread-id)))) (def sql:comments @@ -153,3 +157,40 @@ [conn thread-id] (->> (db/exec! conn [sql:comments thread-id]) (into [] (map decode-row)))) + +;; file-comments-users + +(declare retrieve-file-comments-users) + +(s/def ::file-id ::us/uuid) +(s/def ::share-id (s/nilable ::us/uuid)) + +(s/def ::file-comments-users + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::share-id])) + +(sv/defmethod ::file-comments-users + [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}] + (with-open [conn (db/open pool)] + (files/check-comment-permissions! conn profile-id file-id share-id) + (retrieve-file-comments-users conn file-id profile-id))) + +(def sql:file-comment-users + "select p.id, + p.email, + p.fullname as name, + p.fullname as fullname, + p.photo_id, + p.is_active + from profile p + where p.id in + (select owner_id from comment + where thread_id in + (select id from comment_thread + where file_id=?)) + or p.id=? + ") ;; all the users that had comment the file, plus the current user + +(defn retrieve-file-comments-users + [conn file-id profile-id] + (db/exec! conn [sql:file-comment-users file-id profile-id])) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 946a7902f8..9460be08ff 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -98,7 +98,9 @@ (some? perms) perms (some? ldata) {:type :share-link :can-read true - :flags (:flags ldata)})))) + :is-logged (some? profile-id) + :who-comment (:who-comment ldata) + :who-inspect (:who-inspect ldata)})))) (def has-edit-permissions? (perms/make-edition-predicate-fn get-permissions)) @@ -106,12 +108,26 @@ (def has-read-permissions? (perms/make-read-predicate-fn get-permissions)) +(def has-comment-permissions? + (perms/make-comment-predicate-fn get-permissions)) + (def check-edition-permissions! (perms/make-check-fn has-edit-permissions?)) (def check-read-permissions! (perms/make-check-fn has-read-permissions?)) +;; A user has comment permissions if she has read permissions, or comment permissions +(defn check-comment-permissions! + [conn profile-id file-id share-id] + (let [can-read (has-read-permissions? conn profile-id file-id) + can-comment (has-comment-permissions? conn profile-id file-id share-id) + ] + (when-not (or can-read can-comment) + (ex/raise :type :not-found + :code :object-not-found + :hint "not found")))) + ;; --- Query: Files search ;; TODO: this query need to a good refactor diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index b98de0a381..a82e08e65c 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -9,9 +9,9 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.db :as db] + [app.rpc.queries.comments :as comments] [app.rpc.queries.files :as files] - [app.rpc.queries.share-link :as slnk] - [app.rpc.queries.teams :as teams] + [app.rpc.queries.share-link :as slnk] [app.util.services :as sv] [clojure.spec.alpha :as s] [promesa.core :as p])) @@ -23,11 +23,11 @@ (db/get-by-id pool :project id {:columns [:id :name :team-id]})) (defn- retrieve-bundle - [{:keys [pool] :as cfg} file-id] + [{:keys [pool] :as cfg} file-id profile-id] (p/let [file (files/retrieve-file cfg file-id) project (retrieve-project pool (:project-id file)) libs (files/retrieve-file-libraries cfg false file-id) - users (teams/retrieve-users pool (:team-id project)) + users (comments/retrieve-file-comments-users pool file-id profile-id) links (->> (db/query pool :share-link {:file-id file-id}) (mapv slnk/decode-share-link-row)) @@ -54,7 +54,7 @@ (p/let [slink (slnk/retrieve-share-link pool file-id share-id) perms (files/get-permissions pool profile-id file-id share-id) thumbs (files/retrieve-object-thumbnails cfg file-id) - bundle (p/-> (retrieve-bundle cfg file-id) + bundle (p/-> (retrieve-bundle cfg file-id profile-id) (assoc :permissions perms) (assoc-in [:file :thumbnails] thumbs))] diff --git a/backend/test/app/services_viewer_test.clj b/backend/test/app/services_viewer_test.clj index 1b9d7c0133..e8a01c255a 100644 --- a/backend/test/app/services_viewer_test.clj +++ b/backend/test/app/services_viewer_test.clj @@ -49,7 +49,8 @@ :profile-id (:id prof) :file-id (:id file) :pages #{(get-in file [:data :pages 0])} - :flags #{}} + :who-comment "team" + :who-inspect "all"} out (th/mutation! data)] ;; (th/print-result! out) diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 3dd8741c00..8d749e5f35 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -75,20 +75,23 @@ (ptk/reify ::create-comment-thread ptk/WatchEvent - (watch [_ _ _] - (->> (rp/mutation :create-comment-thread params) - (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)})) - (rx/map #(partial created %)) - (rx/catch #(rx/throw {:type :comment-error}))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id) + params (assoc params :share-id share-id)] + (->> (rp/mutation :create-comment-thread params) + (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %) :share-id share-id})) + (rx/map #(partial created %)) + (rx/catch #(rx/throw {:type :comment-error})))))))) (defn update-comment-thread-status [{:keys [id] :as thread}] (us/assert ::comment-thread thread) (ptk/reify ::update-comment-thread-status ptk/WatchEvent - (watch [_ _ _] - (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)] - (->> (rp/mutation :update-comment-thread-status {:id id}) + (watch [_ state _] + (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0) + share-id (-> state :viewer-local :share-id)] + (->> (rp/mutation :update-comment-thread-status {:id id :share-id share-id}) (rx/map (constantly done)) (rx/catch #(rx/throw {:type :comment-error}))))))) @@ -105,10 +108,11 @@ (d/update-in-when state [:comment-threads id] assoc :is-resolved is-resolved)) ptk/WatchEvent - (watch [_ _ _] - (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)))))) (defn add-comment @@ -119,12 +123,13 @@ (update-in state [:comments (:id thread)] assoc (:id comment) comment))] (ptk/reify ::create-comment ptk/WatchEvent - (watch [_ _ _] - (rx/concat - (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content}) - (rx/map #(partial created %)) - (rx/catch #(rx/throw {:type :comment-error}))) - (rx/of (refresh-comment-thread thread))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (rx/concat + (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content :share-id share-id}) + (rx/map #(partial created %)) + (rx/catch #(rx/throw {:type :comment-error}))) + (rx/of (refresh-comment-thread thread)))))))) (defn update-comment [{:keys [id content thread-id] :as comment}] @@ -135,10 +140,11 @@ (d/update-in-when state [:comments thread-id id] assoc :content content)) ptk/WatchEvent - (watch [_ _ _] - (->> (rp/mutation :update-comment {:id id :content content}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/mutation :update-comment {:id id :content content :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)))))) (defn delete-comment-thread [{:keys [id] :as thread}] @@ -151,10 +157,11 @@ (update :comment-threads dissoc id))) ptk/WatchEvent - (watch [_ _ _] - (->> (rp/mutation :delete-comment-thread {:id id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/mutation :delete-comment-thread {:id id :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)))))) (defn delete-comment [{:keys [id thread-id] :as comment}] @@ -165,10 +172,11 @@ (d/update-in-when state [:comments thread-id] dissoc id)) ptk/WatchEvent - (watch [_ _ _] - (->> (rp/mutation :delete-comment {:id id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/mutation :delete-comment {:id id :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)))))) (defn refresh-comment-thread [{:keys [id file-id] :as thread}] @@ -177,10 +185,11 @@ (assoc-in state [:comment-threads id] thread))] (ptk/reify ::refresh-comment-thread ptk/WatchEvent - (watch [_ _ _] - (->> (rp/query :comment-thread {:file-id file-id :id id}) - (rx/map #(partial fetched %)) - (rx/catch #(rx/throw {:type :comment-error}))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/query :comment-thread {:file-id file-id :id id :share-id share-id}) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error})))))))) (defn retrieve-comment-threads [file-id] @@ -189,10 +198,11 @@ (assoc state :comment-threads (d/index-by :id data)))] (ptk/reify ::retrieve-comment-threads ptk/WatchEvent - (watch [_ _ _] - (->> (rp/query :comment-threads {:file-id file-id}) - (rx/map #(partial fetched %)) - (rx/catch #(rx/throw {:type :comment-error}))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/query :comment-threads {:file-id file-id :share-id share-id}) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error})))))))) (defn retrieve-comments [thread-id] @@ -201,10 +211,11 @@ (update state :comments assoc thread-id (d/index-by :id comments)))] (ptk/reify ::retrieve-comments ptk/WatchEvent - (watch [_ _ _] - (->> (rp/query :comments {:thread-id thread-id}) - (rx/map #(partial fetched %)) - (rx/catch #(rx/throw {:type :comment-error}))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/query :comments {:thread-id thread-id :share-id share-id}) + (rx/map #(partial fetched %)) + (rx/catch #(rx/throw {:type :comment-error})))))))) (defn retrieve-unread-comment-threads "A event used mainly in dashboard for retrieve all unread threads of a team." diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 275c4b1942..7ea9454379 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -436,7 +436,6 @@ (rx/map (constantly (fetch-profile))) (rx/catch on-error)))))) - (defn fetch-users [{:keys [team-id] :as params}] (us/assert ::us/uuid team-id) @@ -450,6 +449,20 @@ (->> (rp/query :team-users {:team-id team-id}) (rx/map #(partial fetched %))))))) +(defn fetch-file-comments-users + [{:keys [team-id] :as params}] + (us/assert ::us/uuid team-id) + (letfn [(fetched [users state] + (->> users + (d/index-by :id) + (assoc state :file-comments-users)))] + (ptk/reify ::fetch-team-users + ptk/WatchEvent + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rp/query :file-comments-users {:team-id team-id :share-id share-id}) + (rx/map #(partial fetched %)))))))) + ;; --- EVENT: request-account-deletion (defn request-account-deletion diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 98f576c940..54c0422221 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -33,7 +33,9 @@ :selected #{} :collapsed #{} :overlays [] - :hover nil}) + :hover nil + :share-id "" + :file-comments-users []}) (declare fetch-comment-threads) (declare fetch-bundle) @@ -50,7 +52,7 @@ :opt-un [::share-id ::page-id])) (defn initialize - [{:keys [file-id] :as params}] + [{:keys [file-id share-id] :as params}] (us/assert ::initialize-params params) (ptk/reify ::initialize ptk/UpdateEvent @@ -61,7 +63,8 @@ (fn [lstate] (if (nil? lstate) default-local-state - lstate))))) + lstate))) + (assoc-in [:viewer-local :share-id] share-id))) ptk/WatchEvent (watch [_ _ _] @@ -138,7 +141,7 @@ (rx/of (go-to-frame-auto)))))))) (defn fetch-comment-threads - [{:keys [file-id page-id] :as params}] + [{:keys [file-id page-id share-id] :as params}] (letfn [(fetched [data state] (->> data (filter #(= page-id (:page-id %))) @@ -153,7 +156,7 @@ (ptk/reify ::fetch-comment-threads ptk/WatchEvent (watch [_ _ _] - (->> (rp/query :comment-threads {:file-id file-id}) + (->> (rp/query :comment-threads {:file-id file-id :share-id share-id}) (rx/map #(partial fetched %)) (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 7885fbc921..d146c13e7e 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -140,7 +140,7 @@ (unchecked-set ug/global "name" name))))) (defn- file-initialized - [{:keys [file users project libraries] :as bundle}] + [{:keys [file users project libraries file-comments-users] :as bundle}] (ptk/reify ::file-initialized ptk/UpdateEvent (update [_ state] @@ -156,7 +156,8 @@ ;; the version number #_(assoc :version 17) #_(app.common.pages.migrations/migrate-data 19)) - :workspace-libraries (d/index-by :id libraries))) + :workspace-libraries (d/index-by :id libraries) + :current-file-comments-users (d/index-by :id file-comments-users))) ptk/WatchEvent (watch [_ _ _] diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index d01df8f382..7b81dc8cda 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -258,20 +258,23 @@ [project-id file-id] (ptk/reify ::fetch-bundle ptk/WatchEvent - (watch [_ _ _] - (->> (rx/zip (rp/query :file-raw {:id file-id}) - (rp/query :team-users {:file-id file-id}) - (rp/query :project {:id project-id}) - (rp/query :file-libraries {:file-id file-id})) - (rx/take 1) - (rx/map (fn [[file-raw users project libraries]] - {:file-raw file-raw - :users users - :project project - :libraries libraries})) - (rx/mapcat (fn [{:keys [project] :as bundle}] - (rx/of (ptk/data-event ::bundle-fetched bundle) - (df/load-team-fonts (:team-id project))))))))) + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id)] + (->> (rx/zip (rp/query :file-raw {:id file-id}) + (rp/query :team-users {:file-id file-id}) + (rp/query :project {:id project-id}) + (rp/query :file-libraries {:file-id file-id}) + (rp/query :file-comments-users {:file-id file-id :share-id share-id})) + (rx/take 1) + (rx/map (fn [[file-raw users project libraries file-comments-users]] + {:file-raw file-raw + :users users + :project project + :libraries libraries + :file-comments-users file-comments-users})) + (rx/mapcat (fn [{:keys [project] :as bundle}] + (rx/of (ptk/data-event ::bundle-fetched bundle) + (df/load-team-fonts (:team-id project)))))))))) ;; --- Helpers diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 3e709aea15..81d51b461d 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -387,6 +387,9 @@ (def users (l/derived :users st/state)) +(def current-file-comments-users + (l/derived :current-file-comments-users st/state)) + (def viewer-fullscreen? (l/derived (fn [state] (dm/get-in state [:viewer-local :fullscreen?])) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index a7019d3627..5aa993a27f 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -345,7 +345,6 @@ (mf/defc comment-thread [{:keys [item users on-click] :as props}] (let [owner (get users (:owner-id item)) - on-click* (mf/use-callback (mf/deps item) diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index 582ea47ac4..11d3ef7011 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -31,7 +31,7 @@ show-dropdown (mf/use-fn #(reset! show-dropdown? true)) hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) threads-map (mf/deref refs/comment-threads) - users (mf/deref refs/users) + users (mf/deref refs/current-file-comments-users) tgroups (->> (vals threads-map) (sort-by :modified-at) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 9152ac4a14..e4ae3e4bc2 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -84,6 +84,5 @@ [] (let [modal (mf/deref modal-ref)] (when modal - (.log js/console "modal"(clj->js modal)) [:& modal-wrapper {:data modal :key (:id modal)}]))) diff --git a/frontend/src/app/main/ui/share_link.cljs b/frontend/src/app/main/ui/share_link.cljs index 467988c430..490cb2f96c 100644 --- a/frontend/src/app/main/ui/share_link.cljs +++ b/frontend/src/app/main/ui/share_link.cljs @@ -30,8 +30,6 @@ :who-comment who-comment :who-inspect who-inspect}) - - (mf/defc share-link-dialog {::mf/register modal/components ::mf/register-as :share-link} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 7849c7bc45..6ccbc169cb 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -25,7 +25,7 @@ [app.main.ui.icons :as i] [app.main.ui.share-link] [app.main.ui.static :as static] - [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] + [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] [app.main.ui.viewer.handoff :as handoff] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.interactions :as interactions] @@ -84,7 +84,7 @@ (when show-comments-list [:& comments-sidebar {:users users :frame frame :page page}]) - + [:div.viewer-wrapper {:style {:width (:width wrapper-size) :height (:height wrapper-size)}} @@ -140,7 +140,7 @@ :on-click #(when (:close-click-outside overlay) (close-overlay (:frame overlay)))}]) [:div.viewport-container.viewer-overlay - + {:id (str "overlay-" (-> overlay :frame :id)) :style {:width (:width size-over) :height (:height size-over) @@ -169,6 +169,17 @@ (let [{:keys [page-id section index]} params {:keys [file users project permissions]} data + allowed (or + (= section :interactions) + (and (= section :comments) + (or (:can-edit permissions) + (and (true? (:is-logged permissions)) + (= (:who-comment permissions) "all")))) + (and (= section :handoff) + (or (:can-edit permissions) + (and (true? (:is-logged permissions)) + (= (:who-inspect permissions) "all"))))) + local (mf/deref refs/viewer-local) nav-scroll (:nav-scroll local) @@ -241,6 +252,9 @@ (when (nil? page) (ex/raise :type :not-found)) + (when (not allowed) + (st/emit! (dv/go-to-section :interactions))) + ;; Set the page title (mf/use-effect (mf/deps (:name file)) @@ -394,24 +408,23 @@ :index index :viewer-pagination viewer-pagination}] - - [:& viewer-wrapper - {:wrapper-size wrapper-size - :scroll scroll - :orig-frame orig-frame - :orig-viewport-ref orig-viewport-ref - :orig-size orig-size - :page page - :file file - :users users - :current-viewport-ref current-viewport-ref - :size size - :frame frame - :interactions-mode interactions-mode - :overlays overlays - :zoom zoom - :section section - :index index}]))]]])) + [:& viewer-wrapper + {:wrapper-size wrapper-size + :scroll scroll + :orig-frame orig-frame + :orig-viewport-ref orig-viewport-ref + :orig-size orig-size + :page page + :file file + :users users + :current-viewport-ref current-viewport-ref + :size size + :frame frame + :interactions-mode interactions-mode + :overlays overlays + :zoom zoom + :section section + :index index}]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index da39c63b70..5cde91686f 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -188,7 +188,7 @@ [:div.main-icon [:a {:on-click go-to-dashboard ;; If the user doesn't have permission we disable the link - :style {:pointer-events (when-not permissions "none")}} i/logo-icon]] + :style {:pointer-events (when-not (:can-edit permissions) "none")}} i/logo-icon]] [:& header-sitemap {:project project :file file :page page :frame frame :index index}]] @@ -199,7 +199,9 @@ :alt (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))} i/play] - (when (:can-edit permissions) + (when (or (:can-edit permissions) + (and (true? (:is-logged permissions)) + (= (:who-comment permissions) "all"))) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :comments) :class (dom/classnames :active (= section :comments)) @@ -208,7 +210,8 @@ (when (or (= (:type permissions) :membership) (and (= (:type permissions) :share-link) - (contains? (:flags permissions) :section-handoff))) + (true? (:is-logged permissions)) + (= (:who-inspect permissions) "all"))) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click go-to-handoff :class (dom/classnames :active (= section :handoff)) diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index 3963cf9330..e66781a01a 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -60,7 +60,7 @@ [{:keys [users threads page-id]}] (let [threads-map (mf/deref refs/threads-ref) profile (mf/deref refs/profile) - users-refs (mf/deref refs/users) + users-refs (mf/deref refs/current-file-comments-users) users (or users users-refs) local (mf/deref refs/comments-local) options? (mf/use-state false) diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index da82aeaf99..f13a88cfc8 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -20,7 +20,7 @@ pos-y (* (- (:y vbox)) zoom) profile (mf/deref refs/profile) - users (mf/deref refs/users) + users (mf/deref refs/current-file-comments-users) local (mf/deref refs/comments-local) threads-map (mf/deref refs/threads-ref) From 73f1418c954bf447377536499b72c51353c39335 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Jun 2022 14:21:45 +0200 Subject: [PATCH 50/86] :bug: Normalize return value from parse-client-ip function --- backend/src/app/loggers/audit.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 89ffb1611a..dc1289a094 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -32,7 +32,7 @@ [request] (or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first) (yrq/get-header request "x-real-ip") - (yrq/remote-addr request))) + (some-> (yrq/remote-addr request) str))) (defn extract-utm-params "Extracts additional data from params and namespace them under From 6de78cabd45658ab220dadbffe9823be20f82961 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Jun 2022 14:22:50 +0200 Subject: [PATCH 51/86] :arrow_up: Update shadow-cljs cljs compiler on frontend and common --- common/deps.edn | 2 +- common/package.json | 2 +- frontend/deps.edn | 2 +- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/common/deps.edn b/common/deps.edn index 370e6d2d9c..2f5e6bdf7c 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -42,7 +42,7 @@ {:extra-deps {org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} - thheller/shadow-cljs {:mvn/version "2.19.3"} + thheller/shadow-cljs {:mvn/version "2.19.5"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} diff --git a/common/package.json b/common/package.json index d8e1a16c07..9e714e3e33 100644 --- a/common/package.json +++ b/common/package.json @@ -13,7 +13,7 @@ "test": "yarn run compile-test && yarn run run-test" }, "devDependencies": { - "shadow-cljs": "2.19.3", + "shadow-cljs": "2.19.5", "source-map-support": "^0.5.19", "ws": "^7.4.6" } diff --git a/frontend/deps.edn b/frontend/deps.edn index c94b5246b2..15fc6a3c81 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -32,7 +32,7 @@ :dev {:extra-paths ["dev"] :extra-deps - {thheller/shadow-cljs {:mvn/version "2.19.3"} + {thheller/shadow-cljs {:mvn/version "2.19.5"} org.clojure/tools.namespace {:mvn/version "RELEASE"} cider/cider-nrepl {:mvn/version "0.28.4"}}} diff --git a/frontend/package.json b/frontend/package.json index d1e140ca70..f3e80cf20e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,7 +48,7 @@ "prettier": "^2.6.1", "rimraf": "^3.0.0", "sass": "^1.49.9", - "shadow-cljs": "2.19.3" + "shadow-cljs": "2.19.5" }, "dependencies": { "@sentry/browser": "^6.17.4", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f3c3236e2f..1364fe0ff6 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5013,10 +5013,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.19.3: - version "2.19.3" - resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.3.tgz#115a33917f8bca1495e0f815dca7ec3957f669af" - integrity sha512-9TsTCRlmR8m1g2ekwblgomRUgJpbifQI99VlRrlH9NMqEzklev3zYAD1dvy4d5h8BoAhgdxOOEg7ld2d45CWTA== +shadow-cljs@2.19.5: + version "2.19.5" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.5.tgz#e51c758d2f942db18e6e4015bcacf1857ad1e751" + integrity sha512-uZelOtmTYg4MOZP1ehJilhQcGDxkdybPKkGZ11qxp8awmfgAQMe+W/QEyZw4aVwFxVXyHIIerzCGkCqAgo/FuA== dependencies: node-libs-browser "^2.2.1" readline-sync "^1.4.7" From 935639411cadd6b542a0ba6ad43e484d9a77aaac Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Jun 2022 14:37:49 +0200 Subject: [PATCH 52/86] :arrow_up: Update devenv to use latest ubuntu lts and jdk18 --- docker/devenv/Dockerfile | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 4523423ed5..7299b9904e 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -1,11 +1,11 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=v16.15.1 \ - CLOJURE_VERSION=1.11.1.1124 \ - CLJKONDO_VERSION=2022.05.31 \ + CLOJURE_VERSION=1.11.1.1149 \ + CLJKONDO_VERSION=2022.06.22 \ BABASHKA_VERSION=0.8.156 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -44,7 +44,6 @@ RUN set -ex; \ RUN set -ex; \ apt-get -qq update; \ apt-get -qqy install --no-install-recommends \ - python \ build-essential \ imagemagick \ ghostscript \ @@ -104,7 +103,7 @@ RUN set -ex; \ rm -rf /var/lib/apt/lists/*; RUN set -ex; \ - curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz; \ + curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz; \ mkdir -p /usr/lib/jvm/openjdk; \ cd /usr/lib/jvm/openjdk; \ tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ @@ -120,7 +119,7 @@ RUN set -ex; \ RUN set -ex; \ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -; \ - echo "deb http://apt.postgresql.org/pub/repos/apt focal-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ + echo "deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ apt-get -qq update; \ apt-get -qqy install postgresql-client-13; \ rm -rf /var/lib/apt/lists/*; @@ -132,8 +131,8 @@ RUN set -ex; \ tar -xf /tmp/nodejs.tar.xz --strip-components=1; \ chown -R root /usr/local/nodejs; \ PATH="$PATH:/usr/local/nodejs/bin"; \ - /usr/local/nodejs/bin/npm install -g yarn; \ - /usr/local/nodejs/bin/npm install -g svgo; \ + /usr/local/nodejs/bin/npm install --location=global yarn; \ + /usr/local/nodejs/bin/npm install --location=global svgo; \ rm -rf /tmp/nodejs.tar.xz; # Install clj-kondo @@ -143,7 +142,6 @@ RUN set -ex; \ unzip /tmp/clj-kondo.zip; \ rm /tmp/clj-kondo.zip; -# Install babashka RUN set -ex; \ cd /tmp; \ curl -LfsSo /tmp/babashka.tar.gz https://github.com/babashka/babashka/releases/download/v$BABASHKA_VERSION/babashka-$BABASHKA_VERSION-linux-amd64.tar.gz; \ @@ -151,8 +149,10 @@ RUN set -ex; \ tar -xf /tmp/babashka.tar.gz; \ rm -rf /tmp/babashka.tar.gz; + +# Install minio client RUN set -ex; \ - curl -LfsSo /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc --user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15"; \ + wget -O /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc; \ mv /tmp/mc /usr/local/bin/; \ chmod +x /usr/local/bin/mc; From cbc58112903bf7559d03ccab35d46eb1a523670a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 27 Jun 2022 14:49:08 +0200 Subject: [PATCH 53/86] :sparkles: Improve websockets impl Make it more extensible and move all the websocket unrelated stuff to the new hooks API. Also adds observability from repl. --- backend/src/app/http/websocket.clj | 338 ++++++++++++------ backend/src/app/msgbus.clj | 5 +- backend/src/app/util/websocket.clj | 210 ++++++----- frontend/src/app/main/data/users.cljs | 3 +- frontend/src/app/main/data/websocket.cljs | 53 +-- .../main/data/workspace/notifications.cljs | 80 ++--- .../app/main/data/workspace/persistence.cljs | 4 +- frontend/src/app/main/errors.cljs | 1 - 8 files changed, 429 insertions(+), 265 deletions(-) diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj index f39a898911..39f961afbd 100644 --- a/backend/src/app/http/websocket.clj +++ b/backend/src/app/http/websocket.clj @@ -9,28 +9,103 @@ (:require [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.pprint :as pp] [app.common.spec :as us] [app.db :as db] [app.metrics :as mtx] + [app.util.time :as dt] [app.util.websocket :as ws] [clojure.core.async :as a] [clojure.spec.alpha :as s] [integrant.core :as ig] [yetti.websocket :as yws])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; WEBSOCKET HOOKS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def state (atom {})) + +(defn- on-connect + [{:keys [metrics]} wsp] + (let [created-at (dt/now)] + (swap! state assoc (::ws/id @wsp) wsp) + (mtx/run! metrics {:id :websocket-active-connections :inc 1}) + (fn [] + (swap! state dissoc (::ws/id @wsp)) + (mtx/run! metrics {:id :websocket-active-connections :dec 1}) + (mtx/run! metrics {:id :websocket-session-timing + :val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)})))) + +(defn- on-rcv-message + [{:keys [metrics]} _ message] + (mtx/run! metrics {:id :websocket-messages-total :labels ["recv"] :inc 1}) + message) + +(defn- on-snd-message + [{:keys [metrics]} _ message] + (mtx/run! metrics {:id :websocket-messages-total :labels ["send"] :inc 1}) + message) + +;; REPL HELPERS + +(defn repl-get-connections-for-file + [file-id] + (->> (vals @state) + (filter #(= file-id (-> % deref ::file-subscription :file-id))) + (map deref) + (map ::ws/id))) + +(defn repl-get-connections-for-team + [team-id] + (->> (vals @state) + (filter #(= team-id (-> % deref ::team-subscription :team-id))) + (map deref) + (map ::ws/id))) + +(defn repl-close-connection + [id] + (when-let [wsp (get @state id)] + (a/>!! (::ws/close-ch @wsp) [8899 "closed from server"]) + (a/close! (::ws/close-ch @wsp)))) + +(defn repl-get-connection-info + [id] + (when-let [wsp (get @state id)] + {:id id + :created-at (dt/instant id) + :profile-id (::profile-id @wsp) + :session-id (::session-id @wsp) + :user-agent (::ws/user-agent @wsp) + :ip-addr (::ws/remote-addr @wsp) + :last-activity-at (::ws/last-activity-at @wsp) + :http-session-id (::ws/http-session-id @wsp) + :subscribed-file (-> wsp deref ::file-subscription :file-id) + :subscribed-team (-> wsp deref ::team-subscription :team-id)})) + +(defn repl-print-connection-info + [id] + (some-> id repl-get-connection-info pp/pprint)) + +(defn repl-print-connection-info-for-file + [file-id] + (some->> (repl-get-connections-for-file file-id) + (map repl-get-connection-info) + (pp/pprint))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; WEBSOCKET HANDLER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defmulti handle-message - (fn [_ message] + (fn [_ _ message] (:type message))) (defmethod handle-message :connect - [wsp _] - (l/trace :fn "handle-message" :event :connect) + [cfg wsp _] - (let [msgbus-fn (:msgbus @wsp) + (let [msgbus-fn (:msgbus cfg) + conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) output-ch (::ws/output-ch @wsp) @@ -38,94 +113,122 @@ xform (remove #(= (:session-id %) session-id)) channel (a/chan (a/dropping-buffer 16) xform)] - (swap! wsp assoc ::profile-subs-channel channel) + (l/trace :fn "handle-message" :event :connect :conn-id conn-id) + + ;; Subscribe to the profile channel and forward all messages to + ;; websocket output channel (send them to the client). + (swap! wsp assoc ::profile-subscription channel) (a/pipe channel output-ch false) (msgbus-fn :cmd :sub :topic profile-id :chan channel))) (defmethod handle-message :disconnect - [wsp _] - (l/trace :fn "handle-message" :event :disconnect) - (a/go - (let [msgbus-fn (:msgbus @wsp) - profile-id (::profile-id @wsp) - session-id (::session-id @wsp) - profile-ch (::profile-subs-channel @wsp) - subs (::subscriptions @wsp)] + [cfg wsp _] + (let [msgbus-fn (:msgbus cfg) + conn-id (::ws/id @wsp) + profile-id (::profile-id @wsp) + session-id (::session-id @wsp) + profile-ch (::profile-subscription @wsp) + fsub (::file-subscription @wsp) + tsub (::team-subscription @wsp) + message {:type :disconnect + :subs-id profile-id + :profile-id profile-id + :session-id session-id}] + + (l/trace :fn "handle-message" + :event :disconnect + :conn-id conn-id) + + (a/go ;; Close the main profile subscription (a/close! profile-ch) (a/! output-ch message) - (recur))) + (l/trace :fn "handle-message" + :event :subscribe-team + :team-id team-id + :conn-id conn-id) + + (a/pipe channel output-ch false) + + (let [state {:team-id team-id :channel channel :topic team-id}] + (swap! wsp assoc ::team-subscription state)) + + (a/go + ;; Close previous subscription if exists + (when-let [channel (:channel prev-subs)] + (a/close! channel) + (a/! output-ch message) + (recur)))) (a/go ;; Subscribe to file topic @@ -134,6 +237,7 @@ ;; Notifify the rest of participants of the new connection. (let [message {:type :join-file :file-id file-id + :subs-id file-id :session-id session-id :profile-id profile-id}] (a/ message - (dissoc :subs-id) - (assoc :profile-id profile-id) - (assoc :session-id session-id))] - + [cfg wsp {:keys [file-id] :as message}] + (let [msgbus-fn (:msgbus cfg) + profile-id (::profile-id @wsp) + session-id (::session-id @wsp) + subs (::file-subscription @wsp) + message (-> message + (assoc :subs-id file-id) + (assoc :profile-id profile-id) + (assoc :session-id session-id))] + (a/go + ;; Only allow receive pointer updates when active subscription + (when subs (a/ cfg - (assoc ::profile-id profile-id) - (assoc ::session-id session-id))] - - (l/trace :hint "http request to websocket" :profile-id profile-id :session-id session-id) + (let [{:keys [session-id]} (us/conform ::handler-params params)] (cond (not profile-id) (raise (ex/error :type :authentication @@ -218,6 +327,15 @@ :hint "this endpoint only accepts websocket connections")) :else - (->> (ws/handler handle-message cfg) - (yws/upgrade req) - (respond)))))) + (do + (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) + + (->> (ws/handler + ::ws/on-rcv-message (partial on-rcv-message cfg) + ::ws/on-snd-message (partial on-snd-message cfg) + ::ws/on-connect (partial on-connect cfg) + ::ws/handler (partial handle-message cfg) + ::profile-id profile-id + ::session-id session-id) + (yws/upgrade req) + (respond))))))) diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index 4bae83abd3..e14bf9e126 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -160,7 +160,6 @@ "Function responsible to attach local subscription to the state. Intended to be used in agent." [state cfg topics chan done-ch] - (l/trace :hint "subscribe-to-topics" :topics topics ::l/async false) (aa/with-closing done-ch (let [state (update state :chans assoc chan topics)] (reduce (fn [state topic] @@ -184,15 +183,15 @@ useful when client disconnects or in-bulk unsubscribe operations. Intended to be executed in agent." [state cfg channels done-ch] - (l/trace :hint "unsubscribe-channels" :chans (count channels) ::l/async false) (aa/with-closing done-ch (reduce #(unsubscribe-single-channel %1 cfg %2) state channels))) + (defn- subscribe [{:keys [::state executor] :as cfg} {:keys [topic topics chan]}] (let [done-ch (a/chan) topics (into [] (map prefix-topic) (if topic [topic] topics))] - (l/trace :hint "subscribe" :topics topics) + (l/debug :hint "subscribe" :topics topics) (send-via executor state subscribe-to-topics cfg topics chan done-ch) done-ch)) diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index e4f8a12fef..4909049fe8 100644 --- a/backend/src/app/util/websocket.clj +++ b/backend/src/app/util/websocket.clj @@ -10,9 +10,10 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.transit :as t] - [app.metrics :as mtx] + [app.loggers.audit :refer [parse-client-ip]] [app.util.time :as dt] [clojure.core.async :as a] + [yetti.request :as yr] [yetti.util :as yu] [yetti.websocket :as yws]) (:import @@ -25,8 +26,10 @@ (declare process-output) (declare ws-ping!) (declare ws-send!) +(declare filter-options) (def noop (constantly nil)) +(def identity-3 (fn [_ _ o] o)) (defn handler "A WebSocket upgrade handler factory. Returns a handler that can be @@ -39,94 +42,123 @@ It also accepts some options that allows you parametrize the protocol behavior. The options map will be used as-as for the initial data of the `ws` data structure" - ([handle-message] (handler handle-message {})) - ([handle-message {:keys [::input-buff-size - ::output-buff-size - ::idle-timeout - metrics] - :or {input-buff-size 64 - output-buff-size 64 - idle-timeout 30000} - :as options}] - (fn [{:keys [::yws/channel] :as request}] - (let [input-ch (a/chan input-buff-size) - output-ch (a/chan output-buff-size) - pong-ch (a/chan (a/sliding-buffer 6)) - close-ch (a/chan) + [& {:keys [::on-rcv-message + ::on-snd-message + ::on-connect + ::input-buff-size + ::output-buff-size + ::handler + ::idle-timeout] + :or {input-buff-size 64 + output-buff-size 64 + idle-timeout 30000 + on-connect noop + on-snd-message identity-3 + on-rcv-message identity-3} + :as options}] - options (atom - (-> options - (assoc ::input-ch input-ch) - (assoc ::output-ch output-ch) - (assoc ::close-ch close-ch) - (assoc ::channel channel) - (dissoc ::metrics))) + (assert (fn? on-rcv-message) "'on-rcv-message' should be a function") + (assert (fn? on-snd-message) "'on-snd-message' should be a function") + (assert (fn? on-connect) "'on-connect' should be a function") - terminated (atom false) - created-at (dt/now) + (fn [{:keys [::yws/channel session-id] :as request}] + (let [input-ch (a/chan input-buff-size) + output-ch (a/chan output-buff-size) + pong-ch (a/chan (a/sliding-buffer 6)) + close-ch (a/chan) + stop-ch (a/chan) - on-open - (fn [channel] - (mtx/run! metrics {:id :websocket-active-connections :inc 1}) - (yws/idle-timeout! channel (dt/duration idle-timeout))) + ip-addr (parse-client-ip request) + uagent (yr/get-header request "user-agent") + id (inst-ms (dt/now)) - on-terminate - (fn [& _args] - (when (compare-and-set! terminated false true) - (mtx/run! metrics {:id :websocket-active-connections :dec 1}) - (mtx/run! metrics {:id :websocket-session-timing :val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)}) + options (-> (filter-options options) + (merge {::id id + ::input-ch input-ch + ::output-ch output-ch + ::close-ch close-ch + ::stop-ch stop-ch + ::channel channel + ::remote-addr ip-addr + ::http-session-id session-id + ::user-agent uagent}) + (atom)) - (a/close! close-ch) - (a/close! pong-ch) - (a/close! output-ch) - (a/close! input-ch))) + ;; call the on-connect hook and memoize the on-terminate instance + on-terminate (on-connect options) - on-error - (fn [_ error] - (on-terminate) - ;; TODO: properly log timeout exceptions - (when-not (or (instance? java.nio.channels.ClosedChannelException error) - (instance? java.net.SocketException error)) - (l/error :hint (ex-message error) :cause error))) + on-ws-open + (fn [channel] + (l/trace :fn "on-ws-open" :conn-id id) + (yws/idle-timeout! channel (dt/duration idle-timeout))) - on-message - (fn [_ message] - (mtx/run! metrics {:id :websocket-messages-total :labels ["recv"] :inc 1}) - (try - (let [message (t/decode-str message)] - (a/offer! input-ch message)) - (catch Throwable e - (l/warn :hint "error on decoding incoming message from websocket" - :wsmsg (pr-str message) - :cause e) - (on-terminate)))) + on-ws-terminate + (fn [_ code reason] + (l/trace :fn "on-ws-terminate" :conn-id id :code code :reason reason) + (a/close! close-ch)) - on-pong - (fn [_ buffers] - (a/>!! pong-ch (yu/copy-many buffers)))] + on-ws-error + (fn [_ error] + (a/close! close-ch) + (when-not (or (instance? java.nio.channels.ClosedChannelException error) + (instance? java.net.SocketException error)) + (l/error :hint (ex-message error) :cause error))) - ;; launch heartbeat process - (-> @options - (assoc ::pong-ch pong-ch) - (assoc ::on-close on-terminate) - (process-heartbeat)) + on-ws-message + (fn [_ message] + (try + (let [message (on-rcv-message options message) + message (t/decode-str message)] + (a/offer! input-ch message) + (swap! options assoc ::last-activity-at (dt/now))) + (catch Throwable e + (l/warn :hint "error on decoding incoming message from websocket" + :wsmsg (pr-str message) + :cause e) + (a/>! close-ch [8801 "decode error"]) + (a/close! close-ch)))) - ;; Forward all messages from output-ch to the websocket - ;; connection - (a/go-loop [] - (when-let [val (a/!! pong-ch (yu/copy-many buffers)))] - ;; React on messages received from the client - (process-input options handle-message) + ;; Launch heartbeat process + (-> @options + (assoc ::pong-ch pong-ch) + (process-heartbeat)) - {:on-open on-open - :on-error on-error - :on-close on-terminate - :on-text on-message - :on-pong on-pong})))) + ;; Wait a close signal + (a/go + (let [[code reason] (a/! output-ch {:type :error :error (ex-data val)}) @@ -193,19 +225,21 @@ (a/= (count issued) max-missed-heartbeats) - (on-close channel -1 "heartbeat-timeout") + (do + (a/>! close-ch [8802 "heart-beat timeout"]) + (a/close! close-ch)) (recur (inc i))))))) (a/go-loop [] @@ -213,3 +247,11 @@ (swap! beats disj (decode-beat buffer)) (recur))))) +(defn- filter-options + "Remove from options all namespace qualified keys that matches the + current namespace." + [options] + (into {} + (remove (fn [[key]] + (= (namespace key) "app.util.websocket"))) + options)) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 7ea9454379..eee3b49cc4 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -173,8 +173,7 @@ (when (is-authenticated? profile) (->> (rx/of (profile-fetched profile) (fetch-teams) - (get-redirect-event) - (ws/initialize)) + (get-redirect-event)) (rx/observe-on :async))))))) (s/def ::invitation-token ::us/not-empty-string) diff --git a/frontend/src/app/main/data/websocket.cljs b/frontend/src/app/main/data/websocket.cljs index 1fbb26770d..3612770d41 100644 --- a/frontend/src/app/main/data/websocket.cljs +++ b/frontend/src/app/main/data/websocket.cljs @@ -7,14 +7,19 @@ (ns app.main.data.websocket (:require [app.common.data.macros :as dm] + [app.common.logging :as l] [app.common.uri :as u] [app.config :as cf] [app.util.websocket :as ws] [beicon.core :as rx] [potok.core :as ptk])) +(l/set-level! :error) + (dm/export ws/send!) +(defonce ws-conn (volatile! nil)) + (defn- prepare-uri [params] (let [base (-> (u/join cf/public-uri "ws/notifications") @@ -30,35 +35,34 @@ [message] (ptk/reify ::send-message ptk/EffectEvent - (effect [_ state _] - (let [ws-conn (:ws-conn state)] - (ws/send! ws-conn message))))) + (effect [_ _ _] + (some-> @ws-conn (ws/send! message))))) (defn initialize [] (ptk/reify ::initialize - ptk/UpdateEvent - (update [_ state] - (let [sid (:session-id state) - uri (prepare-uri {:session-id sid})] - (assoc state :ws-conn (ws/create uri)))) - ptk/WatchEvent (watch [_ state stream] - (let [ws-conn (:ws-conn state) - stoper (rx/merge - (rx/filter (ptk/type? ::finalize) stream) - (rx/filter (ptk/type? ::initialize) stream))] + (l/trace :hint "event:initialize" :fn "watch") + (let [sid (:session-id state) + uri (prepare-uri {:session-id sid}) + ws (ws/create uri)] - (->> (rx/merge - (->> (ws/get-rcv-stream ws-conn) - (rx/filter ws/message-event?) - (rx/map :payload) - (rx/map #(ptk/data-event ::message %))) - (->> (ws/get-rcv-stream ws-conn) - (rx/filter ws/opened-event?) - (rx/map (fn [_] (ptk/data-event ::opened {}))))) - (rx/take-until stoper)))))) + (vreset! ws-conn ws) + + (let [stoper (rx/merge + (rx/filter (ptk/type? ::finalize) stream) + (rx/filter (ptk/type? ::initialize) stream))] + + (->> (rx/merge + (->> (ws/get-rcv-stream ws) + (rx/filter ws/message-event?) + (rx/map :payload) + (rx/map #(ptk/data-event ::message %))) + (->> (ws/get-rcv-stream ws) + (rx/filter ws/opened-event?) + (rx/map (fn [_] (ptk/data-event ::opened {}))))) + (rx/take-until stoper))))))) ;; --- Finalize Websocket @@ -66,5 +70,6 @@ [] (ptk/reify ::finalize ptk/EffectEvent - (effect [_ state _] - (some-> (:ws-conn state) ws/close!)))) + (effect [_ _ _] + (l/trace :hint "event:finalize" :fn "effect") + (some-> @ws-conn ws/close!)))) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 6d1527b7c5..a9388898c7 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] - [app.common.uuid :as uuid] [app.main.data.websocket :as dws] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] @@ -34,51 +33,53 @@ (ptk/reify ::initialize ptk/WatchEvent (watch [_ state stream] - (let [subs-id (uuid/next) - stoper (rx/filter (ptk/type? ::finalize) stream) + (let [stoper (rx/filter (ptk/type? ::finalize) stream) + profile-id (:profile-id state) - initmsg [{:type :subscribe-file - :subs-id subs-id - :file-id file-id} - {:type :subscribe-team - :team-id team-id}] + initmsg [{:type :subscribe-file + :file-id file-id} + {:type :subscribe-team + :team-id team-id}] - endmsg {:type :unsubscribe-file - :subs-id subs-id} + endmsg {:type :unsubscribe-file + :file-id file-id} - stream (->> (rx/merge - ;; Send the subscription message - (->> (rx/from initmsg) - (rx/map dws/send)) + stream (->> (rx/merge + ;; Send the subscription message + (->> (rx/from initmsg) + (rx/map dws/send)) - ;; Subscribe to notifications of the subscription - (->> stream - (rx/filter (ptk/type? ::dws/message)) - (rx/map deref) ;; :library-change events occur in a different file, but need to be processed anyway - (rx/filter #(or (= subs-id (:subs-id %)) (= (:type %) :library-change))) - (rx/map process-message)) + ;; Subscribe to notifications of the subscription + (->> stream + (rx/filter (ptk/type? ::dws/message)) + (rx/map deref) + (rx/filter (fn [{:keys [subs-id] :as msg}] + (or (= subs-id team-id) + (= subs-id profile-id) + (= subs-id file-id)))) + (rx/map process-message)) - ;; On reconnect, send again the subscription messages - (->> stream - (rx/filter (ptk/type? ::dws/opened)) - (rx/mapcat #(->> (rx/from initmsg) - (rx/map dws/send)))) + ;; On reconnect, send again the subscription messages + (->> stream + (rx/filter (ptk/type? ::dws/opened)) + (rx/mapcat #(->> (rx/from initmsg) + (rx/map dws/send)))) - ;; Emit presence event for current user; - ;; this is because websocket server don't - ;; emits this for the same user. - (rx/of (handle-presence {:type :connect - :session-id (:session-id state) - :profile-id (:profile-id state)})) + ;; Emit presence event for current user; + ;; this is because websocket server don't + ;; emits this for the same user. + (rx/of (handle-presence {:type :connect + :session-id (:session-id state) + :profile-id (:profile-id state)})) - ;; Emit to all other connected users the current pointer - ;; position changes. - (->> stream - (rx/filter ms/pointer-event?) - (rx/sample 50) - (rx/map #(handle-pointer-send subs-id file-id (:pt %))))) + ;; Emit to all other connected users the current pointer + ;; position changes. + (->> stream + (rx/filter ms/pointer-event?) + (rx/sample 50) + (rx/map #(handle-pointer-send file-id (:pt %))))) - (rx/take-until stoper))] + (rx/take-until stoper))] (rx/concat stream (rx/of (dws/send endmsg))))))) @@ -95,13 +96,12 @@ nil)) (defn- handle-pointer-send - [subs-id file-id point] + [file-id point] (ptk/reify ::handle-pointer-send ptk/WatchEvent (watch [_ state _] (let [page-id (:current-page-id state) message {:type :pointer-update - :subs-id subs-id :file-id file-id :page-id page-id :position point}] diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 7b81dc8cda..445f5001c0 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -163,7 +163,9 @@ (rx/map #(shapes-changes-persisted file-id %))))))) (rx/catch (fn [cause] (rx/concat - (rx/of (rt/assign-exception cause)) + (if (= :authentication (:type cause)) + (rx/empty) + (rx/of (rt/assign-exception cause))) (rx/throw cause)))))))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index b55d1561fc..7a8edbbbe2 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -106,7 +106,6 @@ (js/console.groupEnd msg))) - ;; Error on parsing an SVG ;; TODO: looks unused and deprecated (defmethod ptk/handle-error :svg-parser From d021ac02265db23cd1af7674895056e73d07c5f3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Jun 2022 12:30:17 +0200 Subject: [PATCH 54/86] :bug: Fix share link migration for backward compatibilty --- backend/src/app/migrations/sql/0075-mod-share-link-table.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/app/migrations/sql/0075-mod-share-link-table.sql b/backend/src/app/migrations/sql/0075-mod-share-link-table.sql index 326091cda5..d291543fe3 100644 --- a/backend/src/app/migrations/sql/0075-mod-share-link-table.sql +++ b/backend/src/app/migrations/sql/0075-mod-share-link-table.sql @@ -1,4 +1,5 @@ ALTER TABLE share_link ADD COLUMN who_comment text NOT NULL DEFAULT('team'), - ADD COLUMN who_inspect text NOT NULL DEFAULT('team'), - DROP COLUMN flags; + ADD COLUMN who_inspect text NOT NULL DEFAULT('team'); + +--- TODO: remove flags column in 1.15.x From ebe8fdcba81f71aecf38ce55410bb46cdea80952 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Jun 2022 14:39:40 +0200 Subject: [PATCH 55/86] :recycle: Refactor temporal files management on exporter --- exporter/src/app/core.cljs | 2 +- exporter/src/app/handlers/export_frames.cljs | 5 +-- exporter/src/app/handlers/export_shapes.cljs | 4 -- exporter/src/app/handlers/resources.cljs | 4 +- exporter/src/app/renderer/bitmap.cljs | 5 +-- exporter/src/app/renderer/pdf.cljs | 3 +- exporter/src/app/renderer/svg.cljs | 32 +++++---------- exporter/src/app/util/shell.cljs | 42 ++++++++++++++++++-- 8 files changed, 57 insertions(+), 40 deletions(-) diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index c3ebbd46a3..79597412d5 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -8,10 +8,10 @@ (:require ["process" :as proc] [app.browser :as bwr] - [app.redis :as redis] [app.common.logging :as l] [app.config] [app.http :as http] + [app.redis :as redis] [promesa.core :as p])) (enable-console-print!) diff --git a/exporter/src/app/handlers/export_frames.cljs b/exporter/src/app/handlers/export_frames.cljs index a8a4a0c85b..716cc0b30d 100644 --- a/exporter/src/app/handlers/export_frames.cljs +++ b/exporter/src/app/handlers/export_frames.cljs @@ -128,8 +128,8 @@ (defn- join-pdf [file-id paths] - (p/let [tmpdir (sh/mktmpdir! "join-pdf") - path (path/join tmpdir (str/concat file-id ".pdf"))] + (p/let [prefix (str/concat "penpot.tmp.pdfunite." file-id ".") + path (sh/tempfile :prefix prefix :suffix ".pdf")] (sh/run-cmd! (str "pdfunite " (str/join " " paths) " " path)) path)) @@ -137,5 +137,4 @@ [{:keys [path] :as resource} output-path] (p/do (sh/move! output-path path) - (sh/rmdir! (path/dirname output-path)) resource)) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index c6ac2f05de..d50dfc7c32 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -102,8 +102,6 @@ total (count exports) topic (str profile-id) - to-delete (atom #{}) - on-progress (fn [{:keys [done]}] (when-not wait (let [data {:type :export-update @@ -137,7 +135,6 @@ :on-progress on-progress) append (fn [{:keys [filename path] :as object}] - (swap! to-delete conj path) (rsc/add-to-zip! zip path filename)) proc (-> (p/do @@ -146,7 +143,6 @@ (p/let [proc (rd/render export append)] (p/recur (rest exports))))) (.finalize zip)) - (p/then (fn [_] (p/run! #(sh/rmdir! (path/dirname %)) @to-delete))) (p/then (constantly resource)) (p/catch on-error)) ] diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index e027056101..02ccfe0b1c 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -14,15 +14,15 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.uuid :as uuid] - [app.util.shell :as sh] [app.util.mime :as mime] + [app.util.shell :as sh] [cljs.core :as c] [cuerdas.core :as str] [promesa.core :as p])) (defn- get-path [type id] - (path/join (os/tmpdir) (str/concat "exporter-resource." (c/name type) "." id))) + (path/join sh/tmpdir (str/concat "penpot.resource." (c/name type) "." id))) (defn create "Generates ephimeral resource object." diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index eaee733466..bacde909b6 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -36,9 +36,8 @@ :userAgent bw/default-user-agent}) (render-object [page {:keys [id] :as object}] - (p/let [tmpdir (sh/mktmpdir! "bitmap-render") - path (path/join tmpdir (str/concat id (mime/get-extension type))) - node (bw/select page (str/concat "#screenshot-" id))] + (p/let [path (sh/tempfile :prefix "penpot.tmp.render.bitmap." :suffix (mime/get-extension type)) + node (bw/select page (str/concat "#screenshot-" id))] (bw/wait-for node) (case type :png (bw/screenshot node {:omit-background? true :type type :path path}) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 124e545adc..2ba6926e8d 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -44,8 +44,7 @@ (render-object [page base-uri {:keys [id] :as object}] (p/let [uri (prepare-uri base-uri id) - tmp (sh/mktmpdir! "pdf-render") - path (path/join tmp (str/concat id (mime/get-extension type)))] + path (sh/tempfile :prefix "penpot.tmp.render.pdf." :suffix (mime/get-extension type))] (l/info :uri uri) (bw/nav! page uri) (p/let [dom (bw/select page (dm/str "#screenshot-" id))] diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 825c51ad91..d1c4e3fcc4 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -116,24 +116,20 @@ (defn render [{:keys [page-id file-id objects token scale type]} on-object] (letfn [(convert-to-ppm [pngpath] - (l/trace :fn :convert-to-ppm) - (let [basepath (path/dirname pngpath) - ppmpath (path/join basepath "origin.ppm")] + (let [ppmpath (str/concat pngpath "origin.ppm")] + (l/trace :fn :convert-to-ppm :path ppmpath) (-> (sh/run-cmd! (str "convert " pngpath " " ppmpath)) (p/then (constantly ppmpath))))) (trace-color-mask [pbmpath] (l/trace :fn :trace-color-mask :pbmpath pbmpath) - (let [basepath (path/dirname pbmpath) - basename (path/basename pbmpath ".pbm") - svgpath (path/join basepath (str basename ".svg"))] + (let [svgpath (str/concat pbmpath ".svg")] (-> (sh/run-cmd! (str "potrace --flat -b svg " pbmpath " -o " svgpath)) (p/then (constantly svgpath))))) (generate-color-layer [ppmpath color] (l/trace :fn :generate-color-layer :ppmpath ppmpath :color color) - (let [basepath (path/dirname ppmpath) - pbmpath (path/join basepath (str "mask-" (subs color 1) ".pbm"))] + (let [pbmpath (str/concat ppmpath ".mask-" (subs color 1) ".pbm")] (-> (sh/run-cmd! (str/format "ppmcolormask \"%s\" %s" color ppmpath)) (p/then (fn [stdout] (-> (sh/write-file! pbmpath stdout) @@ -247,15 +243,14 @@ (trace-node [{:keys [data] :as node}] (l/trace :fn :trace-node) - (p/let [tdpath (sh/mktmpdir! "svgexport") - pngpath (path/join tdpath "origin.png") + (p/let [pngpath (sh/tempfile :prefix "penpot.tmp.render.svg.parse." + :suffix ".origin.png") _ (sh/write-file! pngpath data) ppmpath (convert-to-ppm pngpath) svgdata (convert-to-svg ppmpath node)] (-> node (dissoc :data) - (assoc :tempdir tdpath - :svgdata svgdata)))) + (assoc :svgdata svgdata)))) (extract-element-attrs [^js element] (let [^js attrs (.. element -attributes) @@ -289,17 +284,11 @@ shot (bw/screenshot text-node {:omit-background? true :type "png"})] [shot node])) - (clean-temp-data [{:keys [tempdir] :as node}] - (p/do! - (sh/rmdir! tempdir) - (dissoc node :tempdir))) - (extract-txt-node [page item] (-> (p/resolved item) (p/then (partial resolve-text-node page)) (p/then extract-single-node) - (p/then trace-node) - (p/then clean-temp-data))) + (p/then trace-node))) (extract-txt-nodes [page {:keys [id] :as objects}] (l/trace :fn :process-text-nodes) @@ -323,9 +312,8 @@ :userAgent bw/default-user-agent}) (render-object [page {:keys [id] :as object}] - (p/let [tmpdir (sh/mktmpdir! "svg-render") - path (path/join tmpdir (str/concat id (mime/get-extension type))) - node (bw/select page (str/concat "#screenshot-" id))] + (p/let [path (sh/tempfile :prefix "penpot.tmp.render.svg." :suffix (mime/get-extension type)) + node (bw/select page (str/concat "#screenshot-" id))] (bw/wait-for node) (p/let [xmldata (extract-svg page object) txtdata (extract-txt-nodes page object) diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index 93b5333ed9..5591741673 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -12,13 +12,49 @@ ["os" :as os] ["path" :as path] [app.common.logging :as l] + [app.common.uuid :as uuid] + [app.common.exceptions :as ex] + [cuerdas.core :as str] [promesa.core :as p])) (l/set-level! :trace) -(defn mktmpdir! - [prefix] - (.mkdtemp fs/promises (path/join (os/tmpdir) prefix))) +(def tempfile-minage (* 1000 60 60 1)) ;; 1h + +(def tmpdir + (let [path (path/join (os/tmpdir) "penpot")] + (when-not (fs/existsSync path) + (fs/mkdirSync path #js {:recursive true})) + path)) + + +(defn- schedule-deletion! + [path] + (letfn [(remote-tempfile [] + (when (fs/existsSync path) + (l/trace :hint "permanently remove tempfile" :path path) + (fs/rmSync path #js {:recursive true})))] + (let [ts (js/Date.now)] + (l/trace :hint "schedule tempfile deletion" + :path path + :scheduled-at (.. (js/Date. (+ (js/Date.now) tempfile-minage)) toString)) + (js/setTimeout remote-tempfile tempfile-minage)))) + +(defn tempfile + [& {:keys [prefix suffix] + :or {prefix "penpot." + suffix ".tmp"}}] + (loop [i 0] + (if (< i 1000) + (let [path (path/join tmpdir (str/concat prefix (uuid/next) "-" i suffix))] + (if (fs/existsSync path) + (recur (inc i)) + (do + (schedule-deletion! path) + path))) + (ex/raise :type :internal + :code :unable-to-locate-temporal-file + :hint "unable to find a tempfile candidate")))) (defn move! [origin-path dest-path] From 9c194ee3cb2a98a14bc56523cf224ad6f2b01328 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Jun 2022 14:39:56 +0200 Subject: [PATCH 56/86] :bug: Fix websocket unexpected exception on exportation module A regression caused by the previous commit that refactos the websockets API and its state management. --- frontend/src/app/main/data/websocket.cljs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/main/data/websocket.cljs b/frontend/src/app/main/data/websocket.cljs index 3612770d41..a0b552393c 100644 --- a/frontend/src/app/main/data/websocket.cljs +++ b/frontend/src/app/main/data/websocket.cljs @@ -55,6 +55,7 @@ (rx/filter (ptk/type? ::initialize) stream))] (->> (rx/merge + (rx/of #(assoc % :ws-conn ws)) (->> (ws/get-rcv-stream ws) (rx/filter ws/message-event?) (rx/map :payload) @@ -69,6 +70,10 @@ (defn finalize [] (ptk/reify ::finalize + ptk/UpdateEvent + (update [_ state] + (dissoc state :ws-conn)) + ptk/EffectEvent (effect [_ _ _] (l/trace :hint "event:finalize" :fn "effect") From bfb0ba47f5860311a0d19da879a6505baf51b13d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 29 Jun 2022 14:53:57 +0200 Subject: [PATCH 57/86] :lipstick: Fix linter issues on exporter --- exporter/src/app/browser.cljs | 2 -- exporter/src/app/config.cljs | 3 --- exporter/src/app/core.cljs | 2 +- exporter/src/app/handlers.cljs | 8 ++------ exporter/src/app/handlers/export_frames.cljs | 12 +++++------- exporter/src/app/handlers/export_shapes.cljs | 5 ++--- exporter/src/app/handlers/resources.cljs | 4 +--- exporter/src/app/http.cljs | 1 - exporter/src/app/redis.cljs | 2 +- exporter/src/app/renderer.cljs | 2 +- exporter/src/app/renderer/bitmap.cljs | 7 ------- exporter/src/app/renderer/pdf.cljs | 8 +------- exporter/src/app/renderer/svg.cljs | 10 ++-------- exporter/src/app/util/mime.cljs | 3 +-- exporter/src/app/util/shell.cljs | 13 ++++++------- 15 files changed, 23 insertions(+), 59 deletions(-) diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 588b127498..c885a3d453 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -9,10 +9,8 @@ ["generic-pool" :as gp] ["generic-pool/lib/errors" :as gpe] ["playwright" :as pw] - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] - [app.common.uuid :as uuid] [app.config :as cf] [app.util.object :as obj] [promesa.core :as p])) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 6a312ab687..2d8729c6d1 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -7,13 +7,10 @@ (ns app.config (:refer-clojure :exclude [get]) (:require - ["fs" :as fs] ["process" :as process] - [app.common.exceptions :as ex] [app.common.data :as d] [app.common.spec :as us] [app.common.version :as v] - [app.common.uri :as u] [cljs.core :as c] [cljs.pprint] [cljs.spec.alpha :as s] diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 79597412d5..453ff720bc 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -18,7 +18,7 @@ (l/initialize!) (defn start - [& args] + [& _] (l/info :msg "initializing") (p/do! (bwr/init) diff --git a/exporter/src/app/handlers.cljs b/exporter/src/app/handlers.cljs index a1a8496992..14d6a862ec 100644 --- a/exporter/src/app/handlers.cljs +++ b/exporter/src/app/handlers.cljs @@ -10,22 +10,18 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.common.uri :as u] - [app.config :as cf] [app.handlers.export-frames :as export-frames] [app.handlers.export-shapes :as export-shapes] [app.handlers.resources :as resources] [app.util.transit :as t] [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [promesa.core :as p] - [reitit.core :as r])) + [cuerdas.core :as str])) (l/set-level! :debug) (defn on-error [error exchange] - (let [{:keys [type message code] :as data} (ex-data error)] + (let [{:keys [type code] :as data} (ex-data error)] (cond (or (= :validation type) (= :assertion type)) diff --git a/exporter/src/app/handlers/export_frames.cljs b/exporter/src/app/handlers/export_frames.cljs index 716cc0b30d..0c98167103 100644 --- a/exporter/src/app/handlers/export_frames.cljs +++ b/exporter/src/app/handlers/export_frames.cljs @@ -6,13 +6,10 @@ (ns app.handlers.export-frames (:require - ["path" :as path] [app.common.logging :as l] - [app.common.exceptions :as exc] [app.common.spec :as us] - [app.common.pprint :as pp] - [app.handlers.resources :as rsc] [app.handlers.export-shapes :refer [prepare-exports]] + [app.handlers.resources :as rsc] [app.redis :as redis] [app.renderer :as rd] [app.util.shell :as sh] @@ -41,7 +38,7 @@ :opt-un [::name])) (defn handler - [{:keys [:request/auth-token] :as exchange} {:keys [exports profile-id] :as params}] + [{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}] ;; NOTE: we need to have the `:type` prop because the exports ;; datastructure preparation uses it for creating the groups. (let [exports (-> (map #(assoc % :type :pdf :scale 1 :suffix "") exports) @@ -111,7 +108,8 @@ (-> (p/loop [exports (seq exports)] (when-let [export (first exports)] - (p/let [proc (rd/render export on-object)] + (p/do + (rd/render export on-object) (p/recur (rest exports))))) (p/then (fn [_] (deref result))) @@ -122,7 +120,7 @@ (-> (sh/stat (:path resource)) (p/then #(merge resource %))))) (p/catch on-error) - (p/finally (fn [result cause] + (p/finally (fn [_ cause] (when-not cause (on-complete))))))) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index d50dfc7c32..02e7a824d8 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -6,9 +6,7 @@ (ns app.handlers.export-shapes (:require - ["path" :as path] [app.common.data :as d] - [app.common.exceptions :as exc] [app.common.logging :as l] [app.common.spec :as us] [app.handlers.resources :as rsc] @@ -140,7 +138,8 @@ proc (-> (p/do (p/loop [exports (seq exports)] (when-let [export (first exports)] - (p/let [proc (rd/render export append)] + (p/do + (rd/render export append) (p/recur (rest exports))))) (.finalize zip)) (p/then (constantly resource)) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index 02ccfe0b1c..c6729b08fa 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -9,9 +9,7 @@ (:require ["archiver" :as arc] ["fs" :as fs] - ["os" :as os] ["path" :as path] - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.uuid :as uuid] [app.util.mime :as mime] @@ -49,7 +47,7 @@ "content-length" (:size stat)}})) (defn handler - [{:keys [:request/params response] :as exchange}] + [{:keys [:request/params] :as exchange}] (when-not (contains? params :id) (ex/raise :type :validation :code :missing-id)) diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index 1e3512ab7a..0621911690 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -12,7 +12,6 @@ ["raw-body" :as raw-body] ["stream" :as stream] [app.common.logging :as l] - [app.common.spec :as us] [app.common.transit :as t] [app.config :as cf] [app.handlers :as handlers] diff --git a/exporter/src/app/redis.cljs b/exporter/src/app/redis.cljs index 5d704bc861..10b095cf4c 100644 --- a/exporter/src/app/redis.cljs +++ b/exporter/src/app/redis.cljs @@ -28,7 +28,7 @@ (.on client "reconnect" (fn [ms] (l/warn :hint "reconnecting to redis" :ms ms))) (.on client "end" - (fn [ms] (l/warn :hint "client ended, no more connections will be attempted"))) + (fn [] (l/warn :hint "client ended, no more connections will be attempted"))) client)) (defn init diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs index 42ab6c6ad5..63f1367f56 100644 --- a/exporter/src/app/renderer.cljs +++ b/exporter/src/app/renderer.cljs @@ -31,7 +31,7 @@ (s/def ::render-params (s/keys :req-un [::file-id ::page-id ::scale ::token ::type ::objects])) -(defn- render +(defn render [{:keys [type] :as params} on-object] (us/verify ::render-params params) (us/verify fn? on-object) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index bacde909b6..959da4877b 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -7,19 +7,12 @@ (ns app.renderer.bitmap "A bitmap renderer." (:require - ["path" :as path] [app.browser :as bw] - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.logging :as l] - [app.common.pages :as cp] - [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] [app.util.mime :as mime] [app.util.shell :as sh] - [cljs.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p])) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 2ba6926e8d..14d9be40ae 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -7,18 +7,13 @@ (ns app.renderer.pdf "A pdf renderer." (:require - ["path" :as path] [app.browser :as bw] [app.common.data.macros :as dm] - [app.common.exceptions :as ex :include-macros true] [app.common.logging :as l] - [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] [app.util.mime :as mime] [app.util.shell :as sh] - [cuerdas.core :as str] - [cljs.spec.alpha :as s] [promesa.core :as p])) (defn render @@ -57,8 +52,7 @@ (render [base-uri page] (p/loop [objects (seq objects)] (when-let [object (first objects)] - (p/let [uri (prepare-uri base-uri (:id object)) - path (render-object page base-uri object)] + (p/let [path (render-object page base-uri object)] (on-object (assoc object :path path)) (p/recur (rest objects))))))] diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index d1c4e3fcc4..39cd8d17c1 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -6,20 +6,14 @@ (ns app.renderer.svg (:require - ["path" :as path] ["xml-js" :as xml] [app.browser :as bw] [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex :include-macros true] [app.common.logging :as l] - [app.common.pages :as cp] - [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] [app.util.mime :as mime] [app.util.shell :as sh] - [cljs.spec.alpha :as s] [clojure.walk :as walk] [cuerdas.core :as str] [promesa.core :as p])) @@ -184,7 +178,7 @@ (get-gradients [id mapping] (->> mapping - (filter (fn [[color data]] + (filter (fn [[_color data]] (= (get data "type") "gradient"))) (mapv (partial data->gradient-def id)))) @@ -227,7 +221,7 @@ elements (cond->> elements - (not (empty? gradient-defs)) + (seq gradient-defs) (into [{"type" "element" "name" "defs" "attributes" {} "elements" gradient-defs}]))] diff --git a/exporter/src/app/util/mime.cljs b/exporter/src/app/util/mime.cljs index ed5a19a4c7..fef7e40534 100644 --- a/exporter/src/app/util/mime.cljs +++ b/exporter/src/app/util/mime.cljs @@ -8,7 +8,6 @@ "Mimetype and file extension helpers." (:refer-clojure :exclude [get]) (:require - [app.common.data :as d] [cljs.core :as c])) (defn get-extension @@ -20,7 +19,7 @@ :pdf ".pdf" :zip ".zip")) -(defn- get +(defn get [type] (case type :zip "application/zip" diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index 5591741673..fcb36b9812 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -11,9 +11,9 @@ ["fs" :as fs] ["os" :as os] ["path" :as path] + [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.uuid :as uuid] - [app.common.exceptions :as ex] [cuerdas.core :as str] [promesa.core :as p])) @@ -34,11 +34,10 @@ (when (fs/existsSync path) (l/trace :hint "permanently remove tempfile" :path path) (fs/rmSync path #js {:recursive true})))] - (let [ts (js/Date.now)] - (l/trace :hint "schedule tempfile deletion" - :path path - :scheduled-at (.. (js/Date. (+ (js/Date.now) tempfile-minage)) toString)) - (js/setTimeout remote-tempfile tempfile-minage)))) + (l/trace :hint "schedule tempfile deletion" + :path path + :scheduled-at (.. (js/Date. (+ (js/Date.now) tempfile-minage)) toString)) + (js/setTimeout remote-tempfile tempfile-minage))) (defn tempfile [& {:keys [prefix suffix] @@ -86,7 +85,7 @@ (fn [resolve reject] (l/trace :fn :run-cmd :cmd cmd) (proc/exec cmd #js {:encoding "buffer"} - (fn [error stdout stderr] + (fn [error stdout _stderr] ;; (l/trace :fn :run-cmd :stdout stdout) (if error (reject error) From b38ffdcf30aa70b67ac0d2e80685ff0d19a32f92 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Jun 2022 12:55:15 +0200 Subject: [PATCH 58/86] :recycle: Refactor workspace common --- common/src/app/common/geom/shapes/bounds.cljc | 6 + frontend/src/app/main/data/users.cljs | 2 +- frontend/src/app/main/data/workspace.cljs | 38 +- .../src/app/main/data/workspace/bool.cljs | 9 +- .../src/app/main/data/workspace/collapse.cljs | 51 +++ .../src/app/main/data/workspace/common.cljs | 380 ------------------ .../main/data/workspace/drawing/common.cljs | 6 +- .../src/app/main/data/workspace/edition.cljs | 47 +++ .../src/app/main/data/workspace/groups.cljs | 11 +- .../app/main/data/workspace/interactions.cljs | 4 +- .../app/main/data/workspace/libraries.cljs | 9 +- .../data/workspace/libraries_helpers.cljs | 6 +- .../src/app/main/data/workspace/media.cljs | 4 +- .../app/main/data/workspace/path/drawing.cljs | 4 +- .../app/main/data/workspace/path/edition.cljs | 4 +- .../app/main/data/workspace/path/tools.cljs | 4 +- .../app/main/data/workspace/selection.cljs | 15 +- .../src/app/main/data/workspace/shapes.cljs | 268 ++++++++++++ .../app/main/data/workspace/svg_upload.cljs | 16 +- .../src/app/main/data/workspace/texts.cljs | 3 +- .../app/main/data/workspace/transforms.cljs | 2 +- .../app/main/ui/workspace/sidebar/layers.cljs | 2 +- .../options/menus/color_selection.cljs | 4 +- frontend/src/app/util/names.cljs | 38 ++ frontend/test/app/components_sync_test.cljs | 8 +- 25 files changed, 492 insertions(+), 449 deletions(-) create mode 100644 frontend/src/app/main/data/workspace/collapse.cljs create mode 100644 frontend/src/app/main/data/workspace/edition.cljs create mode 100644 frontend/src/app/main/data/workspace/shapes.cljs create mode 100644 frontend/src/app/util/names.cljs diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 012183e9ba..83a1b25f5b 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -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.common.geom.shapes.bounds (:require [app.common.data :as d] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index eee3b49cc4..bb091334cc 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -455,7 +455,7 @@ (->> users (d/index-by :id) (assoc state :file-comments-users)))] - (ptk/reify ::fetch-team-users + (ptk/reify ::fetch-file-comments-users ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index d146c13e7e..8d2abf1c85 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -28,8 +28,10 @@ [app.main.data.users :as du] [app.main.data.workspace.bool :as dwb] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] + [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.guides :as dwgu] @@ -43,6 +45,7 @@ [app.main.data.workspace.path.shapes-to-path :as dwps] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwth] [app.main.data.workspace.transforms :as dwt] @@ -54,6 +57,7 @@ [app.util.globals :as ug] [app.util.http :as http] [app.util.i18n :as i18n] + [app.util.names :as un] [app.util.router :as rt] [app.util.timers :as tm] [app.util.webapi :as wapi] @@ -264,8 +268,8 @@ ptk/WatchEvent (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-1") + unames (un/retrieve-used-names pages) + name (un/generate-unique-name unames "Page-1") changes (-> (pcb/empty-changes it) (pcb/add-empty-page id name))] @@ -279,9 +283,9 @@ (watch [it state _] (let [id (uuid/next) pages (get-in state [:workspace-data :pages-index]) - unames (dwc/retrieve-used-names pages) + unames (un/retrieve-used-names pages) page (get-in state [:workspace-data :pages-index page-id]) - name (dwc/generate-unique-name unames (:name page)) + name (un/generate-unique-name unames (:name page)) no_thumbnails_objects (->> (:objects page) (d/mapm (fn [_ val] (dissoc val :use-for-thumbnail?)))) @@ -577,7 +581,7 @@ hover-guides (get-in state [:workspace-guides :hover])] (cond (d/not-empty? selected) - (rx/of (dwc/delete-shapes selected) + (rx/of (dwsh/delete-shapes selected) (dws/deselect-all)) (d/not-empty? hover-guides) @@ -795,7 +799,7 @@ ids)] (rx/of (dch/commit-changes changes) - (dwc/expand-collapse parent-id)))))) + (dwco/expand-collapse parent-id)))))) (defn relocate-selected-shapes [parent-id to-index] @@ -820,15 +824,15 @@ (case type :text - (rx/of (dwc/start-edition-mode id)) + (rx/of (dwe/start-edition-mode id)) (:group :bool) - (rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)]))) + (rx/of (dws/select-shapes (into (d/ordered-set) [(last shapes)]))) :svg-raw nil - (rx/of (dwc/start-edition-mode id) + (rx/of (dwe/start-edition-mode id) (dwdp/start-path-edit id))))))))) @@ -1548,7 +1552,7 @@ (into (d/ordered-set)))] (rx/of (dch/commit-changes changes) - (dwc/select-shapes selected))))] + (dws/select-shapes selected))))] (ptk/reify ::paste-shape ptk/WatchEvent @@ -1597,7 +1601,7 @@ :content (as-content text)})] (rx/of (dwu/start-undo-transaction) (dws/deselect-all) - (dwc/add-shape shape) + (dwsh/add-shape shape) (dwu/commit-undo-transaction)))))) ;; TODO: why not implement it in terms of upload-media-workspace? @@ -1682,9 +1686,9 @@ (cp/setup-rect-selrect))] (rx/of (dwu/start-undo-transaction) - (dwc/add-shape shape) + (dwsh/add-shape shape) + (dwsh/move-shapes-into-frame (:id shape) selected) - (dwc/move-shapes-into-frame (:id shape) selected) (dwu/commit-undo-transaction)))))))) @@ -1707,10 +1711,10 @@ (dm/export dwly/set-opacity) ;; Common -(dm/export dwc/add-shape) -(dm/export dwc/clear-edition-mode) -(dm/export dwc/select-shapes) -(dm/export dwc/start-edition-mode) +(dm/export dwsh/add-shape) +(dm/export dwe/clear-edition-mode) +(dm/export dws/select-shapes) +(dm/export dwe/start-edition-mode) ;; Drawing (dm/export dwd/select-for-drawing) diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index 2c316d294e..771c06bb26 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -13,8 +13,9 @@ [app.common.path.shapes-to-path :as stp] [app.common.uuid :as uuid] [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] + [app.util.names :as un] [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk])) @@ -89,8 +90,8 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state) base-name (-> bool-type d/name str/capital (str "-1")) - name (-> (dwc/retrieve-used-names objects) - (dwc/generate-unique-name base-name)) + name (-> (un/retrieve-used-names objects) + (un/generate-unique-name base-name)) shapes (selected-shapes state)] (when-not (empty? shapes) @@ -101,7 +102,7 @@ (pcb/add-object boolean-data {:index index}) (pcb/change-parent shape-id shapes))] (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set shape-id))))))))) + (dws/select-shapes (d/ordered-set shape-id))))))))) (defn group-to-bool [shape-id bool-type] diff --git a/frontend/src/app/main/data/workspace/collapse.cljs b/frontend/src/app/main/data/workspace/collapse.cljs new file mode 100644 index 0000000000..4c23b1747f --- /dev/null +++ b/frontend/src/app/main/data/workspace/collapse.cljs @@ -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 app.main.data.workspace.collapse + (:require + [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] + [potok.core :as ptk])) + +;; --- Shape attrs (Layers Sidebar) + +(defn expand-all-parents + [ids objects] + (ptk/reify ::expand-all-parents + ptk/UpdateEvent + (update [_ state] + (let [expand-fn (fn [expanded] + (merge expanded + (->> ids + (map #(cph/get-parent-ids objects %)) + flatten + (remove #(= % uuid/zero)) + (map (fn [id] {id true})) + (into {}))))] + (update-in state [:workspace-local :expanded] expand-fn))))) + + +(defn toggle-collapse + [id] + (ptk/reify ::toggle-collapse + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :expanded id] not)))) + +(defn expand-collapse + [id] + (ptk/reify ::expand-collapse + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :expanded id] true)))) + +(defn collapse-all + [] + (ptk/reify ::collapse-all + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :expanded)))) + diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 8eddc4758c..d9671bfedf 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -6,33 +6,16 @@ (ns app.main.data.workspace.common (:require - [app.common.data :as d] - [app.common.geom.proportions :as gpr] [app.common.logging :as log] - [app.common.pages :as cp] - [app.common.pages.changes-builder :as pcb] - [app.common.pages.helpers :as cph] - [app.common.spec :as us] - [app.common.types.page :as ctp] - [app.common.types.shape :as cts] - [app.common.types.shape.interactions :as ctsi] - [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] - [app.main.streams :as ms] [app.main.worker :as uw] [beicon.core :as rx] - [cljs.spec.alpha :as s] [potok.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) -(s/def ::shape-attrs ::cts/shape-attrs) -(s/def ::set-of-string (s/every string? :kind set?)) -(s/def ::ordered-set-of-uuid (s/every uuid? :kind d/ordered-set?)) - (defn initialized? "Check if the state is properly intialized in a workspace. This means it has the `:current-page-id` and `:current-file-id` properly set." @@ -56,57 +39,6 @@ (->> (uw/ask! msg) (rx/map (constantly ::index-initialized))))))) -;; --- Common Helpers & Events - -(defn- extract-numeric-suffix - [basename] - (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] - [p1 (+ 1 (d/parse-integer p2))] - [basename 1])) - -(defn retrieve-used-names - [objects] - (into #{} (comp (map :name) (remove nil?)) (vals objects))) - - -(defn generate-unique-name - "A unique name generator" - [used basename] - (s/assert ::set-of-string used) - (s/assert ::us/string basename) - (if-not (contains? used basename) - basename - (let [[prefix initial] (extract-numeric-suffix basename)] - (loop [counter initial] - (let [candidate (str prefix "-" counter)] - (if (contains? used candidate) - (recur (inc counter)) - candidate)))))) - -;; --- Shape attrs (Layers Sidebar) - -(defn toggle-collapse - [id] - (ptk/reify ::toggle-collapse - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace-local :expanded id] not)))) - -(defn expand-collapse - [id] - (ptk/reify ::expand-collapse - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :expanded id] true)))) - -(defn collapse-all - [] - (ptk/reify ::collapse-all - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local dissoc :expanded)))) - - ;; These functions should've been in `src/app/main/data/workspace/undo.cljs` but doing that causes ;; a circular dependency with `src/app/main/data/workspace/changes.cljs` (def undo @@ -177,315 +109,3 @@ :origin it :save-undo? false}))))))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Shapes -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn expand-all-parents - [ids objects] - (ptk/reify ::expand-all-parents - ptk/UpdateEvent - (update [_ state] - (let [expand-fn (fn [expanded] - (merge expanded - (->> ids - (map #(cph/get-parent-ids objects %)) - flatten - (remove #(= % uuid/zero)) - (map (fn [id] {id true})) - (into {}))))] - (update-in state [:workspace-local :expanded] expand-fn))))) - -;; --- Update Shape Attrs - -;; NOTE: This is a generic implementation for update multiple shapes -;; in one single commit/undo entry. - - -(defn select-shapes - [ids] - (us/verify ::ordered-set-of-uuid ids) - (ptk/reify ::select-shapes - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :selected] ids)) - - ptk/WatchEvent - (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) - -(defn start-edition-mode - [id] - (us/assert ::us/uuid id) - (ptk/reify ::start-edition-mode - ptk/UpdateEvent - (update [_ state] - (let [objects (wsh/lookup-page-objects state)] - ;; Can only edit objects that exist - (if (contains? objects id) - (-> state - (assoc-in [:workspace-local :selected] #{id}) - (assoc-in [:workspace-local :edition] id)) - state))) - - ptk/WatchEvent - (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 - (ptk/reify ::clear-edition-mode - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace-local :edition])] - (-> state - (update :workspace-local dissoc :edition) - (cond-> (some? id) (update-in [:workspace-local :edit-path] dissoc id))))))) - -(defn get-shape-layer-position - [objects selected attrs] - - ;; Calculate the frame over which we're drawing - (let [position @ms/mouse-position - frame-id (:frame-id attrs (cph/frame-id-by-position objects position)) - shape (when-not (empty? selected) - (cph/get-base-shape objects selected))] - - ;; When no shapes has been selected or we're over a different frame - ;; we add it as the latest shape of that frame - (if (or (not shape) (not= (:frame-id shape) frame-id)) - [frame-id frame-id nil] - - ;; Otherwise, we add it to next to the selected shape - (let [index (cph/get-position-on-parent objects (:id shape)) - {:keys [frame-id parent-id]} shape] - [frame-id parent-id (inc index)])))) - -(defn make-new-shape - [attrs objects selected] - (let [default-attrs (if (= :frame (:type attrs)) - cp/default-frame-attrs - cp/default-shape-attrs) - - selected-non-frames - (into #{} (comp (map (d/getf objects)) - (remove cph/frame-shape?)) - selected) - - [frame-id parent-id index] - (get-shape-layer-position objects selected-non-frames attrs)] - - (-> (merge default-attrs attrs) - (gpr/setup-proportions) - (assoc :frame-id frame-id - :parent-id parent-id - :index index)))) - -(defn add-shape - ([attrs] - (add-shape attrs {})) - - ([attrs {:keys [no-select?]}] - (us/verify ::shape-attrs attrs) - (ptk/reify ::add-shape - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - - id (or (:id attrs) (uuid/next)) - name (-> objects - (retrieve-used-names) - (generate-unique-name (:name attrs))) - - shape (make-new-shape - (assoc attrs :id id :name name) - objects - selected) - - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object shape) - (cond-> (some? (:parent-id attrs)) - (pcb/change-parent (:parent-id attrs) [shape])))] - - (rx/concat - (rx/of (dch/commit-changes changes) - (when-not no-select? - (select-shapes (d/ordered-set id)))) - (when (= :text (:type attrs)) - (->> (rx/of (start-edition-mode id)) - (rx/observe-on :async))))))))) - -(defn move-shapes-into-frame [frame-id shapes] - (ptk/reify ::move-shapes-into-frame - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - - to-move-shapes - (into [] - (map (d/getf objects)) - (reverse (cph/sort-z-index objects shapes))) - - changes - (when (d/not-empty? to-move-shapes) - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/change-parent frame-id to-move-shapes 0)))] - - (if (some? changes) - (rx/of (dch/commit-changes changes)) - (rx/empty)))))) - -(s/def ::set-of-uuid - (s/every ::us/uuid :kind set?)) - -(defn delete-shapes - [ids] - (us/assert ::set-of-uuid ids) - (ptk/reify ::delete-shapes - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - page (wsh/lookup-page state page-id) - - ids (cph/clean-loops objects ids) - - groups-to-unmask - (reduce (fn [group-ids id] - ;; When the shape to delete is the mask of a masked group, - ;; the mask condition must be removed, and it must be - ;; converted to a normal group. - (let [obj (get objects id) - parent (get objects (:parent-id obj))] - (if (and (:masked-group? parent) - (= id (first (:shapes parent)))) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids) - - interacting-shapes - (filter (fn [shape] - ;; If any of the deleted shapes is the destination of - ;; some interaction, this must be deleted, too. - (let [interactions (:interactions shape)] - (some #(and (ctsi/has-destination %) - (contains? ids (:destination %))) - interactions))) - (vals objects)) - - ;; If any of the deleted shapes is a frame with guides - guides (into {} (map (juxt :id identity) (->> (get-in page [:options :guides]) - (vals) - (filter #(not (contains? ids (:frame-id %))))))) - - starting-flows - (filter (fn [flow] - ;; If any of the deleted is a frame that starts a flow, - ;; this must be deleted, too. - (contains? ids (:starting-frame flow))) - (-> page :options :flows)) - - all-parents - (reduce (fn [res id] - ;; All parents of any deleted shape must be resized. - (into res (cph/get-parent-ids objects id))) - (d/ordered-set) - ids) - - all-children - (->> ids ;; Children of deleted shapes must be also deleted. - (reduce (fn [res id] - (into res (cph/get-children-ids objects id))) - []) - (reverse) - (into (d/ordered-set))) - - find-all-empty-parents (fn recursive-find-empty-parents [empty-parents] - (let [all-ids (into empty-parents ids) - 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? all-ids %) shapes)))) - obj))) - (take-while some?) - (map :id)) - calculated-empty-parents (into #{} empty-parents-xform all-parents)] - - (if (= empty-parents calculated-empty-parents) - empty-parents - (recursive-find-empty-parents calculated-empty-parents)))) - - empty-parents - ;; Any parent whose children are all deleted, must be deleted too. - (into (d/ordered-set) (find-all-empty-parents #{})) - - changes (-> (pcb/empty-changes it page-id) - (pcb/with-page page) - (pcb/with-objects objects) - (pcb/set-page-option :guides guides) - (pcb/remove-objects all-children) - (pcb/remove-objects ids) - (pcb/remove-objects empty-parents) - (pcb/resize-parents all-parents) - (pcb/update-shapes groups-to-unmask - (fn [shape] - (assoc shape :masked-group? false))) - (pcb/update-shapes (map :id interacting-shapes) - (fn [shape] - (update shape :interactions - (fn [interactions] - (when interactions - (d/removev #(and (ctsi/has-destination %) - (contains? ids (:destination %))) - interactions)))))) - (cond-> - (seq starting-flows) - (pcb/update-page-option :flows (fn [flows] - (reduce #(ctp/remove-flow %1 (:id %2)) - flows - starting-flows)))))] - - (rx/of (dch/commit-changes changes)))))) - -;; --- Add shape to Workspace - -(defn- viewport-center - [state] - (let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])] - [(+ x (/ width 2)) (+ y (/ height 2))])) - -(defn create-and-add-shape - [type frame-x frame-y data] - (ptk/reify ::create-and-add-shape - ptk/WatchEvent - (watch [_ state _] - (let [{:keys [width height]} data - - [vbc-x vbc-y] (viewport-center state) - x (:x data (- vbc-x (/ width 2))) - y (:y data (- vbc-y (/ height 2))) - page-id (:current-page-id state) - frame-id (-> (wsh/lookup-page-objects state page-id) - (cph/frame-id-by-position {:x frame-x :y frame-y})) - shape (-> (cp/make-minimal-shape type) - (merge data) - (merge {:x x :y y}) - (assoc :frame-id frame-id) - (cp/setup-rect-selrect))] - (rx/of (add-shape shape)))))) diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 1539b398c6..03f956aa59 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -11,7 +11,7 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.pages.helpers :as cph] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.worker :as uw] @@ -66,7 +66,7 @@ (rx/of (dwu/start-undo-transaction)) (rx/empty)) - (rx/of (dwc/add-shape shape {:no-select? (= tool :curve)})) + (rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)})) (if (= :frame (:type shape)) (->> (uw/ask! {:cmd :selection/query @@ -75,7 +75,7 @@ :include-frames? true :full-frame? true}) (rx/map #(cph/clean-loops objects %)) - (rx/map #(dwc/move-shapes-into-frame (:id shape) %))) + (rx/map #(dwsh/move-shapes-into-frame (:id shape) %))) (rx/empty))))) ;; Delay so the mouse event can read the drawing state diff --git a/frontend/src/app/main/data/workspace/edition.cljs b/frontend/src/app/main/data/workspace/edition.cljs new file mode 100644 index 0000000000..b5514b6f32 --- /dev/null +++ b/frontend/src/app/main/data/workspace/edition.cljs @@ -0,0 +1,47 @@ +;; 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.edition + (:require + [app.common.spec :as us] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn interrupt? [e] (= e :interrupt)) + +(declare clear-edition-mode) + +(defn start-edition-mode + [id] + (us/assert ::us/uuid id) + (ptk/reify ::start-edition-mode + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state)] + ;; Can only edit objects that exist + (if (contains? objects id) + (-> state + (assoc-in [:workspace-local :selected] #{id}) + (assoc-in [:workspace-local :edition] id)) + state))) + + ptk/WatchEvent + (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 + (ptk/reify ::clear-edition-mode + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update :workspace-local dissoc :edition) + (cond-> (some? id) (update-in [:workspace-local :edit-path] dissoc id))))))) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 3dd032e0c9..2b315fc5e1 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -12,8 +12,9 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [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] + [app.util.names :as un] [beicon.core :as rx] [potok.core :as ptk])) @@ -70,8 +71,8 @@ (= (count shapes) 1) (= (:type (first shapes)) :group)) (:name (first shapes)) - (-> (dwc/retrieve-used-names objects) - (dwc/generate-unique-name base-name))) + (-> (un/retrieve-used-names objects) + (un/generate-unique-name base-name))) selrect (gsh/selection-rect shapes) group (-> (cp/make-minimal-group frame-id selrect gname) @@ -142,7 +143,7 @@ (let [[group changes] (prepare-create-group it objects page-id shapes "Group-1" false)] (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set (:id group)))))))))) + (dws/select-shapes (d/ordered-set (:id group)))))))))) (def ungroup-selected (ptk/reify ::ungroup-selected @@ -203,7 +204,7 @@ (pcb/resize-parents [(:id group)]))] (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set (:id group)))))))))) + (dws/select-shapes (d/ordered-set (:id group)))))))))) (def unmask-group (ptk/reify ::unmask-group diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 5ea0d8b830..40c6a80f9e 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -15,9 +15,9 @@ [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] + [app.util.names :as un] [beicon.core :as rx] [potok.core :as ptk])) @@ -32,7 +32,7 @@ flows (get-in page [:options :flows] []) unames (into #{} (map :name flows)) - name (dwc/generate-unique-name unames "Flow-1") + name (un/generate-unique-name unames "Flow-1") new-flow {:id (uuid/next) :name name diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 47c2aa7036..7663a0585e 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -22,14 +22,15 @@ [app.main.data.events :as ev] [app.main.data.messages :as dm] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries-helpers :as dwlh] + [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :refer [tr]] + [app.util.names :as un] [app.util.router :as rt] [app.util.time :as dt] [beicon.core :as rx] @@ -295,7 +296,7 @@ (dwlh/generate-add-component it shapes objects page-id file-id)] (when-not (empty? (:redo-changes changes)) (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set (:id group))))))))))) + (dws/select-shapes (d/ordered-set (:id group))))))))))) (defn add-component "Add a new component to current file library, from the currently selected shapes. @@ -351,7 +352,7 @@ component (cph/get-component libraries id) all-components (-> state :workspace-data :components vals) unames (into #{} (map :name) all-components) - new-name (dwc/generate-unique-name unames (:name component)) + new-name (un/generate-unique-name unames (:name component)) [new-shape new-shapes _updated-shapes] (dwlh/duplicate-component component) @@ -401,7 +402,7 @@ page libraries)] (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set (:id new-shape)))))))) + (dws/select-shapes (d/ordered-set (:id new-shape)))))))) (defn detach-component "Remove all references to components in the shape with the given id, diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index c95763d982..a1e50c89f2 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -16,9 +16,9 @@ [app.common.spec :as us] [app.common.text :as txt] [app.common.types.color :as ctc] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.state-helpers :as wsh] + [app.util.names :as un] [cljs.spec.alpha :as s] [clojure.set :as set])) @@ -144,13 +144,13 @@ delta (gpt/subtract position orig-pos) objects (:objects page) - unames (volatile! (dwc/retrieve-used-names objects)) + unames (volatile! (un/retrieve-used-names objects)) frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta)) update-new-shape (fn [new-shape original-shape] - (let [new-name (dwc/generate-unique-name @unames (:name new-shape))] + (let [new-name (un/generate-unique-name @unames (:name new-shape))] (when (nil? (:parent-id original-shape)) (vswap! unames conj new-name)) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 777dc8d8de..ce0959c33f 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -10,8 +10,8 @@ [app.common.spec :as us] [app.main.data.media :as dmm] [app.main.data.messages :as dm] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.svg-upload :as svg] [app.main.repo :as rp] [app.main.store :as st] @@ -72,7 +72,7 @@ :height height :mtype mtype :id id}}] - (rx/of (dwc/create-and-add-shape :image x y shape)))))) + (rx/of (dwsh/create-and-add-shape :image x y shape)))))) (defn svg-uploaded [svg-data file-id position] diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 5c46ecc456..ff61c094a1 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -13,8 +13,8 @@ [app.common.path.shapes-to-path :as upsp] [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.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.helpers :as helpers] @@ -276,7 +276,7 @@ (watch [_ _ _] (rx/of (setup-frame-path) (dwdc/handle-finish-drawing) - (dwc/start-edition-mode shape-id) + (dwe/start-edition-mode shape-id) (change-edit-mode :draw))))) (defn handle-new-shape diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index fa8c00f51d..b3b0b68123 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -13,7 +13,7 @@ [app.common.path.shapes-to-path :as upsp] [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.drawing :as drawing] [app.main.data.workspace.path.helpers :as helpers] @@ -64,7 +64,7 @@ (let [changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)] (if (empty? new-content) (rx/of (dch/commit-changes changes) - dwc/clear-edition-mode) + dwe/clear-edition-mode) (rx/of (dch/commit-changes changes) (selection/update-selection point-change) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler)))))))))) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 71a6866623..3338c0c9ef 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -9,7 +9,7 @@ [app.common.path.shapes-to-path :as upsp] [app.common.path.subpaths :as ups] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.state :as st] [app.main.data.workspace.state-helpers :as wsh] @@ -40,7 +40,7 @@ (rx/of (dch/update-shapes [id] upsp/convert-to-path)) (rx/of (dch/commit-changes changes) (when (empty? new-content) - dwc/clear-edition-mode)))))))))) + dwe/clear-edition-mode)))))))))) (defn make-corner ([] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index d65dfcb428..777a5d49c8 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -19,13 +19,14 @@ [app.common.uuid :as uuid] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.zoom :as dwz] [app.main.refs :as refs] [app.main.streams :as ms] [app.main.worker :as uw] + [app.util.names :as un] [beicon.core :as rx] [cljs.spec.alpha :as s] [clojure.set :as set] @@ -41,6 +42,8 @@ (s/def ::set-of-string (s/every string? :kind set?)) +(defn interrupt? [e] (= e :interrupt)) + ;; --- Selection Rect (declare select-shapes-by-current-selrect) @@ -59,7 +62,7 @@ ptk/WatchEvent (watch [_ state stream] (let [zoom (get-in state [:workspace-local :zoom] 1) - stop? (fn [event] (or (dwc/interrupt? event) (ms/mouse-up? event))) + stop? (fn [event] (or (interrupt? event) (ms/mouse-up? event))) stoper (->> stream (rx/filter stop?)) init-selrect @@ -281,7 +284,7 @@ move to the desired position, and recalculate parents and frames as needed." [all-objects page ids delta it] (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (dwc/retrieve-used-names (:objects page))) + unames (volatile! (un/retrieve-used-names (:objects page))) update-unames! (fn [new-name] (vswap! unames conj new-name)) all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids all-objects %2))) (d/ordered-set) ids) ids-map (into {} (map #(vector % (uuid/next))) all-ids) @@ -316,7 +319,7 @@ (defn- prepare-duplicate-frame-change [changes objects page unames update-unames! ids-map obj delta] (let [new-id (ids-map (:id obj)) - frame-name (dwc/generate-unique-name @unames (:name obj)) + frame-name (un/generate-unique-name @unames (:name obj)) _ (update-unames! frame-name) new-frame (-> obj @@ -351,7 +354,7 @@ (if (some? obj) (let [new-id (ids-map (:id obj)) parent-id (or parent-id frame-id) - name (dwc/generate-unique-name @unames (:name obj)) + name (un/generate-unique-name @unames (:name obj)) _ (update-unames! name) new-obj (-> obj @@ -392,7 +395,7 @@ (let [update-flows (fn [flows] (reduce (fn [flows frame] - (let [name (dwc/generate-unique-name @unames "Flow-1") + (let [name (un/generate-unique-name @unames "Flow-1") _ (vswap! unames conj name) new-flow {:id (uuid/next) :name name diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs new file mode 100644 index 0000000000..9694db988c --- /dev/null +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -0,0 +1,268 @@ +;; 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.shapes + (:require + [app.common.data :as d] + [app.common.geom.proportions :as gpr] + [app.common.pages :as cp] + [app.common.pages.changes-builder :as pcb] + [app.common.pages.helpers :as cph] + [app.common.spec :as us] + [app.common.types.page :as csp] + [app.common.types.shape :as spec.shape] + [app.common.types.shape.interactions :as csi] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.edition :as dwe] + [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.streams :as ms] + [app.util.names :as un] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [potok.core :as ptk])) + +(s/def ::shape-attrs ::spec.shape/shape-attrs) + +(defn get-shape-layer-position + [objects selected attrs] + + ;; Calculate the frame over which we're drawing + (let [position @ms/mouse-position + frame-id (:frame-id attrs (cph/frame-id-by-position objects position)) + shape (when-not (empty? selected) + (cph/get-base-shape objects selected))] + + ;; When no shapes has been selected or we're over a different frame + ;; we add it as the latest shape of that frame + (if (or (not shape) (not= (:frame-id shape) frame-id)) + [frame-id frame-id nil] + + ;; Otherwise, we add it to next to the selected shape + (let [index (cph/get-position-on-parent objects (:id shape)) + {:keys [frame-id parent-id]} shape] + [frame-id parent-id (inc index)])))) + +(defn make-new-shape + [attrs objects selected] + (let [default-attrs (if (= :frame (:type attrs)) + cp/default-frame-attrs + cp/default-shape-attrs) + + selected-non-frames + (into #{} (comp (map (d/getf objects)) + (remove cph/frame-shape?)) + selected) + + [frame-id parent-id index] + (get-shape-layer-position objects selected-non-frames attrs)] + + (-> (merge default-attrs attrs) + (gpr/setup-proportions) + (assoc :frame-id frame-id + :parent-id parent-id + :index index)))) + +(defn add-shape + ([attrs] + (add-shape attrs {})) + + ([attrs {:keys [no-select?]}] + (us/verify ::shape-attrs attrs) + (ptk/reify ::add-shape + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (wsh/lookup-selected state) + + id (or (:id attrs) (uuid/next)) + name (-> objects + (un/retrieve-used-names) + (un/generate-unique-name (:name attrs))) + + shape (make-new-shape + (assoc attrs :id id :name name) + objects + selected) + + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object shape) + (cond-> (some? (:parent-id attrs)) + (pcb/change-parent (:parent-id attrs) [shape])))] + + (rx/concat + (rx/of (dch/commit-changes changes) + (when-not no-select? + (dws/select-shapes (d/ordered-set id)))) + (when (= :text (:type attrs)) + (->> (rx/of (dwe/start-edition-mode id)) + (rx/observe-on :async))))))))) + +(defn move-shapes-into-frame [frame-id shapes] + (ptk/reify ::move-shapes-into-frame + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + + to-move-shapes + (into [] + (map (d/getf objects)) + (reverse (cph/sort-z-index objects shapes))) + + changes + (when (d/not-empty? to-move-shapes) + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/change-parent frame-id to-move-shapes 0)))] + + (if (some? changes) + (rx/of (dch/commit-changes changes)) + (rx/empty)))))) + +(s/def ::set-of-uuid + (s/every ::us/uuid :kind set?)) + +(defn delete-shapes + [ids] + (us/assert ::set-of-uuid ids) + (ptk/reify ::delete-shapes + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + page (wsh/lookup-page state page-id) + + ids (cph/clean-loops objects ids) + + groups-to-unmask + (reduce (fn [group-ids id] + ;; When the shape to delete is the mask of a masked group, + ;; the mask condition must be removed, and it must be + ;; converted to a normal group. + (let [obj (get objects id) + parent (get objects (:parent-id obj))] + (if (and (:masked-group? parent) + (= id (first (:shapes parent)))) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids) + + interacting-shapes + (filter (fn [shape] + ;; If any of the deleted shapes is the destination of + ;; some interaction, this must be deleted, too. + (let [interactions (:interactions shape)] + (some #(and (csi/has-destination %) + (contains? ids (:destination %))) + interactions))) + (vals objects)) + + ;; If any of the deleted shapes is a frame with guides + guides (into {} (map (juxt :id identity) (->> (get-in page [:options :guides]) + (vals) + (filter #(not (contains? ids (:frame-id %))))))) + + starting-flows + (filter (fn [flow] + ;; If any of the deleted is a frame that starts a flow, + ;; this must be deleted, too. + (contains? ids (:starting-frame flow))) + (-> page :options :flows)) + + all-parents + (reduce (fn [res id] + ;; All parents of any deleted shape must be resized. + (into res (cph/get-parent-ids objects id))) + (d/ordered-set) + ids) + + all-children + (->> ids ;; Children of deleted shapes must be also deleted. + (reduce (fn [res id] + (into res (cph/get-children-ids objects id))) + []) + (reverse) + (into (d/ordered-set))) + + find-all-empty-parents (fn recursive-find-empty-parents [empty-parents] + (let [all-ids (into empty-parents ids) + 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? all-ids %) shapes)))) + obj))) + (take-while some?) + (map :id)) + calculated-empty-parents (into #{} empty-parents-xform all-parents)] + + (if (= empty-parents calculated-empty-parents) + empty-parents + (recursive-find-empty-parents calculated-empty-parents)))) + + empty-parents + ;; Any parent whose children are all deleted, must be deleted too. + (into (d/ordered-set) (find-all-empty-parents #{})) + + changes (-> (pcb/empty-changes it page-id) + (pcb/with-page page) + (pcb/with-objects objects) + (pcb/set-page-option :guides guides) + (pcb/remove-objects all-children) + (pcb/remove-objects ids) + (pcb/remove-objects empty-parents) + (pcb/resize-parents all-parents) + (pcb/update-shapes groups-to-unmask + (fn [shape] + (assoc shape :masked-group? false))) + (pcb/update-shapes (map :id interacting-shapes) + (fn [shape] + (update shape :interactions + (fn [interactions] + (when interactions + (d/removev #(and (csi/has-destination %) + (contains? ids (:destination %))) + interactions)))))) + (cond-> + (seq starting-flows) + (pcb/update-page-option :flows (fn [flows] + (reduce #(csp/remove-flow %1 (:id %2)) + flows + starting-flows)))))] + + (rx/of (dch/commit-changes changes)))))) + +(defn- viewport-center + [state] + (let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])] + [(+ x (/ width 2)) (+ y (/ height 2))])) + +(defn create-and-add-shape + [type frame-x frame-y data] + (ptk/reify ::create-and-add-shape + ptk/WatchEvent + (watch [_ state _] + (prn ">>>create-") + (let [{:keys [width height]} data + + [vbc-x vbc-y] (viewport-center state) + x (:x data (- vbc-x (/ width 2))) + y (:y data (- vbc-y (/ height 2))) + page-id (:current-page-id state) + frame-id (-> (wsh/lookup-page-objects state page-id) + (cph/frame-id-by-position {:x frame-x :y frame-y})) + shape (-> (cp/make-minimal-shape type) + (merge data) + (merge {:x x :y y}) + (assoc :frame-id frame-id) + (cp/setup-rect-selrect))] + (rx/of (add-shape shape)))))) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 0424e73908..3c830bd59b 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -17,9 +17,11 @@ [app.common.spec :refer [max-safe-int min-safe-int]] [app.common.uuid :as uuid] [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.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.util.color :as uc] + [app.util.names :as un] [app.util.path.parser :as upp] [app.util.svg :as usvg] [beicon.core :as rx] @@ -358,7 +360,7 @@ (let [{:keys [tag attrs hidden]} element-data attrs (usvg/format-styles attrs) element-data (cond-> element-data (map? element-data) (assoc :attrs attrs)) - name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag))) + name (un/generate-unique-name unames (or (:id attrs) (tag->name tag))) att-refs (usvg/find-attr-references attrs) references (usvg/find-def-references (:defs svg-data) att-refs) @@ -415,7 +417,7 @@ (if (some? shape) (let [shape-id (:id shape) - new-shape (dwc/make-new-shape shape objects selected) + new-shape (dwsh/make-new-shape shape objects selected) changes (-> changes (pcb/add-object new-shape) (pcb/change-parent parent-id [new-shape] index)) @@ -442,10 +444,10 @@ x (- x vb-x (/ vb-width 2)) y (- y vb-y (/ vb-height 2)) - unames (dwc/retrieve-used-names objects) + unames (un/retrieve-used-names objects) svg-name (->> (str/replace (:name svg-data) ".svg" "") - (dwc/generate-unique-name unames)) + (un/generate-unique-name unames)) svg-data (-> svg-data (assoc :x x @@ -482,7 +484,7 @@ (assoc :content (into [base-background-shape] (:content svg-data)))) ;; Creates the root shape - new-shape (dwc/make-new-shape root-shape objects selected) + new-shape (dwsh/make-new-shape root-shape objects selected) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) @@ -506,7 +508,7 @@ vec))] (rx/of (dch/commit-changes changes) - (dwc/select-shapes (d/ordered-set root-id)))) + (dws/select-shapes (d/ordered-set root-id)))) (catch :default e (.error js/console "Error SVG" e) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 30637d2927..1744642185 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -17,6 +17,7 @@ [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.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.util.router :as rt] @@ -78,7 +79,7 @@ (when (some? id) (rx/of (dws/deselect-shape id) - (dwc/delete-shapes #{id}))))))))) + (dwsh/delete-shapes #{id}))))))))) (defn initialize-editor-state [{:keys [id content] :as shape} decorator] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index ffd31b1fde..4d87ff0f6e 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -17,7 +17,7 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.guides :as dwg] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 6b3132217e..05ad9bdfd5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -11,7 +11,7 @@ [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.collapse :as dwc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.shape-icon :as si] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 234c7cc06a..95951ac5df 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -10,7 +10,7 @@ [app.common.data.macros :as dm] [app.common.text :as txt] [app.main.data.workspace.colors :as dc] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.selection :as dws] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] @@ -148,7 +148,7 @@ (fn [color] (let [shapes-by-color (get @grouped-colors* color) ids (into (d/ordered-set) (map :shape-id) shapes-by-color)] - (st/emit! (dwc/select-shapes ids)))))] + (st/emit! (dws/select-shapes ids)))))] (mf/with-effect [grouped-colors] (reset! grouped-colors* grouped-colors)) diff --git a/frontend/src/app/util/names.cljs b/frontend/src/app/util/names.cljs new file mode 100644 index 0000000000..6a2288fcdb --- /dev/null +++ b/frontend/src/app/util/names.cljs @@ -0,0 +1,38 @@ +;; 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.names + (:require + [app.common.data :as d] + [app.common.spec :as us] + [cljs.spec.alpha :as s])) + +(s/def ::set-of-string (s/every string? :kind set?)) + +(defn- extract-numeric-suffix + [basename] + (if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] + [p1 (+ 1 (d/parse-integer p2))] + [basename 1])) + +(defn retrieve-used-names + [objects] + (into #{} (comp (map :name) (remove nil?)) (vals objects))) + +(defn generate-unique-name + "A unique name generator" + [used basename] + (s/assert ::set-of-string used) + (s/assert ::us/string basename) + (if-not (contains? used basename) + basename + (let [[prefix initial] (extract-numeric-suffix basename)] + (loop [counter initial] + (let [candidate (str prefix "-" counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate)))))) + diff --git a/frontend/test/app/components_sync_test.cljs b/frontend/test/app/components_sync_test.cljs index a34af0f167..b3d754a37a 100644 --- a/frontend/test/app/components_sync_test.cljs +++ b/frontend/test/app/components_sync_test.cljs @@ -6,7 +6,7 @@ [app.common.pages.helpers :as cph] [app.main.data.workspace :as dw] [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries-helpers :as dwlh] [app.main.data.workspace.state-helpers :as wsh] @@ -192,7 +192,7 @@ (ptk/emit! store - (dwc/delete-shapes #{(:id shape1)}) + (dwsh/delete-shapes #{(:id shape1)}) :the/end))))) (t/deftest test-touched-children-move @@ -767,7 +767,7 @@ (ptk/emit! store - (dwc/delete-shapes #{(:id shape1)}) + (dwsh/delete-shapes #{(:id shape1)}) (dwl/reset-component (:id instance1)) :the/end))))) @@ -1538,7 +1538,7 @@ (ptk/emit! store - (dwc/delete-shapes #{(:id shape1)}) + (dwsh/delete-shapes #{(:id shape1)}) (dwl/update-component-sync (:id instance1) (:id file)) :the/end))))) From 288dab3fe7780c3457b9e5994bd84d44176ea97a Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 29 Jun 2022 08:26:40 +0200 Subject: [PATCH 59/86] :sparkles: Signin/Signup from shared link --- CHANGES.md | 1 + backend/src/app/rpc/queries/files.clj | 3 +- .../resources/styles/main/partials/modal.scss | 62 ++++++++++ frontend/src/app/main/ui/auth/login.cljs | 60 ++++++---- .../app/main/ui/auth/recovery_request.cljs | 34 +++--- frontend/src/app/main/ui/auth/register.cljs | 55 +++++---- frontend/src/app/main/ui/viewer.cljs | 6 +- frontend/src/app/main/ui/viewer/header.cljs | 23 ++-- frontend/src/app/main/ui/viewer/login.cljs | 106 ++++++++++++++++++ frontend/translations/en.po | 6 + frontend/translations/es.po | 6 + 11 files changed, 295 insertions(+), 67 deletions(-) create mode 100644 frontend/src/app/main/ui/viewer/login.cljs diff --git a/CHANGES.md b/CHANGES.md index 0bfdb3e46d..080d038156 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982) - View mode improvements to enable access and use in different conditions [Taiga #3023](https://tree.taiga.io/project/penpot/us/3023) - Improved share link options. Now you can allow non-team members to comment and/or inspect [Taiga #3056] (https://tree.taiga.io/project/penpot/us/3056) +- Signin/Signup from shared link [Taiga #3472](https://tree.taiga.io/project/penpot/us/3472) ### :bug: Bugs fixed ### :arrow_up: Deps updates diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 9460be08ff..18ec92848d 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -85,7 +85,8 @@ :is-owner is-owner :is-admin (or is-owner is-admin) :can-edit (or is-owner is-admin can-edit) - :can-read true}))) + :can-read true + :is-logged (some? profile-id)}))) ([conn profile-id file-id share-id] (let [perms (get-permissions conn profile-id file-id) ldata (retrieve-share-link conn file-id share-id)] diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index b6bce0d27d..89f4927ca5 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -1632,3 +1632,65 @@ } } } + +//- LOGIN +.login-register { + background-color: $color-white; + box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2); + display: flex; + font-family: "worksans", sans-serif; + width: 472px; + height: auto; + position: relative; + + .title { + margin-left: 32px; + h2 { + font-size: 24px; + color: $color-black; + line-height: $fs36; + letter-spacing: 0px; + margin: 0 30px 20px 0; + } + + .modal-close-button { + margin-top: 7px; + margin-right: 12px; + justify-content: right; + svg { + fill: $color-black; + } + } + } + + .modal-bottom { + margin: 0 32px; + color: #1f1f1f; + display: flex; + flex-direction: column; + + &.auth-content { + align-items: initial; + height: auto; + } + + .links { + margin: 7px 0 0 0; + text-align: left; + } + } + + .modal-footer { + justify-content: center; + align-items: center; + + .terms-login { + position: relative; + bottom: 0; + } + } + + .hint { + color: #b1b2b5; + } +} diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 8c9cc7b0a7..098386c24c 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -63,7 +63,7 @@ (on-error error))))))) (mf/defc login-form - [{:keys [params] :as props}] + [{:keys [params on-success-callback] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) error (mf/use-state false) @@ -73,10 +73,17 @@ (fn [_] (reset! error (tr "errors.wrong-credentials"))) - on-succes + on-success-default (fn [data] (when-let [token (:invitation-token data)] (st/emit! (rt/nav :auth-verify-token {} {:token token})))) + + on-success + (fn [data] + (if (nil? on-success-callback) + (on-success-default data) + (on-success-callback) + )) on-submit (mf/use-callback @@ -84,7 +91,7 @@ (reset! error nil) (let [params (with-meta (:clean-data @form) {:on-error on-error - :on-success on-succes})] + :on-success on-success})] (st/emit! (du/login params))))) on-submit-ldap @@ -95,7 +102,7 @@ (let [params (:clean-data @form)] (login-with-ldap event (with-meta params {:on-error on-error - :on-success on-succes})))))] + :on-success on-success})))))] [:* (when-let [message @error] [:& msgs/inline-banner @@ -165,32 +172,37 @@ [:a {:on-click #(login-with-oauth % :oidc params)} (tr "auth.login-with-oidc-submit")]])) +(mf/defc login-methods + [{:keys [params on-success-callback] :as props}] + [:* + (when show-alt-login-buttons? + [:* + [:span.separator + [:span.line] + [:span.text (tr "labels.continue-with")] + [:span.line]] + + [:div.buttons + [:& login-buttons {:params params}]] + + (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-ldap)) + [:span.separator + [:span.line] + [:span.text (tr "labels.or")] + [:span.line]])]) + + (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-ldap)) + [:& login-form {:params params :on-success-callback on-success-callback}])]) + (mf/defc login-page [{:keys [params] :as props}] [:div.generic-form.login-form [:div.form-container [:h1 {:data-test "login-title"} (tr "auth.login-title")] - (when show-alt-login-buttons? - [:* - [:span.separator - [:span.line] - [:span.text (tr "labels.continue-with")] - [:span.line]] - - [:div.buttons - [:& login-buttons {:params params}]] - - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-ldap)) - [:span.separator - [:span.line] - [:span.text (tr "labels.or")] - [:span.line]])]) - - (when (or (contains? @cf/flags :login) - (contains? @cf/flags :login-with-ldap)) - [:& login-form {:params params}]) + [:& login-methods {:params params}] [:div.links (when (contains? @cf/flags :login) diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 6477cfb32d..436f7cded8 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -22,15 +22,19 @@ (s/def ::recovery-request-form (s/keys :req-un [::email])) (mf/defc recovery-form - [] + [{:keys [on-success-callback] :as props}] (let [form (fm/use-form :spec ::recovery-request-form :initial {}) submitted (mf/use-state false) + default-success-finish #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))) + on-success (mf/use-callback - (fn [_ _] + (fn [cdata _] (reset! submitted false) - (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))))) + (if (nil? on-success-callback) + (default-success-finish) + (on-success-callback (:email cdata))))) on-error (mf/use-callback @@ -74,15 +78,17 @@ ;; --- Recovery Request Page (mf/defc recovery-request-page - [] - [:section.generic-form - [:div.form-container - [:h1 (tr "auth.recovery-request-title")] - [:div.subtitle (tr "auth.recovery-request-subtitle")] - [:& recovery-form] + [{:keys [params on-success-callback go-back-callback] :as props}] + (let [default-go-back #(st/emit! (rt/nav :auth-login)) + go-back (or go-back-callback default-go-back)] + [:section.generic-form + [:div.form-container + [:h1 (tr "auth.recovery-request-title")] + [:div.subtitle (tr "auth.recovery-request-subtitle")] + [:& recovery-form {:params params :on-success-callback on-success-callback}] - [:div.links - [:div.link-entry - [:a {:on-click #(st/emit! (rt/nav :auth-login)) - :data-test "go-back-link"} - (tr "labels.go-back")]]]]]) + [:div.links + [:div.link-entry + [:a {:on-click go-back + :data-test "go-back-link"} + (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 739b7ad1da..a88fa37378 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -68,17 +68,23 @@ (st/emit! (dm/error (tr "errors.generic"))))) (defn- handle-prepare-register-success - [_ params] + [params] (st/emit! (rt/nav :auth-register-validate {} params))) + (mf/defc register-form - [{:keys [params] :as props}] + [{:keys [params on-success-callback] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) form (fm/use-form :spec ::register-form :validators [validate] :initial initial) submitted? (mf/use-state false) + on-success (fn [p] + (if (nil? on-success-callback) + (handle-prepare-register-success p) + (on-success-callback p))) + on-submit (mf/use-callback (fn [form _event] @@ -87,9 +93,9 @@ (->> (rp/mutation :prepare-register-profile cdata) (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) - (rx/subs (partial handle-prepare-register-success form) - (partial handle-prepare-register-error form)))))) - ] + (rx/subs + on-success + (partial handle-prepare-register-error form))))))] [:& fm/form {:on-submit on-submit @@ -113,15 +119,10 @@ :disabled @submitted? :data-test "register-form-submit"}]])) -(mf/defc register-page - [{:keys [params] :as props}] - [:div.form-container - [:h1 {:data-test "registration-title"} (tr "auth.register-title")] - [:div.subtitle (tr "auth.register-subtitle")] - - (when (contains? @cf/flags :demo-warning) - [:& demo-warning]) +(mf/defc register-methods + [{:keys [params on-success-callback] :as props}] + [:* (when login/show-alt-login-buttons? [:* [:span.separator @@ -139,7 +140,19 @@ [:span.text (tr "labels.or")] [:span.line]])]) - [:& register-form {:params params}] + [:& register-form {:params params :on-success-callback on-success-callback}]]) + +(mf/defc register-page + [{:keys [params] :as props}] + [:div.form-container + + [:h1 {:data-test "registration-title"} (tr "auth.register-title")] + [:div.subtitle (tr "auth.register-subtitle")] + + (when (contains? @cf/flags :demo-warning) + [:& demo-warning]) + + [:& register-methods {:params params}] [:div.links [:div.link-entry @@ -170,7 +183,7 @@ (st/emit! (dm/error (tr "errors.generic")))))) (defn- handle-register-success - [_form data] + [data] (cond (some? (:invitation-token data)) (let [token (:invitation-token data)] @@ -197,11 +210,16 @@ ::accept-newsletter-subscription]))) (mf/defc register-validate-form - [{:keys [params] :as props}] + [{:keys [params on-success-callback] :as props}] (let [form (fm/use-form :spec ::register-validate-form :initial params) submitted? (mf/use-state false) + on-success (fn [p] + (if (nil? on-success-callback) + (handle-register-success p) + (on-success-callback (:email p)))) + on-submit (mf/use-callback (fn [form _event] @@ -209,9 +227,8 @@ (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)))))) - ] + (rx/subs on-success + (partial handle-register-error form))))))] [:& fm/form {:on-submit on-submit :form form} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 6ccbc169cb..f2f86ace93 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -29,6 +29,7 @@ [app.main.ui.viewer.handoff :as handoff] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.interactions :as interactions] + [app.main.ui.viewer.login] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -252,8 +253,9 @@ (when (nil? page) (ex/raise :type :not-found)) - (when (not allowed) - (st/emit! (dv/go-to-section :interactions))) + (mf/with-effect [] + (when (not allowed) + (st/emit! (dv/go-to-section :interactions)))) ;; Set the page title (mf/use-effect diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 5cde91686f..034473649f 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -21,6 +21,10 @@ [app.util.i18n :refer [tr]] [rumext.alpha :as mf])) +(defn open-login-dialog + [] + (modal/show! :login-register {})) + (mf/defc zoom-widget {::mf/wrap [mf/memo]} [{:keys [zoom @@ -111,7 +115,10 @@ [:span.btn-primary {:on-click open-share-dialog} i/export [:span (tr "labels.share-prototype")]]) (when (:can-edit permissions) - [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")])])) + [:span.btn-text-dark {:on-click go-to-workspace} (tr "labels.edit-file")]) + + (when-not (:is-logged permissions) + [:span.btn-text-dark {:on-click open-login-dialog} (tr "labels.log-or-sign")])])) (mf/defc header-sitemap [{:keys [project file page frame] :as props}] @@ -176,12 +183,16 @@ #(st/emit! (dv/go-to-dashboard)) go-to-handoff - (fn [] - (st/emit! dv/close-thumbnails-panel (dv/go-to-section :handoff))) + (fn[] + (if (:is-logged permissions) + (st/emit! dv/close-thumbnails-panel (dv/go-to-section :handoff)) + (open-login-dialog))) navigate (fn [section] - (st/emit! (dv/go-to-section section)))] + (if (or (= section :interactions) (:is-logged permissions)) + (st/emit! (dv/go-to-section section)) + (open-login-dialog)))] [:header.viewer-header [:div.nav-zone @@ -200,8 +211,7 @@ i/play] (when (or (:can-edit permissions) - (and (true? (:is-logged permissions)) - (= (:who-comment permissions) "all"))) + (= (:who-comment permissions) "all")) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(navigate :comments) :class (dom/classnames :active (= section :comments)) @@ -210,7 +220,6 @@ (when (or (= (:type permissions) :membership) (and (= (:type permissions) :share-link) - (true? (:is-logged permissions)) (= (:who-inspect permissions) "all"))) [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click go-to-handoff diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs new file mode 100644 index 0000000000..0745a3c5f1 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -0,0 +1,106 @@ +;; 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.viewer.login + (:require + [app.common.logging :as log] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.auth :refer [terms-login]] + [app.main.ui.auth.login :refer [login-methods]] + [app.main.ui.auth.recovery-request :refer [recovery-request-page]] + [app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.storage :refer [storage]] + [rumext.alpha :as mf])) + +(log/set-level! :warn) + +(mf/defc login-register-modal + {::mf/register modal/components + ::mf/register-as :login-register} + [_] + (let [uri (. (. js/document -location) -href) + user-email (mf/use-state "") + register-token (mf/use-state "") + current-section (mf/use-state :login) + set-current-section (mf/use-fn #(reset! current-section %)) + main-section (or + (= @current-section :login) + (= @current-section :register) + (= @current-section :register-validate)) + close + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide))) + success-email-sent + (fn [email] + (reset! user-email email) + (set-current-section :email-sent)) + success-login + (fn [] + (.reload js/window.location true)) + success-register + (fn [data] + (reset! register-token (:token data)) + (set-current-section :register-validate))] + (mf/with-effect [] + (swap! storage assoc :redirect-url uri)) + + [:div.modal-overlay + [:div.modal-container.login-register + [:div.title + [:div.modal-close-button {:on-click close :title (tr "labels.close")} + i/close] + (when main-section + [:h2 (tr "labels.continue-with-penpot")])] + + [:div.modal-bottom.auth-content + + (case @current-section + :login + [:div.generic-form.login-form + [:div.form-container + [:& login-methods {:on-success-callback success-login}] + [:div.links + [:div.link-entry + [:a {:on-click #(set-current-section :recovery-request)} + (tr "auth.forgot-password")]] + [:div.link-entry + [:span (tr "auth.register") " "] + [:a {:on-click #(set-current-section :register)} + (tr "auth.register-submit")]]]]] + + :register + [:div.form-container + [:& register-methods {:on-success-callback success-register}] + [:div.links + [:div.link-entry + [:span (tr "auth.already-have-account") " "] + [:a {:on-click #(set-current-section :login)} + (tr "auth.login-here")]]]] + + :register-validate + [:div.form-container + [:& register-validate-form {:params {:token @register-token} + :on-success-callback success-email-sent}] + [:div.links + [:div.link-entry + [:a {:on-click #(set-current-section :register)} + (tr "labels.go-back")]]]] + + :recovery-request + [:& recovery-request-page {:go-back-callback #(set-current-section :login) + :on-success-callback success-email-sent}] + :email-sent + [:div.form-container + [:& register-success-page {:params {:email @user-email}}]])] + + (when main-section + [:div.modal-footer.links + [:& terms-login]])]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3ae8bfe836..99b91eafd1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1101,6 +1101,9 @@ msgstr "Continue" msgid "labels.continue-with" msgstr "Continue with" +msgid "labels.continue-with-penpot" +msgstr "You can continue with a Penpot account" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Create" @@ -1208,6 +1211,9 @@ msgstr "Help Center" msgid "labels.hide-resolved-comments" msgstr "Hide resolved comments" +msgid "labels.log-or-sign" +msgstr "Log in or sign up" + msgid "labels.show-comments-list" msgstr "Show comments list" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index dcc6ab4bf1..ad8af9bdf5 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1158,6 +1158,9 @@ msgstr "Continuar" msgid "labels.continue-with" msgstr "Continúa con" +msgid "labels.continue-with-penpot" +msgstr "Puedes continuar con una cuenta de Penpot" + #: src/app/main/ui/workspace/sidebar/assets.cljs msgid "labels.create" msgstr "Crear" @@ -1272,6 +1275,9 @@ msgstr "Centro de ayuda" msgid "labels.hide-resolved-comments" msgstr "Ocultar comentarios resueltos" +msgid "labels.log-or-sign" +msgstr "Entra o regístrate" + msgid "labels.show-comments-list" msgstr "Mostrar lista de comentarios" From 14d1cb90bd35b5438d40883a74ba68af2cc4b8dd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 30 Jun 2022 15:11:41 +0200 Subject: [PATCH 60/86] :recycle: Refactor auth code --- CHANGES.md | 20 + backend/src/app/auth/ldap.clj | 137 +++++ .../src/app/{http/oauth.clj => auth/oidc.clj} | 509 ++++++++++-------- backend/src/app/cli/manage.clj | 5 +- backend/src/app/config.clj | 3 - backend/src/app/http.clj | 50 +- backend/src/app/http/doc.clj | 38 +- backend/src/app/http/errors.clj | 2 - backend/src/app/http/middleware.clj | 1 + backend/src/app/main.clj | 85 ++- backend/src/app/rpc.clj | 73 ++- backend/src/app/rpc/commands/auth.clj | 416 ++++++++++++++ .../app/rpc/{mutations => commands}/demo.clj | 8 +- backend/src/app/rpc/commands/ldap.clj | 75 +++ backend/src/app/rpc/mutations/ldap.clj | 141 ----- backend/src/app/rpc/mutations/profile.clj | 443 +++------------ backend/src/app/srepl/dev.clj | 2 +- backend/test/app/services_media_test.clj | 8 +- backend/test/app/services_profile_test.clj | 24 +- backend/test/app/test_helpers.clj | 41 +- docker/images/config.env | 1 - docker/images/files/nginx-entrypoint.sh | 65 --- frontend/src/app/config.cljs | 12 - frontend/src/app/main/data/exports.cljs | 8 +- frontend/src/app/main/data/users.cljs | 10 +- frontend/src/app/main/repo.cljs | 26 +- frontend/src/app/main/ui/auth/login.cljs | 53 +- frontend/src/app/main/ui/auth/register.cljs | 4 +- .../src/app/main/ui/settings/feedback.cljs | 2 +- frontend/translations/en.po | 4 + 30 files changed, 1306 insertions(+), 960 deletions(-) create mode 100644 backend/src/app/auth/ldap.clj rename backend/src/app/{http/oauth.clj => auth/oidc.clj} (52%) create mode 100644 backend/src/app/rpc/commands/auth.clj rename backend/src/app/rpc/{mutations => commands}/demo.clj (90%) create mode 100644 backend/src/app/rpc/commands/ldap.clj delete mode 100644 backend/src/app/rpc/mutations/ldap.clj diff --git a/CHANGES.md b/CHANGES.md index 0bfdb3e46d..a8a3b94737 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,26 @@ ## :rocket: Next +### :boom: Breaking changes & Deprecations + +- The `PENPOT_LOGIN_WITH_LDAP` environment variable is finally removed (after + many version with deprecation). It is replaced with the + `enable-login-with-ldap` flag. +- The `PENPOT_LDAP_ATTRS_PHOTO` finally removed, it was unused for many + versions. +- If you are using social login (google, github, gitlab or generic OIDC) you + will need to ensure to add the following flags respectivelly to let them + enabled: `enable-login-with-google`, `enable-login-with-github`, + `enable-login-with-gitlab` and `enable-login-with-oidc`. If not, they will + remain disabled after application start independently if you set the client-id + and client-sectet options. +- The `PENPOT_REGISTRATION_ENABLED` is finally removed in favour of + `-registration` flag. +- The OIDC providers are now initialized synchronously, and if you are using the + discovery mechanism of the generic OIDC integration, the start time of the + application will depend on how fast the OIDC provider responds to the + discovery http request. + ### :sparkles: New features - Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982) diff --git a/backend/src/app/auth/ldap.clj b/backend/src/app/auth/ldap.clj new file mode 100644 index 0000000000..f5042e6b1c --- /dev/null +++ b/backend/src/app/auth/ldap.clj @@ -0,0 +1,137 @@ +;; 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.auth.ldap + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.spec :as us] + [app.config :as cf] + [clj-ldap.client :as ldap] + [clojure.spec.alpha :as s] + [clojure.string] + [integrant.core :as ig])) + +(defn- prepare-params + [cfg] + {:ssl? (:ssl cfg) + :startTLS? (:tls cfg) + :bind-dn (:bind-dn cfg) + :password (:bind-password cfg) + :host {:address (:host cfg) + :port (:port cfg)}}) + +(defn- connect + "Connects to the LDAP provider and returns a connection. An + exception is raised if no connection is possible." + ^java.lang.AutoCloseable + [cfg] + (try + (-> cfg prepare-params ldap/connect) + (catch Throwable cause + (ex/raise :type :restriction + :code :unable-to-connect-to-ldap + :hint "unable to connect to ldap server" + :cause cause)))) + +(defn- replace-several [s & {:as replacements}] + (reduce-kv clojure.string/replace s replacements)) + +(defn- search-user + [{:keys [conn attrs base-dn] :as cfg} email] + (let [query (replace-several (:query cfg) ":username" email) + params {:filter query + :sizelimit 1 + :attributes attrs}] + (first (ldap/search conn base-dn params)))) + +(defn- retrieve-user + [{:keys [conn] :as cfg} {:keys [email password]}] + (when-let [{:keys [dn] :as user} (search-user cfg email)] + (when (ldap/bind? conn dn password) + {:fullname (get user (-> cfg :attrs-fullname keyword)) + :email email + :backend "ldap"}))) + +(s/def ::fullname ::us/not-empty-string) +(s/def ::email ::us/email) +(s/def ::backend ::us/not-empty-string) + +(s/def ::info-data + (s/keys :req-un [::fullname ::email ::backend])) + +(defn authenticate + [cfg params] + (with-open [conn (connect cfg)] + (when-let [user (-> (assoc cfg :conn conn) + (retrieve-user params))] + (when-not (s/valid? ::info-data user) + (let [explain (s/explain-str ::info-data user)] + (l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain)) + (ex/raise :type :restriction + :code :wrong-ldap-response + :explain explain))) + user))) + +(defn- try-connectivity + [cfg] + ;; If we have ldap parameters, try to establish connection + (when (and (:bind-dn cfg) + (:bind-password cfg) + (:host cfg) + (:port cfg)) + (try + (with-open [_ (connect cfg)] + (l/info :hint "provider initialized" + :provider "ldap" + :host (:host cfg) + :port (:port cfg) + :tls? (:tls cfg) + :ssl? (:ssl cfg) + :bind-dn (:bind-dn cfg) + :base-dn (:base-dn cfg) + :query (:query cfg)) + cfg) + (catch Throwable cause + (l/error :hint "unable to connect to LDAP server (LDAP auth provider disabled)" + :host (:host cfg) :port (:port cfg) :cause cause) + nil)))) + +(defn- prepare-attributes + [cfg] + (assoc cfg :attrs [(:attrs-username cfg) + (:attrs-email cfg) + (:attrs-fullname cfg)])) + +(defmethod ig/init-key ::provider + [_ cfg] + (when (:enabled? cfg) + (some-> cfg try-connectivity prepare-attributes))) + +(s/def ::enabled? ::us/boolean) +(s/def ::host ::cf/ldap-host) +(s/def ::port ::cf/ldap-port) +(s/def ::ssl ::cf/ldap-ssl) +(s/def ::tls ::cf/ldap-starttls) +(s/def ::query ::cf/ldap-user-query) +(s/def ::base-dn ::cf/ldap-base-dn) +(s/def ::bind-dn ::cf/ldap-bind-dn) +(s/def ::bind-password ::cf/ldap-bind-password) +(s/def ::attrs-email ::cf/ldap-attrs-email) +(s/def ::attrs-fullname ::cf/ldap-attrs-fullname) +(s/def ::attrs-username ::cf/ldap-attrs-username) + +(defmethod ig/pre-init-spec ::provider + [_] + (s/keys :opt-un [::host ::port + ::ssl ::tls + ::enabled? + ::bind-dn + ::bind-password + ::query + ::attrs-email + ::attrs-username + ::attrs-fullname])) diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/auth/oidc.clj similarity index 52% rename from backend/src/app/http/oauth.clj rename to backend/src/app/auth/oidc.clj index 869134f18c..39572bb18c 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/auth/oidc.clj @@ -4,19 +4,23 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.http.oauth +(ns app.auth.oidc + "OIDC client implementation." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] [app.common.uri :as u] [app.config :as cf] [app.db :as db] + [app.http.middleware :as hmw] [app.loggers.audit :as audit] [app.rpc.queries.profile :as profile] [app.util.json :as json] [app.util.time :as dt] + [app.worker :as wrk] [clojure.set :as set] [clojure.spec.alpha :as s] [cuerdas.core :as str] @@ -25,6 +29,218 @@ [promesa.exec :as px] [yetti.response :as yrs])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HELPERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- obfuscate-string + [s] + (if (< (count s) 10) + (apply str (take (count s) (repeat "*"))) + (str (subs s 0 5) + (apply str (take (- (count s) 5) (repeat "*")))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; OIDC PROVIDER (GENERIC) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- discover-oidc-config + [{:keys [http-client]} {:keys [base-uri] :as opts}] + (let [discovery-uri (u/join base-uri ".well-known/openid-configuration") + response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))] + (cond + (ex/exception? response) + (do + (l/warn :hint "unable to discover oidc configuration" + :discover-uri (str discovery-uri) + :cause response) + nil) + + (= 200 (:status response)) + (let [data (json/read (:body response))] + {:token-uri (get data :token_endpoint) + :auth-uri (get data :authorization_endpoint) + :user-uri (get data :userinfo_endpoint)}) + + :else + (do + (l/warn :hint "unable to discover OIDC configuration" + :uri (str discovery-uri) + :response-status-code (:status response)) + nil)))) + +(defn- prepare-oidc-opts + [cfg] + (let [opts {:base-uri (:base-uri cfg) + :client-id (:client-id cfg) + :client-secret (:client-secret cfg) + :token-uri (:token-uri cfg) + :auth-uri (:auth-uri cfg) + :user-uri (:user-uri cfg) + :scopes (:scopes cfg #{"openid" "profile" "email"}) + :roles-attr (:roles-attr cfg) + :roles (:roles cfg) + :name "oidc"} + + opts (d/without-nils opts)] + + (when (and (string? (:base-uri opts)) + (string? (:client-id opts)) + (string? (:client-secret opts))) + (if (and (string? (:token-uri opts)) + (string? (:user-uri opts)) + (string? (:auth-uri opts))) + opts + (some-> (discover-oidc-config cfg opts) + (merge opts {:discover? true})))))) + +(defmethod ig/prep-key ::generic-provider + [_ cfg] + (d/without-nils cfg)) + +(defmethod ig/init-key ::generic-provider + [_ cfg] + (when (:enabled? cfg) + (if-let [opts (prepare-oidc-opts cfg)] + (do + (l/info :hint "provider initialized" + :provider :oidc + :method (if (:discover? opts) "discover" "manual") + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts)) + :scopes (str/join "," (:scopes opts)) + :auth-uri (:auth-uri opts) + :user-uri (:user-uri opts) + :token-uri (:token-uri opts) + :roles-attr (:roles-attr opts) + :roles (:roles opts)) + opts) + (do + (l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc) + nil)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GOOGLE AUTH PROVIDER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/prep-key ::google-provider + [_ cfg] + (d/without-nils cfg)) + +(defmethod ig/init-key ::google-provider + [_ cfg] + (let [opts {:client-id (:client-id cfg) + :client-secret (:client-secret cfg) + :scopes #{"openid" "email" "profile"} + :auth-uri "https://accounts.google.com/o/oauth2/v2/auth" + :token-uri "https://oauth2.googleapis.com/token" + :user-uri "https://openidconnect.googleapis.com/v1/userinfo" + :name "google"}] + + (when (:enabled? cfg) + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :hint "provider initialized" + :provider :google + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) + opts) + + (do + (l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google) + nil))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GITHUB AUTH PROVIDER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- retrieve-github-email + [{:keys [http-client]} tdata info] + (or (some-> info :email p/resolved) + (-> (http-client {:uri "https://api.github.com/user/emails" + :headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))} + :timeout 6000 + :method :get}) + (p/then (fn [{:keys [status body] :as response}] + (when-not (s/int-in-range? 200 300 status) + (ex/raise :type :internal + :code :unable-to-retrieve-github-emails + :hint "unable to retrieve github emails" + :http-status status + :http-body body)) + (->> response :body json/read (filter :primary) first :email)))))) + +(defmethod ig/prep-key ::github-provider + [_ cfg] + (d/without-nils cfg)) + +(defmethod ig/init-key ::github-provider + [_ cfg] + (let [opts {:client-id (:client-id cfg) + :client-secret (:client-secret cfg) + :scopes #{"read:user" "user:email"} + :auth-uri "https://github.com/login/oauth/authorize" + :token-uri "https://github.com/login/oauth/access_token" + :user-uri "https://api.github.com/user" + :name "github" + + ;; Additional hooks for provider specific way of + ;; retrieve emails. + :get-email-fn (partial retrieve-github-email cfg)}] + + (when (:enabled? cfg) + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :hint "provider initialized" + :provider :github + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) + opts) + + (do + (l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github) + nil))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GITLAB AUTH PROVIDER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/prep-key ::gitlab-provider + [_ cfg] + (d/without-nils cfg)) + +(defmethod ig/init-key ::gitlab-provider + [_ cfg] + (let [base (:base-uri cfg "https://gitlab.com") + opts {:base-uri base + :client-id (:client-id cfg) + :client-secret (:client-secret cfg) + :scopes #{"openid" "profile" "email"} + :auth-uri (str base "/oauth/authorize") + :token-uri (str base "/oauth/token") + :user-uri (str base "/oauth/userinfo") + :name "gitlab"}] + (when (:enabled? cfg) + (if (and (string? (:client-id opts)) + (string? (:client-secret opts))) + (do + (l/info :hint "provider initialized" + :provider :gitlab + :base-uri base + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) + opts) + + (do + (l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab) + nil))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HANDLERS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn- build-redirect-uri [{:keys [provider] :as cfg}] (let [public (u/uri (:public-uri cfg))] @@ -81,47 +297,35 @@ :timeout 6000 :method :get})) - (retrieve-emails [] - (if (some? (:emails-uri provider)) - (http-client {:uri (:emails-uri provider) - :headers {"Authorization" (str (:type tdata) " " (:token tdata))} - :timeout 6000 - :method :get}) - (p/resolved {:status 200}))) - - (validate-response [[retrieve-res emails-res]] - (when-not (s/int-in-range? 200 300 (:status retrieve-res)) + (validate-response [response] + (when-not (s/int-in-range? 200 300 (:status response)) (ex/raise :type :internal :code :unable-to-retrieve-user-info :hint "unable to retrieve user info" - :http-status (:status retrieve-res) - :http-body (:body retrieve-res))) - (when-not (s/int-in-range? 200 300 (:status emails-res)) - (ex/raise :type :internal - :code :unable-to-retrieve-user-info - :hint "unable to retrieve user info" - :http-status (:status emails-res) - :http-body (:body emails-res))) - [retrieve-res emails-res]) + :http-status (:status response) + :http-body (:body response))) + response) (get-email [info] - (let [attr-kw (cf/get :oidc-email-attr :email)] - (get info attr-kw))) + ;; Allow providers hook into this for custom email + ;; retrieval method. + (if-let [get-email-fn (:get-email-fn provider)] + (get-email-fn tdata info) + (let [attr-kw (cf/get :oidc-email-attr :email)] + (get info attr-kw)))) (get-name [info] (let [attr-kw (cf/get :oidc-name-attr :name)] (get info attr-kw))) - (process-response [[retrieve-res emails-res]] - (let [info (json/read (:body retrieve-res)) - email (if (some? (:extract-email-callback provider)) - ((:extract-email-callback provider) emails-res) - (get-email info))] + (process-response [response] + (p/let [info (-> response :body json/read) + email (get-email info)] {:backend (:name provider) :email email :fullname (or (get-name info) email) - :props (->> (dissoc info :name :email) - (qualify-props provider))})) + :props (->> (dissoc info :name :email) + (qualify-props provider))})) (validate-info [info] (when-not (s/valid? ::info info) @@ -133,10 +337,10 @@ :info info)) info)] - (-> (p/all [(retrieve) (retrieve-emails)]) - (p/then' validate-response) - (p/then' process-response) - (p/then' validate-info)))) + (-> (retrieve) + (p/then validate-response) + (p/then process-response) + (p/then validate-info)))) (s/def ::backend ::us/not-empty-string) (s/def ::email ::us/not-empty-string) @@ -195,8 +399,6 @@ (p/then' validate-oidc) (p/then' (partial post-process state)))))) -;; --- HTTP HANDLERS - (defn- retrieve-profile [{:keys [pool executor] :as cfg} info] (px/with-dispatch executor @@ -256,21 +458,18 @@ (redirect-response uri)))) (defn- auth-handler - [{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise] - (try - (let [props (audit/extract-utm-params params) - state (tokens :generate - {:iss :oauth - :invitation-token (:invitation-token params) - :props props - :exp (dt/in-future "15m")}) - uri (build-auth-uri cfg state)] - (respond (yrs/response 200 {:redirect-uri uri}))) - (catch Throwable cause - (raise cause)))) + [{:keys [tokens] :as cfg} {:keys [params] :as request}] + (let [props (audit/extract-utm-params params) + state (tokens :generate + {:iss :oauth + :invitation-token (:invitation-token params) + :props props + :exp (dt/in-future "15m")}) + uri (build-auth-uri cfg state)] + (yrs/response 200 {:redirect-uri uri}))) (defn- callback-handler - [cfg request respond _] + [cfg request] (letfn [(process-request [] (p/let [info (retrieve-info cfg request) profile (retrieve-profile cfg info)] @@ -278,182 +477,62 @@ (handle-error [cause] (l/error :hint "error on oauth process" :cause cause) - (respond (generate-error-redirect cfg cause)))] + (generate-error-redirect cfg cause))] (-> (process-request) - (p/then respond) (p/catch handle-error)))) -;; --- INIT - -(declare initialize) +(def provider-lookup + {:compile + (fn [& _] + (fn [handler] + (fn [{:keys [providers] :as cfg} request] + (let [provider (some-> request :path-params :provider keyword)] + (if-let [provider (get providers provider)] + (handler (assoc cfg :provider provider) request) + (ex/raise :type :restriction + :code :provider-not-configured + :provider provider + :hint "provider not configured"))))))}) (s/def ::public-uri ::us/not-empty-string) +(s/def ::http-client fn?) (s/def ::session map?) (s/def ::tokens fn?) -(s/def ::rpc map?) +(s/def ::providers map?) -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool])) +(defmethod ig/pre-init-spec ::routes + [_] + (s/keys :req-un [::public-uri + ::session + ::tokens + ::http-client + ::providers + ::db/pool + ::wrk/executor])) -(defn wrap-handler - [cfg handler] - (fn [request respond raise] - (let [provider (get-in request [:path-params :provider]) - provider (get-in @cfg [:providers provider])] - (if provider - (handler (assoc @cfg :provider provider) - request - respond - raise) - (raise - (ex/error - :type :not-found - :provider provider - :hint "provider not configured")))))) +(defmethod ig/init-key ::routes + [_ {:keys [executor session] :as cfg}] + (let [cfg (update cfg :provider d/without-nils)] + ["" {:middleware [[(:middleware session)] + [hmw/with-promise-async executor] + [hmw/with-config cfg] + [provider-lookup] + ]} + ;; We maintain the both URI prefixes for backward compatibility. -(defmethod ig/init-key ::handler - [_ cfg] - (let [cfg (initialize cfg)] - {:handler (wrap-handler cfg auth-handler) - :callback-handler (wrap-handler cfg callback-handler)})) + ["/auth/oauth" + ["/:provider" + {:handler auth-handler + :allowed-methods #{:post}}] + ["/:provider/callback" + {:handler callback-handler + :allowed-methods #{:get}}]] -(defn- discover-oidc-config - [{:keys [http-client]} {:keys [base-uri] :as opts}] - - (let [discovery-uri (u/join base-uri ".well-known/openid-configuration") - response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))] - (cond - (ex/exception? response) - (do - (l/warn :hint "unable to discover oidc configuration" - :discover-uri (str discovery-uri) - :cause response) - nil) - - (= 200 (:status response)) - (let [data (json/read (:body response))] - {:token-uri (get data :token_endpoint) - :auth-uri (get data :authorization_endpoint) - :user-uri (get data :userinfo_endpoint)}) - - :else - (do - (l/warn :hint "unable to discover OIDC configuration" - :uri (str discovery-uri) - :response-status-code (:status response)) - nil)))) - -(defn- obfuscate-string - [s] - (if (< (count s) 10) - (apply str (take (count s) (repeat "*"))) - (str (subs s 0 5) - (apply str (take (- (count s) 5) (repeat "*")))))) - -(defn- initialize-oidc-provider - [cfg] - (let [opts {:base-uri (cf/get :oidc-base-uri) - :client-id (cf/get :oidc-client-id) - :client-secret (cf/get :oidc-client-secret) - :token-uri (cf/get :oidc-token-uri) - :auth-uri (cf/get :oidc-auth-uri) - :user-uri (cf/get :oidc-user-uri) - :scopes (cf/get :oidc-scopes #{"openid" "profile" "email"}) - :roles-attr (cf/get :oidc-roles-attr) - :roles (cf/get :oidc-roles) - :name "oidc"}] - - (if (and (string? (:base-uri opts)) - (string? (:client-id opts)) - (string? (:client-secret opts))) - (do - (l/debug :hint "initialize oidc provider" :name "generic-oidc" - :opts (update opts :client-secret obfuscate-string)) - (if (and (string? (:token-uri opts)) - (string? (:user-uri opts)) - (string? (:auth-uri opts))) - (do - (l/debug :hint "initialized with user provided configuration") - (assoc-in cfg [:providers "oidc"] opts)) - (do - (l/debug :hint "trying to discover oidc provider configuration using BASE_URI") - (if-let [opts' (discover-oidc-config cfg opts)] - (do - (l/debug :hint "discovered opts" :additional-opts opts') - (assoc-in cfg [:providers "oidc"] (merge opts opts'))) - - cfg)))) - cfg))) - -(defn- initialize-google-provider - [cfg] - (let [opts {:client-id (cf/get :google-client-id) - :client-secret (cf/get :google-client-secret) - :scopes #{"openid" "email" "profile"} - :auth-uri "https://accounts.google.com/o/oauth2/v2/auth" - :token-uri "https://oauth2.googleapis.com/token" - :user-uri "https://openidconnect.googleapis.com/v1/userinfo" - :name "google"}] - (if (and (string? (:client-id opts)) - (string? (:client-secret opts))) - (do - (l/info :action "initialize" :provider "google" - :opts (pr-str (update opts :client-secret obfuscate-string))) - (assoc-in cfg [:providers "google"] opts)) - cfg))) - -(defn extract-github-email - [response] - (let [emails (json/read (:body response)) - primary-email (->> emails - (filter #(:primary %)) - first)] - (:email primary-email))) - -(defn- initialize-github-provider - [cfg] - (let [opts {:client-id (cf/get :github-client-id) - :client-secret (cf/get :github-client-secret) - :scopes #{"read:user" "user:email"} - :auth-uri "https://github.com/login/oauth/authorize" - :token-uri "https://github.com/login/oauth/access_token" - :emails-uri "https://api.github.com/user/emails" - :extract-email-callback extract-github-email - :user-uri "https://api.github.com/user" - :name "github"}] - (if (and (string? (:client-id opts)) - (string? (:client-secret opts))) - (do - (l/info :action "initialize" :provider "github" - :opts (pr-str (update opts :client-secret obfuscate-string))) - (assoc-in cfg [:providers "github"] opts)) - cfg))) - -(defn- initialize-gitlab-provider - [cfg] - (let [base (cf/get :gitlab-base-uri "https://gitlab.com") - opts {:base-uri base - :client-id (cf/get :gitlab-client-id) - :client-secret (cf/get :gitlab-client-secret) - :scopes #{"openid" "profile" "email"} - :auth-uri (str base "/oauth/authorize") - :token-uri (str base "/oauth/token") - :user-uri (str base "/oauth/userinfo") - :name "gitlab"}] - (if (and (string? (:client-id opts)) - (string? (:client-secret opts))) - (do - (l/info :action "initialize" :provider "gitlab" - :opts (pr-str (update opts :client-secret obfuscate-string))) - (assoc-in cfg [:providers "gitlab"] opts)) - cfg))) - -(defn- initialize - [cfg] - (let [cfg (agent cfg :error-mode :continue)] - (send-off cfg initialize-google-provider) - (send-off cfg initialize-gitlab-provider) - (send-off cfg initialize-github-provider) - (send-off cfg initialize-oidc-provider) - cfg)) + ["/auth/oidc" + ["/:provider" + {:handler auth-handler + :allowed-methods #{:post}}] + ["/:provider/callback" + {:handler callback-handler + :allowed-methods #{:get}}]]])) diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index dad68fc6b5..ba0abae85d 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -10,6 +10,7 @@ [app.common.logging :as l] [app.db :as db] [app.main :as main] + [app.rpc.commands.auth :as cmd.auth] [app.rpc.mutations.profile :as profile] [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] [clojure.string :as str] @@ -54,13 +55,13 @@ :type :password}))] (try (db/with-atomic [conn (:app.db/pool system)] - (->> (profile/create-profile conn + (->> (cmd.auth/create-profile conn {:fullname fullname :email email :password password :is-active true :is-demo false}) - (profile/create-profile-relations conn))) + (cmd.auth/create-profile-relations conn))) (when (pos? (:verbosity options)) (println "User created successfully.")) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 6acb96cf49..79972b2c98 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -79,7 +79,6 @@ :ldap-attrs-username "uid" :ldap-attrs-email "mail" :ldap-attrs-fullname "cn" - :ldap-attrs-photo "jpegPhoto" ;; a server prop key where initial project is stored. :initial-project-skey "initial-project"}) @@ -149,7 +148,6 @@ (s/def ::initial-project-skey ::us/string) (s/def ::ldap-attrs-email ::us/string) (s/def ::ldap-attrs-fullname ::us/string) -(s/def ::ldap-attrs-photo ::us/string) (s/def ::ldap-attrs-username ::us/string) (s/def ::ldap-base-dn ::us/string) (s/def ::ldap-bind-dn ::us/string) @@ -256,7 +254,6 @@ ::initial-project-skey ::ldap-attrs-email ::ldap-attrs-fullname - ::ldap-attrs-photo ::ldap-attrs-username ::ldap-base-dn ::ldap-bind-dn diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 56984d8c55..7bef64e789 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.logging :as l] [app.common.transit :as t] - [app.http.doc :as doc] [app.http.errors :as errors] [app.http.middleware :as middleware] [app.metrics :as mtx] @@ -67,8 +66,10 @@ :xnio/worker-threads (:worker-threads cfg) :xnio/dispatch (:executor cfg) :ring/async true} + handler (if (some? router) (wrap-router router) + handler) server (yt/server handler (d/without-nils options))] (assoc cfg :server (yt/start! server)))) @@ -113,7 +114,6 @@ ;; HTTP ROUTER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::rpc map?) (s/def ::oauth map?) (s/def ::storage map?) (s/def ::assets map?) @@ -122,15 +122,27 @@ (s/def ::audit-handler fn?) (s/def ::awsns-handler fn?) (s/def ::session map?) -(s/def ::debug-routes vector?) +(s/def ::rpc-routes (s/nilable vector?)) +(s/def ::debug-routes (s/nilable vector?)) +(s/def ::oidc-routes (s/nilable vector?)) +(s/def ::doc-routes (s/nilable vector?)) (defmethod ig/pre-init-spec ::router [_] - (s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets - ::session ::feedback ::awsns-handler ::debug-routes - ::audit-handler])) + (s/keys :req-un [::mtx/metrics + ::ws + ::storage + ::assets + ::session + ::feedback + ::awsns-handler + ::debug-routes + ::oidc-routes + ::audit-handler + ::rpc-routes + ::doc-routes])) (defmethod ig/init-key ::router - [_ {:keys [ws session rpc oauth metrics assets feedback debug-routes] :as cfg}] + [_ {:keys [ws session metrics assets feedback] :as cfg}] (rr/router [["" {:middleware [[middleware/server-timing] [middleware/format-response] @@ -145,7 +157,7 @@ ["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}] ["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]] - debug-routes + (:debug-routes cfg) ["/webhooks" ["/sns" {:handler (:awsns-handler cfg) @@ -156,22 +168,12 @@ :allowed-methods #{:get}}] ["/api" {:middleware [[middleware/cors] - (:middleware session)]} - ["/_doc" {:handler (doc/handler rpc) - :allowed-methods #{:get}}] - ["/feedback" {:handler feedback - :allowed-methods #{:post}}] - - ["/auth/oauth/:provider" {:handler (:handler oauth) - :allowed-methods #{:post}}] - ["/auth/oauth/:provider/callback" {:handler (:callback-handler oauth) - :allowed-methods #{:get}}] - + [(:middleware session)]]} ["/audit/events" {:handler (:audit-handler cfg) :allowed-methods #{:post}}] + ["/feedback" {:handler feedback + :allowed-methods #{:post}}] + (:doc-routes cfg) + (:oidc-routes cfg) + (:rpc-routes cfg)]]])) - ["/rpc" - ["/command/:command" {:handler (:command-handler rpc)}] - ["/query/:type" {:handler (:query-handler rpc)}] - ["/mutation/:type" {:handler (:mutation-handler rpc) - :allowed-methods #{:post}}]]]]])) diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj index d079d89303..5addbb9a65 100644 --- a/backend/src/app/http/doc.clj +++ b/backend/src/app/http/doc.clj @@ -9,14 +9,16 @@ (:require [app.common.data :as d] [app.config :as cf] + [app.rpc :as-alias rpc] [app.util.services :as sv] [app.util.template :as tmpl] [clojure.java.io :as io] [clojure.spec.alpha :as s] + [integrant.core :as ig] [pretty-spec.core :as ps] [yetti.response :as yrs])) -(defn get-spec-str +(defn- get-spec-str [k] (with-out-str (ps/pprint (s/form k) @@ -24,8 +26,8 @@ "clojure.core.specs.alpha" "score" "clojure.core" nil}}))) -(defn prepare-context - [rpc] +(defn- prepare-context + [methods] (letfn [(gen-doc [type [name f]] (let [mdata (meta f)] ;; (prn name mdata) @@ -38,22 +40,32 @@ {:command-methods (into [] (map (partial gen-doc :command)) - (->> rpc :methods :command (sort-by first))) + (->> methods :commands (sort-by first))) :query-methods (into [] (map (partial gen-doc :query)) - (->> rpc :methods :query (sort-by first))) + (->> methods :queries (sort-by first))) :mutation-methods (into [] (map (partial gen-doc :mutation)) - (->> rpc :methods :mutation (sort-by first)))})) + (->> methods :mutations (sort-by first)))})) -(defn handler - [rpc] - (let [context (prepare-context rpc)] - (if (contains? cf/flags :backend-api-doc) +(defn- handler + [methods] + (if (contains? cf/flags :backend-api-doc) + (let [context (prepare-context methods)] (fn [_ respond _] (respond (yrs/response 200 (-> (io/resource "api-doc.tmpl") - (tmpl/render context))))) - (fn [_ respond _] - (respond (yrs/response 404)))))) + (tmpl/render context)))))) + (fn [_ respond _] + (respond (yrs/response 404))))) + + +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req-un [::rpc/methods])) + +(defmethod ig/init-key ::routes + [_ {:keys [methods] :as cfg}] + ["/_doc" {:handler (handler methods) + :allowed-methods #{:get}}]) + diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index f7533306ec..5118dc5e96 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -143,13 +143,11 @@ (defn handle [cause request] - (cond (or (instance? java.util.concurrent.CompletionException cause) (instance? java.util.concurrent.ExecutionException cause)) (handle-exception (.getCause ^Throwable cause) request) - (ex/wrapped? cause) (let [context (meta cause) cause (deref cause)] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index f0be700a85..a6a6057e6f 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -201,6 +201,7 @@ (fn [handler executor] (fn [request respond raise] (-> (px/submit! executor #(handler request)) + (p/bind p/wrap) (p/then respond) (p/catch raise)))))}) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 529bfc6606..4041340df2 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -6,6 +6,7 @@ (ns app.main (:require + [app.auth.oidc] [app.common.logging :as l] [app.config :as cf] [app.util.time :as dt] @@ -90,6 +91,9 @@ :app.http/session {:store (ig/ref :app.http.session/store)} + :app.http.doc/routes + {:methods (ig/ref :app.rpc/methods)} + :app.http.session/store {:pool (ig/ref :app.db/pool) :tokens (ig/ref :app.tokens/tokens) @@ -123,20 +127,81 @@ :max-body-size (cf/get :http-server-max-body-size) :max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} + :app.auth.ldap/provider + {:host (cf/get :ldap-host) + :port (cf/get :ldap-port) + :ssl (cf/get :ldap-ssl) + :tls (cf/get :ldap-starttls) + :query (cf/get :ldap-user-query) + :attrs-email (cf/get :ldap-attrs-email) + :attrs-fullname (cf/get :ldap-attrs-fullname) + :attrs-username (cf/get :ldap-attrs-username) + :base-dn (cf/get :ldap-base-dn) + :bind-dn (cf/get :ldap-bind-dn) + :bind-password (cf/get :ldap-bind-password) + :enabled? (contains? cf/flags :login-with-ldap)} + + :app.auth.oidc/google-provider + {:enabled? (contains? cf/flags :login-with-google) + :client-id (cf/get :google-client-id) + :client-secret (cf/get :google-client-secret)} + + :app.auth.oidc/github-provider + {:enabled? (contains? cf/flags :login-with-github) + :http-client (ig/ref :app.http/client) + :client-id (cf/get :github-client-id) + :client-secret (cf/get :github-client-secret)} + + :app.auth.oidc/gitlab-provider + {:enabled? (contains? cf/flags :login-with-gitlab) + :base-uri (cf/get :gitlab-base-uri "https://gitlab.com") + :client-id (cf/get :gitlab-client-id) + :client-secret (cf/get :gitlab-client-secret)} + + :app.auth.oidc/generic-provider + {:enabled? (contains? cf/flags :login-with-oidc) + :http-client (ig/ref :app.http/client) + + :client-id (cf/get :oidc-client-id) + :client-secret (cf/get :oidc-client-secret) + + :base-uri (cf/get :oidc-base-uri) + + :token-uri (cf/get :oidc-token-uri) + :auth-uri (cf/get :oidc-auth-uri) + :user-uri (cf/get :oidc-user-uri) + + :scopes (cf/get :oidc-scopes) + :roles-attr (cf/get :oidc-roles-attr) + :roles (cf/get :oidc-roles)} + + :app.auth.oidc/routes + {:providers {:google (ig/ref :app.auth.oidc/google-provider) + :github (ig/ref :app.auth.oidc/github-provider) + :gitlab (ig/ref :app.auth.oidc/gitlab-provider) + :oidc (ig/ref :app.auth.oidc/generic-provider)} + :tokens (ig/ref :app.tokens/tokens) + :http-client (ig/ref :app.http/client) + :pool (ig/ref :app.db/pool) + :session (ig/ref :app.http/session) + :public-uri (cf/get :public-uri) + :executor (ig/ref [::default :app.worker/executor])} + :app.http/router {:assets (ig/ref :app.http.assets/handlers) :feedback (ig/ref :app.http.feedback/handler) :session (ig/ref :app.http/session) :awsns-handler (ig/ref :app.http.awsns/handler) - :oauth (ig/ref :app.http.oauth/handler) :debug-routes (ig/ref :app.http.debug/routes) + :oidc-routes (ig/ref :app.auth.oidc/routes) :ws (ig/ref :app.http.websocket/handler) :metrics (ig/ref :app.metrics/metrics) :public-uri (cf/get :public-uri) :storage (ig/ref :app.storage/storage) :tokens (ig/ref :app.tokens/tokens) :audit-handler (ig/ref :app.loggers.audit/http-handler) - :rpc (ig/ref :app.rpc/rpc) + :rpc-routes (ig/ref :app.rpc/routes) + :doc-routes (ig/ref :app.http.doc/routes) :executor (ig/ref [::default :app.worker/executor])} :app.http.debug/routes @@ -162,17 +227,7 @@ {:pool (ig/ref :app.db/pool) :executor (ig/ref [::default :app.worker/executor])} - :app.http.oauth/handler - {:rpc (ig/ref :app.rpc/rpc) - :session (ig/ref :app.http/session) - :pool (ig/ref :app.db/pool) - :tokens (ig/ref :app.tokens/tokens) - :audit (ig/ref :app.loggers.audit/collector) - :executor (ig/ref [::default :app.worker/executor]) - :http-client (ig/ref :app.http/client) - :public-uri (cf/get :public-uri)} - - :app.rpc/rpc + :app.rpc/methods {:pool (ig/ref :app.db/pool) :session (ig/ref :app.http/session) :tokens (ig/ref :app.tokens/tokens) @@ -181,9 +236,13 @@ :msgbus (ig/ref :app.msgbus/msgbus) :public-uri (cf/get :public-uri) :audit (ig/ref :app.loggers.audit/collector) + :ldap (ig/ref :app.auth.ldap/provider) :http-client (ig/ref :app.http/client) :executors (ig/ref :app.worker/executors)} + :app.rpc/routes + {:methods (ig/ref :app.rpc/methods)} + :app.worker/worker {:executor (ig/ref [::worker :app.worker/executor]) :tasks (ig/ref :app.worker/registry) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index a1c758f695..bfe570d7ee 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -223,15 +223,13 @@ (defn- resolve-mutation-methods [cfg] (let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)] - (->> (sv/scan-ns 'app.rpc.mutations.demo - 'app.rpc.mutations.media + (->> (sv/scan-ns 'app.rpc.mutations.media 'app.rpc.mutations.profile 'app.rpc.mutations.files 'app.rpc.mutations.comments 'app.rpc.mutations.projects 'app.rpc.mutations.teams 'app.rpc.mutations.management - 'app.rpc.mutations.ldap 'app.rpc.mutations.fonts 'app.rpc.mutations.share-link 'app.rpc.mutations.verify-token) @@ -241,26 +239,65 @@ (defn- resolve-command-methods [cfg] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] - (->> (sv/scan-ns 'app.rpc.commands.binfile) + (->> (sv/scan-ns 'app.rpc.commands.binfile + 'app.rpc.commands.auth + 'app.rpc.commands.ldap + 'app.rpc.commands.demo) (map (partial process-method cfg)) (into {})))) -(s/def ::storage some?) -(s/def ::session map?) -(s/def ::tokens fn?) (s/def ::audit (s/nilable fn?)) (s/def ::executors (s/map-of keyword? ::wrk/executor)) +(s/def ::executors map?) +(s/def ::http-client fn?) +(s/def ::ldap (s/nilable map?)) +(s/def ::msgbus fn?) +(s/def ::public-uri ::us/not-empty-string) +(s/def ::session map?) +(s/def ::storage some?) +(s/def ::tokens fn?) -(defmethod ig/pre-init-spec ::rpc [_] - (s/keys :req-un [::storage ::session ::tokens ::audit - ::executors ::mtx/metrics ::db/pool])) +(defmethod ig/pre-init-spec ::methods [_] + (s/keys :req-un [::storage + ::session + ::tokens + ::audit + ::executors + ::public-uri + ::msgbus + ::http-client + ::mtx/metrics + ::db/pool + ::ldap])) -(defmethod ig/init-key ::rpc +(defmethod ig/init-key ::methods [_ cfg] - (let [mq (resolve-query-methods cfg) - mm (resolve-mutation-methods cfg) - cm (resolve-command-methods cfg)] - {:methods {:query mq :mutation mm :command cm} - :command-handler (partial rpc-command-handler cm) - :query-handler (partial rpc-query-handler mq) - :mutation-handler (partial rpc-mutation-handler mm)})) + {:mutations (resolve-mutation-methods cfg) + :queries (resolve-query-methods cfg) + :commands (resolve-command-methods cfg)}) + +(s/def ::mutations + (s/map-of keyword? fn?)) + +(s/def ::queries + (s/map-of keyword? fn?)) + +(s/def ::commands + (s/map-of keyword? fn?)) + +(s/def ::methods + (s/keys :req-un [::mutations + ::queries + ::commands])) + +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req-un [::methods])) + +(defmethod ig/init-key ::routes + [_ {:keys [methods] :as cfg}] + [["/rpc" + ["/command/:command" {:handler (partial rpc-command-handler (:commands methods))}] + ["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}] + ["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods)) + :allowed-methods #{:post}}]]]) + diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj new file mode 100644 index 0000000000..8d1a3bb18d --- /dev/null +++ b/backend/src/app/rpc/commands/auth.clj @@ -0,0 +1,416 @@ +;; 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.rpc.commands.auth + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.emails :as eml] + [app.loggers.audit :as audit] + [app.rpc.mutations.teams :as teams] + [app.rpc.queries.profile :as profile] + [app.rpc.rlimit :as rlimit] + [app.util.services :as sv] + [app.util.time :as dt] + [buddy.hashers :as hashers] + [clojure.spec.alpha :as s] + [cuerdas.core :as str])) + +(s/def ::email ::us/email) +(s/def ::fullname ::us/not-empty-string) +(s/def ::lang ::us/string) +(s/def ::path ::us/string) +(s/def ::profile-id ::us/uuid) +(s/def ::password ::us/not-empty-string) +(s/def ::old-password ::us/not-empty-string) +(s/def ::theme ::us/string) +(s/def ::invitation-token ::us/not-empty-string) +(s/def ::token ::us/not-empty-string) + +;; ---- HELPERS + +(defn derive-password + [password] + (hashers/derive password + {:alg :argon2id + :memory 16384 + :iterations 20 + :parallelism 2})) + +(defn verify-password + [attempt password] + (try + (hashers/verify attempt password) + (catch Exception _e + {:update false + :valid false}))) + +(defn email-domain-in-whitelist? + "Returns true if email's domain is in the given whitelist or if + given whitelist is an empty string." + [domains email] + (if (or (empty? domains) + (nil? domains)) + true + (let [[_ candidate] (-> (str/lower email) + (str/split #"@" 2))] + (contains? domains candidate)))) + +(def ^:private sql:profile-existence + "select exists (select * from profile + where email = ? + and deleted_at is null) as val") + +(defn check-profile-existence! + [conn {:keys [email] :as params}] + (let [email (str/lower email) + result (db/exec-one! conn [sql:profile-existence email])] + (when (:val result) + (ex/raise :type :validation + :code :email-already-exists)) + params)) + +;; ---- COMMAND: login with password + +(defn login-with-password + [{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}] + + (when-not (contains? cf/flags :login) + (ex/raise :type :restriction + :code :login-disabled + :hint "login is disabled in this instance")) + + (letfn [(check-password [profile password] + (when (= (:password profile) "!") + (ex/raise :type :validation + :code :account-without-password + :hint "the current account does not have password")) + (:valid (verify-password password (:password profile)))) + + (validate-profile [profile] + (when-not (:is-active profile) + (ex/raise :type :validation + :code :wrong-credentials)) + (when-not profile + (ex/raise :type :validation + :code :wrong-credentials)) + (when-not (check-password profile password) + (ex/raise :type :validation + :code :wrong-credentials)) + profile)] + + (db/with-atomic [conn pool] + (let [profile (->> (profile/retrieve-profile-data-by-email conn email) + (validate-profile) + (profile/strip-private-attrs) + (profile/populate-additional-data conn) + (profile/decode-profile-row)) + + invitation (when-let [token (:invitation-token params)] + (tokens :verify {:token token :iss :team-invitation})) + + ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the + ;; invitation because invitations matches exactly; and user can't loging with other email and + ;; accept invitation with other email + response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) + {:invitation-token (:invitation-token params)} + profile)] + + (with-meta response + {:transform-response ((:create session) (:id profile)) + ::audit/props (audit/profile->props profile) + ::audit/profile-id (:id profile)}))))) + +(s/def ::login-with-password + (s/keys :req-un [::email ::password] + :opt-un [::invitation-token])) + +(sv/defmethod ::login-with-password + "Performs authentication using penpot password." + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [cfg params] + (login-with-password cfg params)) + +;; ---- COMMAND: Logout + +(s/def ::logout + (s/keys :opt-un [::profile-id])) + +(sv/defmethod ::logout + "Clears the authentication cookie and logout the current session." + {:auth false} + [{:keys [session] :as cfg} _] + (with-meta {} + {:transform-response (:delete session)})) + +;; ---- COMMAND: Recover Profile + +(defn recover-profile + [{:keys [pool tokens] :as cfg} {:keys [token password]}] + (letfn [(validate-token [token] + (let [tdata (tokens :verify {:token token :iss :password-recovery})] + (:profile-id tdata))) + + (update-password [conn profile-id] + (let [pwd (derive-password password)] + (db/update! conn :profile {:password pwd} {:id profile-id})))] + + (db/with-atomic [conn pool] + (->> (validate-token token) + (update-password conn)) + nil))) + +(s/def ::token ::us/not-empty-string) +(s/def ::recover-profile + (s/keys :req-un [::token ::password])) + +(sv/defmethod ::recover-profile + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [cfg params] + (recover-profile cfg params)) + +;; ---- COMMAND: Prepare Register + +(defn prepare-register + [{:keys [pool tokens] :as cfg} params] + (when-not (contains? cf/flags :registration) + (if-not (contains? params :invitation-token) + (ex/raise :type :restriction + :code :registration-disabled) + (let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})] + (when-not (= (:email params) (:member-email invitation)) + (ex/raise :type :restriction + :code :email-does-not-match-invitation + :hint "email should match the invitation"))))) + + (when-let [domains (cf/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 spammer. + (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) + + (when (= (str/lower (:email params)) + (str/lower (:password params))) + (ex/raise :type :validation + :code :email-as-password + :hint "you can't use your email as password")) + + (let [params {:email (:email params) + :password (:password params) + :invitation-token (:invitation-token params) + :backend "penpot" + :iss :prepared-register + :exp (dt/in-future "48h")} + + token (tokens :generate params)] + (with-meta {:token token} + {::audit/profile-id uuid/zero}))) + +(s/def ::prepare-register-profile + (s/keys :req-un [::email ::password] + :opt-un [::invitation-token])) + +(sv/defmethod ::prepare-register-profile {:auth false} + [cfg params] + (prepare-register cfg params)) + +;; ---- COMMAND: Register Profile + +(defn create-profile + "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 (-> (audit/extract-utm-params params) + (merge (:props params)) + (db/tjson)) + + password (if-let [password (:password params)] + (derive-password password) + "!") + + locale (:locale params) + locale (when (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 false) + email (str/lower (:email params)) + + params {:id id + :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) + (profile/decode-profile-row)) + (catch org.postgresql.util.PSQLException e + (let [state (.getSQLState e)] + (if (not= state "23505") + (throw e) + (ex/raise :type :validation + :code :email-already-exists + :cause e))))))) + +(defn create-profile-relations + [conn profile] + (let [team (teams/create-team conn {:profile-id (:id profile) + :name "Default" + :is-default true})] + (-> profile + (profile/strip-private-attrs) + (assoc :default-team-id (:id team)) + (assoc :default-project-id (:default-project-id team))))) + +(defn register-profile + [{:keys [conn tokens session] :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 [is-active (or (:is-active params) + (contains? cf/flags :insecure-register)) + profile (->> (assoc params :is-active is-active) + (create-profile conn) + (create-profile-relations conn) + (profile/decode-profile-row)) + invitation (when-let [token (:invitation-token params)] + (tokens :verify {:token token :iss :team-invitation}))] + (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). This happens only if the invitation email matches with the register email. + (and (some? invitation) (= (:email profile) (:member-email invitation))) + (let [claims (assoc invitation :member-id (:id profile)) + token (tokens :generate claims) + resp {:invitation-token token}] + (with-meta resp + {:transform-response ((:create session) (:id profile)) + ::audit/replace-props (audit/profile->props profile) + ::audit/profile-id (:id profile)})) + + ;; If auth backend is different from "penpot" means user is + ;; registering 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)) + ::audit/replace-props (audit/profile->props profile) + ::audit/profile-id (:id profile)}) + + ;; If the `:enable-insecure-register` flag is set, we proceed + ;; to sign in the user directly, without email verification. + (true? is-active) + (with-meta (profile/strip-private-attrs profile) + {:transform-response ((:create session) (:id profile)) + ::audit/replace-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 + {::audit/replace-props (audit/profile->props profile) + ::audit/profile-id (:id profile)})))))) + +(s/def ::register-profile + (s/keys :req-un [::token ::fullname])) + +(sv/defmethod ::register-profile + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [{:keys [pool] :as cfg} params] + (db/with-atomic [conn pool] + (-> (assoc cfg :conn conn) + (register-profile params)))) + +;; ---- COMMAND: Request Profile Recovery + +(defn request-profile-recovery + [{:keys [pool tokens] :as cfg} {:keys [email] :as params}] + (letfn [(create-recovery-token [{:keys [id] :as profile}] + (let [token (tokens :generate + {:iss :password-recovery + :exp (dt/in-future "15m") + :profile-id id})] + (assoc profile :token token))) + + (send-email-notification [conn profile] + (let [ptoken (tokens :generate-predefined + {:iss :profile-identity + :profile-id (:id profile)})] + (eml/send! {::eml/conn conn + ::eml/factory eml/password-recovery + :public-uri (:public-uri cfg) + :to (:email profile) + :token (:token profile) + :name (:fullname profile) + :extra-data ptoken}) + nil))] + + (db/with-atomic [conn pool] + (when-let [profile (profile/retrieve-profile-data-by-email conn email)] + (when-not (eml/allow-send-emails? conn profile) + (ex/raise :type :validation + :code :profile-is-muted + :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) + + (when-not (:is-active profile) + (ex/raise :type :validation + :code :profile-not-verified + :hint "the user need to validate profile before recover password")) + + (when (eml/has-bounce-reports? conn (:email profile)) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) + + (->> profile + (create-recovery-token) + (send-email-notification conn)))))) + +(s/def ::request-profile-recovery + (s/keys :req-un [::email])) + +(sv/defmethod ::request-profile-recovery {:auth false} + [cfg params] + (request-profile-recovery cfg params)) + + diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/commands/demo.clj similarity index 90% rename from backend/src/app/rpc/mutations/demo.clj rename to backend/src/app/rpc/commands/demo.clj index 12786757f5..3ec8f37aed 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) UXBOX Labs SL -(ns app.rpc.mutations.demo +(ns app.rpc.commands.demo "A demo specific mutations." (:require [app.common.exceptions :as ex] @@ -12,7 +12,7 @@ [app.config :as cf] [app.db :as db] [app.loggers.audit :as audit] - [app.rpc.mutations.profile :as profile] + [app.rpc.commands.auth :as cmd.auth] [app.util.services :as sv] [app.util.time :as dt] [buddy.core.codecs :as bc] @@ -45,8 +45,8 @@ :hint "Demo users are disabled by config.")) (db/with-atomic [conn pool] - (->> (#'profile/create-profile conn params) - (#'profile/create-profile-relations conn)) + (->> (cmd.auth/create-profile conn params) + (cmd.auth/create-profile-relations conn)) (with-meta {:email email :password password} diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj new file mode 100644 index 0000000000..d5012058e7 --- /dev/null +++ b/backend/src/app/rpc/commands/ldap.clj @@ -0,0 +1,75 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.rpc.commands.ldap + (:require + [app.auth.ldap :as ldap] + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.db :as db] + [app.loggers.audit :as-alias audit] + [app.rpc.commands.auth :as cmd.auth] + [app.rpc.queries.profile :as profile] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +;; --- COMMAND: login-with-ldap + +(declare login-or-register) + +(s/def ::email ::us/email) +(s/def ::password ::us/string) +(s/def ::invitation-token ::us/string) + +(s/def ::login-with-ldap + (s/keys :req-un [::email ::password] + :opt-un [::invitation-token])) + +(sv/defmethod ::login-with-ldap {:auth false} + [{:keys [session tokens ldap] :as cfg} params] + (when-not ldap + (ex/raise :type :restriction + :code :ldap-not-initialized + :hide "ldap auth provider is not initialized")) + + (let [info (ldap/authenticate ldap params)] + (when-not info + (ex/raise :type :validation + :code :wrong-credentials)) + + (let [profile (login-or-register cfg 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- login-or-register + [{:keys [pool] :as cfg} info] + (db/with-atomic [conn pool] + (or (some->> (:email info) + (profile/retrieve-profile-data-by-email conn) + (profile/populate-additional-data conn) + (profile/decode-profile-row)) + (->> (assoc info :is-active true :is-demo false) + (cmd.auth/create-profile conn) + (cmd.auth/create-profile-relations conn) + (profile/strip-private-attrs))))) + diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj deleted file mode 100644 index 8ff5c23058..0000000000 --- a/backend/src/app/rpc/mutations/ldap.clj +++ /dev/null @@ -1,141 +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.rpc.mutations.ldap - (:require - [app.common.exceptions :as ex] - [app.common.logging :as l] - [app.common.spec :as us] - [app.config :as cfg] - [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] - [clojure.string])) - - -(s/def ::fullname ::us/not-empty-string) -(s/def ::email ::us/email) -(s/def ::backend ::us/not-empty-string) - -(s/def ::info-data - (s/keys :req-un [::fullname ::email ::backend])) - -(defn connect - ^java.lang.AutoCloseable - [] - (let [params {:ssl? (cfg/get :ldap-ssl) - :startTLS? (cfg/get :ldap-starttls) - :bind-dn (cfg/get :ldap-bind-dn) - :password (cfg/get :ldap-bind-password) - :host {:address (cfg/get :ldap-host) - :port (cfg/get :ldap-port)}}] - (try - (ldap/connect params) - (catch Exception e - (ex/raise :type :restriction - :code :ldap-disabled - :hint "ldap disabled or unable to connect" - :cause e))))) - -;; --- Mutation: login-with-ldap - -(declare authenticate) -(declare login-or-register) - -(s/def ::email ::us/email) -(s/def ::password ::us/string) -(s/def ::invitation-token ::us/string) - -(s/def ::login-with-ldap - (s/keys :req-un [::email ::password] - :opt-un [::invitation-token])) - -(sv/defmethod ::login-with-ldap {:auth false} - [{:keys [pool session tokens] :as cfg} params] - (db/with-atomic [conn pool] - (let [info (authenticate params) - cfg (assoc cfg :conn conn)] - - (when-not info - (ex/raise :type :validation - :code :wrong-credentials)) - - (when-not (s/valid? ::info-data info) - (let [explain (s/explain-str ::info-data info)] - (l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain)) - (ex/raise :type :restriction - :code :wrong-ldap-response - :reason explain))) - - (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)) - -(defn- get-ldap-user - [cpool {:keys [email] :as params}] - (let [query (-> (cfg/get :ldap-user-query) - (replace-several ":username" email)) - - attrs [(cfg/get :ldap-attrs-username) - (cfg/get :ldap-attrs-email) - (cfg/get :ldap-attrs-photo) - (cfg/get :ldap-attrs-fullname)] - - base-dn (cfg/get :ldap-base-dn) - params {:filter query - :sizelimit 1 - :attributes attrs}] - (first (ldap/search cpool base-dn params)))) - -(defn- authenticate - [{: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 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/profile.clj b/backend/src/app/rpc/mutations/profile.clj index e82e59a8c1..dea366f5d7 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -9,19 +9,18 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.emails :as eml] [app.loggers.audit :as audit] [app.media :as media] + [app.rpc.commands.auth :as cmd.auth] [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] [app.rpc.rlimit :as rlimit] [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [buddy.hashers :as hashers] [clojure.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p] @@ -37,310 +36,6 @@ (s/def ::password ::us/not-empty-string) (s/def ::old-password ::us/not-empty-string) (s/def ::theme ::us/string) -(s/def ::invitation-token ::us/not-empty-string) - -(declare check-profile-existence!) -(declare create-profile) -(declare create-profile-relations) -(declare register-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." - [domains email] - (if (or (empty? domains) - (nil? domains)) - true - (let [[_ candidate] (-> (str/lower email) - (str/split #"@" 2))] - (contains? domains candidate)))) - -(def ^:private sql:profile-existence - "select exists (select * from profile - where email = ? - and deleted_at is null) as val") - -(defn check-profile-existence! - [conn {:keys [email] :as params}] - (let [email (str/lower email) - result (db/exec-one! conn [sql:profile-existence email])] - (when (:val result) - (ex/raise :type :validation - :code :email-already-exists)) - params)) - -(defn derive-password - [password] - (hashers/derive password - {:alg :argon2id - :memory 16384 - :iterations 20 - :parallelism 2})) - -(defn verify-password - [attempt password] - (try - (hashers/verify attempt password) - (catch Exception _e - {: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 (contains? cf/flags :registration) - (if-not (contains? params :invitation-token) - (ex/raise :type :restriction - :code :registration-disabled) - (let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})] - (when-not (= (:email params) (:member-email invitation)) - (ex/raise :type :restriction - :code :email-does-not-match-invitation - :hint "email should match the invitation"))))) - - (when-let [domains (cf/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 spammer. - (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) - - (when (= (str/lower (:email params)) - (str/lower (:password params))) - (ex/raise :type :validation - :code :email-as-password - :hint "you can't use your email as password")) - - (let [params {:email (:email params) - :password (:password params) - :invitation-token (:invitation-token params) - :backend "penpot" - :iss :prepared-register - :exp (dt/in-future "48h")} - - token (tokens :generate params)] - (with-meta {:token token} - {::audit/profile-id uuid/zero}))) - -;; --- MUTATION: Register Profile - -(s/def ::token ::us/not-empty-string) -(s/def ::register-profile - (s/keys :req-un [::token ::fullname])) - -(sv/defmethod ::register-profile - {:auth false ::rlimit/permits (cf/get :rlimit-password)} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (-> (assoc cfg :conn conn) - (register-profile params)))) - -(defn register-profile - [{:keys [conn tokens session] :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 [is-active (or (:is-active params) - (contains? cf/flags :insecure-register)) - profile (->> (assoc params :is-active is-active) - (create-profile conn) - (create-profile-relations conn) - (decode-profile-row)) - invitation (when-let [token (:invitation-token params)] - (tokens :verify {:token token :iss :team-invitation}))] - (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). This happens only if the invitation email matches with the register email. - (and (some? invitation) (= (:email profile) (:member-email invitation))) - (let [claims (assoc invitation :member-id (:id profile)) - token (tokens :generate claims) - resp {:invitation-token token}] - (with-meta resp - {:transform-response ((:create session) (:id profile)) - ::audit/replace-props (audit/profile->props profile) - ::audit/profile-id (:id profile)})) - - ;; If auth backend is different from "penpot" means user is - ;; registering 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)) - ::audit/replace-props (audit/profile->props profile) - ::audit/profile-id (:id profile)}) - - ;; If the `:enable-insecure-register` flag is set, we proceed - ;; to sign in the user directly, without email verification. - (true? is-active) - (with-meta (profile/strip-private-attrs profile) - {:transform-response ((:create session) (:id profile)) - ::audit/replace-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 - {::audit/replace-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 params] - (let [id (or (:id params) (uuid/next)) - - props (-> (audit/extract-utm-params params) - (merge (:props params)) - (db/tjson)) - - password (if-let [password (:password params)] - (derive-password password) - "!") - - locale (:locale params) - locale (when (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 false) - email (str/lower (:email params)) - - params {:id id - :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) - (decode-profile-row)) - (catch org.postgresql.util.PSQLException e - (let [state (.getSQLState e)] - (if (not= state "23505") - (throw e) - (ex/raise :type :validation - :code :email-already-exists - :cause e))))))) - -(defn create-profile-relations - [conn profile] - (let [team (teams/create-team conn {:profile-id (:id profile) - :name "Default" - :is-default true})] - (-> profile - (profile/strip-private-attrs) - (assoc :default-team-id (:id team)) - (assoc :default-project-id (:default-project-id team))))) - -;; --- MUTATION: Login - -(s/def ::email ::us/email) -(s/def ::scope ::us/string) - -(s/def ::login - (s/keys :req-un [::email ::password] - :opt-un [::scope ::invitation-token])) - -(sv/defmethod ::login - {:auth false ::rlimit/permits (cf/get :rlimit-password)} - [{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}] - - (when-not (contains? cf/flags :login) - (ex/raise :type :restriction - :code :login-disabled - :hint "login is disabled in this instance")) - - (letfn [(check-password [profile password] - (when (= (:password profile) "!") - (ex/raise :type :validation - :code :account-without-password)) - (:valid (verify-password password (:password profile)))) - - (validate-profile [profile] - (when-not (:is-active profile) - (ex/raise :type :validation - :code :wrong-credentials)) - (when-not profile - (ex/raise :type :validation - :code :wrong-credentials)) - (when-not (check-password profile password) - (ex/raise :type :validation - :code :wrong-credentials)) - profile)] - - (db/with-atomic [conn pool] - (let [profile (->> (profile/retrieve-profile-data-by-email conn email) - (validate-profile) - (profile/strip-private-attrs) - (profile/populate-additional-data conn) - (decode-profile-row)) - - invitation (when-let [token (:invitation-token params)] - (tokens :verify {:token token :iss :team-invitation})) - - ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the - ;; invitation because invitations matches exactly; and user can't loging with other email and - ;; accept invitation with other email - response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) - {:invitation-token (:invitation-token params)} - profile)] - - (with-meta response - {:transform-response ((:create session) (:id profile)) - ::audit/props (audit/profile->props profile) - ::audit/profile-id (:id profile)}))))) - -;; --- MUTATION: Logout - -(s/def ::logout - (s/keys :opt-un [::profile-id])) - -(sv/defmethod ::logout {:auth false} - [{:keys [session] :as cfg} _] - (with-meta {} - {:transform-response (:delete session)})) ;; --- MUTATION: Update Profile (own) @@ -414,7 +109,7 @@ (defn- validate-password! [conn {:keys [profile-id old-password] :as params}] (let [profile (db/get-by-id conn :profile profile-id)] - (when-not (:valid (verify-password old-password (:password profile))) + (when-not (:valid (cmd.auth/verify-password old-password (:password profile))) (ex/raise :type :validation :code :old-password-not-match)) profile)) @@ -422,7 +117,7 @@ (defn update-profile-password! [conn {:keys [id password] :as profile}] (db/update! conn :profile - {:password (derive-password password)} + {:password (cmd.auth/derive-password password)} {:id id})) ;; --- MUTATION: Update Photo @@ -481,7 +176,7 @@ (defn- change-email-immediately [{:keys [conn]} {:keys [profile email] :as params}] (when (not= email (:email profile)) - (check-profile-existence! conn params)) + (cmd.auth/check-profile-existence! conn params)) (db/update! conn :profile {:email email} {:id (:id profile)}) @@ -499,7 +194,7 @@ :profile-id (:id profile)})] (when (not= email (:email profile)) - (check-profile-existence! conn params)) + (cmd.auth/check-profile-existence! conn params)) (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation @@ -526,76 +221,6 @@ [conn id] (db/get-by-id conn :profile id {:for-update true})) -;; --- MUTATION: Request Profile Recovery - -(s/def ::request-profile-recovery - (s/keys :req-un [::email])) - -(sv/defmethod ::request-profile-recovery {:auth false} - [{:keys [pool tokens] :as cfg} {:keys [email] :as params}] - (letfn [(create-recovery-token [{:keys [id] :as profile}] - (let [token (tokens :generate - {:iss :password-recovery - :exp (dt/in-future "15m") - :profile-id id})] - (assoc profile :token token))) - - (send-email-notification [conn profile] - (let [ptoken (tokens :generate-predefined - {:iss :profile-identity - :profile-id (:id profile)})] - (eml/send! {::eml/conn conn - ::eml/factory eml/password-recovery - :public-uri (:public-uri cfg) - :to (:email profile) - :token (:token profile) - :name (:fullname profile) - :extra-data ptoken}) - nil))] - - (db/with-atomic [conn pool] - (when-let [profile (profile/retrieve-profile-data-by-email conn email)] - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) - - (when-not (:is-active profile) - (ex/raise :type :validation - :code :profile-not-verified - :hint "the user need to validate profile before recover password")) - - (when (eml/has-bounce-reports? conn (:email profile)) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - - (->> profile - (create-recovery-token) - (send-email-notification conn)))))) - - -;; --- MUTATION: Recover Profile - -(s/def ::token ::us/not-empty-string) -(s/def ::recover-profile - (s/keys :req-un [::token ::password])) - -(sv/defmethod ::recover-profile - {:auth false ::rlimit/permits (cf/get :rlimit-password)} - [{:keys [pool tokens] :as cfg} {:keys [token password]}] - (letfn [(validate-token [token] - (let [tdata (tokens :verify {:token token :iss :password-recovery})] - (:profile-id tdata))) - - (update-password [conn profile-id] - (let [pwd (derive-password password)] - (db/update! conn :profile {:password pwd} {:id profile-id})))] - - (db/with-atomic [conn pool] - (->> (validate-token token) - (update-password conn)) - nil))) ;; --- MUTATION: Update Profile Props @@ -668,3 +293,61 @@ :code :owner-teams-with-people :hint "The user need to transfer ownership of owned teams." :context {:teams (mapv :team-id rows)})))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- MUTATION: Login + +(s/def ::login ::cmd.auth/login-with-password) + +(sv/defmethod ::login + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [cfg params] + (cmd.auth/login-with-password cfg params)) + +;; --- MUTATION: Logout + +(s/def ::logout ::cmd.auth/logout) + +(sv/defmethod ::logout {:auth false} + [{:keys [session] :as cfg} _] + (with-meta {} + {:transform-response (:delete session)})) + +;; --- MUTATION: Recover Profile + +(s/def ::recover-profile ::cmd.auth/recover-profile) + +(sv/defmethod ::recover-profile + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [cfg params] + (cmd.auth/recover-profile cfg params)) + +;; --- MUTATION: Prepare Register + +(s/def ::prepare-register-profile ::cmd.auth/prepare-register-profile) + +(sv/defmethod ::prepare-register-profile {:auth false} + [cfg params] + (cmd.auth/prepare-register cfg params)) + +;; --- MUTATION: Register Profile + +(s/def ::register-profile ::cmd.auth/register-profile) + +(sv/defmethod ::register-profile + {:auth false ::rlimit/permits (cf/get :rlimit-password)} + [{:keys [pool] :as cfg} params] + (db/with-atomic [conn pool] + (-> (assoc cfg :conn conn) + (cmd.auth/register-profile params)))) + +;; --- MUTATION: Request Profile Recovery + +(s/def ::request-profile-recovery ::cmd.auth/request-profile-recovery) + +(sv/defmethod ::request-profile-recovery {:auth false} + [cfg params] + (cmd.auth/request-profile-recovery cfg params)) diff --git a/backend/src/app/srepl/dev.clj b/backend/src/app/srepl/dev.clj index d8d243296c..61ec418f52 100644 --- a/backend/src/app/srepl/dev.clj +++ b/backend/src/app/srepl/dev.clj @@ -3,7 +3,7 @@ (:require [app.db :as db] [app.config :as cfg] - [app.rpc.mutations.profile :refer [derive-password]] + [app.rpc.commands.auth :refer [derive-password]] [app.main :refer [system]])) (defn reset-passwords diff --git a/backend/test/app/services_media_test.clj b/backend/test/app/services_media_test.clj index d0ce566b00..aa5f9f5cad 100644 --- a/backend/test/app/services_media_test.clj +++ b/backend/test/app/services_media_test.clj @@ -46,7 +46,13 @@ (t/is (sto/storage-object? mobj1)) (t/is (sto/storage-object? mobj2)) (t/is (= 122785 (:size mobj1))) - (t/is (= 3303 (:size mobj2))))) + + ;; This is because in ubuntu 21.04 generates different + ;; thumbnail that in ubuntu 22.04. This hack should be removed + ;; when we all use the ubuntu 22.04 devenv image. + (t/is (or + (= 3302 (:size mobj2)) + (= 3303 (:size mobj2)))))) )) (t/deftest media-object-upload diff --git a/backend/test/app/services_profile_test.clj b/backend/test/app/services_profile_test.clj index c87e7cfa8f..f9c4bd10a8 100644 --- a/backend/test/app/services_profile_test.clj +++ b/backend/test/app/services_profile_test.clj @@ -10,6 +10,7 @@ [app.config :as cf] [app.db :as db] [app.rpc.mutations.profile :as profile] + [app.rpc.commands.auth :as cauth] [app.test-helpers :as th] [app.util.time :as dt] [clojure.java.io :as io] @@ -27,11 +28,10 @@ ;; Test with wrong credentials (t/deftest profile-login-failed-1 (let [profile (th/create-profile* 1) - data {::th/type :login + data {::th/type :login-with-password :email "profile1.test@nodomain.com" - :password "foobar" - :scope "foobar"} - out (th/mutation! data)] + :password "foobar"} + out (th/command! data)] #_(th/print-result! out) (let [error (:error out)] @@ -42,11 +42,10 @@ ;; Test with good credentials but profile not activated. (t/deftest profile-login-failed-2 (let [profile (th/create-profile* 1) - data {::th/type :login + data {::th/type :login-with-password :email "profile1.test@nodomain.com" - :password "123123" - :scope "foobar"} - out (th/mutation! data)] + :password "123123"} + out (th/command! data)] ;; (th/print-result! out) (let [error (:error out)] (t/is (th/ex-info? error)) @@ -58,8 +57,7 @@ (let [profile (th/create-profile* 1 {:is-active true}) data {::th/type :login :email "profile1.test@nodomain.com" - :password "123123" - :scope "foobar"} + :password "123123"} out (th/mutation! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -161,11 +159,11 @@ (t/deftest registration-domain-whitelist (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] (t/testing "allowed email domain" - (t/is (true? (profile/email-domain-in-whitelist? whitelist "username@ya.ru"))) - (t/is (true? (profile/email-domain-in-whitelist? #{} "username@somedomain.com")))) + (t/is (true? (cauth/email-domain-in-whitelist? whitelist "username@ya.ru"))) + (t/is (true? (cauth/email-domain-in-whitelist? #{} "username@somedomain.com")))) (t/testing "not allowed email domain" - (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) + (t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) (t/deftest prepare-register-and-register-profile (let [data {::th/type :prepare-register-profile diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index f94e60701e..8849ac3ccc 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -9,14 +9,15 @@ [app.common.data :as d] [app.common.flags :as flags] [app.common.pages :as cp] + [app.common.pprint :as pp] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.common.pprint :as pp] [app.config :as cf] [app.db :as db] [app.main :as main] [app.media] [app.migrations] + [app.rpc.commands.auth :as cmd.auth] [app.rpc.mutations.files :as files] [app.rpc.mutations.profile :as profile] [app.rpc.mutations.projects :as projects] @@ -31,8 +32,8 @@ [expound.alpha :as expound] [integrant.core :as ig] [mockery.core :as mk] - [yetti.request :as yrq] - [promesa.core :as p]) + [promesa.core :as p] + [yetti.request :as yrq]) (:import org.postgresql.ds.PGSimpleDataSource)) (def ^:dynamic *system* nil) @@ -59,10 +60,12 @@ :app.http/router :app.http.awsns/handler :app.http.session/updater - :app.http.oauth/google - :app.http.oauth/gitlab - :app.http.oauth/github - :app.http.oauth/all + :app.auth.oidc/google-provider + :app.auth.oidc/gitlab-provider + :app.auth.oidc/github-provider + :app.auth.oidc/generic-provider + :app.auth.oidc/routes + ;; :app.auth.ldap/provider :app.worker/executors-monitor :app.http.oauth/handler :app.notifications/handler @@ -81,9 +84,9 @@ (try (binding [*system* system *pool* (:app.db/pool system)] - (mk/with-mocks [mock1 {:target 'app.rpc.mutations.profile/derive-password + (mk/with-mocks [mock1 {:target 'app.rpc.commands.auth/derive-password :return identity} - mock2 {:target 'app.rpc.mutations.profile/verify-password + mock2 {:target 'app.rpc.commands.auth/verify-password :return (fn [a b] {:valid (= a b)})}] (next))) (finally @@ -140,8 +143,8 @@ :is-demo false} params)] (->> params - (#'profile/create-profile conn) - (#'profile/create-profile-relations conn))))) + (cmd.auth/create-profile conn) + (cmd.auth/create-profile-relations conn))))) (defn create-project* ([i params] (create-project* *pool* i params)) @@ -267,17 +270,21 @@ {:error (handle-error e#) :result nil}))) +(defn command! + [{:keys [::type] :as data}] + (let [method-fn (get-in *system* [:app.rpc/methods :commands type])] + ;; (app.common.pprint/pprint (:app.rpc/methods *system*)) + (try-on! (method-fn (dissoc data ::type))))) + (defn mutation! [{:keys [::type] :as data}] - (let [method-fn (get-in *system* [:app.rpc/rpc :methods :mutation type])] - (try-on! - (method-fn (dissoc data ::type))))) + (let [method-fn (get-in *system* [:app.rpc/methods :mutations type])] + (try-on! (method-fn (dissoc data ::type))))) (defn query! [{:keys [::type] :as data}] - (let [method-fn (get-in *system* [:app.rpc/rpc :methods :query type])] - (try-on! - (method-fn (dissoc data ::type))))) + (let [method-fn (get-in *system* [:app.rpc/methods :queries type])] + (try-on! (method-fn (dissoc data ::type))))) ;; --- UTILS diff --git a/docker/images/config.env b/docker/images/config.env index eccfbe0bfa..cc880d70f1 100644 --- a/docker/images/config.env +++ b/docker/images/config.env @@ -97,4 +97,3 @@ PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com # PENPOT_LDAP_ATTRS_USERNAME=uid # PENPOT_LDAP_ATTRS_EMAIL=mail # PENPOT_LDAP_ATTRS_FULLNAME=cn -# PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 270b7e26ae..1e349e5866 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -4,69 +4,10 @@ log() { echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*" } - ######################################### ## App Frontend config ######################################### -update_google_client_id() { - if [ -n "$PENPOT_GOOGLE_CLIENT_ID" ]; then - log "Updating Google Client Id: $PENPOT_GOOGLE_CLIENT_ID" - sed -i \ - -e "s|^//var penpotGoogleClientID = \".*\";|var penpotGoogleClientID = \"$PENPOT_GOOGLE_CLIENT_ID\";|g" \ - "$1" - fi -} - - -update_gitlab_client_id() { - if [ -n "$PENPOT_GITLAB_CLIENT_ID" ]; then - log "Updating GitLab Client Id: $PENPOT_GITLAB_CLIENT_ID" - sed -i \ - -e "s|^//var penpotGitlabClientID = \".*\";|var penpotGitlabClientID = \"$PENPOT_GITLAB_CLIENT_ID\";|g" \ - "$1" - fi -} - - -update_github_client_id() { - if [ -n "$PENPOT_GITHUB_CLIENT_ID" ]; then - log "Updating GitHub Client Id: $PENPOT_GITHUB_CLIENT_ID" - sed -i \ - -e "s|^//var penpotGithubClientID = \".*\";|var penpotGithubClientID = \"$PENPOT_GITHUB_CLIENT_ID\";|g" \ - "$1" - fi -} - -update_oidc_client_id() { - if [ -n "$PENPOT_OIDC_CLIENT_ID" ]; then - log "Updating Oidc Client Id: $PENPOT_OIDC_CLIENT_ID" - sed -i \ - -e "s|^//var penpotOIDCClientID = \".*\";|var penpotOIDCClientID = \"$PENPOT_OIDC_CLIENT_ID\";|g" \ - "$1" - fi -} - -# DEPRECATED -update_login_with_ldap() { - if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then - log "Updating Login with LDAP: $PENPOT_LOGIN_WITH_LDAP" - sed -i \ - -e "s|^//var penpotLoginWithLDAP = .*;|var penpotLoginWithLDAP = $PENPOT_LOGIN_WITH_LDAP;|g" \ - "$1" - fi -} - -# DEPRECATED -update_registration_enabled() { - if [ -n "$PENPOT_REGISTRATION_ENABLED" ]; then - log "Updating Registration Enabled: $PENPOT_REGISTRATION_ENABLED" - sed -i \ - -e "s|^//var penpotRegistrationEnabled = .*;|var penpotRegistrationEnabled = $PENPOT_REGISTRATION_ENABLED;|g" \ - "$1" - fi -} - update_flags() { if [ -n "$PENPOT_FLAGS" ]; then sed -i \ @@ -75,11 +16,5 @@ update_flags() { fi } -update_google_client_id /var/www/app/js/config.js -update_gitlab_client_id /var/www/app/js/config.js -update_github_client_id /var/www/app/js/config.js -update_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_flags /var/www/app/js/config.js exec "$@"; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 7cf0ede1a1..47906059f8 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -80,10 +80,6 @@ (def default-theme "default") (def default-language "en") -(def google-client-id (obj/get global "penpotGoogleClientID" nil)) -(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil)) -(def github-client-id (obj/get global "penpotGithubClientID" nil)) -(def oidc-client-id (obj/get global "penpotOIDCClientID" nil)) (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def translations (obj/get global "penpotTranslations")) (def themes (obj/get global "penpotThemes")) @@ -100,14 +96,6 @@ (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil)) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil)) -;; maintain for backward compatibility -(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false) - registration (obj/get global "penpotRegistrationEnabled" true)] - (when login-with-ldap - (swap! flags conj :login-with-ldap)) - (when (false? registration) - (swap! flags disj :registration))) - (defn get-public-uri [] (let [uri (u/uri (or (obj/get global "penpotPublicURI") diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 768108d8e6..cb70f6362d 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -145,7 +145,7 @@ ptk/WatchEvent (watch [_ _ _] (when (= status "ended") - (->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id}) + (->> (rp/command! :export {:cmd :get-resource :blob? true :id resource-id}) (rx/delay 500) (rx/map #(dom/trigger-download filename %))))))) @@ -165,9 +165,9 @@ :wait true}] (rx/concat (rx/of ::dwp/force-persist) - (->> (rp/query! :exporter params) + (->> (rp/command! :export params) (rx/mapcat (fn [{:keys [id filename]}] - (->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id}) + (->> (rp/command! :export {:cmd :get-resource :blob? true :id id}) (rx/map (fn [data] (dom/trigger-download filename data) (clear-export-state uuid/zero)))))) @@ -213,7 +213,7 @@ ;; Launch the exportation process and stores the resource id ;; locally. - (->> (rp/query! :exporter params) + (->> (rp/command! :export params) (rx/map (fn [{:keys [id] :as resource}] (vreset! resource-id id) (initialize-export-status exports cmd resource)))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index eee3b49cc4..9a98a17b9c 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -206,7 +206,7 @@ ;; the returned profile is an NOT authenticated profile, we ;; proceed to logout and show an error message. - (->> (rp/mutation :login (d/without-nils params)) + (->> (rp/command :login-with-password (d/without-nils params)) (rx/merge-map (fn [data] (rx/merge (rx/of (fetch-profile)) @@ -292,7 +292,7 @@ (ptk/reify ::logout ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :logout) + (->> (rp/command :logout) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) (rx/map #(logged-out params))))))) @@ -494,7 +494,7 @@ :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/mutation :request-profile-recovery data) + (->> (rp/command :request-profile-recovery data) (rx/tap on-success) (rx/catch on-error)))))) @@ -513,7 +513,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/mutation :recover-profile data) + (->> (rp/command :recover-profile data) (rx/tap on-success) (rx/catch on-error)))))) @@ -524,7 +524,7 @@ (ptk/reify ::create-demo-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :create-demo-profile {}) + (->> (rp/command :create-demo-profile {}) (rx/map login))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index ee068f205a..b1d7b581cb 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -73,10 +73,22 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) +(defn- send-command! + "A simple helper for a common case of sending and receiving transit + data to the penpot mutation api." + [id params] + (->> (http/send! {:method :post + :uri (u/join base-uri "api/rpc/command/" (name id)) + :credentials "include" + :body (http/transit-data params)}) + (rx/map http/conditional-decode-transit) + (rx/mapcat handle-response))) + (defn- dispatch [& args] (first args)) (defmulti query dispatch) (defmulti mutation dispatch) +(defmulti command dispatch) (defmethod query :default [id params] @@ -90,6 +102,10 @@ [id params] (send-mutation! id params)) +(defmethod command :default + [id params] + (send-command! id params)) + (defn query! ([id] (query id {})) ([id params] (query id params))) @@ -98,7 +114,11 @@ ([id] (mutation id {})) ([id params] (mutation id params))) -(defmethod mutation :login-with-oauth +(defn command! + ([id] (command id {})) + ([id params] (command id params))) + +(defmethod command :login-with-oidc [_ {:keys [provider] :as params}] (let [uri (u/join base-uri "api/auth/oauth/" (d/name provider)) params (dissoc params :provider)] @@ -109,7 +129,7 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) -(defmethod mutation :send-feedback +(defmethod command :send-feedback [_ params] (->> (http/send! {:method :post :uri (u/join base-uri "api/feedback") @@ -128,7 +148,7 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) -(defmethod query :exporter +(defmethod command :export [_ params] (let [default {:wait false :blob? false}] (send-export (merge default params)))) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 8c9cc7b0a7..1a1864a733 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -23,10 +23,11 @@ [rumext.alpha :as mf])) (def show-alt-login-buttons? - (or cf/google-client-id - cf/gitlab-client-id - cf/github-client-id - cf/oidc-client-id)) + (some (partial contains? @cf/flags) + [:login-with-google + :login-with-github + :login-with-gitlab + :login-with-oidc])) (s/def ::email ::us/email) (s/def ::password ::us/not-empty-string) @@ -36,19 +37,27 @@ (s/keys :req-un [::email ::password] :opt-un [::invitation-token])) -(defn- login-with-oauth +(defn- login-with-oidc [event provider params] (dom/prevent-default event) - (->> (rp/mutation! :login-with-oauth (assoc params :provider provider)) + (->> (rp/command! :login-with-oidc (assoc params :provider provider)) (rx/subs (fn [{:keys [redirect-uri] :as rsp}] - (.replace js/location redirect-uri))))) + (.replace js/location redirect-uri)) + (fn [{:keys [type code] :as error}] + (cond + (and (= type :restriction) + (= code :provider-not-configured)) + (st/emit! (dm/error (tr "errors.auth-provider-not-configured"))) + + :else + (st/emit! (dm/error (tr "errors.generic")))))))) (defn- login-with-ldap [event params] (dom/prevent-default event) (dom/stop-propagation event) (let [{:keys [on-error]} (meta params)] - (->> (rp/mutation! :login-with-ldap params) + (->> (rp/command! :login-with-ldap params) (rx/subs (fn [profile] (if-let [token (:invitation-token profile)] (st/emit! (rt/nav :auth-verify-token {} {:token token})) @@ -56,11 +65,15 @@ (fn [{:keys [type code] :as error}] (cond (and (= type :restriction) - (= code :ldap-disabled)) + (= code :ldap-not-initialized)) (st/emit! (dm/error (tr "errors.ldap-disabled"))) (fn? on-error) - (on-error error))))))) + (on-error error) + + :else + (st/emit! (dm/error (tr "errors.generic"))))))))) + (mf/defc login-form [{:keys [params] :as props}] @@ -134,35 +147,35 @@ (mf/defc login-buttons [{:keys [params] :as props}] [:div.auth-buttons - (when cf/google-client-id + (when (contains? @cf/flags :login-with-google) [:a.btn-primary.btn-large.btn-google-auth - {:on-click #(login-with-oauth % :google params)} + {:on-click #(login-with-oidc % :google params)} [:span.logo i/brand-google] (tr "auth.login-with-google-submit")]) - (when cf/github-client-id + (when (contains? @cf/flags :login-with-github) [:a.btn-primary.btn-large.btn-github-auth - {:on-click #(login-with-oauth % :github params)} + {:on-click #(login-with-oidc % :github params)} [:span.logo i/brand-github] (tr "auth.login-with-github-submit")]) - (when cf/gitlab-client-id + (when (contains? @cf/flags :login-with-gitlab) [:a.btn-primary.btn-large.btn-gitlab-auth - {:on-click #(login-with-oauth % :gitlab params)} + {:on-click #(login-with-oidc % :gitlab params)} [:span.logo i/brand-gitlab] (tr "auth.login-with-gitlab-submit")]) - (when cf/oidc-client-id + (when (contains? @cf/flags :login-with-oidc) [:a.btn-primary.btn-large.btn-github-auth - {:on-click #(login-with-oauth % :oidc params)} + {:on-click #(login-with-oidc % :oidc params)} [:span.logo i/brand-openid] (tr "auth.login-with-oidc-submit")])]) (mf/defc login-button-oidc [{:keys [params] :as props}] - (when cf/oidc-client-id + (when (contains? @cf/flags :login-with-oidc) [:div.link-entry.link-oidc - [:a {:on-click #(login-with-oauth % :oidc params)} + [:a {:on-click #(login-with-oidc % :oidc params)} (tr "auth.login-with-oidc-submit")]])) (mf/defc login-page diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 739b7ad1da..c721f43895 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -84,7 +84,7 @@ (fn [form _event] (reset! submitted? true) (let [cdata (:clean-data @form)] - (->> (rp/mutation :prepare-register-profile cdata) + (->> (rp/command :prepare-register-profile cdata) (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) (rx/subs (partial handle-prepare-register-success form) @@ -207,7 +207,7 @@ (fn [form _event] (reset! submitted? true) (let [params (:clean-data @form)] - (->> (rp/mutation :register-profile params) + (->> (rp/command :register-profile params) (rx/finalize #(reset! submitted? false)) (rx/subs (partial handle-register-success form) (partial handle-register-error form)))))) diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index 1bd671406a..4b5055d85d 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -55,7 +55,7 @@ (fn [form _] (reset! loading true) (let [data (:clean-data @form)] - (->> (rp/mutation! :send-feedback data) + (->> (rp/command! :send-feedback data) (rx/subs on-succes on-error)))))] [:& fm/form {:class "feedback-form" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3ae8bfe836..4f4889e306 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -699,6 +699,10 @@ msgstr "This invite might be canceled or may be expired." msgid "errors.ldap-disabled" msgstr "LDAP authentication is disabled." +#: src/app/main/ui/auth/login.cljs +msgid "errors.auth-provider-not-configured" +msgstr "Authentication provider not configured." + msgid "errors.media-format-unsupported" msgstr "The image format is not supported (must be svg, jpg or png)." From f2140a1421397cabec5be5816f4f27995cffc880 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Jul 2022 11:31:01 +0200 Subject: [PATCH 61/86] :bug: Fix cron scheduler locking mechanism And add improved logging to the worker/cron code --- backend/src/app/worker.clj | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 249bfba067..52ce5ddd44 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -203,8 +203,7 @@ (instance? Exception val) (do - (l/warn :cause val - :hint "unexpected error ocurried on polling the database (will resume in some instants)") + (l/warn :hint "unexpected error ocurried on polling the database (will resume in some instants)" :cause val) (a/> (filter some? entries) (run! (partial schedule-cron-task cfg))) @@ -494,16 +493,12 @@ on conflict (id) do update set cron_expr=?") -(defn- synchronize-cron-item - [conn {:keys [id cron]}] - (let [cron (str cron)] - (l/debug :action "initialize scheduled task" :id id :cron cron) - (db/exec-one! conn [sql:upsert-cron-task id cron cron]))) - -(defn- synchronize-cron-entries - [{:keys [pool schedule]}] +(defn- synchronize-cron-entries! + [{:keys [pool entries]}] (db/with-atomic [conn pool] - (run! (partial synchronize-cron-item conn) schedule))) + (doseq [{:keys [id cron]} entries] + (l/trace :hint "register cron task" :id id :cron (str cron)) + (db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)])))) (def sql:lock-cron-task "select id from scheduled_task where id=? for update skip locked") @@ -512,7 +507,7 @@ [{:keys [executor pool] :as cfg} {:keys [id] :as task}] (letfn [(run-task [conn] (when (db/exec-one! conn [sql:lock-cron-task (d/name id)]) - (l/debug :action "execute scheduled task" :id id) + (l/trace :hint "execute cron task" :id id) ((:fn task) task))) (handle-task [] @@ -567,9 +562,10 @@ (defmethod ig/init-key ::registry [_ {:keys [metrics tasks]}] + (l/info :hint "registry initialized" :tasks (count tasks)) (reduce-kv (fn [res k v] (let [tname (name k)] - (l/debug :hint "register task" :name tname) + (l/trace :hint "register task" :name tname) (assoc res k (wrap-task-handler metrics tname v)))) {} tasks)) From a3580a5ab9925e975797c9040cc42962bcae9f8a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Jul 2022 12:41:55 +0200 Subject: [PATCH 62/86] :paperclip: Update log4j2 default configuration --- backend/resources/log4j2-devenv.xml | 13 +++++++------ backend/resources/log4j2.xml | 13 ++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 6305bccef8..51f565b6fc 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -20,15 +20,16 @@ - - + - - - - + + + + + + diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index e9feb9e001..5033f4726b 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -7,13 +7,16 @@ + - + - - - - + + + + + + From 70028e13711894d657cd26a883f30de1926810b9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Jul 2022 13:34:17 +0200 Subject: [PATCH 63/86] :paperclip: Allow set statement timeout on db module --- backend/src/app/db.clj | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 6604df6b56..81e2d9d093 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -7,6 +7,7 @@ (ns app.db (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.logging :as l] @@ -60,17 +61,22 @@ (s/def ::validation-timeout ::us/integer) (s/def ::read-only? ::us/boolean) +(s/def ::statement-timeout ::us/integer) +(s/def ::idle-in-transaction-timeout ::us/integer) + (s/def ::pool-options (s/keys :opt-un [::uri ::name ::min-size ::max-size ::connection-timeout ::validation-timeout + ::statement-timeout + ::idle-in-transaction-timeout ::migrations ::username ::password - ::mtx/metrics - ::read-only?])) + ::read-only? + ::mtx/metrics])) (def defaults {:name :main @@ -80,6 +86,8 @@ :validation-timeout 10000 :idle-timeout 120000 ; 2min :max-lifetime 1800000 ; 30m + :statement-timeout 300000 + :idle-in-transaction-timeout 300000 :read-only? false}) (defmethod ig/prep-key ::pool @@ -127,10 +135,6 @@ ;; API & Impl ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def initsql - (str "SET statement_timeout = 300000;\n" - "SET idle_in_transaction_session_timeout = 300000;")) - (defn- create-datasource-config [{:keys [metrics uri] :as cfg}] (let [config (HikariConfig.)] @@ -145,7 +149,11 @@ (.setMaxLifetime (:max-lifetime cfg)) (.setMinimumIdle (:min-size cfg)) (.setMaximumPoolSize (:max-size cfg)) - (.setConnectionInitSql initsql) + (.setConnectionInitSql + (dm/fmt "SET statement_timeout=%; SET idle_in_transaction_session_timeout=%;" + (:statement-timeout cfg) + (:idle-in-transaction-timeout cfg))) + (.setInitializationFailTimeout -1)) ;; When metrics namespace is provided From 356ff4683d512cfc47e118ddcc740faaa321e385 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 4 Jul 2022 14:04:56 +0200 Subject: [PATCH 64/86] Revert ":paperclip: Allow set statement timeout on db module" This reverts commit 70028e13711894d657cd26a883f30de1926810b9. --- backend/src/app/db.clj | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 81e2d9d093..6604df6b56 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -7,7 +7,6 @@ (ns app.db (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.logging :as l] @@ -61,22 +60,17 @@ (s/def ::validation-timeout ::us/integer) (s/def ::read-only? ::us/boolean) -(s/def ::statement-timeout ::us/integer) -(s/def ::idle-in-transaction-timeout ::us/integer) - (s/def ::pool-options (s/keys :opt-un [::uri ::name ::min-size ::max-size ::connection-timeout ::validation-timeout - ::statement-timeout - ::idle-in-transaction-timeout ::migrations ::username ::password - ::read-only? - ::mtx/metrics])) + ::mtx/metrics + ::read-only?])) (def defaults {:name :main @@ -86,8 +80,6 @@ :validation-timeout 10000 :idle-timeout 120000 ; 2min :max-lifetime 1800000 ; 30m - :statement-timeout 300000 - :idle-in-transaction-timeout 300000 :read-only? false}) (defmethod ig/prep-key ::pool @@ -135,6 +127,10 @@ ;; API & Impl ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def initsql + (str "SET statement_timeout = 300000;\n" + "SET idle_in_transaction_session_timeout = 300000;")) + (defn- create-datasource-config [{:keys [metrics uri] :as cfg}] (let [config (HikariConfig.)] @@ -149,11 +145,7 @@ (.setMaxLifetime (:max-lifetime cfg)) (.setMinimumIdle (:min-size cfg)) (.setMaximumPoolSize (:max-size cfg)) - (.setConnectionInitSql - (dm/fmt "SET statement_timeout=%; SET idle_in_transaction_session_timeout=%;" - (:statement-timeout cfg) - (:idle-in-transaction-timeout cfg))) - + (.setConnectionInitSql initsql) (.setInitializationFailTimeout -1)) ;; When metrics namespace is provided From dd130615a1c5225afb4242275ee07c272e375eae Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Jul 2022 11:03:28 +0200 Subject: [PATCH 65/86] :sparkles: Improve _doc endpoint output format --- backend/resources/api-doc.css | 10 ++++++++++ backend/resources/api-doc.tmpl | 4 ++++ backend/src/app/http/doc.clj | 22 ++++++++++++---------- backend/src/app/rpc.clj | 3 ++- backend/src/app/util/services.clj | 11 ++++++++--- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/backend/resources/api-doc.css b/backend/resources/api-doc.css index b9b14a889c..2404aea936 100644 --- a/backend/resources/api-doc.css +++ b/backend/resources/api-doc.css @@ -85,6 +85,16 @@ header { .rpc-row-info > .name { width: 280px; /* font-weight: bold; */ + border-right: 1px dotted #777; + padding-right: 10px; +} + +.rpc-row-info > .module { + width: 120px; + font-weight: bold; + border-right: 1px dotted #777; + text-align: right; + padding-right: 10px; } .rpc-row-info > .tags > .tag > span:first-child { diff --git a/backend/resources/api-doc.tmpl b/backend/resources/api-doc.tmpl index 35d3a700d0..fa1a6d9ab9 100644 --- a/backend/resources/api-doc.tmpl +++ b/backend/resources/api-doc.tmpl @@ -26,6 +26,7 @@
  • {#
    {{item.type}}
    #} +
    {{item.module}}:
    {{item.name}}
    @@ -53,6 +54,8 @@
  • {#
    {{item.type}}
    #} + +
    {{item.module}}:
    {{item.name}}
    @@ -80,6 +83,7 @@
  • {#
    {{item.type}}
    #} +
    {{item.module}}:
    {{item.name}}
    diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj index 5addbb9a65..c467e8644d 100644 --- a/backend/src/app/http/doc.clj +++ b/backend/src/app/http/doc.clj @@ -14,6 +14,7 @@ [app.util.template :as tmpl] [clojure.java.io :as io] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [pretty-spec.core :as ps] [yetti.response :as yrs])) @@ -30,25 +31,26 @@ [methods] (letfn [(gen-doc [type [name f]] (let [mdata (meta f)] - ;; (prn name mdata) {:type (d/name type) :name (d/name name) + :module (-> (:ns mdata) (str/split ".") last) :auth (:auth mdata true) :docs (::sv/docs mdata) :spec (get-spec-str (::sv/spec mdata))}))] {:command-methods - (into [] - (map (partial gen-doc :command)) - (->> methods :commands (sort-by first))) + (->> (:commands methods) + (map (partial gen-doc :command)) + (sort-by (juxt :module :name))) + :query-methods - (into [] - (map (partial gen-doc :query)) - (->> methods :queries (sort-by first))) + (->> (:queries methods) + (map (partial gen-doc :query)) + (sort-by (juxt :module :name))) :mutation-methods - (into [] - (map (partial gen-doc :mutation)) - (->> methods :mutations (sort-by first)))})) + (->> (:mutations methods) + (map (partial gen-doc :query)) + (sort-by (juxt :module :name)))})) (defn- handler [methods] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index bfe570d7ee..801fc5c1f8 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -204,8 +204,9 @@ (defn- process-method [cfg vfn] (let [mdata (meta vfn)] + ;; (prn mdata) [(keyword (::sv/name mdata)) - (wrap cfg (deref vfn) mdata)])) + (wrap cfg vfn mdata)])) (defn- resolve-query-methods [cfg] diff --git a/backend/src/app/util/services.clj b/backend/src/app/util/services.clj index 9faa8adcb5..5131f45c49 100644 --- a/backend/src/app/util/services.clj +++ b/backend/src/app/util/services.clj @@ -40,9 +40,14 @@ (comp (d/domap require) (map find-ns) - (mapcat ns-publics) - (map second) - (filter #(::spec (meta %))))) + (mapcat (fn [ns] + (->> (ns-publics ns) + (map second) + (filter #(::spec (meta %))) + (map (fn [fvar] + (with-meta (deref fvar) + (-> (meta fvar) + (assoc :ns (-> ns ns-name str))))))))))) (defn scan-ns [& nsyms] From 537fff4c801501deca6edbd4a3c11006bf040848 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Jul 2022 11:50:56 +0200 Subject: [PATCH 66/86] :arrow_up: Update docker images system dependencies --- docker/images/Dockerfile.backend | 14 +++++++------- docker/images/Dockerfile.exporter | 6 +++--- docker/images/Dockerfile.frontend | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index a2690c8891..4729d7cdb8 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 LABEL maintainer="Andrey Antukh " ENV LANG='en_US.UTF-8' LC_ALL='en_US.UTF-8' @@ -27,16 +27,16 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='7217a9f9be3b0c8dfc78538f95fd2deb493eb651152d975062920566492b2574'; \ - BINARY_URL='https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16%2B36/OpenJDK16-jdk_aarch64_linux_hotspot_16_36.tar.gz'; \ + ESUM='37ceaf232a85cce46bcccfd71839854e8b14bf3160e7ef72a676b9cae45ee8af'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.1_10.tar.gz'; \ ;; \ armhf|armv7l) \ - ESUM='f1d32ba01a40c98889f31368c0e987d6bbda65a7c50b8c088623b48e3a90104a'; \ - BINARY_URL='https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16%2B36/OpenJDK16-jdk_arm_linux_hotspot_16_36.tar.gz'; \ + ESUM='0ddec3c165ab0b662a57a845db3fdaeb840660b493f164696b03df76aadf61c8'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_arm_linux_hotspot_18.0.1_10.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='2e031cf37018161c9e59b45fa4b98ff2ce4ce9297b824c512989d579a70f8422'; \ - BINARY_URL='https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16%2B36/OpenJDK16-jdk_x64_linux_hotspot_16_36.tar.gz'; \ + ESUM='16b1d9d75f22c157af04a1fd9c664324c7f4b5163c022b382a2f2e8897c1b0a2'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 6b5d6c9349..5b1e904581 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -1,11 +1,11 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v16.14.2 + NODE_VERSION=v16.15.1 RUN set -ex; \ mkdir -p /etc/resolvconf/resolv.conf.d; \ @@ -95,7 +95,7 @@ WORKDIR /opt/app ADD ./bundle-exporter/ /opt/app/ RUN set -ex; \ - yarn install; \ + yarn; \ npx playwright install chromium; CMD ["/usr/local/nodejs/bin/node", "app.js"] diff --git a/docker/images/Dockerfile.frontend b/docker/images/Dockerfile.frontend index 697a9c0768..f2708974e9 100644 --- a/docker/images/Dockerfile.frontend +++ b/docker/images/Dockerfile.frontend @@ -1,4 +1,4 @@ -FROM nginx:latest +FROM nginx:1.23 LABEL maintainer="Andrey Antukh " ADD ./bundle-frontend/ /var/www/app/ From 50f30eb12f701fcee020c18e91ff6e1343de8bf8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Jul 2022 16:52:10 +0200 Subject: [PATCH 67/86] :sparkles: Add the ability to embed assets on export binfile --- backend/resources/templates/debug.tmpl | 9 ++- backend/src/app/http/debug.clj | 5 +- backend/src/app/rpc/commands/binfile.clj | 77 +++++++++++++++++++++--- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/backend/resources/templates/debug.tmpl b/backend/resources/templates/debug.tmpl index 40f2e3c5bb..56093e708d 100644 --- a/backend/resources/templates/debug.tmpl +++ b/backend/resources/templates/debug.tmpl @@ -56,7 +56,12 @@ Debug Main Page
    - + +
    + +
    + +
    @@ -100,7 +105,7 @@ Debug Main Page
    - +
    Do not break on index lookup erros (remap operation). diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 03c2a090d2..4753d08937 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -267,7 +267,8 @@ [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] (let [file-id (some-> params :file-id parse-uuid) libs? (contains? params :includelibs) - clone? (contains? params :clone)] + clone? (contains? params :clone) + embed? (contains? params :embedassets)] (when-not file-id (ex/raise :type :validation @@ -275,6 +276,7 @@ (let [path (-> cfg (assoc ::binf/file-id file-id) + (assoc ::binf/embed-assets? embed?) (assoc ::binf/include-libraries? libs?) (binf/export!))] (if clone? @@ -283,6 +285,7 @@ (assoc cfg ::binf/input path ::binf/overwrite? false + ::binf/ignore-index-errors? true ::binf/profile-id profile-id ::binf/project-id project-id)) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index dc46177e39..621b8fae71 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -330,6 +330,58 @@ (with-open [^AutoCloseable conn (db/open pool)] (db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)]))) +(defn- embed-file-assets + [pool {:keys [id] :as file}] + (letfn [(walk-map-form [state form] + (cond + (uuid? (:fill-color-ref-file form)) + (do + (vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)]) + (assoc form :fill-color-ref-file id)) + + (uuid? (:stroke-color-ref-file form)) + (do + (vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)]) + (assoc form :stroke-color-ref-file id)) + + (uuid? (:typography-ref-file form)) + (do + (vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)]) + (assoc form :typography-ref-file id)) + + (uuid? (:component-file form)) + (do + (vswap! state conj [(:component-file form) :components (:component-id form)]) + (assoc form :component-file id)) + + :else + form)) + + (process-group-of-assets [data [lib-id items]] + ;; NOTE: there are a posibility that shape refers to a not + ;; existing file because the file was removed. In this + ;; case we just ignore the asset. + (if-let [lib (retrieve-file pool lib-id)] + (reduce #(process-asset %1 lib %2) data items) + data)) + + (process-asset [data lib [bucket asset-id]] + (let [asset (get-in lib [:data bucket asset-id]) + ;; Add a special case for colors that need to have + ;; correctly set the :file-id prop (pending of the + ;; refactor that will remove it). + asset (cond-> asset + (= bucket :colors) (assoc :file-id id))] + (update data bucket assoc asset-id asset)))] + + (update file :data (fn [data] + (let [assets (volatile! [])] + (walk/postwalk #(cond->> % (map? %) (walk-map-form assets)) data) + (->> (deref assets) + (filter #(as-> (first %) $ (and (uuid? $) (not= $ id)))) + (d/group-by first rest) + (reduce process-group-of-assets data))))))) + (defn write-export! "Do the exportation of a speficied file in custom penpot binary format. There are some options available for customize the output: @@ -337,20 +389,28 @@ `::include-libraries?`: additionaly to the specified file, all the linked libraries also will be included (including transitive dependencies). + + `::embed-assets?`: instead of including the libraryes, embedd in the + same file library all assets used from external libraries. " - [{:keys [pool storage ::output ::file-id ::include-libraries?]}] - (let [libs (when include-libraries? - (retrieve-libraries pool file-id)) - rels (when include-libraries? - (retrieve-library-relations pool (cons file-id libs))) + [{:keys [pool storage ::output ::file-id ::include-libraries? ::embed-assets?]}] + + (when (and include-libraries? embed-assets?) + (ex/raise :type :restriction + :code :mutual-exclusive-options-provided + :hint "the `include-libraries?` option is mutually exclusive with `embed-assets?`")) + + (let [libs (when include-libraries? (retrieve-libraries pool file-id)) + rels (when include-libraries? (retrieve-library-relations pool (cons file-id libs))) files (into [file-id] libs) - sids (atom #{})] + sids (volatile! #{})] ;; Write header with metadata (l/debug :hint "exportation summary" :files (count files) :rels (count rels) + :embed-assets? embed-assets? :include-libs? include-libraries? ::l/async false) @@ -363,12 +423,13 @@ (l/debug :hint "write section" :section :v1/files :total (count files) ::l/async false) (write-label! output :v1/files) (doseq [file-id files] - (let [file (retrieve-file pool file-id) + (let [file (cond->> (retrieve-file pool file-id) + embed-assets? (embed-file-assets pool)) media (retrieve-file-media pool file)] ;; Collect all storage ids for later write them all under ;; specific storage objects section. - (swap! sids into (sequence storage-object-id-xf media)) + (vswap! sids into (sequence storage-object-id-xf media)) (l/trace :hint "write penpot file" :id file-id From c405e9a7a389b66d864907858cd42f75fc876878 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Jul 2022 11:26:48 +0200 Subject: [PATCH 68/86] :fire: Remove unused code --- .../src/app/main/data/workspace/colors.cljs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index d1243cbea0..904412bd23 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -14,34 +14,10 @@ [app.main.data.workspace.layout :as layout] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] - [app.main.repo :as rp] [app.util.color :as uc] [beicon.core :as rx] [potok.core :as ptk])) -(def clear-color-for-rename - (ptk/reify ::clear-color-for-rename - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :color-for-rename] nil)))) - -(declare rename-color-result) - -(defn rename-color - [file-id color-id name] - (ptk/reify ::rename-color - ptk/WatchEvent - (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] - (ptk/reify ::rename-color-result - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace-file :colors] #(d/replace-by-id % color))))) - (defn change-palette-selected "Change the library used by the general palette tool" [selected] From ade41f77f3fe7d0d202d27fcd3072f646a630351 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Jul 2022 11:27:20 +0200 Subject: [PATCH 69/86] :paperclip: Add some notes to assets ns in sidebar --- .../app/main/ui/workspace/sidebar/assets.cljs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 71c5fb260d..1352747f0f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -47,14 +47,10 @@ [potok.core :as ptk] [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. -;; +;; NOTE: TODO: for avoid too many arguments, I think we can use react +;; context variables for pass to the down tree all the common +;; variables that are defined on the MAIN container/box component. + ;; 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 @@ -205,8 +201,6 @@ create-typed-assets-group (partial create-typed-assets-group components-to-group)] (modal/show! :name-group-dialog {:accept create-typed-assets-group})))))) - - (defn- on-drag-enter-asset [event asset dragging? selected-assets selected-assets-paths] (when (and @@ -275,8 +269,6 @@ (:id target-asset) (cph/merge-path-item prefix (:name target-asset)))))))) - - ;; ---- Common blocks ---- (def auto-pos-menu-state {:open? false @@ -1090,6 +1082,7 @@ :else (:value color)) ;; TODO: looks like the first argument is not necessary + ;; TODO: this code should be out of this UI component apply-color (fn [_ event] (let [objects (wsh/lookup-page-objects @st/state) From d90b4370fb0646bacc62449ab39f5192b30e0cb9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Jul 2022 14:36:37 +0200 Subject: [PATCH 70/86] :paperclip: Update default devenv logging configuration --- backend/resources/log4j2-devenv.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 51f565b6fc..ff13a6c43f 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -24,12 +24,12 @@ - + - - - - + + + + From f4f58bc1637f87420ff6ee5b793aad0a03d766ae Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Jul 2022 15:39:25 +0200 Subject: [PATCH 71/86] :sparkles: Add parameters validation to binfile write-export! fn --- backend/src/app/rpc/commands/binfile.clj | 29 ++++++++++++++++++++---- backend/src/app/util/bytes.clj | 12 ++++++++++ common/src/app/common/data.cljc | 3 +++ common/src/app/common/spec.cljc | 27 ++++++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 621b8fae71..abdb440818 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -394,12 +394,31 @@ same file library all assets used from external libraries. " - [{:keys [pool storage ::output ::file-id ::include-libraries? ::embed-assets?]}] + [{:keys [pool storage ::output ::file-id ::include-libraries? ::embed-assets?] :as options}] - (when (and include-libraries? embed-assets?) - (ex/raise :type :restriction - :code :mutual-exclusive-options-provided - :hint "the `include-libraries?` option is mutually exclusive with `embed-assets?`")) + (us/assert! :spec ::db/pool :val pool) + (us/assert! :spec ::sto/storage :val storage) + + (us/assert! + :expr (uuid? file-id) + :hint "`file-id` should be an uuid") + + (us/assert! + :expr (bs/data-output-stream? output) + :hint "`output` should be an instance of OutputStream") + + (us/assert! + :expr (d/boolean-or-nil? include-libraries?) + :hint "invalid value provided for `include-libraries?` option, expected boolean") + + (us/assert! + :expr (d/boolean-or-nil? embed-assets?) + :hint "invalid value provided for `embed-assets?` option, expected boolean") + + (us/assert! + :always? true + :expr (not (and include-libraries? embed-assets?)) + :hint "the `include-libraries?` and `embed-assets?` are mutally excluding options") (let [libs (when include-libraries? (retrieve-libraries pool file-id)) rels (when include-libraries? (retrieve-library-relations pool (cons file-id libs))) diff --git a/backend/src/app/util/bytes.clj b/backend/src/app/util/bytes.clj index 5be58f4054..1a36151abd 100644 --- a/backend/src/app/util/bytes.clj +++ b/backend/src/app/util/bytes.clj @@ -28,6 +28,18 @@ (def ^:const default-buffer-size (:xnio/buffer-size yt/defaults)) +(defn input-stream? + [s] + (instance? InputStream s)) + +(defn output-stream? + [s] + (instance? OutputStream s)) + +(defn data-output-stream? + [s] + (instance? DataOutputStream s)) + (defn copy! [src dst & {:keys [offset size buffer-size] :or {offset 0 buffer-size default-buffer-size}}] diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 874ee56b92..baa0f2fe33 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -23,6 +23,9 @@ #?(:clj (:import linked.set.LinkedSet))) +(def boolean-or-nil? + (some-fn nil? boolean?)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Structures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index 5fc4b8d9f6..ffc24188e1 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -261,6 +261,33 @@ message (str "spec verify: '" (pr-str spec) "'")] `(spec-assert* ~spec ~x ~message ~context))) +(defmacro assert! + "General purpose assertion macro." + [& {:keys [expr spec always? hint val]}] + (cond + (some? spec) + (let [context (if-let [nsdata (:ns &env)] + {:ns (str (:name nsdata)) + :name (pr-str spec) + :line (:line &env) + :file (:file (:meta nsdata))} + {:ns (str (ns-name *ns*)) + :name (pr-str spec) + :line (:line (meta &form))}) + message (or hint (str "spec assert: " (pr-str spec)))] + (when (or always? *assert*) + `(spec-assert* ~spec ~val ~message ~context))) + + (some? expr) + (let [message (or hint (str "expr assert: " (pr-str expr)))] + (when (or always? *assert*) + `(when-not ~expr + (ex/raise :type :assertion + :code :expr-validation + :hint ~message)))) + + :else nil)) + ;; --- Public Api (defn conform From d0329531218c79d453cbf25ef9bfa0cf324772ea Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 Jul 2022 15:58:32 +0200 Subject: [PATCH 72/86] :sparkles: Enable exporte multiple files in binfile format --- backend/resources/templates/debug.tmpl | 7 +++++-- backend/resources/templates/styles.css | 9 +++++++++ backend/src/app/http/debug.clj | 17 ++++++++++------- backend/src/app/rpc/commands/binfile.clj | 20 +++++++++++--------- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/backend/resources/templates/debug.tmpl b/backend/resources/templates/debug.tmpl index 56093e708d..7479dd9827 100644 --- a/backend/resources/templates/debug.tmpl +++ b/backend/resources/templates/debug.tmpl @@ -50,8 +50,11 @@ Debug Main Page file.
    -
    - +
    + + + +
    diff --git a/backend/resources/templates/styles.css b/backend/resources/templates/styles.css index 74b19390a3..32fcea8883 100644 --- a/backend/resources/templates/styles.css +++ b/backend/resources/templates/styles.css @@ -168,3 +168,12 @@ form .row { padding: 5px 0; } +.set-of-inputs { + flex-direction: column; + display: flex; +} + +.set-of-inputs input:not(:last-child) { + margin-bottom: 3px; +} + diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 4753d08937..1e04be78fc 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -265,17 +265,20 @@ (defn export-handler [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] - (let [file-id (some-> params :file-id parse-uuid) - libs? (contains? params :includelibs) - clone? (contains? params :clone) - embed? (contains? params :embedassets)] - (when-not file-id + (let [file-ids (->> (:file-ids params) + (remove empty?) + (map parse-uuid)) + libs? (contains? params :includelibs) + clone? (contains? params :clone) + embed? (contains? params :embedassets)] + + (when-not (seq file-ids) (ex/raise :type :validation :code :missing-arguments)) (let [path (-> cfg - (assoc ::binf/file-id file-id) + (assoc ::binf/file-ids file-ids) (assoc ::binf/embed-assets? embed?) (assoc ::binf/include-libraries? libs?) (binf/export!))] @@ -297,7 +300,7 @@ (yrs/response :status 200 :headers {"content-type" "application/octet-stream" - "content-disposition" (str "attachmen; filename=" file-id ".penpot")} + "content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")} :body (io/input-stream path)))))) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index abdb440818..6a2fe91457 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -306,7 +306,7 @@ SELECT fl.id, fl.deleted_at FROM file AS fl JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id) - WHERE flr.file_id = ?::uuid + WHERE flr.file_id = ANY(?) UNION SELECT fl.id, fl.deleted_at FROM file AS fl @@ -318,8 +318,10 @@ WHERE l.deleted_at IS NULL OR l.deleted_at > now();") (defn- retrieve-libraries - [pool file-id] - (map :id (db/exec! pool [sql:file-libraries file-id]))) + [pool ids] + (with-open [^AutoCloseable conn (db/open pool)] + (let [ids (db/create-array conn "uuid" ids)] + (map :id (db/exec! pool [sql:file-libraries ids]))))) (def ^:private sql:file-library-rels "SELECT * FROM file_library_rel @@ -394,14 +396,14 @@ same file library all assets used from external libraries. " - [{:keys [pool storage ::output ::file-id ::include-libraries? ::embed-assets?] :as options}] + [{:keys [pool storage ::output ::file-ids ::include-libraries? ::embed-assets?] :as options}] (us/assert! :spec ::db/pool :val pool) (us/assert! :spec ::sto/storage :val storage) (us/assert! - :expr (uuid? file-id) - :hint "`file-id` should be an uuid") + :expr (every? uuid? file-ids) + :hint "`files` should be a vector of uuid") (us/assert! :expr (bs/data-output-stream? output) @@ -420,9 +422,9 @@ :expr (not (and include-libraries? embed-assets?)) :hint "the `include-libraries?` and `embed-assets?` are mutally excluding options") - (let [libs (when include-libraries? (retrieve-libraries pool file-id)) - rels (when include-libraries? (retrieve-library-relations pool (cons file-id libs))) - files (into [file-id] libs) + (let [libs (when include-libraries? (retrieve-libraries pool file-ids)) + files (into file-ids libs) + rels (when include-libraries? (retrieve-library-relations pool file-ids)) sids (volatile! #{})] ;; Write header with metadata From 17645bb2a7d3763912ca4d42c61ae5ca6b837e58 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 5 Jul 2022 12:35:28 +0200 Subject: [PATCH 73/86] :sparkles: Frontend support for binary files --- backend/src/app/rpc/commands/binfile.clj | 49 ++++++-- frontend/src/app/main/repo.cljs | 5 +- .../src/app/main/ui/dashboard/export.cljs | 8 +- .../src/app/main/ui/dashboard/file_menu.cljs | 53 ++++++--- .../src/app/main/ui/dashboard/import.cljs | 42 ++++--- .../src/app/main/ui/workspace/header.cljs | 83 +++++++------ frontend/src/app/worker/export.cljs | 31 ++++- frontend/src/app/worker/import.cljs | 111 +++++++++++++----- frontend/translations/en.po | 12 ++ frontend/translations/es.po | 12 ++ 10 files changed, 281 insertions(+), 125 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 6a2fe91457..5dfd9ff373 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -16,7 +16,8 @@ [app.config :as cf] [app.db :as db] [app.media :as media] - [app.rpc.queries.files :refer [decode-row]] + [app.rpc.queries.files :refer [decode-row check-edition-permissions!]] + [app.rpc.queries.profile :as profile] [app.storage :as sto] [app.storage.tmp :as tmp] [app.tasks.file-gc] @@ -84,10 +85,10 @@ [v type] `(let [expected# (get-mark ~type) val# (long ~v)] - (when (not= val# expected#) - (ex/raise :type :validation - :code :unexpected-mark - :hint (format "received mark %s, expected %s" val# expected#))))) + (when (not= val# expected#) + (ex/raise :type :validation + :code :unexpected-mark + :hint (format "received mark %s, expected %s" val# expected#))))) (defmacro assert-label [expr label] @@ -759,8 +760,8 @@ (finally (l/info :hint "exportation finished" :export-id id - :elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms") - :cause @cs))))) + :elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms") + :cause @cs))))) (defn import! [{:keys [::input] :as cfg}] @@ -787,12 +788,38 @@ (s/def ::file-id ::us/uuid) (s/def ::profile-id ::us/uuid) +(s/def ::include-libraries? ::us/boolean) +(s/def ::embed-assets? ::us/boolean) (s/def ::export-binfile - (s/keys :req-un [::profile-id ::file-id])) + (s/keys :req-un [::profile-id ::file-id ::include-libraries? ::embed-assets?])) -#_:clj-kondo/ignore (sv/defmethod ::export-binfile "Export a penpot file in a binary format." - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - {:hello "world"}) + [{:keys [pool] :as cfg} {:keys [profile-id file-id include-libraries? embed-assets?] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id file-id) + (let [path (export! (assoc cfg + ::file-ids [file-id] + ::embed-assets? embed-assets? + ::include-libraries? include-libraries?))] + (with-meta {} + {:transform-response (fn [_ response] + (assoc response + :body (io/input-stream path) + :headers {"content-type" "application/octet-stream"}))})))) + +(s/def ::input ::media/upload) + +(s/def ::import-binfile + (s/keys :req-un [::profile-id ::input])) + +(sv/defmethod ::import-binfile + "Import a penpot file in a binary format." + [{:keys [pool] :as cfg} {:keys [profile-id input] :as params}] + (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] + (import! (assoc cfg + ::input (:path input) + ::project-id project-id + ::ignore-index-errors? true)))) + diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index b1d7b581cb..d1c3c1e1fe 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -76,11 +76,12 @@ (defn- send-command! "A simple helper for a common case of sending and receiving transit data to the penpot mutation api." - [id params] + [id {:keys [blob? form-data?] :as params}] (->> (http/send! {:method :post :uri (u/join base-uri "api/rpc/command/" (name id)) :credentials "include" - :body (http/transit-data params)}) + :body (if form-data? (http/form-data params) (http/transit-data params)) + :response-type (if blob? :blob :text)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs index 5a1425ba6b..58f8d957d5 100644 --- a/frontend/src/app/main/ui/dashboard/export.cljs +++ b/frontend/src/app/main/ui/dashboard/export.cljs @@ -51,7 +51,7 @@ (mf/defc export-dialog {::mf/register modal/components ::mf/register-as :export} - [{:keys [team-id files has-libraries?]}] + [{:keys [team-id files has-libraries? binary?]}] (let [state (mf/use-state {:status :prepare :files (->> files (mapv #(assoc % :loading? true)))}) selected-option (mf/use-state :all) @@ -60,10 +60,11 @@ (fn [] (swap! state assoc :status :exporting) (->> (uw/ask-many! - {:cmd :export-file + {:cmd (if binary? :export-binary-file :export-standard-file) :team-id team-id :export-type @selected-option - :files (->> files (mapv :id))}) + :files files + }) (rx/delay-emit 1000) (rx/subs (fn [msg] @@ -73,6 +74,7 @@ (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] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index bbcdb74e05..7903f591ae 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -158,26 +158,38 @@ :on-accept del-shared}))) on-export-files + (fn [event-name binary?] + (st/emit! (ptk/event ::ev/event {::ev/name event-name + ::ev/origin "dashboard" + :num-files (count files)})) + + (->> (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 + :binary? binary?})))))) + + on-export-binary-files (mf/use-callback (mf/deps files current-team-id) (fn [_] - (st/emit! (ptk/event ::ev/event {::ev/name "export-files" - ::ev/origin "dashboard" - :num-files (count files)})) - (->> (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}))))))) + (on-export-files "export-binary-files" true))) + + on-export-standard-files + (mf/use-callback + (mf/deps files current-team-id) + (fn [_] + (on-export-files "export-standard-files" false))) ;; NOTE: this is used for detect if component is still mounted mounted-ref (mf/use-ref true)] @@ -210,7 +222,8 @@ [[(tr "dashboard.duplicate-multi" file-count) on-duplicate nil "duplicate-multi"] (when (or (seq current-projects) (seq other-teams)) [(tr "dashboard.move-to-multi" file-count) nil sub-options "move-to-multi"]) - [(tr "dashboard.export-multi" file-count) on-export-files] + [(tr "dashboard.export-binary-multi" file-count) on-export-binary-files] + [(tr "dashboard.export-standard-multi" file-count) on-export-standard-files] [:separator] [(tr "labels.delete-multi-files" file-count) on-delete nil "delete-multi-files"]] @@ -222,7 +235,9 @@ (if (:is-shared file) [(tr "dashboard.remove-shared") on-del-shared nil "file-del-shared"] [(tr "dashboard.add-shared") on-add-shared nil "file-add-shared"]) - [(tr "dashboard.export-single") on-export-files nil "file-export"] + [:separator] + [(tr "dashboard.download-binary-file") on-export-binary-files nil "download-binary-file"] + [(tr "dashboard.download-standard-file") on-export-standard-files nil "download-standard-file"] [:separator] [(tr "labels.delete") on-delete nil "file-delete"]])] diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 6fa2f4697b..93b48d9870 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -49,7 +49,7 @@ (let [on-file-selected (use-import-file project-id on-finish-import)] [:form.import-file - [:& file-uploader {:accept ".penpot" + [:& file-uploader {:accept ".penpot,.zip" :multi true :ref external-ref :on-selected on-file-selected}]])) @@ -78,19 +78,20 @@ (= uri (:uri file)) (assoc :status :analyze-error)))))) -(defn set-analyze-result [files uri data] +(defn set-analyze-result [files uri type data] (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) replace-file (fn [file] - (if (and (= uri (:uri file) ) + (if (and (= uri (:uri file)) (= (:status file) :analyzing)) (->> (:files data) - (remove (comp existing-files? first) ) + (remove (comp existing-files? first)) (mapv (fn [[file-id file-data]] (-> file-data (assoc :file-id file-id :status :ready - :uri uri))))) + :uri uri + :type type))))) [file]))] (into [] (mapcat replace-file) files))) @@ -139,7 +140,7 @@ (str message))) (mf/defc import-entry - [{:keys [state file editing?]}] + [{:keys [state file editing? can-be-deleted?]}] (let [loading? (or (= :analyzing (:status file)) (= :importing (:status file))) @@ -206,9 +207,11 @@ [: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]]] + [:div.edit-entry-buttons + (when (= "application/zip" (:type file)) + [:button {:on-click handle-edit-entry} i/pencil]) + (when can-be-deleted? + [:button {:on-click handle-remove-entry} i/trash])]] (cond analyze-error? @@ -245,21 +248,20 @@ (fn [files] (->> (uw/ask-many! {:cmd :analyze-import - :files (->> files (mapv :uri))}) + :files files}) (rx/delay-emit emit-delay) (rx/subs - (fn [{:keys [uri data error] :as msg}] + (fn [{:keys [uri data error type] :as msg}] (log/debug :uri uri :data data :error error) (if (some? error) (swap! state update :files set-analyze-error uri) - (swap! state update :files set-analyze-result uri data))))))) + (swap! state update :files set-analyze-result uri type data))))))) import-files (mf/use-callback (fn [project-id files] (st/emit! (ptk/event ::ev/event {::ev/name "import-files" :num-files (count files)})) - (->> (uw/ask-many! {:cmd :import-files :project-id project-id @@ -281,7 +283,7 @@ (mf/deps project-id (:files @state)) (fn [event] (dom/prevent-default event) - (let [files (->> @state :files (filterv #(= :ready (:status %))))] + (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] (import-files project-id files)) (swap! state @@ -300,7 +302,8 @@ warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0) - pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)] + pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0) + files (->> (:files @state) (filterv (comp not :deleted?)))] (mf/use-effect (fn [] @@ -333,12 +336,13 @@ [: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)))] + (for [file files] + (let [editing? (and (some? (:file-id file)) + (= (:file-id file) (:editing @state)))] [:& import-entry {:state state :file file - :editing? editing?}]))] + :editing? editing? + :can-be-deleted? (> (count files) 1)}]))] [:div.modal-footer [:div.action-buttons diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 2b4fd97288..5f93e4def3 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -121,26 +121,26 @@ (mf/use-fn (mf/deps file) #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.add-shared-confirm.message" (:name file)) - :hint (tr "modals.add-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.add-shared-confirm.accept") - :accept-style :primary - :on-accept add-shared-fn}))) + {:type :confirm + :message "" + :title (tr "modals.add-shared-confirm.message" (:name file)) + :hint (tr "modals.add-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.add-shared-confirm.accept") + :accept-style :primary + :on-accept add-shared-fn}))) on-remove-shared (mf/use-fn (mf/deps file) #(st/emit! (modal/show - {:type :confirm - :message "" - :title (tr "modals.remove-shared-confirm.message" (:name file)) - :hint (tr "modals.remove-shared-confirm.hint") - :cancel-label :omit - :accept-label (tr "modals.remove-shared-confirm.accept") - :on-accept del-shared-fn}))) + {:type :confirm + :message "" + :title (tr "modals.remove-shared-confirm.message" (:name file)) + :hint (tr "modals.remove-shared-confirm.hint") + :cancel-label :omit + :accept-label (tr "modals.remove-shared-confirm.accept") + :on-accept del-shared-fn}))) handle-blur (fn [_] (let [value (-> edit-input-ref mf/ref-val dom/get-value)] @@ -160,27 +160,38 @@ (st/emit! (de/show-workspace-export-dialog)))) on-export-file + (fn [event-name binary?] + (st/emit! (ptk/event ::ev/event {::ev/name event-name + ::ev/origin "workspace" + :num-files 1})) + + (->> (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 + :binary? binary?})))))) + + on-export-binary-file (mf/use-callback (mf/deps file team-id) (fn [_] - (st/emit! (ptk/event ::ev/event {::ev/name "export-files" - ::ev/origin "workspace" - :num-files 1})) + (on-export-file "export-binary-files" true))) - (->> (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}))))))) + on-export-standard-file + (mf/use-callback + (mf/deps file team-id) + (fn [_] + (on-export-file "export-standard-files" false))) on-export-frames (mf/use-callback @@ -274,10 +285,12 @@ [:li.export-file {:on-click on-export-shapes} [:span (tr "dashboard.export-shapes")] [:span.shortcut (sc/get-tooltip :export-shapes)]] - [:li.export-file {:on-click on-export-file} - [:span (tr "dashboard.export-single")]] + [:li.separator.export-file {:on-click on-export-binary-file} + [:span (tr "dashboard.download-binary-file")]] + [:li.export-file {:on-click on-export-standard-file} + [:span (tr "dashboard.download-standard-file")]] (when (seq frames) - [:li.export-file {:on-click on-export-frames} + [:li.separator.export-file {:on-click on-export-frames} [:span (tr "dashboard.export-frames")]])]] [:& dropdown {:show (= @show-sub-menu? :edit) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 6a568b61be..0e48fe922c 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -450,13 +450,34 @@ (->> (uz/compress-files data) (rx/map #(vector (get files file-id) %))))))))) -(defmethod impl/handler :export-file +(defmethod impl/handler :export-binary-file + [{:keys [files export-type] :as message}] + (->> (rx/from files) + (rx/mapcat + (fn [file] + (->> (rp/command! :export-binfile {:file-id (:id file) + :include-libraries? (= export-type :all) + :embed-assets? (= export-type :merge) + :blob? true}) + (rx/map #(hash-map :type :finish + :file-id (:id file) + :filename (:name file) + :mtype "application/penpot" + :description "Penpot export (*.penpot)" + :uri (wapi/create-uri (wapi/create-blob %)))) + (rx/catch + (fn [err] + (rx/of {:type :error + :error (str err) + :file-id (:id file)})))))))) + +(defmethod impl/handler :export-standard-file [{:keys [team-id files export-type] :as message}] (->> (rx/from files) (rx/mapcat (fn [file] - (->> (export-file team-id file export-type) + (->> (export-file team-id (:id file) export-type) (rx/map (fn [value] (if (contains? value :type) @@ -465,11 +486,11 @@ {:type :finish :file-id (:id file) :filename (:name file) - :mtype "application/penpot" - :description "Penpot export (*.penpot)" + :mtype "application/zip" + :description "Penpot export (*.zip)" :uri (wapi/create-uri export-blob)})))) (rx/catch (fn [err] (rx/of {:type :error :error (str err) - :file-id file})))))))) + :file-id (:id file)})))))))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 9038d81815..6e8cc8facc 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -7,6 +7,7 @@ (ns app.worker.import (:refer-clojure :exclude [resolve]) (:require + ["jszip" :as zip] [app.common.data :as d] [app.common.file-builder :as fb] [app.common.geom.point :as gpt] @@ -20,6 +21,7 @@ [app.util.http :as http] [app.util.import.parser :as cip] [app.util.json :as json] + [app.util.webapi :as wapi] [app.util.zip :as uz] [app.worker.impl :as impl] [beicon.core :as rx] @@ -519,48 +521,95 @@ (rx/flat-map link-file-libraries) (rx/ignore))))) +(defn parse-mtype [ba] + (let [u8 (js/Uint8Array. ba 0 4) + sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))] + (case sg + "120 113 3 4" "application/zip" + "application/octet-stream"))) + (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 %)}))))))) + (fn [file] + (let [st (->> (http/send! + {:uri (:uri file) + :response-type :blob + :method :get}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-array-buffer) + (rx/map (fn [data] + {:type (parse-mtype data) + :uri (:uri file) + :body data})))] + (->> (rx/merge + (->> st + (rx/filter (fn [data] (= "application/zip" (:type data)))) + (rx/flat-map #(zip/loadAsync (:body %))) + (rx/flat-map #(get-file {:zip %} :manifest)) + (rx/map (comp d/kebab-keys cip/string->uuid)) + (rx/map #(hash-map :uri (:uri file) :data % :type "application/zip"))) + (->> st + (rx/filter (fn [data] (= "application/octet-stream" (:type data)))) + (rx/map (fn [_] + (let [file-id (uuid/next)] + {:uri (:uri file) + :data {:name (:name file) + :file-id file-id + :files {file-id {:name (:name file)}} + :status :ready} + :type "application/octet-stream"}))))) + (rx/catch #(rx/of {:uri (:uri file) :error (.-message %)})))))))) (defmethod impl/handler :import-files [{:keys [project-id files]}] (let [context {:project-id project-id - :resolve (resolve-factory)}] + :resolve (resolve-factory)} + zip-files (filter #(= "application/zip" (:type %)) files) + binary-files (filter #(= "application/octet-stream" (:type %)) files)] - (->> (create-files context files) - (rx/flat-map - (fn [[file data]] - (->> (uz/load-from-url (:uri data)) - (rx/map #(-> context (assoc :zip %) (merge data))) - (rx/merge-map - (fn [context] - ;; process file retrieves a stream that will emit progress notifications - ;; and other that will emit the files once imported - (let [[progress-stream file-stream] (process-file context file)] - (rx/merge progress-stream - (->> file-stream - (rx/map - (fn [file] - {:status :import-finish - :errors (:errors file) - :file-id (:file-id data)}))))))) - (rx/catch (fn [cause] - (log/error :hint (ex-message cause) :file-id (:file-id data) :cause cause) - (rx/of {:status :import-error - :file-id (:file-id data) - :error (ex-message cause) - :error-data (ex-data cause)})))))) + (->> (rx/merge + (->> (create-files context zip-files) + (rx/flat-map + (fn [[file data]] + (->> (uz/load-from-url (:uri data)) + (rx/map #(-> context (assoc :zip %) (merge data))) + (rx/merge-map + (fn [context] + ;; process file retrieves a stream that will emit progress notifications + ;; and other that will emit the files once imported + (let [[progress-stream file-stream] (process-file context file)] + (rx/merge progress-stream + (->> file-stream + (rx/map + (fn [file] + {:status :import-finish + :errors (:errors file) + :file-id (:file-id data)}))))))) + (rx/catch (fn [cause] + (log/error :hint (ex-message cause) :file-id (:file-id data) :cause cause) + (rx/of {:status :import-error + :file-id (:file-id data) + :error (ex-message cause) + :error-data (ex-data cause)}))))))) + + (->> (rx/from binary-files) + (rx/flat-map + (fn [data] + (->> (http/send! + {:uri (:uri data) + :response-type :blob + :method :get}) + (rx/map :body) + (rx/mapcat #(rp/command! :import-binfile {:input % + :form-data? true})) + (rx/map + (fn [_] + {:status :import-finish + :file-id (:file-id data)}))))))) (rx/catch (fn [cause] (log/error :hint "unexpected error on import process" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f6969b443a..1977cb0920 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -309,6 +309,18 @@ msgstr "Export selection" msgid "dashboard.export-single" msgstr "Export Penpot file" +msgid "dashboard.download-binary-file" +msgstr "Download Penpot file (.penpot)" + +msgid "dashboard.download-standard-file" +msgstr "Download standard file (.svg + .json)" + +msgid "dashboard.export-binary-multi" +msgstr "Download %s Penpot files (.penpot)" + +msgid "dashboard.export-standard-multi" +msgstr "Download %s standard files (.svg + .json)" + msgid "dashboard.export.detail" msgstr "* Might include components, graphics, colors and/or typographies." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ad8af9bdf5..63dea9c57c 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -315,6 +315,18 @@ msgstr "Exportar selección" msgid "dashboard.export-single" msgstr "Exportar archivo Penpot" +msgid "dashboard.download-binary-file" +msgstr "Descargar archivo Penpot (.penpot)" + +msgid "dashboard.download-standard-file" +msgstr "Descargar archivo estándar (.svg + .json)" + +msgid "dashboard.export-binary-multi" +msgstr "Descargar %s archivos Penpot (.penpot)" + +msgid "dashboard.export-standard-multi" +msgstr "Descargar %s archivos estándar (.svg + .json)" + msgid "dashboard.export.detail" msgstr "* Pueden incluir components, gráficos, colores y/o tipografias." From 5c696851bfd8441ebb8ef71491d9fdb3d09c03df Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 7 Jul 2022 11:48:41 +0200 Subject: [PATCH 74/86] :paperclip: Update CHANGES.md file --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index ad83c5a041..2d9c127187 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ - View mode improvements to enable access and use in different conditions [Taiga #3023](https://tree.taiga.io/project/penpot/us/3023) - Improved share link options. Now you can allow non-team members to comment and/or inspect [Taiga #3056] (https://tree.taiga.io/project/penpot/us/3056) - Signin/Signup from shared link [Taiga #3472](https://tree.taiga.io/project/penpot/us/3472) +- Support for import/export binary format [Taiga #2991](https://tree.taiga.io/project/penpot/us/2991) ### :bug: Bugs fixed ### :arrow_up: Deps updates From cdcf3facd23304ae1cf543ddd1829fc7e4de9cd9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 09:09:20 +0200 Subject: [PATCH 75/86] :bug: Fix flags parsing order --- backend/src/app/config.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 79972b2c98..18a5e2e75a 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -83,7 +83,7 @@ ;; a server prop key where initial project is stored. :initial-project-skey "initial-project"}) -(s/def ::flags ::us/set-of-keywords) +(s/def ::flags ::us/vec-of-keywords) ;; DEPRECATED PROPERTIES (s/def ::registration-enabled ::us/boolean) From 2a9c8eb9af79800cd39d5264652fc7a393e7b044 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 09:09:51 +0200 Subject: [PATCH 76/86] :paperclip: Print parsed flags on start --- backend/src/app/config.clj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 18a5e2e75a..ba03fcf33c 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.flags :as flags] + [app.common.logging :as l] [app.common.spec :as us] [app.common.version :as v] [app.util.time :as dt] @@ -351,8 +352,12 @@ (str/trim)) "%version%"))) -(def ^:dynamic config (read-config)) -(def ^:dynamic flags (parse-flags config)) +(defonce ^:dynamic config (read-config)) + +(defonce ^:dynamic flags + (let [flags (parse-flags config)] + (l/info :hint "flags initialized" :flags (str/join "," (map name flags))) + flags)) (def deletion-delay (dt/duration {:days 7})) From f9447029f30ef57d6167f0b6e73bfcb615f67cc2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 09:10:27 +0200 Subject: [PATCH 77/86] :fire: Remove some deprecated config attrs --- backend/src/app/config.clj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index ba03fcf33c..10bc7e4b7c 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -87,10 +87,7 @@ (s/def ::flags ::us/vec-of-keywords) ;; DEPRECATED PROPERTIES -(s/def ::registration-enabled ::us/boolean) -(s/def ::smtp-enabled ::us/boolean) (s/def ::telemetry-enabled ::us/boolean) -(s/def ::asserts-enabled ::us/boolean) ;; END DEPRECATED (s/def ::audit-log-archive-uri ::us/string) @@ -274,7 +271,6 @@ ::public-uri ::redis-uri ::registration-domain-whitelist - ::registration-enabled ::rlimit-font ::rlimit-file-update ::rlimit-image @@ -285,7 +281,6 @@ ::sentry-trace-sample-rate ::smtp-default-from ::smtp-default-reply-to - ::smtp-enabled ::smtp-host ::smtp-password ::smtp-port From 5e2c1fb4cdc30936ed7bfbbdb7be321baa770b80 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 12:27:02 +0200 Subject: [PATCH 78/86] :tada: Add missing predicate on util/bytes ns --- backend/src/app/util/bytes.clj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/app/util/bytes.clj b/backend/src/app/util/bytes.clj index 1a36151abd..50a73d3350 100644 --- a/backend/src/app/util/bytes.clj +++ b/backend/src/app/util/bytes.clj @@ -36,6 +36,10 @@ [s] (instance? OutputStream s)) +(defn data-input-stream? + [s] + (instance? DataInputStream s)) + (defn data-output-stream? [s] (instance? DataOutputStream s)) From 4d55ed48608be62d8e10757256eb9d5df0809f89 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 12:24:12 +0200 Subject: [PATCH 79/86] :sparkles: Ensure vector ids on export debug handler --- backend/src/app/http/debug.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 1e04be78fc..1b1002dce5 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -268,7 +268,7 @@ (let [file-ids (->> (:file-ids params) (remove empty?) - (map parse-uuid)) + (mapv parse-uuid)) libs? (contains? params :includelibs) clone? (contains? params :clone) embed? (contains? params :embedassets)] From c02e8ff88364bcc746d0421d968a7cd7e00e7f81 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 12:24:42 +0200 Subject: [PATCH 80/86] :sparkles: Print the spec error explain to logging message --- backend/src/app/http/errors.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 5118dc5e96..852cd1cafc 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -71,7 +71,7 @@ [error request] (let [edata (ex-data error) explain (us/pretty-explain edata)] - (l/error ::l/raw (ex-message error) + (l/error ::l/raw (str (ex-message error) "\n" explain) ::l/context (get-context request) :cause error) (yrs/response :status 500 From 98190ed92daef28e4719df104bf11c30a94d819a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 12:27:31 +0200 Subject: [PATCH 81/86] :recycle: Improve the asserts framework --- common/src/app/common/spec.cljc | 145 +++++++++++++++++++------------- frontend/src/app/config.cljs | 4 +- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index ffc24188e1..e596179ad1 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -5,7 +5,7 @@ ;; Copyright (c) UXBOX Labs SL (ns app.common.spec - "Data manipulation and query helper functions." + "Data validation & assertion helpers." (:refer-clojure :exclude [assert bytes?]) #?(:cljs (:require-macros [app.common.spec :refer [assert]])) (:require @@ -31,8 +31,6 @@ (def max-safe-int (int 1e6)) (def min-safe-int (int -1e6)) -(def valid? s/valid?) - ;; --- Conformers (defn uuid-conformer @@ -220,73 +218,102 @@ (fn [s] (str/join "," s)))) -;; --- Macros +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MACROS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn spec-assert* - [spec val hint ctx] - (if (s/valid? spec val) - val - (let [data (s/explain-data spec val)] - (ex/raise :type :assertion - :code :spec-validation - :hint hint - ::ex/data (merge ctx data))))) +(defn explain-data + [spec value] + (s/explain-data spec value)) -(defmacro assert - "Development only assertion macro." - [spec x] - (when *assert* - (let [nsdata (:ns &env) - context (if nsdata - {:ns (str (:name nsdata)) - :name (pr-str spec) - :line (:line &env) - :file (:file (:meta nsdata))} - (let [mdata (meta &form)] - {:ns (str (ns-name *ns*)) - :name (pr-str spec) - :line (:line mdata)})) - message (str "spec assert: '" (pr-str spec) "'")] - `(spec-assert* ~spec ~x ~message ~context)))) +(defn valid? + [spec value] + (s/valid? spec value)) -(defmacro verify - "Always active assertion macro (does not obey to :elide-asserts)" - [spec x] - (let [nsdata (:ns &env) - context (when nsdata +(defmacro assert-expr* + "Auxiliar macro for expression assertion." + [expr hint] + `(when-not ~expr + (ex/raise :type :assertion + :code :expr-validation + :hint ~hint))) + +(defmacro assert-spec* + "Auxiliar macro for spec assertion." + [spec value hint] + (let [context (if-let [nsdata (:ns &env)] {:ns (str (:name nsdata)) :name (pr-str spec) :line (:line &env) - :file (:file (:meta nsdata))}) - message (str "spec verify: '" (pr-str spec) "'")] - `(spec-assert* ~spec ~x ~message ~context))) + :file (:file (:meta nsdata))} + {:ns (str (ns-name *ns*)) + :name (pr-str spec) + :line (:line (meta &form))}) + hint (or hint (str "spec assert: " (pr-str spec)))] + + `(if (valid? ~spec ~value) + ~value + (let [data# (explain-data ~spec ~value)] + (ex/raise :type :assertion + :code :spec-validation + :hint ~hint + ::ex/data (merge ~context data#)))))) + +(defmacro assert + "Is a spec specific assertion macro that only evaluates if *assert* + is true. DEPRECATED: it should be replaced by the new, general + purpose assert! macro." + [spec value] + (when *assert* + `(assert-spec* ~spec ~value nil))) + +(defmacro verify + "Is a spec specific assertion macro that evaluates always, + independently of *assert* value. DEPRECATED: should be replaced by + the new, general purpose `verify!` macro." + [spec value] + `(assert-spec* ~spec ~value nil)) (defmacro assert! "General purpose assertion macro." - [& {:keys [expr spec always? hint val]}] - (cond - (some? spec) - (let [context (if-let [nsdata (:ns &env)] - {:ns (str (:name nsdata)) - :name (pr-str spec) - :line (:line &env) - :file (:file (:meta nsdata))} - {:ns (str (ns-name *ns*)) - :name (pr-str spec) - :line (:line (meta &form))}) - message (or hint (str "spec assert: " (pr-str spec)))] - (when (or always? *assert*) - `(spec-assert* ~spec ~val ~message ~context))) + [& params] + ;; If we only receive two arguments, this means we use the simplified form + (let [pcnt (count params)] + (cond + ;; When we have a single argument, this means a simplified form + ;; of expr assertion + (= 1 pcnt) + (let [expr (first params) + hint (str "expr assert failed:" (pr-str expr))] + (when *assert* + `(assert-expr* ~expr ~hint))) - (some? expr) - (let [message (or hint (str "expr assert: " (pr-str expr)))] - (when (or always? *assert*) - `(when-not ~expr - (ex/raise :type :assertion - :code :expr-validation - :hint ~message)))) + ;; If we have two arguments, this can be spec or expr + ;; assertion. The spec assertion is determined if the first + ;; argument is a qualified keyword. + (= 2 pcnt) + (let [[spec-or-expr value-or-msg] params] + (if (qualified-keyword? spec-or-expr) + `(assert-spec* ~spec-or-expr ~value-or-msg nil) + `(assert-expr* ~spec-or-expr ~value-or-msg))) - :else nil)) + (= 3 pcnt) + (let [[spec value hint] params] + `(assert-spec* ~spec ~value ~hint)) + + :else + (let [{:keys [spec expr hint always? val]} params] + (when (or always? *assert*) + (if spec + `(assert-spec* ~spec ~val ~hint) + `(assert-expr* ~expr ~hint))))))) + +(defmacro verify! + "A variant of `assert!` macro that evaluates always, independently + of the *assert* value." + [& params] + (binding [*assert* true] + `(assert! ~@params))) ;; --- Public Api diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 47906059f8..1de776e7c6 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -111,11 +111,11 @@ ;; --- Helper Functions (defn ^boolean check-browser? [candidate] - (us/verify ::browser candidate) + (us/verify! ::browser candidate) (= candidate @browser)) (defn ^boolean check-platform? [candidate] - (us/verify ::platform candidate) + (us/verify! ::platform candidate) (= candidate @platform)) (defn resolve-profile-photo-url From d60f8490894a58f81ca92ceda723a26678c7950b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 12:26:46 +0200 Subject: [PATCH 82/86] :lipstick: Cosmetic refactor of binfile internal API impl --- backend/src/app/rpc/commands/binfile.clj | 375 ++++++++++++----------- frontend/src/app/main/repo.cljs | 14 +- frontend/src/app/worker/export.cljs | 3 +- frontend/src/app/worker/import.cljs | 3 +- 4 files changed, 206 insertions(+), 189 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 5dfd9ff373..40d080d624 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -16,7 +16,7 @@ [app.config :as cf] [app.db :as db] [app.media :as media] - [app.rpc.queries.files :refer [decode-row check-edition-permissions!]] + [app.rpc.queries.files :as files] [app.rpc.queries.profile :as profile] [app.storage :as sto] [app.storage.tmp :as tmp] @@ -41,7 +41,32 @@ (set! *warn-on-reflection* true) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; LOW LEVEL STREAM IO +;; VARS & DEFAULTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Threshold in MiB when we pass from using +;; in-memory byte-array's to use temporal files. +(def temp-file-threshold + (* 1024 1024 2)) + +;; Represents the current processing file-id on +;; export process. +(def ^:dynamic *file-id*) + +;; Stores all media file object references of +;; processed files on import process. +(def ^:dynamic *media*) + +;; Stores the objects index on reamping subprocess +;; part of the import process. +(def ^:dynamic *index*) + +;; Has the current connection used on the import +;; process. +(def ^:dynamic *conn*) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; LOW LEVEL STREAM IO API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:const buffer-size (:xnio/buffer-size yt/defaults)) @@ -62,18 +87,6 @@ :code :invalid-mark-id :hint (format "invalid mark id %s" id)))) -;; (defn buffered-output-stream -;; "Returns a buffered output stream that ignores flush calls. This is -;; needed because transit-java calls flush very aggresivelly on each -;; object write." -;; [^java.io.OutputStream os ^long chunk-size] -;; (proxy [java.io.BufferedOutputStream] [os (int chunk-size)] -;; ;; Explicitly do not forward flush -;; (flush []) -;; (close [] -;; (proxy-super flush) -;; (proxy-super close))) - (defmacro assert [expr hint] `(when-not ~expr @@ -98,7 +111,7 @@ :code :unexpected-label :hint (format "received label %s, expected %s" v# ~label))))) -;; --- PRIMITIVE +;; --- PRIMITIVE IO (defn write-byte! [^DataOutputStream output data] @@ -142,7 +155,7 @@ (swap! *position* + readed) readed)) -;; --- COMPOSITE +;; --- COMPOSITE IO (defn write-uuid! [^DataOutputStream output id] @@ -241,9 +254,6 @@ (copy-stream! output stream size)) -(def size-2mib - (* 1024 1024 2)) - (defn read-stream! [^DataInputStream input] (l/trace :fn "read-stream!" :position @*position* ::l/async false) @@ -257,15 +267,12 @@ :code :max-file-size-reached :hint (str/ffmt "unable to import storage object with size % bytes" s))) - (if (> s size-2mib) - ;; If size is more than 2MiB, use a temporal file. + (if (> s temp-file-threshold) (with-open [^OutputStream output (io/output-stream p)] (let [readed (bs/copy! input output :offset 0 :size s)] (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/async false) (swap! *position* + readed) [s p])) - - ;; If not, use an in-memory byte-array. [s (bs/read-as-bytes input :size s)]))) (defmacro assert-read-label! @@ -278,13 +285,15 @@ :hint (format "unxpected label found: %s, expected: %s" readed# expected#))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; HIGH LEVEL IMPL +;; API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- HELPERS + (defn- retrieve-file [pool file-id] (->> (db/query pool :file {:id file-id}) - (map decode-row) + (map files/decode-row) (first))) (def ^:private sql:file-media-objects @@ -333,29 +342,130 @@ (with-open [^AutoCloseable conn (db/open pool)] (db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)]))) -(defn- embed-file-assets - [pool {:keys [id] :as file}] - (letfn [(walk-map-form [state form] +;; --- EXPORT WRITTER + +(s/def ::output bs/output-stream?) +(s/def ::file-ids (s/every ::us/uuid :kind vector? :min-count 1)) +(s/def ::include-libraries? (s/nilable ::us/boolean)) +(s/def ::embed-assets? (s/nilable ::us/boolean)) + +(s/def ::write-export-options + (s/keys :req-un [::db/pool ::sto/storage] + :req [::output ::file-ids] + :opt [::include-libraries? ::embed-assets?])) + +(defn write-export! + "Do the exportation of a speficied file in custom penpot binary + format. There are some options available for customize the output: + + `::include-libraries?`: additionaly to the specified file, all the + linked libraries also will be included (including transitive + dependencies). + + `::embed-assets?`: instead of including the libraryes, embedd in the + same file library all assets used from external libraries. + " + + [{:keys [pool storage ::output ::file-ids ::include-libraries? ::embed-assets?] :as options}] + + (us/assert! ::write-export-options options) + + (us/verify! + :expr (not (and include-libraries? embed-assets?)) + :hint "the `include-libraries?` and `embed-assets?` are mutally excluding options") + + (letfn [(write-header [output files] + (let [sections [:v1/files :v1/rels :v1/sobjects] + mdata {:penpot-version (:full cf/version) + :sections sections + :files files}] + (write-header! output :version 1 :metadata mdata))) + + (write-files [output files sids] + (l/debug :hint "write section" :section :v1/files :total (count files) ::l/async false) + (write-label! output :v1/files) + (doseq [file-id files] + (let [file (cond-> (retrieve-file pool file-id) + embed-assets? (update :data embed-file-assets file-id)) + media (retrieve-file-media pool file)] + + ;; Collect all storage ids for later write them all under + ;; specific storage objects section. + (vswap! sids into (sequence storage-object-id-xf media)) + + (l/trace :hint "write penpot file" + :id file-id + :media (count media) + ::l/async false) + + (doto output + (write-obj! file) + (write-obj! media))))) + + (write-rels [output files] + (let [rels (when include-libraries? (retrieve-library-relations pool files))] + (l/debug :hint "write section" :section :v1/rels :total (count rels) ::l/async false) + (doto output + (write-label! :v1/rels) + (write-obj! rels)))) + + (write-sobjects [output sids] + (l/debug :hint "write section" + :section :v1/sobjects + :items (count sids) + ::l/async false) + + ;; Write all collected storage objects + (doto output + (write-label! :v1/sobjects) + (write-obj! sids)) + + (let [storage (media/configure-assets-storage storage)] + (doseq [id sids] + (let [{:keys [size] :as obj} @(sto/get-object storage id)] + (l/trace :hint "write sobject" :id id ::l/async false) + + (doto output + (write-uuid! id) + (write-obj! (meta obj))) + + (with-open [^InputStream stream @(sto/get-object-data storage obj)] + (let [written (write-stream! output stream size)] + (when (not= written size) + (ex/raise :type :validation + :code :mismatch-readed-size + :hint (str/ffmt "found unexpected object size; size=% written=%" size written))))))))) + + (embed-file-assets [data file-id] + (binding [*file-id* file-id] + (let [assets (volatile! [])] + (walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data) + (->> (deref assets) + (filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id)))) + (d/group-by first rest) + (reduce process-group-of-assets data))))) + + (walk-map-form [form state] (cond (uuid? (:fill-color-ref-file form)) (do (vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)]) - (assoc form :fill-color-ref-file id)) + (assoc form :fill-color-ref-file *file-id*)) (uuid? (:stroke-color-ref-file form)) (do (vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)]) - (assoc form :stroke-color-ref-file id)) + (assoc form :stroke-color-ref-file *file-id*)) (uuid? (:typography-ref-file form)) (do (vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)]) - (assoc form :typography-ref-file id)) + (assoc form :typography-ref-file *file-id*)) (uuid? (:component-file form)) (do (vswap! state conj [(:component-file form) :components (:component-id form)]) - (assoc form :component-file id)) + (assoc form :component-file *file-id*)) :else form)) @@ -374,133 +484,37 @@ ;; correctly set the :file-id prop (pending of the ;; refactor that will remove it). asset (cond-> asset - (= bucket :colors) (assoc :file-id id))] + (= bucket :colors) (assoc :file-id *file-id*))] (update data bucket assoc asset-id asset)))] - (update file :data (fn [data] - (let [assets (volatile! [])] - (walk/postwalk #(cond->> % (map? %) (walk-map-form assets)) data) - (->> (deref assets) - (filter #(as-> (first %) $ (and (uuid? $) (not= $ id)))) - (d/group-by first rest) - (reduce process-group-of-assets data))))))) + (with-open [output (bs/zstd-output-stream output :level 12)] + (with-open [output (bs/data-output-stream output)] + (let [libs (when include-libraries? (retrieve-libraries pool file-ids)) + files (into file-ids libs) + sids (volatile! #{})] -(defn write-export! - "Do the exportation of a speficied file in custom penpot binary - format. There are some options available for customize the output: + ;; Write header with metadata + (l/debug :hint "exportation summary" + :files (count files) + :embed-assets? embed-assets? + :include-libs? include-libraries? + ::l/async false) - `::include-libraries?`: additionaly to the specified file, all the - linked libraries also will be included (including transitive - dependencies). + (write-header output files) + (write-files output files sids) + (write-rels output files) + (write-sobjects output (vec @sids))))))) - `::embed-assets?`: instead of including the libraryes, embedd in the - same file library all assets used from external libraries. - " +(s/def ::project-id ::us/uuid) +(s/def ::input bs/input-stream?) +(s/def ::overwrite? (s/nilable ::us/boolean)) +(s/def ::migrate? (s/nilable ::us/boolean)) +(s/def ::ignore-index-errors? (s/nilable ::us/boolean)) - [{:keys [pool storage ::output ::file-ids ::include-libraries? ::embed-assets?] :as options}] - - (us/assert! :spec ::db/pool :val pool) - (us/assert! :spec ::sto/storage :val storage) - - (us/assert! - :expr (every? uuid? file-ids) - :hint "`files` should be a vector of uuid") - - (us/assert! - :expr (bs/data-output-stream? output) - :hint "`output` should be an instance of OutputStream") - - (us/assert! - :expr (d/boolean-or-nil? include-libraries?) - :hint "invalid value provided for `include-libraries?` option, expected boolean") - - (us/assert! - :expr (d/boolean-or-nil? embed-assets?) - :hint "invalid value provided for `embed-assets?` option, expected boolean") - - (us/assert! - :always? true - :expr (not (and include-libraries? embed-assets?)) - :hint "the `include-libraries?` and `embed-assets?` are mutally excluding options") - - (let [libs (when include-libraries? (retrieve-libraries pool file-ids)) - files (into file-ids libs) - rels (when include-libraries? (retrieve-library-relations pool file-ids)) - sids (volatile! #{})] - - ;; Write header with metadata - (l/debug :hint "exportation summary" - :files (count files) - :rels (count rels) - :embed-assets? embed-assets? - :include-libs? include-libraries? - ::l/async false) - - (let [sections [:v1/files :v1/rels :v1/sobjects] - mdata {:penpot-version (:full cf/version) - :sections sections - :files files}] - (write-header! output :version 1 :metadata mdata)) - - (l/debug :hint "write section" :section :v1/files :total (count files) ::l/async false) - (write-label! output :v1/files) - (doseq [file-id files] - (let [file (cond->> (retrieve-file pool file-id) - embed-assets? (embed-file-assets pool)) - media (retrieve-file-media pool file)] - - ;; Collect all storage ids for later write them all under - ;; specific storage objects section. - (vswap! sids into (sequence storage-object-id-xf media)) - - (l/trace :hint "write penpot file" - :id file-id - :media (count media) - ::l/async false) - - (doto output - (write-obj! file) - (write-obj! media)))) - - (l/debug :hint "write section" :section :v1/rels :total (count rels) ::l/async false) - (doto output - (write-label! :v1/rels) - (write-obj! rels)) - - (let [sids (into [] @sids)] - (l/debug :hint "write section" - :section :v1/sobjects - :items (count sids) - ::l/async false) - - ;; Write all collected storage objects - (doto output - (write-label! :v1/sobjects) - (write-obj! sids)) - - (let [storage (media/configure-assets-storage storage)] - (doseq [id sids] - (let [{:keys [size] :as obj} @(sto/get-object storage id)] - (l/trace :hint "write sobject" :id id ::l/async false) - - (doto output - (write-uuid! id) - (write-obj! (meta obj))) - - (with-open [^InputStream stream @(sto/get-object-data storage obj)] - (let [written (write-stream! output stream size)] - (when (not= written size) - (ex/raise :type :validation - :code :mismatch-readed-size - :hint (str/ffmt "found unexpected object size; size=% written=%" size written))))))))))) - - -;; Dynamic variables for importation process. - -(def ^:dynamic *files*) -(def ^:dynamic *media*) -(def ^:dynamic *index*) -(def ^:dynamic *conn*) +(s/def ::read-import-options + (s/keys :req-un [::db/pool ::sto/storage] + :req [::project-id ::input] + :opt [::overwrite? ::migrate? ::ignore-index-errors?])) (defn read-import! "Do the importation of the specified resource in penpot custom binary @@ -517,9 +531,11 @@ happen with broken files; defaults to: `false`. " - [{:keys [pool storage ::project-id ::ts ::input ::overwrite? ::migrate? ::ignore-index-errors?] - :or {overwrite? false migrate? false ts (dt/now)} - :as cfg}] + [{:keys [pool storage ::project-id ::timestamp ::input ::overwrite? ::migrate? ::ignore-index-errors?] + :or {overwrite? false migrate? false timestamp (dt/now)} + :as options}] + + (us/assert! ::read-import-options options) (letfn [(lookup-index [id] (if ignore-index-errors? @@ -608,12 +624,12 @@ (:modified-at params) (:data params)]))) - (read-files-section! [input] + (read-files-section! [input expected-files] (l/debug :hint "reading section" :section :v1/files ::l/async false) (assert-read-label! input :v1/files) ;; Process/Read all file - (doseq [expected-file-id *files*] + (doseq [expected-file-id expected-files] (let [file (read-obj! input) media' (read-obj! input) file-id (:id file)] @@ -648,8 +664,8 @@ :revn (:revn file) :is-shared (:is-shared file) :data (blob/encode data) - :created-at ts - :modified-at ts}] + :created-at timestamp + :modified-at timestamp}] (l/trace :hint "create file" :id file-id' ::l/async false) @@ -668,7 +684,7 @@ ;; Insert all file relations (doseq [rel rels] (let [rel (-> rel - (assoc :synced-at ts) + (assoc :synced-at timestamp) (update :file-id lookup-index) (update :library-file-id lookup-index))] (l/trace :hint "create file library link" @@ -717,13 +733,7 @@ (update :file-id lookup-index) (d/update-when :media-id lookup-index) (d/update-when :thumbnail-id lookup-index)) - {:on-conflict-do-nothing overwrite?})))) - - (read-section! [section input] - (case section - :v1/rels (read-rels-section! input) - :v1/files (read-files-section! input) - :v1/sobjects (read-sobjects-section! input)))] + {:on-conflict-do-nothing overwrite?}))))] (with-open [input (bs/zstd-input-stream input)] (with-open [input (bs/data-input-stream input)] @@ -735,9 +745,13 @@ (l/debug :hint "import verified" :files files :overwrite? overwrite?) (binding [*index* (volatile! (update-index {} files)) *media* (volatile! []) - *files* files *conn* conn] - (run! #(read-section! % input) sections)))))))) + + (doseq [section sections] + (case section + :v1/rels (read-rels-section! input) + :v1/files (read-files-section! input files) + :v1/sobjects (read-sobjects-section! input)))))))))) (defn export! [cfg] @@ -748,11 +762,9 @@ (try (l/info :hint "start exportation" :export-id id) (with-open [output (io/output-stream path)] - (with-open [output (bs/zstd-output-stream output :level 12)] - (with-open [output (bs/data-output-stream output)] - (binding [*position* (atom 0)] - (write-export! (assoc cfg ::output output)) - path)))) + (binding [*position* (atom 0)] + (write-export! (assoc cfg ::output output)) + path)) (catch Throwable cause (vreset! cs cause) @@ -798,7 +810,7 @@ "Export a penpot file in a binary format." [{:keys [pool] :as cfg} {:keys [profile-id file-id include-libraries? embed-assets?] :as params}] (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id file-id) + (files/check-read-permissions! conn profile-id file-id) (let [path (export! (assoc cfg ::file-ids [file-id] ::embed-assets? embed-assets? @@ -809,17 +821,16 @@ :body (io/input-stream path) :headers {"content-type" "application/octet-stream"}))})))) -(s/def ::input ::media/upload) - +(s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req-un [::profile-id ::input])) + (s/keys :req-un [::profile-id ::file])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." - [{:keys [pool] :as cfg} {:keys [profile-id input] :as params}] - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] + [{:keys [pool] :as cfg} {:keys [profile-id file] :as params}] + (let [project-id (-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] (import! (assoc cfg - ::input (:path input) + ::input (:path file) ::project-id project-id ::ignore-index-errors? true)))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index d1c3c1e1fe..3028d7c2db 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -76,12 +76,12 @@ (defn- send-command! "A simple helper for a common case of sending and receiving transit data to the penpot mutation api." - [id {:keys [blob? form-data?] :as params}] + [id params {:keys [response-type form-data?]}] (->> (http/send! {:method :post :uri (u/join base-uri "api/rpc/command/" (name id)) :credentials "include" :body (if form-data? (http/form-data params) (http/transit-data params)) - :response-type (if blob? :blob :text)}) + :response-type (or response-type :text)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) @@ -105,7 +105,15 @@ (defmethod command :default [id params] - (send-command! id params)) + (send-command! id params nil)) + +(defmethod command :export-binfile + [id params] + (send-command! id params {:response-type :blob})) + +(defmethod command :import-binfile + [id params] + (send-command! id params {:form-data? true})) (defn query! ([id] (query id {})) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 0e48fe922c..bd55211100 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -457,8 +457,7 @@ (fn [file] (->> (rp/command! :export-binfile {:file-id (:id file) :include-libraries? (= export-type :all) - :embed-assets? (= export-type :merge) - :blob? true}) + :embed-assets? (= export-type :merge)}) (rx/map #(hash-map :type :finish :file-id (:id file) :filename (:name file) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 6e8cc8facc..9c890a2741 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -604,8 +604,7 @@ :response-type :blob :method :get}) (rx/map :body) - (rx/mapcat #(rp/command! :import-binfile {:input % - :form-data? true})) + (rx/mapcat #(rp/command! :import-binfile {:file %})) (rx/map (fn [_] {:status :import-finish From aadb7cb1bf4ab858c91cdfa730546b82edc8e42f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jul 2022 13:12:38 +0200 Subject: [PATCH 83/86] :sparkles: Don't call rp/command internal method --- frontend/src/app/main/data/users.cljs | 10 +++++----- frontend/src/app/main/ui/auth/register.cljs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 8ac192d970..104b2619ef 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -206,7 +206,7 @@ ;; the returned profile is an NOT authenticated profile, we ;; proceed to logout and show an error message. - (->> (rp/command :login-with-password (d/without-nils params)) + (->> (rp/command! :login-with-password (d/without-nils params)) (rx/merge-map (fn [data] (rx/merge (rx/of (fetch-profile)) @@ -292,7 +292,7 @@ (ptk/reify ::logout ptk/WatchEvent (watch [_ _ _] - (->> (rp/command :logout) + (->> (rp/command! :logout) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) (rx/map #(logged-out params))))))) @@ -494,7 +494,7 @@ :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command :request-profile-recovery data) + (->> (rp/command! :request-profile-recovery data) (rx/tap on-success) (rx/catch on-error)))))) @@ -513,7 +513,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command :recover-profile data) + (->> (rp/command! :recover-profile data) (rx/tap on-success) (rx/catch on-error)))))) @@ -524,7 +524,7 @@ (ptk/reify ::create-demo-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/command :create-demo-profile {}) + (->> (rp/command! :create-demo-profile {}) (rx/map login))))) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 6eeeed2434..f2e3a4fd09 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -90,7 +90,7 @@ (fn [form _event] (reset! submitted? true) (let [cdata (:clean-data @form)] - (->> (rp/command :prepare-register-profile cdata) + (->> (rp/command! :prepare-register-profile cdata) (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) (rx/subs @@ -225,7 +225,7 @@ (fn [form _event] (reset! submitted? true) (let [params (:clean-data @form)] - (->> (rp/command :register-profile params) + (->> (rp/command! :register-profile params) (rx/finalize #(reset! submitted? false)) (rx/subs on-success (partial handle-register-error form))))))] From 37e9adc6b6c3e36cd3179174671b07fc03139de2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 8 Jul 2022 06:37:13 +0200 Subject: [PATCH 84/86] :bug: Fix set project on binary file import --- backend/src/app/rpc/commands/binfile.clj | 14 ++++++-------- frontend/src/app/worker/import.cljs | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 40d080d624..8e12e4a973 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -17,7 +17,6 @@ [app.db :as db] [app.media :as media] [app.rpc.queries.files :as files] - [app.rpc.queries.profile :as profile] [app.storage :as sto] [app.storage.tmp :as tmp] [app.tasks.file-gc] @@ -823,14 +822,13 @@ (s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req-un [::profile-id ::file])) + (s/keys :req-un [::project-id ::file])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." - [{:keys [pool] :as cfg} {:keys [profile-id file] :as params}] - (let [project-id (-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] - (import! (assoc cfg - ::input (:path file) - ::project-id project-id - ::ignore-index-errors? true)))) + [cfg {:keys [project-id file] :as params}] + (import! (assoc cfg + ::input (:path file) + ::project-id project-id + ::ignore-index-errors? true))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 9c890a2741..aff53ab5b0 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -604,7 +604,8 @@ :response-type :blob :method :get}) (rx/map :body) - (rx/mapcat #(rp/command! :import-binfile {:file %})) + (rx/mapcat #(rp/command! :import-binfile {:file % + :project-id project-id})) (rx/map (fn [_] {:status :import-finish From d140f15f37d07ad6704abffa90a4fe6684d9a577 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 8 Jul 2022 07:43:44 +0200 Subject: [PATCH 85/86] :bug: Fix set project on binary file import --- backend/src/app/rpc/commands/binfile.clj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 8e12e4a973..94a3351317 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -17,6 +17,7 @@ [app.db :as db] [app.media :as media] [app.rpc.queries.files :as files] + [app.rpc.queries.projects :as projects] [app.storage :as sto] [app.storage.tmp :as tmp] [app.tasks.file-gc] @@ -822,13 +823,14 @@ (s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req-un [::project-id ::file])) + (s/keys :req-un [::profile-id ::project-id ::file])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." - [cfg {:keys [project-id file] :as params}] - (import! (assoc cfg - ::input (:path file) - ::project-id project-id - ::ignore-index-errors? true))) - + [{:keys [pool] :as cfg} {:keys [profile-id project-id file] :as params}] + (db/with-atomic [conn pool] + (projects/check-read-permissions! conn profile-id project-id) + (import! (assoc cfg + ::input (:path file) + ::project-id project-id + ::ignore-index-errors? true)))) From f2bd6a552fe18b1bda8232aa32253b46a2a06c64 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 11 Jul 2022 11:16:39 +0200 Subject: [PATCH 86/86] :sparkles: Feature toggle --- frontend/src/app/main.cljs | 1 + frontend/src/app/main/ui/features.cljs | 62 ++++++++++++++++++++++++++ frontend/src/features.cljs | 14 ++++++ 3 files changed, 77 insertions(+) create mode 100644 frontend/src/app/main/ui/features.cljs create mode 100644 frontend/src/features.cljs diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 287e3fe039..a7e38e1850 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -25,6 +25,7 @@ [app.util.theme :as theme] [beicon.core :as rx] [debug] + [features] [potok.core :as ptk] [rumext.alpha :as mf])) diff --git a/frontend/src/app/main/ui/features.cljs b/frontend/src/app/main/ui/features.cljs new file mode 100644 index 0000000000..30f0ebb596 --- /dev/null +++ b/frontend/src/app/main/ui/features.cljs @@ -0,0 +1,62 @@ +;; 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.features + (:require + [app.common.data :as d] + [app.common.logging :as log] + [app.main.store :as st] + [okulary.core :as l] + [potok.core :as ptk] + [rumext.alpha :as mf])) + +(log/set-level! :debug) + +(def features-list #{:auto-layout}) + +(defn toggle-feature + [feature] + (ptk/reify ::toggle-feature + ptk/UpdateEvent + (update [_ state] + (log/debug :msg "toggle-feature" + :feature (d/name feature) + :result (if (not (contains? (:features state) feature)) + "enabled" + "disabled")) + + (-> state + (update :features + (fn [features] + (let [features (or features #{})] + (if (contains? features feature) + (disj features feature) + (conj features feature))))))))) + +(defn toggle-feature! + [feature] + (assert (contains? features-list feature) "Not supported feature") + (st/emit! (toggle-feature feature))) + +(def features + (l/derived :features st/state)) + +(defn active-feature + [feature] + (l/derived #(contains? % feature) features)) + +(defn use-feature + [feature] + (assert (contains? features-list feature) "Not supported feature") + (let [active-feature-ref (mf/use-memo (mf/deps feature) #(active-feature feature)) + active-feature? (mf/deref active-feature-ref)] + active-feature?)) + +;; By default the features are active in local environments +(when *assert* + ;; Activate all features in local environment + (doseq [f features-list] + (toggle-feature! f))) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs new file mode 100644 index 0000000000..353504a1e3 --- /dev/null +++ b/frontend/src/features.cljs @@ -0,0 +1,14 @@ +;; 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 + +;; This namespace is only to export the functions for toggle features +(ns features + (:require + [app.main.ui.features :as features])) + +(defn ^:export autolayout [] + (features/toggle-feature! :auto-layout)) +