diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d0d0ea12b0..bdbe87a01c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -8,49 +8,48 @@ assignees: ''
---
-**Describe the bug**
-A clear and concise description of what the bug is.
-
**To Reproduce**
+
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
-4. See error
**Expected behavior**
+
A clear and concise description of what you expected to happen.
+**Actual behavior**
+
+A clear and concise description of what happens instead; what the bug is.
+
**Screenshots**
+
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
-
-- OS: (e.g. iOS)
-- Browser (e.g. chrome, safari)
-- Version (e.g. 22)
+- OS (e.g. iOS):
+- Browser & version (e.g. Chrome 89.0):
**Smartphone (please complete the following information):**
-
-- Device: (e.g. iPhone6)
-- OS: (e.g. iOS8.1)
-- Browser (e.g. stock browser, safari)
-- Version (e.g. 22)
+- Device & model (e.g. iPhone 6):
+- OS & version (e.g. iOS 8.1):
+- Browser & version (e.g. stock browser 22):
**Environment (please complete the following information):**
-Specify if using SAAS (https://design.penpot.app) or self-hosted instance.
+- Host (e.g. https://design.penpot.app, local instance):
-If self-hosted instance, add OS and runtime information to help explain your problem.
+*If self-hosted:*
+- OS Version (e.g. Ubuntu 16.04):
+- Docker / Docker-compose version (e.g. Docker version 18.03.0-ce, build 0520e24):
+- Image version (e.g. Alpine):
-- OS Version: (e.g. Ubuntu 16.04)
+Docker commands or docker-compose file (if possible and if proceed.x):
+```
-Also provide Docker commands or docker-compose file if possible and if proceed.x
-
-- Docker / Docker-compose Version: (e.g. Docker version 18.03.0-ce, build 0520e24)
-- Image (e.g. alpine)
-
-**Frontend Stack Trace (if self-hosted)**
+```
+Frontend Stack Trace:
```
@@ -59,8 +58,7 @@ Also provide Docker commands or docker-compose file if possible and if proceed.x
-**Backend Stack Trace (if self-hosted)**
-
+Backend Stack Trace:
```
@@ -69,5 +67,6 @@ Also provide Docker commands or docker-compose file if possible and if proceed.x
-**Additional context**
-Add any other context about the problem here.
+**Additional context:**
+
+Any other context about the problem.
diff --git a/.gitignore b/.gitignore
index 330eadd832..22ae73c022 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*-init.clj
*.jar
*.penpot
+*.orig
.calva
.clj-kondo
.cpcache
@@ -33,13 +34,16 @@
/exporter/.shadow-cljs
/exporter/target
/frontend/.shadow-cljs
+/frontend/package-lock.json
/frontend/cypress/videos/*/
+/frontend/cypress/fixtures/validuser.json
/frontend/dist/
/frontend/npm-debug.log
/frontend/out/
/frontend/resources/fonts/experiments
/frontend/resources/public/*
/frontend/target/
+/frontend/cypress/videos/*/
/media
/telemetry/
/vendor/**/target
diff --git a/CHANGES.md b/CHANGES.md
index 06a5c558f6..05fa25ce0e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,52 @@
# CHANGELOG
+## 1.12.0-beta
+
+### :boom: Breaking changes
+
+### :sparkles: New features
+
+- Open feedback in a new window [Taiga #2901](https://tree.taiga.io/project/penpot/us/2901)
+- Improve usage of file menu [Taiga #2853](https://tree.taiga.io/project/penpot/us/2853)
+- Rotation to snap to 15ยบ intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437)
+- Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
+- Disallow using same password as user email [Taiga #2454](https://tree.taiga.io/project/penpot/us/2454)
+- Add configurable nudge amount [Taiga #910](https://tree.taiga.io/project/penpot/us/910)
+- Add stroke properties for image shapes [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
+- On user settings, hide the theme selector as long as we only have one theme [Taiga #2610](https://tree.taiga.io/project/penpot/us/2610)
+- Automatically open comments from dashboard notifications [Taiga #2605](https://tree.taiga.io/project/penpot/us/2605)
+- Enhance the behaviour of the artboards list on view mode [Taiga #2634](https://tree.taiga.io/project/penpot/us/2634)
+- Add recent used fonts in font selection widget [Taiga #1381](https://tree.taiga.io/project/penpot/us/1381)
+- Allow to align items relative to groups [Taiga #2533](https://tree.taiga.io/project/penpot/us/2533)
+- Scroll bars [Taiga #2550](https://tree.taiga.io/project/penpot/task/2550)
+- Add select layer option to context menu [Taiga #2474](https://tree.taiga.io/project/penpot/us/2474)
+- Guides [Taiga #290](https://tree.taiga.io/project/penpot/us/290)
+- Improve file menu by adding semantically groups [Github #1203](https://github.com/penpot/penpot/issues/1203)
+- Add update components in bulk option in context menu [Taiga #1975](https://tree.taiga.io/project/penpot/us/1975)
+- Create first E2E tests [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608), [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608)
+- Redesign of workspace toolbars [Taiga #2319](https://tree.taiga.io/project/penpot/us/2319)
+- Graphic Tablet usability improvements [Taiga #1913](https://tree.taiga.io/project/penpot/us/1913)
+- Improved mouse collision detection for groups and text shapes [Taiga #2452](https://tree.taiga.io/project/penpot/us/2452), [Taiga #2453](https://tree.taiga.io/project/penpot/us/2453)
+- Add support for alternative S3 storage providers and all aws regions [#1267](https://github.com/penpot/penpot/issues/1267)
+
+### :bug: Bugs fixed
+
+- Fixed ungroup typography when editing it [Taiga #2391](https://tree.taiga.io/project/penpot/issue/2391)
+- Fixed error when trying to post an empty comment [Taiga #2603](https://tree.taiga.io/project/penpot/issue/2603)
+- Fixed missing translation strings [Taiga #2786](https://tree.taiga.io/project/penpot/issue/2786)
+- Fixed color palette outside viewport [Taiga #2715](https://tree.taiga.io/project/penpot/issue/2715)
+- Fixed missing translate string [Taiga #2780](https://tree.taiga.io/project/penpot/issue/2780)
+- Fixed handoff shadow type text [Taiga #2717](https://tree.taiga.io/project/penpot/issue/2717)
+- Fixed components get "dirty" marker when moved [Taiga #2764](https://tree.taiga.io/project/penpot/issue/2764)
+- Fixed cannot align objects in a group that is not part of a frame [Taiga #2762](https://tree.taiga.io/project/penpot/issue/2762)
+- Fix problem with double click on exit path editing [Taiga #2906](https://tree.taiga.io/project/penpot/issue/2906)
+- Fixed alignment of layers with children [Taiga #2862](https://tree.taiga.io/project/penpot/issue/2862)
+
+### :heart: Community contributions by (Thank you!)
+
+- Cleanup unused static images (by @rhcarvalho) [#1561](https://github.com/penpot/penpot/pull/1561)
+- Compress static images to save space (by @rhcarvalho) [#1562](https://github.com/penpot/penpot/pull/1562)
+
## 1.11.2-beta
### :bug: Bugs fixed
@@ -18,7 +65,6 @@
- Increase default max connection pool size to 60
- Reduce resource usage of the error reporter.
-
## 1.11.1-beta
### :bug: Bugs fixed
@@ -30,11 +76,8 @@
- Update nodejs version to 16.13.1 on docker images.
-
## 1.11.0-beta
-### :boom: Breaking changes
-
### :sparkles: New features
- Add an option to hide artboards names on the viewport [Taiga #2034](https://tree.taiga.io/project/penpot/issue/2034)
@@ -112,7 +155,7 @@
### :arrow_up: Deps updates
-- Update devenv docker image dependencies.
+- Update devenv docker image dependencies
### :heart: Community contributions by (Thank you!)
@@ -124,13 +167,13 @@
### :sparkles: Enhacements
-- Allow parametrice file snapshoting interval.
+- Allow parametrice file snapshoting interval
### :bug: Bugs fixed
-- Fix issue on :mov-object change impl.
-- Minor fix on how file changes log is persisted.
-- Fix many issues on error reporting.
+- Fix issue on :mov-object change impl
+- Minor fix on how file changes log is persisted
+- Fix many issues on error reporting
## 1.10.3-beta
diff --git a/backend/deps.edn b/backend/deps.edn
index f2c71a40de..42014d8fbd 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -6,7 +6,7 @@
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.1-1"}
+ com.github.luben/zstd-jni {:mvn/version "1.5.2-1"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.14.1"}
@@ -25,7 +25,7 @@
com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"}
metosin/reitit-ring {:mvn/version "0.5.15"}
- org.postgresql/postgresql {:mvn/version "42.3.1"}
+ org.postgresql/postgresql {:mvn/version "42.3.2"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
funcool/datoteka {:mvn/version "2.0.0"}
@@ -39,11 +39,11 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"}
- io.sentry/sentry {:mvn/version "5.5.2"}
+ io.sentry/sentry {:mvn/version "5.6.1"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
- software.amazon.awssdk/s3 {:mvn/version "2.17.111"}}
+ software.amazon.awssdk/s3 {:mvn/version "2.17.122"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -59,13 +59,10 @@
:extra-paths ["test" "dev"]}
:build
- {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.7.4" :git/sha "ac442da"}}
+ {:extra-deps
+ {io.github.clojure/tools.build {:git/tag "v0.7.5" :git/sha "34727f7"}}
:ns-default build}
- :kaocha
- {:extra-deps {lambdaisland/kaocha {:mvn/version "RELEASE"}}
- :main-opts ["-m" "kaocha.runner"]}
-
:test
{:extra-paths ["test"]
:extra-deps
diff --git a/backend/dev/user.clj b/backend/dev/user.clj
index 5e7fdcb94b..88fa512754 100644
--- a/backend/dev/user.clj
+++ b/backend/dev/user.clj
@@ -6,6 +6,7 @@
(ns user
(:require
+ [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.perf :as perf]
diff --git a/backend/scripts/repl b/backend/scripts/repl
index 22bebe8c73..ebb3e1554a 100755
--- a/backend/scripts/repl
+++ b/backend/scripts/repl
@@ -10,6 +10,18 @@
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
# export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
+# Initialize MINIO config
+# mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
+# mc admin user add penpot-s3 penpot-devenv penpot-devenv
+# mc admin policy set penpot-s3 readwrite user=penpot-devenv
+# 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-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="
-A:dev \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index 199f067e0a..c0aa7ddc9d 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -41,9 +41,7 @@
data))
(def defaults
- {:http-server-port 6060
- :http-server-host "0.0.0.0"
- :host "devenv"
+ {:host "devenv"
:tenant "dev"
:database-uri "postgresql://postgres/penpot"
:database-username "penpot"
@@ -106,12 +104,21 @@
(s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration)
+(s/def ::default-executor-parallelism ::us/integer)
+(s/def ::blocking-executor-parallelism ::us/integer)
+(s/def ::worker-executor-parallelism ::us/integer)
+
(s/def ::secret-key ::us/string)
(s/def ::allow-demo-users ::us/boolean)
(s/def ::assets-path ::us/string)
+(s/def ::authenticated-cookie-domain ::us/string)
(s/def ::database-password (s/nilable ::us/string))
(s/def ::database-uri ::us/string)
(s/def ::database-username (s/nilable ::us/string))
+(s/def ::database-readonly ::us/boolean)
+(s/def ::database-min-pool-size ::us/integer)
+(s/def ::database-max-pool-size ::us/integer)
+
(s/def ::default-blob-version ::us/integer)
(s/def ::error-report-webhook ::us/string)
(s/def ::user-feedback-destination ::us/string)
@@ -134,6 +141,8 @@
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string)
+(s/def ::http-server-min-threads ::us/integer)
+(s/def ::http-server-max-threads ::us/integer)
(s/def ::http-session-idle-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-size ::us/integer)
@@ -179,9 +188,11 @@
(s/def ::storage-assets-fs-directory ::us/string)
(s/def ::storage-assets-s3-bucket ::us/string)
(s/def ::storage-assets-s3-region ::us/keyword)
+(s/def ::storage-assets-s3-endpoint ::us/string)
(s/def ::storage-fdata-s3-bucket ::us/string)
(s/def ::storage-fdata-s3-region ::us/keyword)
(s/def ::storage-fdata-s3-prefix ::us/string)
+(s/def ::storage-fdata-s3-endpoint ::us/string)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
@@ -198,11 +209,18 @@
::allow-demo-users
::audit-log-archive-uri
::audit-log-gc-max-age
+ ::authenticated-cookie-domain
::database-password
::database-uri
::database-username
+ ::database-readonly
+ ::database-min-pool-size
+ ::database-max-pool-size
::default-blob-version
::error-report-webhook
+ ::default-executor-parallelism
+ ::blocking-executor-parallelism
+ ::worker-executor-parallelism
::file-change-snapshot-every
::file-change-snapshot-timeout
::user-feedback-destination
@@ -225,6 +243,8 @@
::host
::http-server-host
::http-server-port
+ ::http-server-max-threads
+ ::http-server-min-threads
::http-session-idle-max-age
::http-session-updater-batch-max-age
::http-session-updater-batch-max-size
@@ -274,10 +294,12 @@
::storage-assets-fs-directory
::storage-assets-s3-bucket
::storage-assets-s3-region
+ ::storage-assets-s3-endpoint
::fdata-storage-backend
::storage-fdata-s3-bucket
::storage-fdata-s3-region
::storage-fdata-s3-prefix
+ ::storage-fdata-s3-endpoint
::telemetry-enabled
::telemetry-uri
::telemetry-referer
diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj
index 80fb8d5c99..a45fcd90f4 100644
--- a/backend/src/app/db.clj
+++ b/backend/src/app/db.clj
@@ -47,13 +47,12 @@
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(declare instrument-jdbc!)
(declare apply-migrations!)
(s/def ::connection-timeout ::us/integer)
-(s/def ::max-pool-size ::us/integer)
+(s/def ::max-size ::us/integer)
+(s/def ::min-size ::us/integer)
(s/def ::migrations map?)
-(s/def ::min-pool-size ::us/integer)
(s/def ::name keyword?)
(s/def ::password ::us/string)
(s/def ::read-only ::us/boolean)
@@ -62,38 +61,49 @@
(s/def ::validation-timeout ::us/integer)
(defmethod ig/pre-init-spec ::pool [_]
- (s/keys :req-un [::uri ::name ::username ::password]
- :opt-un [::min-pool-size
- ::max-pool-size
+ (s/keys :req-un [::uri ::name
+ ::min-size
+ ::max-size
::connection-timeout
- ::validation-timeout
- ::migrations
+ ::validation-timeout]
+ :opt-un [::migrations
+ ::username
+ ::password
::mtx/metrics
::read-only]))
+(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)))
+
(defmethod ig/init-key ::pool
- [_ {:keys [migrations metrics name] :as cfg}]
- (l/info :action "initialize connection pool" :name (d/name name) :uri (:uri cfg))
- (some-> metrics :registry instrument-jdbc!)
+ [_ {: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))
(let [pool (create-pool cfg)]
- (some->> (seq migrations) (apply-migrations! pool))
+ (when-not read-only
+ (some->> (seq migrations) (apply-migrations! pool)))
pool))
(defmethod ig/halt-key! ::pool
[_ pool]
(.close ^HikariDataSource pool))
-(defn- instrument-jdbc!
- [registry]
- (mtx/instrument-vars!
- [#'next.jdbc/execute-one!
- #'next.jdbc/execute!]
- {:registry registry
- :type :counter
- :name "database_query_total"
- :help "An absolute counter of database queries."}))
-
(defn- apply-migrations!
[pool migrations]
(with-open [conn ^AutoCloseable (open pool)]
@@ -110,22 +120,19 @@
"SET idle_in_transaction_session_timeout = 300000;"))
(defn- create-datasource-config
- [{:keys [metrics read-only] :or {read-only false} :as cfg}]
- (let [dburi (:uri cfg)
- username (:username cfg)
- password (:password cfg)
- config (HikariConfig.)]
+ [{:keys [metrics uri] :as cfg}]
+ (let [config (HikariConfig.)]
(doto config
- (.setJdbcUrl (str "jdbc:" dburi))
- (.setPoolName (d/name (:name cfg)))
+ (.setJdbcUrl (str "jdbc:" uri))
+ (.setPoolName (d/name (:name cfg)))
(.setAutoCommit true)
- (.setReadOnly read-only)
- (.setConnectionTimeout (:connection-timeout cfg 10000)) ;; 10seg
- (.setValidationTimeout (:validation-timeout cfg 10000)) ;; 10seg
- (.setIdleTimeout 120000) ;; 2min
- (.setMaxLifetime 1800000) ;; 30min
- (.setMinimumIdle (:min-pool-size cfg 0))
- (.setMaximumPoolSize (:max-pool-size cfg 50))
+ (.setReadOnly (:read-only cfg))
+ (.setConnectionTimeout (:connection-timeout cfg))
+ (.setValidationTimeout (:validation-timeout cfg))
+ (.setIdleTimeout (:idle-timeout cfg))
+ (.setMaxLifetime (:max-lifetime cfg))
+ (.setMinimumIdle (:min-size cfg))
+ (.setMaximumPoolSize (:max-size cfg))
(.setConnectionInitSql initsql)
(.setInitializationFailTimeout -1))
@@ -135,8 +142,8 @@
(PrometheusMetricsTrackerFactory.)
(.setMetricsTrackerFactory config)))
- (when username (.setUsername config username))
- (when password (.setPassword config password))
+ (some->> ^String (:username cfg) (.setUsername config))
+ (some->> ^String (:password cfg) (.setPassword config))
config))
@@ -146,10 +153,14 @@
(s/def ::pool pool?)
-(defn pool-closed?
+(defn closed?
[pool]
(.isClosed ^HikariDataSource pool))
+(defn read-only?
+ [pool]
+ (.isReadOnly ^HikariDataSource pool))
+
(defn create-pool
[cfg]
(let [dsc (create-datasource-config cfg)]
diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj
index 594916a06f..b49d8bf150 100644
--- a/backend/src/app/http.clj
+++ b/backend/src/app/http.clj
@@ -10,6 +10,7 @@
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
+ [app.config :as cf]
[app.http.doc :as doc]
[app.http.errors :as errors]
[app.http.middleware :as middleware]
@@ -24,19 +25,30 @@
(declare wrap-router)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; HTTP SERVER
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(s/def ::handler fn?)
(s/def ::router some?)
(s/def ::port ::us/integer)
(s/def ::host ::us/string)
(s/def ::name ::us/string)
-
-(defmethod ig/pre-init-spec ::server [_]
- (s/keys :req-un [::port]
- :opt-un [::name ::mtx/metrics ::router ::handler ::host]))
+(s/def ::max-threads ::cf/http-server-max-threads)
+(s/def ::min-threads ::cf/http-server-min-threads)
(defmethod ig/prep-key ::server
[_ cfg]
- (merge {:name "http"} (d/without-nils cfg)))
+ (merge {:name "http"
+ :min-threads 4
+ :max-threads 60
+ :port 6060
+ :host "0.0.0.0"}
+ (d/without-nils cfg)))
+
+(defmethod ig/pre-init-spec ::server [_]
+ (s/keys :req-un [::port ::host ::name ::min-threads ::max-threads]
+ :opt-un [::mtx/metrics ::router ::handler]))
(defn- instrument-metrics
[^Server server metrics]
@@ -48,15 +60,22 @@
(defmethod ig/init-key ::server
[_ {:keys [handler router port name metrics host] :as opts}]
- (l/info :msg "starting http server" :port port :host host :name name)
- (let [options {:http/port port :http/host host}
+ (l/info :hint "starting http server"
+ :port port :host host :name name
+ :min-threads (:min-threads opts)
+ :max-threads (:max-threads opts))
+ (let [options {:http/port port
+ :http/host host
+ :thread-pool/max-threads (:max-threads opts)
+ :thread-pool/min-threads (:min-threads opts)
+ :ring/async true}
handler (cond
(fn? handler) handler
(some? router) (wrap-router router)
:else (ex/raise :type :internal
:code :invalid-argument
:hint "Missing `handler` or `router` option."))
- server (-> (yt/server handler options)
+ server (-> (yt/server handler (d/without-nils options))
(cond-> metrics (instrument-metrics metrics)))]
(assoc opts :server (yt/start! server))))
@@ -70,20 +89,20 @@
(let [default (rr/routes
(rr/create-resource-handler {:path "/"})
(rr/create-default-handler))
- options {:middleware [middleware/server-timing]}
+ options {:middleware [middleware/wrap-server-timing]
+ :inject-match? false
+ :inject-router? false}
handler (rr/ring-handler router default options)]
- (fn [request]
- (try
- (handler request)
- (catch Throwable e
- (l/error :hint "unexpected error processing request"
- ::l/context (errors/get-error-context request e)
- :query-string (:query-string request)
- :cause e)
- {:status 500 :body "internal server error"})))))
+ (fn [request respond _]
+ (handler request respond (fn [cause]
+ (l/error :hint "unexpected error processing request"
+ ::l/context (errors/get-error-context request cause)
+ :query-string (:query-string request)
+ :cause cause)
+ (respond {:status 500 :body "internal server error"}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Http Router
+;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::rpc map?)
@@ -145,7 +164,6 @@
[middleware/multipart-params]
[middleware/keyword-params]
[middleware/format-response-body]
- [middleware/etag]
[middleware/parse-request-body]
[middleware/errors errors/handle]
[middleware/cookies]]}
diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj
index 18c14462e9..439b9f32e6 100644
--- a/backend/src/app/http/assets.clj
+++ b/backend/src/app/http/assets.clj
@@ -13,9 +13,12 @@
[app.db :as db]
[app.metrics :as mtx]
[app.storage :as sto]
+ [app.util.async :as async]
[app.util.time :as dt]
+ [app.worker :as wrk]
[clojure.spec.alpha :as s]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [promesa.core :as p]))
(def ^:private cache-max-age
(dt/duration {:hours 24}))
@@ -52,10 +55,10 @@
:body (sto/get-object-bytes storage obj)}
:s3
- (let [url (sto/get-object-url storage obj {:max-age signature-max-age})]
+ (let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
{:status 307
:headers {"location" (str url)
- "x-host" (:host url)
+ "x-host" (cond-> host port (str ":" port))
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
:body ""})
@@ -69,29 +72,38 @@
:body ""}))))
(defn- generic-handler
- [{:keys [storage] :as cfg} _request id]
- (let [obj (sto/get-object storage id)]
- (if obj
- (serve-object cfg obj)
- {:status 404 :body ""})))
+ [{:keys [storage executor] :as cfg} request kf]
+ (async/with-dispatch executor
+ (let [id (get-in request [:path-params :id])
+ mobj (get-file-media-object storage id)
+ obj (sto/get-object storage (kf mobj))]
+ (if obj
+ (serve-object cfg obj)
+ {:status 404 :body ""}))))
(defn objects-handler
- [cfg request]
- (let [id (get-in request [:path-params :id])]
- (generic-handler cfg request (coerce-id id))))
+ [{:keys [storage executor] :as cfg} request respond raise]
+ (-> (async/with-dispatch executor
+ (let [id (get-in request [:path-params :id])
+ id (coerce-id id)
+ obj (sto/get-object storage id)]
+ (if obj
+ (serve-object cfg obj)
+ {:status 404 :body ""})))
+ (p/then respond)
+ (p/catch raise)))
(defn file-objects-handler
- [{:keys [storage] :as cfg} request]
- (let [id (get-in request [:path-params :id])
- mobj (get-file-media-object storage id)]
- (generic-handler cfg request (:media-id mobj))))
+ [cfg request respond raise]
+ (-> (generic-handler cfg request :media-id)
+ (p/then respond)
+ (p/catch raise)))
(defn file-thumbnails-handler
- [{:keys [storage] :as cfg} request]
- (let [id (get-in request [:path-params :id])
- mobj (get-file-media-object storage id)]
- (generic-handler cfg request (or (:thumbnail-id mobj) (:media-id mobj)))))
-
+ [cfg request respond raise]
+ (-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
+ (p/then respond)
+ (p/catch raise)))
;; --- Initialization
@@ -101,10 +113,16 @@
(s/def ::signature-max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handlers [_]
- (s/keys :req-un [::storage ::mtx/metrics ::assets-path ::cache-max-age ::signature-max-age]))
+ (s/keys :req-un [::storage
+ ::wrk/executor
+ ::mtx/metrics
+ ::assets-path
+ ::cache-max-age
+ ::signature-max-age]))
(defmethod ig/init-key ::handlers
[_ cfg]
- {:objects-handler #(objects-handler cfg %)
- :file-objects-handler #(file-objects-handler cfg %)
- :file-thumbnails-handler #(file-thumbnails-handler cfg %)})
+ {:objects-handler (partial objects-handler cfg)
+ :file-objects-handler (partial file-objects-handler cfg)
+ :file-thumbnails-handler (partial file-thumbnails-handler cfg)})
+
diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj
index 11cfe28a88..d4c9eaca4d 100644
--- a/backend/src/app/http/awsns.clj
+++ b/backend/src/app/http/awsns.clj
@@ -26,25 +26,30 @@
(defmethod ig/init-key ::handler
[_ cfg]
- (fn [request]
- (let [body (parse-json (slurp (:body request)))
- mtype (get body "Type")]
- (cond
- (= mtype "SubscriptionConfirmation")
- (let [surl (get body "SubscribeURL")
- stopic (get body "TopicArn")]
- (l/info :action "subscription received" :topic stopic :url surl)
- (http/send! {:uri surl :method :post :timeout 10000}))
+ (fn [request respond _]
+ (try
+ (let [body (parse-json (slurp (:body request)))
+ mtype (get body "Type")]
+ (cond
+ (= mtype "SubscriptionConfirmation")
+ (let [surl (get body "SubscribeURL")
+ stopic (get body "TopicArn")]
+ (l/info :action "subscription received" :topic stopic :url surl)
+ (http/send! {:uri surl :method :post :timeout 10000}))
- (= mtype "Notification")
- (when-let [message (parse-json (get body "Message"))]
- (let [notification (parse-notification cfg message)]
- (process-report cfg notification)))
+ (= mtype "Notification")
+ (when-let [message (parse-json (get body "Message"))]
+ (let [notification (parse-notification cfg message)]
+ (process-report cfg notification)))
- :else
- (l/warn :hint "unexpected data received"
- :report (pr-str body)))
- {:status 200 :body ""})))
+ :else
+ (l/warn :hint "unexpected data received"
+ :report (pr-str body))))
+ (catch Throwable cause
+ (l/error :hint "unexpected exception on awsns handler"
+ :cause cause)))
+
+ (respond {:status 200 :body ""})))
(defn- parse-bounce
[data]
diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj
index e310406188..16190e7061 100644
--- a/backend/src/app/http/debug.clj
+++ b/backend/src/app/http/debug.clj
@@ -14,14 +14,18 @@
[app.db :as db]
[app.rpc.mutations.files :as m.files]
[app.rpc.queries.profile :as profile]
+ [app.util.async :as async]
[app.util.blob :as blob]
[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]
[fipp.edn :as fpp]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [promesa.core :as p]))
;; (selmer.parser/cache-off!)
@@ -201,12 +205,23 @@
(db/exec-one! conn ["select count(*) as count from server_prop;"])
{:status 200 :body "Ok"}))
+(defn- wrap-async
+ [{:keys [executor] :as cfg} f]
+ (fn [request respond raise]
+ (-> (async/with-dispatch executor
+ (f cfg request))
+ (p/then respond)
+ (p/catch raise))))
+
+(defmethod ig/pre-init-spec ::handlers [_]
+ (s/keys :req-un [::db/pool ::wrk/executor]))
+
(defmethod ig/init-key ::handlers
[_ cfg]
- {:index (partial index cfg)
- :health-check (partial health-check cfg)
- :retrieve-file-data (partial retrieve-file-data cfg)
- :retrieve-file-changes (partial retrieve-file-changes cfg)
- :retrieve-error (partial retrieve-error cfg)
- :retrieve-error-list (partial retrieve-error-list cfg)
- :upload-file-data (partial upload-file-data cfg)})
+ {:index (wrap-async cfg index)
+ :health-check (wrap-async cfg health-check)
+ :retrieve-file-data (wrap-async cfg retrieve-file-data)
+ :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)
+ :upload-file-data (wrap-async cfg upload-file-data)})
diff --git a/backend/src/app/http/doc.clj b/backend/src/app/http/doc.clj
index 29796a1170..07f63bb044 100644
--- a/backend/src/app/http/doc.clj
+++ b/backend/src/app/http/doc.clj
@@ -46,8 +46,9 @@
[rpc]
(let [context (prepare-context rpc)]
(if (contains? cf/flags :backend-api-doc)
- (fn [_]
- {:status 200
- :body (-> (io/resource "api-doc.tmpl")
- (tmpl/render context))})
- (constantly {:status 404 :body ""}))))
+ (fn [_ respond _]
+ (respond {:status 200
+ :body (-> (io/resource "api-doc.tmpl")
+ (tmpl/render context))}))
+ (fn [_ respond _]
+ (respond {:status 404 :body ""})))))
diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj
index 8b0938bbe3..1f7e92a039 100644
--- a/backend/src/app/http/feedback.clj
+++ b/backend/src/app/http/feedback.clj
@@ -14,48 +14,55 @@
[app.db :as db]
[app.emails :as eml]
[app.rpc.queries.profile :as profile]
+ [app.worker :as wrk]
[clojure.spec.alpha :as s]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [promesa.core :as p]
+ [promesa.exec :as px]))
-(declare send-feedback)
+(declare ^:private send-feedback)
+(declare ^:private handler)
(defmethod ig/pre-init-spec ::handler [_]
- (s/keys :req-un [::db/pool]))
+ (s/keys :req-un [::db/pool ::wrk/executor]))
(defmethod ig/init-key ::handler
- [_ {:keys [pool] :as scfg}]
- (let [ftoken (cf/get :feedback-token ::no-token)
- enabled (contains? cf/flags :user-feedback)]
- (fn [{:keys [profile-id] :as request}]
- (let [token (get-in request [:headers "x-feedback-token"])
- params (d/merge (:params request)
- (:body-params request))]
+ [_ {:keys [executor] :as cfg}]
+ (let [enabled? (contains? cf/flags :user-feedback)]
+ (if enabled?
+ (fn [request respond raise]
+ (-> (px/submit! executor #(handler cfg request))
+ (p/then' respond)
+ (p/catch raise)))
+ (fn [_ _ raise]
+ (raise (ex/error :type :validation
+ :code :feedback-disabled
+ :hint "feedback module is disabled"))))))
- (when-not enabled
- (ex/raise :type :validation
- :code :feedback-disabled
- :hint "feedback module is disabled"))
+(defn- handler
+ [{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
+ (let [ftoken (cf/get :feedback-token ::no-token)
+ token (get-in request [:headers "x-feedback-token"])
+ params (d/merge (:params request)
+ (:body-params request))]
+ (cond
+ (uuid? profile-id)
+ (let [profile (profile/retrieve-profile-data pool profile-id)
+ params (assoc params :from (:email profile))]
+ (send-feedback pool profile params))
- (cond
- (uuid? profile-id)
- (let [profile (profile/retrieve-profile-data pool profile-id)
- params (assoc params :from (:email profile))]
- (when-not (:is-muted profile)
- (send-feedback pool profile params)))
+ (= token ftoken)
+ (send-feedback cfg nil params))
- (= token ftoken)
- (send-feedback scfg nil params))
-
- {:status 204 :body ""}))))
+ {:status 204 :body ""}))
(s/def ::content ::us/string)
(s/def ::from ::us/email)
(s/def ::subject ::us/string)
-
(s/def ::feedback
(s/keys :req-un [::from ::subject ::content]))
-(defn send-feedback
+(defn- send-feedback
[pool profile params]
(let [params (us/conform ::feedback params)
destination (cf/get :feedback-destination)]
diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj
index 153ddc7277..c84413e301 100644
--- a/backend/src/app/http/middleware.clj
+++ b/backend/src/app/http/middleware.clj
@@ -10,8 +10,6 @@
[app.common.transit :as t]
[app.config :as cf]
[app.util.json :as json]
- [buddy.core.codecs :as bc]
- [buddy.core.hash :as bh]
[ring.core.protocols :as rp]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
@@ -21,13 +19,15 @@
(defn wrap-server-timing
[handler]
- (let [seconds-from #(float (/ (- (System/nanoTime) %) 1000000000))]
- (fn [request]
- (let [start (System/nanoTime)
- response (handler request)]
- (update response :headers
- (fn [headers]
- (assoc headers "Server-Timing" (str "total;dur=" (seconds-from start)))))))))
+ (letfn [(get-age [start]
+ (float (/ (- (System/nanoTime) start) 1000000000)))
+
+ (update-headers [headers start]
+ (assoc headers "Server-Timing" (str "total;dur=" (get-age start))))]
+
+ (fn [request respond raise]
+ (let [start (System/nanoTime)]
+ (handler request #(respond (update % :headers update-headers start)) raise)))))
(defn wrap-parse-request-body
[handler]
@@ -36,32 +36,40 @@
(t/read! reader)))
(parse-json [body]
- (json/read body))]
- (fn [{:keys [headers body] :as request}]
+ (json/read body))
+
+ (handle-request [{:keys [headers body] :as request}]
+ (let [ctype (get headers "content-type")]
+ (case ctype
+ "application/transit+json"
+ (let [params (parse-transit body)]
+ (-> request
+ (assoc :body-params params)
+ (update :params merge params)))
+
+ "application/json"
+ (let [params (parse-json body)]
+ (-> request
+ (assoc :body-params params)
+ (update :params merge params)))
+
+ request)))
+
+ (handle-exception [cause]
+ (let [data {:type :validation
+ :code :unable-to-parse-request-body
+ :hint "malformed params"}]
+ (l/error :hint (ex-message cause) :cause cause)
+ {:status 400
+ :headers {"content-type" "application/transit+json"}
+ :body (t/encode-str data {:type :json-verbose})}))]
+
+ (fn [request respond raise]
(try
- (let [ctype (get headers "content-type")]
- (handler (case ctype
- "application/transit+json"
- (let [params (parse-transit body)]
- (-> request
- (assoc :body-params params)
- (update :params merge params)))
-
- "application/json"
- (let [params (parse-json body)]
- (-> request
- (assoc :body-params params)
- (update :params merge params)))
-
- request)))
- (catch Exception e
- (let [data {:type :validation
- :code :unable-to-parse-request-body
- :hint "malformed params"}]
- (l/error :hint (ex-message e) :cause e)
- {:status 400
- :headers {"content-type" "application/transit+json"}
- :body (t/encode-str data {:type :json-verbose})}))))))
+ (let [request (handle-request request)]
+ (handler request respond raise))
+ (catch Exception cause
+ (respond (handle-exception cause)))))))
(def parse-request-body
{:name ::parse-request-body
@@ -81,48 +89,50 @@
(def ^:const buffer-size (:http/output-buffer-size yt/base-defaults))
-(defn- transit-streamable-body
- [data opts]
- (reify rp/StreamableResponseBody
- (write-body-to-stream [_ _ output-stream]
- ;; Use the same buffer as jetty output buffer size
- (try
- (with-open [bos (buffered-output-stream output-stream buffer-size)]
- (let [tw (t/writer bos opts)]
- (t/write! tw data)))
- (catch org.eclipse.jetty.io.EofException _cause
- ;; Do nothing, EOF means client closes connection abruptly
- nil)
- (catch Throwable cause
- (l/warn :hint "unexpected error on encoding response"
- :cause cause))))))
-
-(defn- impl-format-response-body
- [response {:keys [query-params] :as request}]
- (let [body (:body response)
- opts {:type (if (contains? query-params "transit_verbose") :json-verbose :json)}]
-
- (cond
- (:ws response)
- response
-
- (coll? body)
- (-> response
- (update :headers assoc "content-type" "application/transit+json")
- (assoc :body (transit-streamable-body body opts)))
-
- (nil? body)
- (assoc response :status 204 :body "")
-
- :else
- response)))
-
-(defn- wrap-format-response-body
+(defn wrap-format-response-body
[handler]
- (fn [request]
- (let [response (handler request)]
- (cond-> response
- (map? response) (impl-format-response-body request)))))
+ (letfn [(transit-streamable-body [data opts]
+ (reify rp/StreamableResponseBody
+ (write-body-to-stream [_ _ output-stream]
+ ;; Use the same buffer as jetty output buffer size
+ (try
+ (with-open [bos (buffered-output-stream output-stream buffer-size)]
+ (let [tw (t/writer bos opts)]
+ (t/write! tw data)))
+ (catch org.eclipse.jetty.io.EofException _cause
+ ;; Do nothing, EOF means client closes connection abruptly
+ nil)
+ (catch Throwable cause
+ (l/warn :hint "unexpected error on encoding response"
+ :cause cause))))))
+
+ (impl-format-response-body [response {:keys [query-params] :as request}]
+ (let [body (:body response)
+ opts {:type (if (contains? query-params "transit_verbose") :json-verbose :json)}]
+ (cond
+ (:ws response)
+ response
+
+ (coll? body)
+ (-> response
+ (update :headers assoc "content-type" "application/transit+json")
+ (assoc :body (transit-streamable-body body opts)))
+
+ (nil? body)
+ (assoc response :status 204 :body "")
+
+ :else
+ response)))
+
+ (handle-response [response request]
+ (cond-> response
+ (map? response) (impl-format-response-body request)))]
+
+ (fn [request respond raise]
+ (handler request
+ (fn [response]
+ (respond (handle-response response request)))
+ raise))))
(def format-response-body
{:name ::format-response-body
@@ -130,11 +140,9 @@
(defn wrap-errors
[handler on-error]
- (fn [request]
- (try
- (handler request)
- (catch Throwable e
- (on-error e request)))))
+ (fn [request respond _]
+ (handler request respond (fn [cause]
+ (-> cause (on-error request) respond)))))
(def errors
{:name ::errors
@@ -160,41 +168,7 @@
{:name ::server-timing
:compile (constantly wrap-server-timing)})
-(defn wrap-etag
- [handler]
- (letfn [(encode [data]
- (when (string? data)
- (str "W/\"" (-> data bh/blake2b-128 bc/bytes->hex) "\"")))]
- (fn [{method :request-method headers :headers :as request}]
- (cond-> (handler request)
- (= :get method)
- (as-> $ (if-let [etag (-> $ :body meta :etag encode)]
- (cond-> (update $ :headers assoc "etag" etag)
- (= etag (get headers "if-none-match"))
- (-> (assoc :body "")
- (assoc :status 304)))
- $))))))
-
-(def etag
- {:name ::etag
- :compile (constantly wrap-etag)})
-
-(defn activity-logger
- [handler]
- (let [logger "penpot.profile-activity"]
- (fn [{:keys [headers] :as request}]
- (let [ip-addr (get headers "x-forwarded-for")
- profile-id (:profile-id request)
- qstring (:query-string request)]
- (l/info ::l/async true
- ::l/logger logger
- :ip-addr ip-addr
- :profile-id profile-id
- :uri (str (:uri request) (when qstring (str "?" qstring)))
- :method (name (:request-method request)))
- (handler request)))))
-
-(defn- wrap-cors
+(defn wrap-cors
[handler]
(if-not (contains? cf/flags :cors)
handler
@@ -209,12 +183,15 @@
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))))]
- (fn [request]
+ (fn [request respond raise]
(if (= (:request-method request) :options)
(-> {:status 200 :body ""}
- (add-cors-headers request))
- (let [response (handler request)]
- (add-cors-headers response request)))))))
+ (add-cors-headers request)
+ (respond))
+ (handler request
+ (fn [response]
+ (respond (add-cors-headers response request)))
+ raise))))))
(def cors
{:name ::cors
diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj
index c116836a60..17b3720686 100644
--- a/backend/src/app/http/oauth.clj
+++ b/backend/src/app/http/oauth.clj
@@ -21,7 +21,10 @@
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [promesa.exec :as px]))
+
+;; TODO: make it fully async (?)
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
@@ -213,28 +216,35 @@
(redirect-response uri))))
(defn- auth-handler
- [{:keys [tokens] :as cfg} {:keys [params] :as request}]
- (let [invitation (:invitation-token params)
- props (extract-utm-props params)
- state (tokens :generate
- {:iss :oauth
- :invitation-token invitation
- :props props
- :exp (dt/in-future "15m")})
- uri (build-auth-uri cfg state)]
- {:status 200
- :body {:redirect-uri uri}}))
+ [{:keys [tokens executor] :as cfg} {:keys [params] :as request} respond _]
+ (px/run!
+ executor
+ (fn []
+ (let [invitation (:invitation-token params)
+ props (extract-utm-props params)
+ state (tokens :generate
+ {:iss :oauth
+ :invitation-token invitation
+ :props props
+ :exp (dt/in-future "15m")})
+ uri (build-auth-uri cfg state)]
+
+ (respond
+ {:status 200
+ :body {:redirect-uri uri}})))))
(defn- callback-handler
- [cfg request]
- (try
- (let [info (retrieve-info cfg request)
- profile (retrieve-profile cfg info)]
- (generate-redirect cfg request info profile))
- (catch Exception e
- (l/warn :hint "error on oauth process"
- :cause e)
- (generate-error-redirect cfg e))))
+ [{:keys [executor] :as cfg} request respond _]
+ (px/run!
+ executor
+ (fn []
+ (try
+ (let [info (retrieve-info cfg request)
+ profile (retrieve-profile cfg info)]
+ (respond (generate-redirect cfg request info profile)))
+ (catch Exception cause
+ (l/warn :hint "error on oauth process" :cause cause)
+ (respond (generate-error-redirect cfg cause)))))))
;; --- INIT
@@ -250,15 +260,19 @@
(defn wrap-handler
[cfg handler]
- (fn [request]
+ (fn [request respond raise]
(let [provider (get-in request [:path-params :provider])
provider (get-in @cfg [:providers provider])]
- (when-not provider
- (ex/raise :type :not-found
- :context {:provider provider}
- :hint "provider not configured"))
- (-> (assoc @cfg :provider provider)
- (handler request)))))
+ (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 ::handler
[_ cfg]
diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj
index b09ffaf1c9..7a98abff1c 100644
--- a/backend/src/app/http/session.clj
+++ b/backend/src/app/http/session.clj
@@ -11,97 +11,167 @@
[app.common.logging :as l]
[app.config :as cfg]
[app.db :as db]
+ [app.db.sql :as sql]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [ring.middleware.session.store :as rss]))
-;; A default cookie name for storing the session. We don't allow
-;; configure it.
-(def cookie-name "auth-token")
+;; A default cookie name for storing the session. We don't allow to configure it.
+(def token-cookie-name "auth-token")
+
+;; A cookie that we can use to check from other sites of the same domain if a user
+;; is registered. Is not intended for on premise installations, although nothing
+;; prevents using it if some one wants to.
+(def authenticated-cookie-name "authenticated")
+
+(deftype DatabaseStore [pool tokens]
+ rss/SessionStore
+ (read-session [_ token]
+ (db/exec-one! pool (sql/select :http-session {:id token})))
+
+ (write-session [_ _ data]
+ (let [profile-id (:profile-id data)
+ user-agent (:user-agent data)
+ token (tokens :generate {:iss "authentication"
+ :iat (dt/now)
+ :uid profile-id})
+
+ now (dt/now)
+ params {:user-agent user-agent
+ :profile-id profile-id
+ :created-at now
+ :updated-at now
+ :id token}]
+ (db/insert! pool :http-session params)
+ token))
+
+ (delete-session [_ token]
+ (db/delete! pool :http-session {:id token})
+ nil))
+
+(deftype MemoryStore [cache tokens]
+ rss/SessionStore
+ (read-session [_ token]
+ (get @cache token))
+
+ (write-session [_ _ data]
+ (let [profile-id (:profile-id data)
+ user-agent (:user-agent data)
+ token (tokens :generate {:iss "authentication"
+ :iat (dt/now)
+ :uid profile-id})
+ params {:user-agent user-agent
+ :profile-id profile-id
+ :id token}]
+
+ (swap! cache assoc token params)
+ token))
+
+ (delete-session [_ token]
+ (swap! cache dissoc token)
+ nil))
;; --- IMPL
(defn- create-session
- [{:keys [conn tokens] :as cfg} {:keys [profile-id headers] :as request}]
- (let [token (tokens :generate {:iss "authentication"
- :iat (dt/now)
- :uid profile-id})
- now (dt/now)
- params {:user-agent (get headers "user-agent")
- :profile-id profile-id
- :created-at now
- :updated-at now
- :id token}]
- (db/insert! conn :http-session params)))
+ [store request profile-id]
+ (let [params {:user-agent (get-in request [:headers "user-agent"])
+ :profile-id profile-id}]
+ (rss/write-session store nil params)))
(defn- delete-session
- [{:keys [conn] :as cfg} {:keys [cookies] :as request}]
- (when-let [token (get-in cookies [cookie-name :value])]
- (db/delete! conn :http-session {:id token}))
- nil)
+ [store {:keys [cookies] :as request}]
+ (when-let [token (get-in cookies [token-cookie-name :value])]
+ (rss/delete-session store token)))
(defn- retrieve-session
- [{:keys [conn] :as cfg} id]
- (when id
- (db/exec-one! conn ["select id, profile_id from http_session where id = ?" id])))
+ [store token]
+ (when token
+ (rss/read-session store token)))
(defn- retrieve-from-request
- [cfg {:keys [cookies] :as request}]
- (->> (get-in cookies [cookie-name :value])
- (retrieve-session cfg)))
+ [store {:keys [cookies] :as request}]
+ (->> (get-in cookies [token-cookie-name :value])
+ (retrieve-session store)))
(defn- add-cookies
- [response {:keys [id] :as session}]
+ [response token]
(let [cors? (contains? cfg/flags :cors)
- secure? (contains? cfg/flags :secure-session-cookies)]
- (assoc response :cookies {cookie-name {:path "/"
- :http-only true
- :value id
- :same-site (if cors? :none :lax)
- :secure secure?}})))
+ secure? (contains? cfg/flags :secure-session-cookies)
+ authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
+ (update response :cookies
+ (fn [cookies]
+ (cond-> cookies
+ :always
+ (assoc token-cookie-name {:path "/"
+ :http-only true
+ :value token
+ :same-site (if cors? :none :lax)
+ :secure secure?})
+
+ (some? authenticated-cookie-domain)
+ (assoc authenticated-cookie-name {:domain authenticated-cookie-domain
+ :path "/"
+ :value true
+ :same-site :strict
+ :secure secure?}))))))
(defn- clear-cookies
[response]
- (assoc response :cookies {cookie-name {:value "" :max-age -1}}))
+ (let [authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
+ (assoc response :cookies {token-cookie-name {:path "/"
+ :value ""
+ :max-age -1}
+ authenticated-cookie-name {:domain authenticated-cookie-domain
+ :path "/"
+ :value ""
+ :max-age -1}})))
(defn- middleware
- [cfg handler]
- (fn [request]
- (if-let [{:keys [id profile-id] :as session} (retrieve-from-request cfg request)]
+ [events-ch store handler]
+ (fn [request respond raise]
+ (if-let [{:keys [id profile-id] :as session} (retrieve-from-request store request)]
(do
- (a/>!! (::events-ch cfg) id)
+ (a/>!! events-ch id)
(l/set-context! {:profile-id profile-id})
- (handler (assoc request :profile-id profile-id :session-id id)))
- (handler request))))
+ (handler (assoc request :profile-id profile-id :session-id id) respond raise))
+ (handler request respond raise))))
;; --- STATE INIT: SESSION
+(s/def ::tokens fn?)
(defmethod ig/pre-init-spec ::session [_]
- (s/keys :req-un [::db/pool]))
+ (s/keys :req-un [::db/pool ::tokens]))
(defmethod ig/prep-key ::session
[_ cfg]
- (d/merge {:buffer-size 128} (d/without-nils cfg)))
+ (d/merge {:buffer-size 128}
+ (d/without-nils cfg)))
(defmethod ig/init-key ::session
- [_ {:keys [pool] :as cfg}]
- (let [events (a/chan (a/dropping-buffer (:buffer-size cfg)))
- cfg (-> cfg
- (assoc :conn pool)
- (assoc ::events-ch events))]
+ [_ {:keys [pool tokens] :as cfg}]
+ (let [events-ch (a/chan (a/dropping-buffer (:buffer-size cfg)))
+ store (if (db/read-only? pool)
+ (->MemoryStore (atom {}) tokens)
+ (->DatabaseStore pool tokens))]
+
+ (when (db/read-only? pool)
+ (l/warn :hint "sessions module initialized with in-memory store"))
+
(-> cfg
- (assoc :middleware #(middleware cfg %))
+ (assoc ::events-ch events-ch)
+ (assoc :middleware (partial middleware events-ch store))
(assoc :create (fn [profile-id]
(fn [request response]
- (let [request (assoc request :profile-id profile-id)
- session (create-session cfg request)]
- (add-cookies response session)))))
+ (let [token (create-session store request profile-id)]
+ (add-cookies response token)))))
(assoc :delete (fn [request response]
- (delete-session cfg request)
+ (delete-session store request)
(-> response
(assoc :status 204)
(assoc :body "")
@@ -138,16 +208,11 @@
:max-batch-size (str (:max-batch-size cfg)))
(let [input (aa/batch (::events-ch session)
{:max-batch-size (:max-batch-size cfg)
- :max-batch-age (inst-ms (:max-batch-age cfg))})
- mcnt (mtx/create
- {:name "http_session_update_total"
- :help "A counter of session update batch events."
- :registry (:registry metrics)
- :type :counter})]
+ :max-batch-age (inst-ms (:max-batch-age cfg))})]
(a/go-loop []
(when-let [[reason batch] (a/ (merge cfg params)
- (assoc :profile-id profile-id)
- (assoc :team-id (:team-id file))
- (assoc ::ws/metrics metrics))]
+ [_ {:keys [pool] :as cfg}]
+ (fn [{:keys [profile-id params] :as req} respond raise]
+ (let [params (us/conform ::handler-params params)
+ file (retrieve-file pool (:file-id params))
+ cfg (-> (merge cfg params)
+ (assoc :profile-id profile-id)
+ (assoc :team-id (:team-id file)))]
- (when-not profile-id
- (ex/raise :type :authentication
- :hint "Authentication required."))
+ (cond
+ (not profile-id)
+ (raise (ex/error :type :authentication
+ :hint "Authentication required."))
- (when-not file
- (ex/raise :type :not-found
- :code :object-not-found))
+ (not file)
+ (raise (ex/error :type :not-found
+ :code :object-not-found))
- (when-not (yws/upgrade-request? req)
- (ex/raise :type :validation
- :code :websocket-request-expected
- :hint "this endpoint only accepts websocket connections"))
+ (not (yws/upgrade-request? req))
+ (raise (ex/error :type :validation
+ :code :websocket-request-expected
+ :hint "this endpoint only accepts websocket connections"))
+
+ :else
(->> (ws/handler handle-message cfg)
- (yws/upgrade req))))))
+ (yws/upgrade req)
+ (respond))))))
(def ^:private
sql:retrieve-file
diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj
index 86e41d3f40..661b824b06 100644
--- a/backend/src/app/loggers/audit.clj
+++ b/backend/src/app/loggers/audit.clj
@@ -24,6 +24,7 @@
[cuerdas.core :as str]
[integrant.core :as ig]
[lambdaisland.uri :as u]
+ [promesa.core :as p]
[promesa.exec :as px]))
(defn parse-client-ip
@@ -41,33 +42,26 @@
(defn clean-props
[{:keys [profile-id] :as event}]
- (letfn [(clean-common [props]
- (-> props
- (dissoc :session-id)
- (dissoc :password)
- (dissoc :old-password)
- (dissoc :token)))
+ (let [invalid-keys #{:session-id
+ :password
+ :old-password
+ :token}
+ xform (comp
+ (remove (fn [kv]
+ (qualified-keyword? (first kv))))
+ (remove (fn [kv]
+ (contains? invalid-keys (first kv))))
+ (remove (fn [[k v]]
+ (and (= k :profile-id)
+ (= v profile-id))))
+ (filter (fn [[_ v]]
+ (or (string? v)
+ (keyword? v)
+ (uuid? v)
+ (boolean? v)
+ (number? v)))))]
- (clean-profile-id [props]
- (cond-> props
- (= profile-id (:profile-id props))
- (dissoc :profile-id)))
-
- (clean-complex-data [props]
- (reduce-kv (fn [props k v]
- (cond-> props
- (or (string? v)
- (uuid? v)
- (boolean? v)
- (number? v))
- (assoc k v)
-
- (keyword? v)
- (assoc k (name v))))
- {}
- props))]
-
- (update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
+ (update event :props #(into {} xform %))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP Handler
@@ -82,52 +76,62 @@
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
-(s/def ::event
+(s/def ::frontend-event
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
:opt-un [::context]))
-(s/def ::events (s/every ::event))
+(s/def ::frontend-events (s/every ::frontend-event))
(defmethod ig/init-key ::http-handler
- [_ {:keys [executor] :as cfg}]
- (fn [{:keys [params profile-id] :as request}]
- (when (contains? cf/flags :audit-log)
- (let [events (->> (:events params)
- (remove #(not= profile-id (:profile-id %)))
- (us/conform ::events))
- ip-addr (parse-client-ip request)
- cfg (-> cfg
- (assoc :source "frontend")
- (assoc :events events)
- (assoc :ip-addr ip-addr))]
- (px/run! executor #(persist-http-events cfg))))
- {:status 204 :body ""}))
+ [_ {:keys [executor pool] :as cfg}]
+ (if (or (db/read-only? pool) (not (contains? cf/flags :audit-log)))
+ (do
+ (l/warn :hint "audit log http handler disabled or db is read-only")
+ (fn [_ respond _]
+ (respond {:status 204 :body ""})))
+
+
+ (letfn [(handler [{:keys [params profile-id] :as request}]
+ (let [events (->> (:events params)
+ (remove #(not= profile-id (:profile-id %)))
+ (us/conform ::frontend-events))
+
+ ip-addr (parse-client-ip request)
+ cfg (-> cfg
+ (assoc :source "frontend")
+ (assoc :events events)
+ (assoc :ip-addr ip-addr))]
+ (persist-http-events cfg)))
+
+ (handle-error [cause]
+ (let [xdata (ex-data cause)]
+ (if (= :spec-validation (:code xdata))
+ (l/error ::l/raw (str "spec validation on persist-events:\n" (us/pretty-explain xdata)))
+ (l/error :hint "error on persist-events" :cause cause))))]
+
+ (fn [request respond _]
+ ;; Fire and forget, log error in case of errro
+ (-> (px/submit! executor #(handler request))
+ (p/catch handle-error))
+
+ (respond {:status 204 :body ""})))))
(defn- persist-http-events
[{:keys [pool events ip-addr source] :as cfg}]
- (try
- (let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
- prepare-xf (map (fn [event]
- [(uuid/next)
- (:name event)
- source
- (:type event)
- (:timestamp event)
- (:profile-id event)
- (db/inet ip-addr)
- (db/tjson (:props event))
- (db/tjson (d/without-nils (:context event)))]))
- events (us/conform ::events events)]
- (when (seq events)
- (->> (into [] prepare-xf events)
- (db/insert-multi! pool :audit-log columns))))
- (catch Throwable e
- (let [xdata (ex-data e)]
- (if (= :spec-validation (:code xdata))
- (l/error ::l/raw (str "spec validation on persist-events:\n"
- (:explain xdata)))
- (l/error :hint "error on persist-events"
- :cause e))))))
+ (let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
+ prepare-xf (map (fn [event]
+ [(uuid/next)
+ (:name event)
+ source
+ (:type event)
+ (:timestamp event)
+ (:profile-id event)
+ (db/inet ip-addr)
+ (db/tjson (:props event))
+ (db/tjson (d/without-nils (:context event)))]))]
+ (when (seq events)
+ (->> (into [] prepare-xf events)
+ (db/insert-multi! pool :audit-log columns)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Collector
@@ -142,49 +146,65 @@
(defmethod ig/pre-init-spec ::collector [_]
(s/keys :req-un [::db/pool ::wrk/executor]))
-(def event-xform
+(s/def ::ip-addr string?)
+(s/def ::backend-event
+ (s/keys :req-un [::type ::name ::profile-id]
+ :opt-un [::ip-addr ::props]))
+
+(def ^:private backend-event-xform
(comp
- (filter :profile-id)
+ (filter #(us/valid? ::backend-event %))
(map clean-props)))
(defmethod ig/init-key ::collector
- [_ cfg]
- (when (contains? cf/flags :audit-log)
- (l/info :msg "initializing audit log collector")
- (let [input (a/chan 512 event-xform)
+ [_ {:keys [pool] :as cfg}]
+ (cond
+ (not (contains? cf/flags :audit-log))
+ (do
+ (l/info :hint "audit log collection disabled")
+ (constantly nil))
+
+ (db/read-only? pool)
+ (do
+ (l/warn :hint "audit log collection disabled, db is read-only")
+ (constantly nil))
+
+ :else
+ (let [input (a/chan 512 backend-event-xform)
buffer (aa/batch input {:max-batch-size 100
:max-batch-age (* 10 1000) ; 10s
:init []})]
+ (l/info :hint "audit log collector initialized")
(a/go-loop []
(when-let [[_type events] (a/ params
- (dissoc :cmd)
- (assoc :tracked-at (dt/now)))]
- (case cmd
- :stop (a/close! input)
- :submit (when-not (a/offer! input params)
- (l/warn :msg "activity channel is full"))))))))
+ (case cmd
+ :stop
+ (a/close! input)
+ :submit
+ (let [params (-> params
+ (dissoc :cmd)
+ (assoc :tracked-at (dt/now)))]
+ (when-not (a/offer! input params)
+ (l/warn :hint "activity channel is full"))))))))
(defn- persist-events
[{:keys [pool executor] :as cfg} events]
(letfn [(event->row [event]
- (when (:profile-id event)
- [(uuid/next)
- (:name event)
- (:type event)
- (:profile-id event)
- (:tracked-at event)
- (some-> (:ip-addr event) db/inet)
- (db/tjson (:props event))
- "backend"]))]
+ [(uuid/next)
+ (:name event)
+ (:type event)
+ (:profile-id event)
+ (:tracked-at event)
+ (some-> (:ip-addr event) db/inet)
+ (db/tjson (:props event))
+ "backend"])]
(aa/with-thread executor
(when (seq events)
(db/with-atomic [conn pool]
@@ -217,6 +237,7 @@
(:enabled props false))
uri (or uri (:uri props))
cfg (assoc cfg :uri uri)]
+
(when (and enabled (not uri))
(ex/raise :type :internal
:code :task-not-configured
diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj
index 734eae460b..e7efd84edd 100644
--- a/backend/src/app/loggers/database.clj
+++ b/backend/src/app/loggers/database.clj
@@ -27,8 +27,9 @@
(defonce enabled (atom true))
(defn- persist-on-database!
- [{:keys [pool]} {:keys [id] :as event}]
- (db/insert! pool :server-error-report {:id id :content (db/tjson event)}))
+ [{:keys [pool] :as cfg} {:keys [id] :as event}]
+ (when-not (db/read-only? pool)
+ (db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
(defn- parse-event-data
[event]
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index 90bf86016b..0248a249c8 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -17,11 +17,37 @@
{:uri (cf/get :database-uri)
:username (cf/get :database-username)
:password (cf/get :database-password)
+ :read-only (cf/get :database-readonly false)
:metrics (ig/ref :app.metrics/metrics)
:migrations (ig/ref :app.migrations/all)
:name :main
- :min-pool-size 0
- :max-pool-size 60}
+ :min-size (cf/get :database-min-pool-size 0)
+ :max-size (cf/get :database-max-pool-size 30)}
+
+ ;; Default thread pool for IO operations
+ [::default :app.worker/executor]
+ {:parallelism (cf/get :default-executor-parallelism 60)
+ :prefix :default}
+
+ ;; Constrained thread pool. Should only be used from high demand
+ ;; RPC methods.
+ [::blocking :app.worker/executor]
+ {:parallelism (cf/get :blocking-executor-parallelism 20)
+ :prefix :blocking}
+
+ ;; Dedicated thread pool for backround tasks execution.
+ [::worker :app.worker/executor]
+ {:parallelism (cf/get :worker-executor-parallelism 10)
+ :prefix :worker}
+
+ :app.worker/executors
+ {:default (ig/ref [::default :app.worker/executor])
+ :worker (ig/ref [::worker :app.worker/executor])
+ :blocking (ig/ref [::blocking :app.worker/executor])}
+
+ :app.worker/executors-monitor
+ {:metrics (ig/ref :app.metrics/metrics)
+ :executors (ig/ref :app.worker/executors)}
:app.migrations/migrations
{}
@@ -32,7 +58,6 @@
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
-
:app.msgbus/msgbus
{:backend (cf/get :msgbus-backend :redis)
:redis-uri (cf/get :redis-uri)}
@@ -48,13 +73,9 @@
:app.storage/gc-touched-task
{:pool (ig/ref :app.db/pool)}
- :app.storage/recheck-task
- {:pool (ig/ref :app.db/pool)
- :storage (ig/ref :app.storage/storage)}
-
:app.http.session/session
- {:pool (ig/ref :app.db/pool)
- :tokens (ig/ref :app.tokens/tokens)}
+ {:pool (ig/ref :app.db/pool)
+ :tokens (ig/ref :app.tokens/tokens)}
:app.http.session/gc-task
{:pool (ig/ref :app.db/pool)
@@ -63,7 +84,7 @@
:app.http.session/updater
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
- :executor (ig/ref :app.worker/executor)
+ :executor (ig/ref [::worker :app.worker/executor])
:session (ig/ref :app.http.session/session)
:max-batch-age (cf/get :http-session-updater-batch-max-age)
:max-batch-size (cf/get :http-session-updater-batch-max-size)}
@@ -73,10 +94,13 @@
:pool (ig/ref :app.db/pool)}
:app.http/server
- {:port (cf/get :http-server-port)
- :host (cf/get :http-server-host)
- :router (ig/ref :app.http/router)
- :metrics (ig/ref :app.metrics/metrics)}
+ {:port (cf/get :http-server-port)
+ :host (cf/get :http-server-host)
+ :router (ig/ref :app.http/router)
+ :metrics (ig/ref :app.metrics/metrics)
+
+ :max-threads (cf/get :http-server-max-threads)
+ :min-threads (cf/get :http-server-min-threads)}
:app.http/router
{:assets (ig/ref :app.http.assets/handlers)
@@ -94,11 +118,11 @@
:rpc (ig/ref :app.rpc/rpc)}
:app.http.debug/handlers
- {:pool (ig/ref :app.db/pool)}
+ {:pool (ig/ref :app.db/pool)
+ :executor (ig/ref [::default :app.worker/executor])}
:app.http.websocket/handler
{:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)
:metrics (ig/ref :app.metrics/metrics)
:msgbus (ig/ref :app.msgbus/msgbus)}
@@ -106,11 +130,13 @@
{:metrics (ig/ref :app.metrics/metrics)
:assets-path (cf/get :assets-path)
:storage (ig/ref :app.storage/storage)
+ :executor (ig/ref [::default :app.worker/executor])
:cache-max-age (dt/duration {:hours 24})
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
:app.http.feedback/handler
- {:pool (ig/ref :app.db/pool)}
+ {:pool (ig/ref :app.db/pool)
+ :executor (ig/ref [::default :app.worker/executor])}
:app.http.oauth/handler
{:rpc (ig/ref :app.rpc/rpc)
@@ -118,6 +144,7 @@
: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])
:public-uri (cf/get :public-uri)}
:app.rpc/rpc
@@ -128,22 +155,17 @@
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri)
- :audit (ig/ref :app.loggers.audit/collector)}
-
- :app.worker/executor
- {:min-threads 0
- :max-threads 256
- :idle-timeout 60000
- :name :worker}
+ :audit (ig/ref :app.loggers.audit/collector)
+ :executors (ig/ref :app.worker/executors)}
:app.worker/worker
- {:executor (ig/ref :app.worker/executor)
+ {:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry)
:metrics (ig/ref :app.metrics/metrics)
:pool (ig/ref :app.db/pool)}
:app.worker/scheduler
- {:executor (ig/ref :app.worker/executor)
+ {:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry)
:pool (ig/ref :app.db/pool)
:schedule
@@ -162,9 +184,6 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :session-gc}
- {:cron #app/cron "0 0 * * * ?" ;; hourly
- :task :storage-recheck}
-
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
@@ -197,7 +216,6 @@
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:storage-deleted-gc (ig/ref :app.storage/gc-deleted-task)
:storage-touched-gc (ig/ref :app.storage/gc-touched-task)
- :storage-recheck (ig/ref :app.storage/recheck-task)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:session-gc (ig/ref :app.http.session/gc-task)
@@ -261,11 +279,11 @@
:app.loggers.audit/http-handler
{:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)}
+ :executor (ig/ref [::worker :app.worker/executor])}
:app.loggers.audit/collector
{:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)}
+ :executor (ig/ref [::worker :app.worker/executor])}
:app.loggers.audit/archive-task
{:uri (cf/get :audit-log-archive-uri)
@@ -279,51 +297,43 @@
:app.loggers.loki/reporter
{:uri (cf/get :loggers-loki-uri)
:receiver (ig/ref :app.loggers.zmq/receiver)
- :executor (ig/ref :app.worker/executor)}
+ :executor (ig/ref [::worker :app.worker/executor])}
:app.loggers.mattermost/reporter
{:uri (cf/get :error-report-webhook)
:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)}
+ :executor (ig/ref [::worker :app.worker/executor])}
:app.loggers.database/reporter
{:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)}
-
- :app.loggers.sentry/reporter
- {:dsn (cf/get :sentry-dsn)
- :trace-sample-rate (cf/get :sentry-trace-sample-rate 1.0)
- :attach-stack-trace (cf/get :sentry-attach-stack-trace false)
- :debug (cf/get :sentry-debug false)
- :receiver (ig/ref :app.loggers.zmq/receiver)
- :pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)}
+ :executor (ig/ref [::worker :app.worker/executor])}
:app.storage/storage
{:pool (ig/ref :app.db/pool)
- :executor (ig/ref :app.worker/executor)
+ :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])
- :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])
+ :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])}}
+ ;; 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)
- :prefix (cf/get :storage-fdata-s3-prefix)}
+ {: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)}
[::assets :app.storage.s3/backend]
- {:region (cf/get :storage-assets-s3-region)
- :bucket (cf/get :storage-assets-s3-bucket)}
+ {:region (cf/get :storage-assets-s3-region)
+ :endpoint (cf/get :storage-assets-s3-endpoint)
+ :bucket (cf/get :storage-assets-s3-bucket)}
[::assets :app.storage.fs/backend]
{:directory (cf/get :storage-assets-fs-directory)}
diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj
index 6948df513d..176b3cc6fa 100644
--- a/backend/src/app/media.clj
+++ b/backend/src/app/media.clj
@@ -326,8 +326,10 @@
(defn configure-assets-storage
"Given storage map, returns a storage configured with the appropriate
backend for assets."
- [storage conn]
- (-> storage
- (assoc :conn conn)
- (assoc :backend (cf/get :assets-storage-backend :assets-fs))))
+ ([storage]
+ (assoc storage :backend (cf/get :assets-storage-backend :assets-fs)))
+ ([storage conn]
+ (-> storage
+ (assoc :conn conn)
+ (assoc :backend (cf/get :assets-storage-backend :assets-fs)))))
diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj
index 57e1ba531e..f21d588895 100644
--- a/backend/src/app/metrics.clj
+++ b/backend/src/app/metrics.clj
@@ -5,46 +5,40 @@
;; Copyright (c) UXBOX Labs SL
(ns app.metrics
+ (:refer-clojure :exclude [run!])
(:require
- [app.common.exceptions :as ex]
[app.common.logging :as l]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
io.prometheus.client.CollectorRegistry
io.prometheus.client.Counter
+ io.prometheus.client.Counter$Child
io.prometheus.client.Gauge
+ io.prometheus.client.Gauge$Child
io.prometheus.client.Summary
+ io.prometheus.client.Summary$Child
+ io.prometheus.client.Summary$Builder
io.prometheus.client.Histogram
+ io.prometheus.client.Histogram$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.hotspot.DefaultExports
io.prometheus.client.jetty.JettyStatisticsCollector
org.eclipse.jetty.server.handler.StatisticsHandler
java.io.StringWriter))
-(declare instrument-vars!)
-(declare instrument)
+(set! *warn-on-reflection* true)
+
(declare create-registry)
(declare create)
(declare handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Defaults
+;; METRICS SERVICE PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
(def default-metrics
- {:profile-register
- {:name "actions_profile_register_count"
- :help "A global counter of user registrations."
- :type :counter}
-
- :profile-activation
- {:name "actions_profile_activation_count"
- :help "A global counter of profile activations"
- :type :counter}
-
- :update-file-changes
+ {:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
@@ -54,6 +48,18 @@
:help "A total number of bytes processed by update-file."
:type :counter}
+ :rpc-mutation-timing
+ {:name "rpc_mutation_timing"
+ :help "RPC mutation method call timming."
+ :labels ["name"]
+ :type :histogram}
+
+ :rpc-query-timing
+ {:name "rpc_query_timing"
+ :help "RPC query method call timing."
+ :labels ["name"]
+ :type :histogram}
+
:websocket-active-connections
{:name "websocket_active_connections"
:help "Active websocket connections gauge"
@@ -68,12 +74,60 @@
:websocket-session-timing
{:name "websocket_session_timing"
:help "Websocket session timing (seconds)."
- :quantiles []
- :type :summary}})
+ :type :summary}
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Entry Point
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+ :session-update-total
+ {:name "http_session_update_total"
+ :help "A counter of session update batch events."
+ :type :counter}
+
+ :tasks-timing
+ {:name "penpot_tasks_timing"
+ :help "Background tasks timing (milliseconds)."
+ :labels ["name"]
+ :type :summary}
+
+ :rlimit-queued-submissions
+ {:name "penpot_rlimit_queued_submissions"
+ :help "Current number of queued submissions on RLIMIT."
+ :labels ["name"]
+ :type :gauge}
+
+ :rlimit-used-permits
+ {:name "penpot_rlimit_used_permits"
+ :help "Current number of used permits on RLIMIT."
+ :labels ["name"]
+ :type :gauge}
+
+ :rlimit-acquires-total
+ {:name "penpot_rlimit_acquires_total"
+ :help "Total number of acquire operations on RLIMIT."
+ :labels ["name"]
+ :type :counter}
+
+ :executors-active-threads
+ {:name "penpot_executors_active_threads"
+ :help "Current number of threads available in the executor service."
+ :labels ["name"]
+ :type :gauge}
+
+ :executors-completed-tasks
+ {:name "penpot_executors_completed_tasks_total"
+ :help "Aproximate number of completed tasks by the executor."
+ :labels ["name"]
+ :type :counter}
+
+ :executors-running-threads
+ {:name "penpot_executors_running_threads"
+ :help "Current number of threads with state RUNNING."
+ :labels ["name"]
+ :type :gauge}
+
+ :executors-queued-submissions
+ {:name "penpot_executors_queued_submissions"
+ :help "Current number of queued submissions."
+ :labels ["name"]
+ :type :gauge}})
(defmethod ig/init-key ::metrics
[_ _]
@@ -95,31 +149,44 @@
(s/keys :req-un [::registry ::handler]))
(defn- handler
- [registry _request]
+ [registry _ respond _]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
- {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
- :body (.toString writer)}))
+ (respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
+ :body (.toString writer)})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(def default-empty-labels (into-array String []))
+
+(def default-quantiles
+ [[0.5 0.01]
+ [0.90 0.01]
+ [0.99 0.001]])
+
+(def default-histogram-buckets
+ [1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
+
+(defn run!
+ [{:keys [definitions]} {:keys [id] :as params}]
+ (when-let [mobj (get definitions id)]
+ ((::fn mobj) params)
+ true))
+
(defn create-registry
[]
(let [registry (CollectorRegistry.)]
(DefaultExports/register registry)
registry))
-(defmacro with-measure
- [& {:keys [expr cb]}]
- `(let [start# (System/nanoTime)
- tdown# ~cb]
- (try
- ~expr
- (finally
- (tdown# (/ (- (System/nanoTime) start#) 1000000))))))
+(defn- is-array?
+ [o]
+ (let [oc (class o)]
+ (and (.isArray ^Class oc)
+ (= (.getComponentType oc) String))))
(defn make-counter
[{:keys [name help registry reg labels] :as props}]
@@ -132,12 +199,9 @@
instance (.register instance registry)]
{::instance instance
- ::fn (fn [{:keys [by labels] :or {by 1}}]
- (if labels
- (.. ^Counter instance
- (labels (into-array String labels))
- (inc by))
- (.inc ^Counter instance by)))}))
+ ::fn (fn [{:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
+ (let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
+ (.inc ^Counter$Child instance (double inc))))}))
(defn make-gauge
[{:keys [name help registry reg labels] :as props}]
@@ -148,48 +212,33 @@
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
-
{::instance instance
- ::fn (fn [{:keys [cmd by labels] :or {by 1}}]
- (if labels
- (let [labels (into-array String [labels])]
- (case cmd
- :inc (.. ^Gauge instance (labels labels) (inc by))
- :dec (.. ^Gauge instance (labels labels) (dec by))))
- (case cmd
- :inc (.inc ^Gauge instance by)
- :dec (.dec ^Gauge instance by))))}))
-
-(def default-quantiles
- [[0.75 0.02]
- [0.99 0.001]])
+ ::fn (fn [{:keys [inc dec labels val] :or {labels default-empty-labels}}]
+ (let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
+ (cond (number? inc) (.inc ^Gauge$Child instance (double inc))
+ (number? dec) (.dec ^Gauge$Child instance (double dec))
+ (number? val) (.set ^Gauge$Child instance (double val)))))}))
(defn make-summary
[{:keys [name help registry reg labels max-age quantiles buckets]
- :or {max-age 3600 buckets 6 quantiles default-quantiles} :as props}]
+ :or {max-age 3600 buckets 12 quantiles default-quantiles} :as props}]
(let [registry (or registry reg)
- instance (doto (Summary/build)
+ builder (doto (Summary/build)
(.name name)
(.help help))
_ (when (seq quantiles)
- (.maxAgeSeconds ^Summary instance max-age)
- (.ageBuckets ^Summary instance buckets))
+ (.maxAgeSeconds ^Summary$Builder builder ^long max-age)
+ (.ageBuckets ^Summary$Builder builder buckets))
_ (doseq [[q e] quantiles]
- (.quantile ^Summary instance q e))
+ (.quantile ^Summary$Builder builder q e))
_ (when (seq labels)
- (.labelNames instance (into-array String labels)))
- instance (.register instance registry)]
+ (.labelNames ^Summary$Builder builder (into-array String labels)))
+ instance (.register ^Summary$Builder builder registry)]
{::instance instance
- ::fn (fn [{:keys [val labels]}]
- (if labels
- (.. ^Summary instance
- (labels (into-array String labels))
- (observe val))
- (.observe ^Summary instance val)))}))
-
-(def default-histogram-buckets
- [1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
+ ::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
+ (let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
+ (.observe ^Summary$Child instance val)))}))
(defn make-histogram
[{:keys [name help registry reg labels buckets]
@@ -204,12 +253,9 @@
instance (.register instance registry)]
{::instance instance
- ::fn (fn [{:keys [val labels]}]
- (if labels
- (.. ^Histogram instance
- (labels (into-array String labels))
- (observe val))
- (.observe ^Histogram instance val)))}))
+ ::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
+ (let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
+ (.observe ^Histogram$Child instance val)))}))
(defn create
[{:keys [type] :as props}]
@@ -219,114 +265,6 @@
:summary (make-summary props)
:histogram (make-histogram props)))
-(defn wrap-counter
- ([rootf mobj]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn
- ([a]
- ((::fn mobj) nil)
- (origf a))
- ([a b]
- ((::fn mobj) nil)
- (origf a b))
- ([a b c]
- ((::fn mobj) nil)
- (origf a b c))
- ([a b c d]
- ((::fn mobj) nil)
- (origf a b c d))
- ([a b c d & more]
- ((::fn mobj) nil)
- (apply origf a b c d more)))
- (assoc mdata ::original origf))))
- ([rootf mobj labels]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn
- ([a]
- ((::fn mobj) {:labels labels})
- (origf a))
- ([a b]
- ((::fn mobj) {:labels labels})
- (origf a b))
- ([a b & more]
- ((::fn mobj) {:labels labels})
- (apply origf a b more)))
- (assoc mdata ::original origf)))))
-
-(defn wrap-summary
- ([rootf mobj]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn
- ([a]
- (with-measure
- :expr (origf a)
- :cb #((::fn mobj) {:val %})))
- ([a b]
- (with-measure
- :expr (origf a b)
- :cb #((::fn mobj) {:val %})))
- ([a b & more]
- (with-measure
- :expr (apply origf a b more)
- :cb #((::fn mobj) {:val %}))))
- (assoc mdata ::original origf))))
-
- ([rootf mobj labels]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn
- ([a]
- (with-measure
- :expr (origf a)
- :cb #((::fn mobj) {:val % :labels labels})))
- ([a b]
- (with-measure
- :expr (origf a b)
- :cb #((::fn mobj) {:val % :labels labels})))
- ([a b & more]
- (with-measure
- :expr (apply origf a b more)
- :cb #((::fn mobj) {:val % :labels labels}))))
- (assoc mdata ::original origf)))))
-
-(defn instrument-vars!
- [vars {:keys [wrap] :as props}]
- (let [obj (create props)]
- (cond
- (instance? Counter (::instance obj))
- (doseq [var vars]
- (alter-var-root var (or wrap wrap-counter) obj))
-
- (instance? Summary (::instance obj))
- (doseq [var vars]
- (alter-var-root var (or wrap wrap-summary) obj))
-
- :else
- (ex/raise :type :not-implemented))))
-
-(defn instrument
- [f {:keys [wrap] :as props}]
- (let [obj (create props)]
- (cond
- (instance? Counter (::instance obj))
- ((or wrap wrap-counter) f obj)
-
- (instance? Summary (::instance obj))
- ((or wrap wrap-summary) f obj)
-
- (instance? Histogram (::instance obj))
- ((or wrap wrap-summary) f obj)
-
- :else
- (ex/raise :type :not-implemented))))
-
(defn instrument-jetty!
[^CollectorRegistry registry ^StatisticsHandler handler]
(doto (JettyStatisticsCollector. handler)
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 9ea2129f7b..ac1ea0b780 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -205,6 +205,9 @@
{:name "0065-add-trivial-spelling-fixes"
:fn (mg/resource "app/migrations/sql/0065-add-trivial-spelling-fixes.sql")}
+
+ {:name "0066-add-frame-thumbnail-table"
+ :fn (mg/resource "app/migrations/sql/0066-add-frame-thumbnail-table.sql")}
])
diff --git a/backend/src/app/migrations/sql/0066-add-frame-thumbnail-table.sql b/backend/src/app/migrations/sql/0066-add-frame-thumbnail-table.sql
new file mode 100644
index 0000000000..3134cbe21a
--- /dev/null
+++ b/backend/src/app/migrations/sql/0066-add-frame-thumbnail-table.sql
@@ -0,0 +1,10 @@
+CREATE TABLE file_frame_thumbnail (
+ file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
+ frame_id uuid NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+
+ data text NULL,
+
+ PRIMARY KEY(file_id, frame_id)
+);
diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj
index 285f185c74..7d621a3527 100644
--- a/backend/src/app/msgbus.clj
+++ b/backend/src/app/msgbus.clj
@@ -18,7 +18,6 @@
[integrant.core :as ig]
[promesa.core :as p])
(:import
- java.time.Duration
io.lettuce.core.RedisClient
io.lettuce.core.RedisURI
io.lettuce.core.api.StatefulConnection
@@ -29,7 +28,10 @@
io.lettuce.core.codec.StringCodec
io.lettuce.core.pubsub.RedisPubSubListener
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
- io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands))
+ io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands
+ io.lettuce.core.resource.ClientResources
+ io.lettuce.core.resource.DefaultClientResources
+ java.time.Duration))
(def ^:private prefix (cfg/get :tenant))
@@ -136,27 +138,35 @@
(declare impl-redis-sub)
(declare impl-redis-unsub)
+
(defmethod init-backend :redis
[{:keys [redis-uri] :as cfg}]
(let [codec (RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE)
- uri (RedisURI/create redis-uri)
- rclient (RedisClient/create ^RedisURI uri)
+ resources (.. (DefaultClientResources/builder)
+ (ioThreadPoolSize 4)
+ (computationThreadPoolSize 4)
+ (build))
- pub-conn (.connect ^RedisClient rclient ^RedisCodec codec)
- sub-conn (.connectPubSub ^RedisClient rclient ^RedisCodec codec)]
+ uri (RedisURI/create redis-uri)
+ rclient (RedisClient/create ^ClientResources resources ^RedisURI uri)
+
+ pub-conn (.connect ^RedisClient rclient ^RedisCodec codec)
+ sub-conn (.connectPubSub ^RedisClient rclient ^RedisCodec codec)]
(.setTimeout ^StatefulRedisConnection pub-conn ^Duration (dt/duration {:seconds 10}))
(.setTimeout ^StatefulRedisPubSubConnection sub-conn ^Duration (dt/duration {:seconds 10}))
(-> cfg
+ (assoc ::resources resources)
(assoc ::pub-conn pub-conn)
(assoc ::sub-conn sub-conn))))
(defmethod stop-backend :redis
- [{:keys [::pub-conn ::sub-conn] :as cfg}]
+ [{:keys [::pub-conn ::sub-conn ::resources] :as cfg}]
(.close ^StatefulRedisConnection pub-conn)
- (.close ^StatefulRedisPubSubConnection sub-conn))
+ (.close ^StatefulRedisPubSubConnection sub-conn)
+ (.shutdown ^ClientResources resources))
(defmethod init-pub-loop :redis
[{:keys [::pub-conn ::pub-ch]}]
diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj
index 0f390bbcb9..e9ef65a257 100644
--- a/backend/src/app/rpc.clj
+++ b/backend/src/app/rpc.clj
@@ -13,125 +13,182 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.metrics :as mtx]
- [app.util.retry :as retry]
- [app.util.rlimit :as rlimit]
+ [app.rpc.retry :as retry]
+ [app.rpc.rlimit :as rlimit]
+ [app.util.async :as async]
[app.util.services :as sv]
+ [app.worker :as wrk]
[clojure.spec.alpha :as s]
- [integrant.core :as ig]))
+ [integrant.core :as ig]
+ [promesa.core :as p]
+ [promesa.exec :as px]))
(defn- default-handler
[_]
- (ex/raise :type :not-found))
+ (p/rejected (ex/error :type :not-found)))
-(defn- run-hook
- [hook-fn response]
- (ex/ignoring (hook-fn))
+(defn- handle-response-transformation
+ [response request mdata]
+ (if-let [transform-fn (:transform-response mdata)]
+ (transform-fn request response)
+ response))
+
+(defn- handle-before-comple-hook
+ [response mdata]
+ (when-let [hook-fn (:before-complete mdata)]
+ (ex/ignoring (hook-fn)))
response)
(defn- rpc-query-handler
- [methods {:keys [profile-id session-id] :as request}]
- (let [type (keyword (get-in request [:path-params :type]))
+ "Ring handler that dispatches query requests and convert between
+ internal async flow into ring async flow."
+ [methods {:keys [profile-id session-id] :as request} respond raise]
+ (letfn [(handle-response [result]
+ (let [mdata (meta result)]
+ (-> {:status 200 :body result}
+ (handle-response-transformation request mdata))))]
- data (merge (:params request)
- (:body-params request)
- (:uploads request)
- {::request request})
+ (let [type (keyword (get-in request [:path-params :type]))
+ data (merge (:params request)
+ (:body-params request)
+ (:uploads request)
+ {::request request})
- data (if profile-id
- (assoc data :profile-id profile-id ::session-id session-id)
- (dissoc data :profile-id))
+ data (if profile-id
+ (assoc data :profile-id profile-id ::session-id session-id)
+ (dissoc data :profile-id))
- result ((get methods type default-handler) data)
- mdata (meta result)]
+ ;; Get the method from methods registry and if method does
+ ;; not exists asigns it to the default handler.
+ method (get methods type default-handler)]
- (cond->> {:status 200 :body result}
- (fn? (:transform-response mdata))
- ((:transform-response mdata) request))))
+ (-> (method data)
+ (p/then #(respond (handle-response %)))
+ (p/catch raise)))))
(defn- rpc-mutation-handler
- [methods {:keys [profile-id session-id] :as request}]
- (let [type (keyword (get-in request [:path-params :type]))
- data (merge (:params request)
- (:body-params request)
- (:uploads request)
- {::request request})
+ "Ring handler that dispatches mutation requests and convert between
+ internal async flow into ring async flow."
+ [methods {:keys [profile-id session-id] :as request} respond raise]
+ (letfn [(handle-response [result]
+ (let [mdata (meta result)]
+ (-> {:status 200 :body result}
+ (handle-response-transformation request mdata)
+ (handle-before-comple-hook mdata))))]
- data (if profile-id
- (assoc data :profile-id profile-id ::session-id session-id)
- (dissoc data :profile-id))
+ (let [type (keyword (get-in request [:path-params :type]))
+ data (merge (:params request)
+ (:body-params request)
+ (:uploads request)
+ {::request request})
- result ((get methods type default-handler) data)
- mdata (meta result)]
- (cond->> {:status 200 :body result}
- (fn? (:transform-response mdata))
- ((:transform-response mdata) request)
+ data (if profile-id
+ (assoc data :profile-id profile-id ::session-id session-id)
+ (dissoc data :profile-id))
- (fn? (:before-complete mdata))
- (run-hook (:before-complete mdata)))))
+ method (get methods type default-handler)]
-(defn- wrap-with-metrics
- [cfg f mdata]
- (mtx/wrap-summary f (::mobj cfg) [(::sv/name mdata)]))
+ (-> (method data)
+ (p/then #(respond (handle-response %)))
+ (p/catch raise)))))
-(defn- wrap-impl
+(defn- wrap-metrics
+ "Wrap service method with metrics measurement."
+ [{:keys [metrics ::metrics-id]} f mdata]
+ (let [labels (into-array String [(::sv/name mdata)])]
+ (fn [cfg params]
+ (let [start (System/nanoTime)]
+ (p/finally
+ (f cfg params)
+ (fn [_ _]
+ (mtx/run! metrics
+ {:id metrics-id
+ :val (/ (- (System/nanoTime) start) 1000000)
+ :labels labels})))))))
+
+(defn- wrap-dispatch
+ "Wraps service method into async flow, with the ability to dispatching
+ it to a preconfigured executor service."
+ [{:keys [executors] :as cfg} f mdata]
+ (let [dname (::async/dispatch mdata :none)]
+ (if (= :none dname)
+ (with-meta
+ (fn [cfg params]
+ (p/do! (f cfg params)))
+ mdata)
+
+ (let [executor (get executors dname)]
+ (when-not executor
+ (ex/raise :type :internal
+ :code :executor-not-configured
+ :hint (format "executor %s not configured" dname)))
+ (with-meta
+ (fn [cfg params]
+ (-> (px/submit! executor #(f cfg params))
+ (p/bind p/wrap)))
+ mdata)))))
+
+(defn- wrap-audit
[{:keys [audit] :as cfg} f mdata]
+ (if audit
+ (with-meta
+ (fn [cfg {:keys [::request] :as params}]
+ (p/finally (f cfg params)
+ (fn [result _]
+ (when result
+ (let [resultm (meta result)
+ profile-id (or (:profile-id params)
+ (:profile-id result)
+ (::audit/profile-id resultm))
+ props (d/merge params (::audit/props resultm))]
+ (audit :cmd :submit
+ :type (or (::audit/type resultm)
+ (::type cfg))
+ :name (or (::audit/name resultm)
+ (::sv/name mdata))
+ :profile-id profile-id
+ :ip-addr (audit/parse-client-ip request)
+ :props (dissoc props ::request)))))))
+ mdata)
+ f))
+
+(defn- wrap
+ [cfg f mdata]
(let [f (as-> f $
+ (wrap-dispatch cfg $ mdata)
(rlimit/wrap-rlimit cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
- (wrap-with-metrics cfg $ mdata))
+ (wrap-audit cfg $ mdata)
+ (wrap-metrics cfg $ mdata)
+ )
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(l/trace :action "register" :name (::sv/name mdata))
(with-meta
- (fn [params]
+ (fn [{:keys [::request] :as params}]
;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request.
- (when (and auth? (not (uuid? (:profile-id params))))
- (ex/raise :type :authentication
- :code :authentication-required
- :hint "authentication required for this endpoint"))
+ (p/do!
+ (if (and auth? (not (uuid? (:profile-id params))))
+ (ex/raise :type :authentication
+ :code :authentication-required
+ :hint "authentication required for this endpoint")
+ (let [params (us/conform spec (dissoc params ::request))]
+ (f cfg (assoc params ::request request))))))
- (let [params' (dissoc params ::request)
- params' (us/conform spec params')
- result (f cfg params')]
-
- ;; When audit log is enabled (default false).
- (when (fn? audit)
- (let [resultm (meta result)
- request (::request params)
- profile-id (or (:profile-id params')
- (:profile-id result)
- (::audit/profile-id resultm))
- props (d/merge params' (::audit/props resultm))]
- (audit :cmd :submit
- :type (or (::audit/type resultm)
- (::type cfg))
- :name (or (::audit/name resultm)
- (::sv/name mdata))
- :profile-id profile-id
- :ip-addr (audit/parse-client-ip request)
- :props props)))
-
- result))
mdata)))
(defn- process-method
[cfg vfn]
(let [mdata (meta vfn)]
[(keyword (::sv/name mdata))
- (wrap-impl cfg (deref vfn) mdata)]))
+ (wrap cfg (deref vfn) mdata)]))
(defn- resolve-query-methods
[cfg]
- (let [mobj (mtx/create
- {:name "rpc_query_timing"
- :labels ["name"]
- :registry (get-in cfg [:metrics :registry])
- :type :histogram
- :help "Timing of query services."})
- cfg (assoc cfg ::mobj mobj ::type "query")]
+ (let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
(->> (sv/scan-ns 'app.rpc.queries.projects
'app.rpc.queries.files
'app.rpc.queries.teams
@@ -144,13 +201,7 @@
(defn- resolve-mutation-methods
[cfg]
- (let [mobj (mtx/create
- {:name "rpc_mutation_timing"
- :labels ["name"]
- :registry (get-in cfg [:metrics :registry])
- :type :histogram
- :help "Timing of mutation services."})
- cfg (assoc cfg ::mobj mobj ::type "mutation")]
+ (let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns 'app.rpc.mutations.demo
'app.rpc.mutations.media
'app.rpc.mutations.profile
@@ -170,15 +221,16 @@
(s/def ::session map?)
(s/def ::tokens fn?)
(s/def ::audit (s/nilable fn?))
+(s/def ::executors (s/map-of keyword? ::wrk/executor))
(defmethod ig/pre-init-spec ::rpc [_]
(s/keys :req-un [::storage ::session ::tokens ::audit
- ::mtx/metrics ::db/pool]))
+ ::executors ::mtx/metrics ::db/pool]))
(defmethod ig/init-key ::rpc
[_ cfg]
(let [mq (resolve-query-methods cfg)
mm (resolve-mutation-methods cfg)]
{:methods {:query mq :mutation mm}
- :query-handler #(rpc-query-handler mq %)
- :mutation-handler #(rpc-mutation-handler mm %)}))
+ :query-handler (partial rpc-query-handler mq)
+ :mutation-handler (partial rpc-mutation-handler mm)}))
diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj
index 82f8d9b778..438cfdebd9 100644
--- a/backend/src/app/rpc/mutations/comments.clj
+++ b/backend/src/app/rpc/mutations/comments.clj
@@ -7,12 +7,13 @@
(ns app.rpc.mutations.comments
(:require
[app.common.exceptions :as ex]
+ [app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.queries.comments :as comments]
[app.rpc.queries.files :as files]
+ [app.rpc.retry :as retry]
[app.util.blob :as blob]
- [app.util.retry :as retry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -26,15 +27,14 @@
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
-(s/def ::position ::us/point)
+(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]))
(sv/defmethod ::create-comment-thread
- {::retry/enabled true
- ::retry/max-retries 3
+ {::retry/max-retries 3
::retry/matches retry/conflict-db-insert?}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj
index c145e2ceba..a27ef5512b 100644
--- a/backend/src/app/rpc/mutations/files.clj
+++ b/backend/src/app/rpc/mutations/files.clj
@@ -18,6 +18,7 @@
[app.rpc.queries.files :as files]
[app.rpc.queries.projects :as proj]
[app.storage.impl :as simpl]
+ [app.util.async :as async]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -27,6 +28,8 @@
;; --- Helpers & Specs
+(s/def ::frame-id ::us/uuid)
+(s/def ::file-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
@@ -270,6 +273,7 @@
(contains? o :changes-with-metadata)))))
(sv/defmethod ::update-file
+ {::async/dispatch :blocking}
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(db/xact-lock! conn id)
@@ -305,24 +309,21 @@
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
- (let [mtx1 (get-in metrics [:definitions :update-file-changes])
- mtx2 (get-in metrics [:definitions :update-file-bytes-processed])
-
- changes (if changes-with-metadata
+ (let [changes (if changes-with-metadata
(mapcat :changes changes-with-metadata)
changes)
changes (vec changes)
;; Trace the number of changes processed
- _ ((::mtx/fn mtx1) {:by (count changes)})
+ _ (mtx/run! metrics {:id :update-file-changes :inc (count changes)})
ts (dt/now)
file (-> (files/retrieve-data cfg file)
(update :revn inc)
(update :data (fn [data]
;; Trace the length of bytes of processed data
- ((::mtx/fn mtx2) {:by (alength data)})
+ (mtx/run! metrics {:id :update-file-bytes-processed :inc (alength data)})
(-> data
(blob/decode)
(assoc :id (:id file))
@@ -472,3 +473,25 @@
{:id id})))
nil)))
+
+
+;; --- Mutation: Upsert frame thumbnail
+
+(def sql:upsert-frame-thumbnail
+ "insert into file_frame_thumbnail(file_id, frame_id, data)
+ values (?, ?, ?)
+ on conflict(file_id, frame_id) do
+ update set data = ?;")
+
+(s/def ::data ::us/string)
+(s/def ::upsert-frame-thumbnail
+ (s/keys :req-un [::profile-id ::file-id ::frame-id ::data]))
+
+(sv/defmethod ::upsert-frame-thumbnail
+ [{:keys [pool] :as cfg} {:keys [profile-id file-id frame-id data]}]
+ (db/with-atomic [conn pool]
+ (files/check-edition-permissions! conn profile-id file-id)
+ (db/exec-one! conn [sql:upsert-frame-thumbnail file-id frame-id data data])
+ nil))
+
+
diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj
index 6416567d0e..f36a00ae8d 100644
--- a/backend/src/app/rpc/mutations/fonts.clj
+++ b/backend/src/app/rpc/mutations/fonts.clj
@@ -9,12 +9,10 @@
[app.common.exceptions :as ex]
[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.teams :as teams]
[app.storage :as sto]
- [app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -39,52 +37,57 @@
::font-id ::font-family ::font-weight ::font-style]))
(sv/defmethod ::create-font-variant
- {::rlimit/permits (cf/get :rlimit-font)}
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
- (db/with-atomic [conn pool]
- (let [cfg (assoc cfg :conn conn)]
- (teams/check-edition-permissions! conn profile-id team-id)
- (create-font-variant cfg params))))
+ (teams/check-edition-permissions! pool profile-id team-id)
+ (create-font-variant cfg params))
(defn create-font-variant
- [{:keys [conn storage] :as cfg} {:keys [data] :as params}]
+ [{:keys [storage pool] :as cfg} {:keys [data] :as params}]
(let [data (media/run {:cmd :generate-fonts :input data})
- storage (media/configure-assets-storage storage conn)
+ storage (media/configure-assets-storage storage)]
- otf (when-let [fdata (get data "font/otf")]
- (sto/put-object storage {:content (sto/content fdata)
- :content-type "font/otf"}))
-
- ttf (when-let [fdata (get data "font/ttf")]
- (sto/put-object storage {:content (sto/content fdata)
- :content-type "font/ttf"}))
-
- woff1 (when-let [fdata (get data "font/woff")]
- (sto/put-object storage {:content (sto/content fdata)
- :content-type "font/woff"}))
-
- woff2 (when-let [fdata (get data "font/woff2")]
- (sto/put-object storage {:content (sto/content fdata)
- :content-type "font/woff2"}))]
-
- (when (and (nil? otf)
- (nil? ttf)
- (nil? woff1)
- (nil? woff2))
+ (when (and (not (contains? data "font/otf"))
+ (not (contains? data "font/ttf"))
+ (not (contains? data "font/woff"))
+ (not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload))
- (db/insert! conn :team-font-variant
- {:id (uuid/next)
- :team-id (:team-id params)
- :font-id (:font-id params)
- :font-family (:font-family params)
- :font-weight (:font-weight params)
- :font-style (:font-style params)
- :woff1-file-id (:id woff1)
- :woff2-file-id (:id woff2)
- :otf-file-id (:id otf)
- :ttf-file-id (:id ttf)})))
+ (let [otf (when-let [fdata (get data "font/otf")]
+ (sto/put-object storage {:content (sto/content fdata)
+ :content-type "font/otf"
+ :reference :team-font-variant
+ :touched-at (dt/now)}))
+
+ ttf (when-let [fdata (get data "font/ttf")]
+ (sto/put-object storage {:content (sto/content fdata)
+ :content-type "font/ttf"
+ :touched-at (dt/now)
+ :reference :team-font-variant}))
+
+ woff1 (when-let [fdata (get data "font/woff")]
+ (sto/put-object storage {:content (sto/content fdata)
+ :content-type "font/woff"
+ :touched-at (dt/now)
+ :reference :team-font-variant}))
+
+ woff2 (when-let [fdata (get data "font/woff2")]
+ (sto/put-object storage {:content (sto/content fdata)
+ :content-type "font/woff2"
+ :touched-at (dt/now)
+ :reference :team-font-variant}))]
+
+ (db/insert! pool :team-font-variant
+ {:id (uuid/next)
+ :team-id (:team-id params)
+ :font-id (:font-id params)
+ :font-family (:font-family params)
+ :font-weight (:font-weight params)
+ :font-style (:font-style params)
+ :woff1-file-id (:id woff1)
+ :woff2-file-id (:id woff2)
+ :otf-file-id (:id otf)
+ :ttf-file-id (:id ttf)}))))
;; --- UPDATE FONT FAMILY
diff --git a/backend/src/app/rpc/mutations/ldap.clj b/backend/src/app/rpc/mutations/ldap.clj
index 0f6675f241..b4cc37afb6 100644
--- a/backend/src/app/rpc/mutations/ldap.clj
+++ b/backend/src/app/rpc/mutations/ldap.clj
@@ -56,7 +56,7 @@
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
-(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
+(sv/defmethod ::login-with-ldap {:auth false}
[{:keys [pool session tokens] :as cfg} params]
(db/with-atomic [conn pool]
(let [info (authenticate params)
diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj
index 8f9075cf15..ed9e8acea8 100644
--- a/backend/src/app/rpc/mutations/media.clj
+++ b/backend/src/app/rpc/mutations/media.clj
@@ -14,9 +14,10 @@
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
+ [app.rpc.rlimit :as rlimit]
[app.storage :as sto]
+ [app.util.async :as async]
[app.util.http :as http]
- [app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -49,13 +50,12 @@
:opt-un [::id]))
(sv/defmethod ::upload-file-media-object
- {::rlimit/permits (cf/get :rlimit-image)}
+ {::rlimit/permits (cf/get :rlimit-image)
+ ::async/dispatch :default}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
- (db/with-atomic [conn pool]
- (let [file (select-file conn file-id)]
- (teams/check-edition-permissions! conn profile-id (:team-id file))
- (-> (assoc cfg :conn conn)
- (create-file-media-object params)))))
+ (let [file (select-file pool file-id)]
+ (teams/check-edition-permissions! pool profile-id (:team-id file))
+ (create-file-media-object cfg params)))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@@ -77,6 +77,9 @@
:code :unable-to-access-to-url
:cause e))))
+;; TODO: we need to check the size before fetch resource, if not we
+;; can start downloading very big object and cause OOM errors.
+
(defn- download-media
[{:keys [storage] :as cfg} url]
(let [result (fetch-url url)
@@ -90,6 +93,7 @@
(-> (assoc storage :backend :tmp)
(sto/put-object {:content (sto/content data)
:content-type mtype
+ :reference :file-media-object
:expired-at (dt/in-future {:minutes 30})}))))
;; NOTE: we use the `on conflict do update` instead of `do nothing`
@@ -102,13 +106,27 @@
on conflict (id) do update set created_at=file_media_object.created_at
returning *")
+;; NOTE: the following function executes without a transaction, this
+;; means that if something fails in the middle of this function, it
+;; will probably leave leaked/unreferenced objects in the database and
+;; probably in the storage layer. For handle possible object leakage,
+;; we create all media objects marked as touched, this ensures that if
+;; something fails, all leaked (already created storage objects) will
+;; be eventually marked as deleted by the touched-gc task.
+;;
+;; The touched-gc task, performs periodic analisis of all touched
+;; storage objects and check references of it. This is the reason why
+;; `reference` metadata exists: it indicates the name of the table
+;; witch holds the reference to storage object (it some kind of
+;; inverse, soft referential integrity).
+
(defn create-file-media-object
- [{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}]
+ [{:keys [storage pool] :as cfg} {:keys [id file-id is-local name content] :as params}]
(media/validate-media-type (:content-type content))
- (let [storage (media/configure-assets-storage storage conn)
- source-path (fs/path (:tempfile content))
+ (let [source-path (fs/path (:tempfile content))
source-mtype (:content-type content)
source-info (media/run {:cmd :info :input {:path source-path :mtype source-mtype}})
+ storage (media/configure-assets-storage storage)
thumb (when (and (not (svg-image? source-info))
(big-enough-for-thumbnail? source-info))
@@ -119,16 +137,25 @@
image (if (= (:mtype source-info) "image/svg+xml")
(let [data (slurp source-path)]
- (sto/put-object storage {:content (sto/content data)
- :content-type (:mtype source-info)}))
- (sto/put-object storage {:content (sto/content source-path)
- :content-type (:mtype source-info)}))
+ (sto/put-object storage
+ {:content (sto/content data)
+ :content-type (:mtype source-info)
+ :reference :file-media-object
+ :touched-at (dt/now)}))
+ (sto/put-object storage
+ {:content (sto/content source-path)
+ :content-type (:mtype source-info)
+ :reference :file-media-object
+ :touched-at (dt/now)}))
thumb (when thumb
- (sto/put-object storage {:content (sto/content (:data thumb) (:size thumb))
- :content-type (:mtype thumb)}))]
+ (sto/put-object storage
+ {:content (sto/content (:data thumb) (:size thumb))
+ :content-type (:mtype thumb)
+ :reference :file-media-object
+ :touched-at (dt/now)}))]
- (db/exec-one! conn [sql:create-file-media-object
+ (db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
@@ -144,20 +171,19 @@
:opt-un [::id ::name]))
(sv/defmethod ::create-file-media-object-from-url
+ {::rlimit/permits (cf/get :rlimit-image)
+ ::async/dispatch :default}
[{:keys [pool storage] :as cfg} {:keys [profile-id file-id url name] :as params}]
- (db/with-atomic [conn pool]
- (let [file (select-file conn file-id)]
- (teams/check-edition-permissions! conn profile-id (:team-id file))
- (let [mobj (download-media cfg url)
- content {:filename "tempfile"
- :size (:size mobj)
- :tempfile (sto/get-object-path storage mobj)
- :content-type (:content-type (meta mobj))}
- params' (merge params {:content content
- :name (or name (:filename content))})]
- (-> (assoc cfg :conn conn)
- (create-file-media-object params'))))))
+ (let [file (select-file pool file-id)]
+ (teams/check-edition-permissions! pool profile-id (:team-id file))
+ (let [mobj (download-media cfg url)
+ content {:filename "tempfile"
+ :size (:size mobj)
+ :tempfile (sto/get-object-path storage mobj)
+ :content-type (:content-type (meta mobj))}]
+ (->> (merge params {:content content :name (or name (:filename content))})
+ (create-file-media-object cfg)))))
;; --- Clone File Media object (Upload and create from url)
@@ -189,7 +215,6 @@
:height (:height mobj)
:mtype (:mtype mobj)})))
-
;; --- HELPERS
(def ^:private
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index ab1a5a405f..4e2d207c8a 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -15,11 +15,11 @@
[app.http.oauth :refer [extract-utm-props]]
[app.loggers.audit :as audit]
[app.media :as media]
- [app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
+ [app.rpc.rlimit :as rlimit]
[app.storage :as sto]
- [app.util.rlimit :as rlimit]
+ [app.util.async :as async]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
@@ -38,7 +38,6 @@
(s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
-(declare annotate-profile-register)
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
@@ -102,6 +101,7 @@
(when-not (contains? cf/flags :registration)
(ex/raise :type :restriction
:code :registration-disabled))
+
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
@@ -116,10 +116,17 @@
(check-profile-existence! pool params)
- (let [params (assoc params
- :backend "penpot"
- :iss :prepared-register
- :exp (dt/in-future "48h"))
+ (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)
+ :invitation-token (:invitation-token params)
+ :backend "penpot"
+ :iss :prepared-register
+ :exp (dt/in-future "48h")}
token (tokens :generate params)]
{:token token}))
@@ -136,43 +143,33 @@
(-> (assoc cfg :conn conn)
(register-profile params))))
-(defn- annotate-profile-register
- "A helper for properly increase the profile-register metric once the
- transaction is completed."
- [metrics]
- (fn []
- (let [mobj (get-in metrics [:definitions :profile-register])]
- ((::mtx/fn mobj) {:by 1}))))
-
(defn register-profile
- [{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
+ [{: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))]
+ (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).
- (some? (:invitation-token params))
- (let [token (:invitation-token params)
- claims (tokens :verify {:token token :iss :team-invitation})
- claims (assoc claims
- :member-id (:id profile)
- :member-email (:email profile))
+ ;; 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))
- :before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
@@ -182,7 +179,6 @@
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
- :before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
@@ -191,7 +187,6 @@
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
- :before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
@@ -214,8 +209,7 @@
:extra-data ptoken})
(with-meta profile
- {:before-complete (annotate-profile-register metrics)
- ::audit/props (audit/profile->props profile)
+ {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(defn create-profile
@@ -284,7 +278,9 @@
:opt-un [::scope ::invitation-token]))
(sv/defmethod ::login
- {:auth false ::rlimit/permits (cf/get :rlimit-password)}
+ {:auth false
+ ::async/dispatch :default
+ ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
@@ -305,32 +301,26 @@
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))]
- (if-let [token (:invitation-token params)]
- ;; If the request comes with an invitation token, this means
- ;; that user wants to accept it with different user. A very
- ;; strange case but still can happen. In this case, we
- ;; proceed in the same way as in register: regenerate the
- ;; invitation token and return it to the user for proper
- ;; invitation acceptation.
- (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 (audit/profile->props profile)
- ::audit/profile-id (:id profile)}))
+ (let [profile (->> (profile/retrieve-profile-data-by-email conn email)
+ (validate-profile)
+ (profile/strip-private-attrs)
+ (profile/populate-additional-data conn)
+ (decode-profile-row))
- (with-meta profile
- {:transform-response ((:create session) (:id profile))
- ::audit/props (audit/profile->props profile)
- ::audit/profile-id (:id profile)}))))))
+ 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
@@ -360,6 +350,7 @@
:opt-un [::lang ::theme]))
(sv/defmethod ::update-profile
+ {::async/dispatch :default}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(let [profile (update-profile conn params)]
@@ -381,6 +372,11 @@
(db/with-atomic [conn pool]
(let [profile (validate-password! conn params)
session-id (:app.rpc/session-id params)]
+ (when (= (str/lower (:email profile))
+ (str/lower (:password params)))
+ (ex/raise :type :validation
+ :code :email-as-password
+ :hint "you can't use your email as password"))
(update-profile-password! conn (assoc profile :password password))
(invalidate-profile-session! conn (:id profile) session-id)
nil)))
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index e6cc7288b3..a104f3604e 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -18,8 +18,8 @@
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.rpc.queries.teams :as teams]
+ [app.rpc.rlimit :as rlimit]
[app.storage :as sto]
- [app.util.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -379,8 +379,7 @@
:code :member-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
- ;; Secondly check if the invited member email is part of the
- ;; global spam/bounce report.
+ ;; Secondly check if the invited member email is part of the global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
@@ -403,13 +402,21 @@
(s/and ::create-team (s/keys :req-un [::emails ::role])))
(sv/defmethod ::create-team-and-invite-members
- [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
+ [{:keys [pool audit] :as cfg} {:keys [profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(doseq [email emails]
+ (audit :cmd :submit
+ :type "mutation"
+ :name "create-team-invitation"
+ :profile-id profile-id
+ :props {:email email
+ :role role
+ :profile-id profile-id})
+
(create-team-invitation
(assoc cfg
:conn conn
diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj
index 1065803165..1b79b74f15 100644
--- a/backend/src/app/rpc/mutations/verify_token.clj
+++ b/backend/src/app/rpc/mutations/verify_token.clj
@@ -10,7 +10,6 @@
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
- [app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
@@ -44,16 +43,8 @@
::audit/props {:email email}
::audit/profile-id profile-id}))
-(defn- annotate-profile-activation
- "A helper for properly increase the profile-activation metric once the
- transaction is completed."
- [metrics]
- (fn []
- (let [mobj (get-in metrics [:definitions :profile-activation])]
- ((::mtx/fn mobj) {:by 1}))))
-
(defmethod process-token :verify-email
- [{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]
+ [{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
@@ -69,7 +60,6 @@
(with-meta claims
{:transform-response ((:create session) profile-id)
- :before-complete (annotate-profile-activation metrics)
::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
@@ -118,77 +108,39 @@
(assoc member :is-active true)))
(defmethod process-token :team-invitation
- [{:keys [session] :as cfg} {:keys [profile-id token]} {:keys [member-id] :as claims}]
+ [cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
(us/assert ::team-invitation-claims claims)
(cond
;; This happens when token is filled with member-id and current
- ;; user is already logged in with some account.
- (and (uuid? profile-id)
- (uuid? member-id))
+ ;; user is already logged in with exactly invited account.
+ (and (uuid? profile-id) (uuid? member-id) (= member-id profile-id))
(let [profile (accept-invitation cfg claims)]
- (if (= member-id profile-id)
- ;; If the current session is already matches the invited
- ;; member, then just return the token and leave the frontend
- ;; app redirect to correct team.
- (assoc claims :state :created)
-
- ;; If the session does not matches the invited member, replace
- ;; the session with a new one matching the invited member.
- ;; This technique should be considered secure because the
- ;; user clicking the link he already has access to the email
- ;; account.
- (with-meta
- (assoc claims :state :created)
- {:transform-response ((:create session) member-id)
- ::audit/name "accept-team-invitation"
- ::audit/props (merge
- (audit/profile->props profile)
- {:team-id (:team-id claims)
- :role (:role claims)})
- ::audit/profile-id profile-id})))
-
- ;; This happens when member-id is not filled in the invitation but
- ;; the user already has an account (probably with other mail) and
- ;; is already logged-in.
- (and (uuid? profile-id)
- (nil? member-id))
- (let [profile (accept-invitation cfg (assoc claims :member-id profile-id))]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
- ::audit/props (merge
- (audit/profile->props profile)
- {:team-id (:team-id claims)
- :role (:role claims)})
- ::audit/profile-id profile-id}))
-
- ;; This happens when member-id is filled but the accessing user is
- ;; not logged-in. In this case we proceed to accept invitation and
- ;; leave the user logged-in.
- (and (nil? profile-id)
- (uuid? member-id))
- (let [profile (accept-invitation cfg claims)]
- (with-meta
- (assoc claims :state :created)
- {:transform-response ((:create session) member-id)
- ::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
- ;; In this case, we wait until frontend app redirect user to
- ;; registration page, the user is correctly registered and the
- ;; register mutation call us again with the same token to finally
- ;; create the corresponding team-profile relation from the first
- ;; condition of this if.
+ ;; This case means that invitation token does not match with
+ ;; registred user, so we need to indicate to frontend to redirect
+ ;; it to register page.
+ (nil? member-id)
+ {:invitation-token token
+ :iss :team-invitation
+ :redirect-to :auth-register
+ :state :pending}
+
+ ;; In all other cases, just tell to fontend to redirect the user
+ ;; to the login page.
:else
{:invitation-token token
:iss :team-invitation
+ :redirect-to :auth-login
:state :pending}))
-
;; --- Default
(defmethod process-token :default
diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj
index 8e22f66371..50ed648377 100644
--- a/backend/src/app/rpc/queries/files.clj
+++ b/backend/src/app/rpc/queries/files.clj
@@ -7,7 +7,7 @@
(ns app.rpc.queries.files
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
@@ -26,6 +26,7 @@
;; --- Helpers & Specs
+(s/def ::frame-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project-id ::us/uuid)
@@ -242,13 +243,10 @@
(defn- trim-file-data
[file {:keys [page-id object-id]}]
(let [page (get-in file [:data :pages-index page-id])
- objects (->> (:objects page)
- (cp/get-object-with-children object-id)
- (map #(dissoc % :thumbnail)))
-
- objects (d/index-by :id objects)
+ objects (->> (cph/get-children-with-self (:objects page) object-id)
+ (map #(dissoc % :thumbnail))
+ (d/index-by :id))
page (assoc page :objects objects)]
-
(-> file
(update :data assoc :pages-index {page-id page})
(update :data assoc :pages [page-id]))))
@@ -395,6 +393,7 @@
)
select * from recent_files where row_num <= 10;")
+
(s/def ::team-recent-files
(s/keys :req-un [::profile-id ::team-id]))
@@ -404,6 +403,25 @@
(teams/check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:team-recent-files team-id])))
+
+;; --- QUERY: get the thumbnail for an frame
+
+(def ^:private sql:file-frame-thumbnail
+ "select data
+ from file_frame_thumbnail
+ where file_id = ?
+ and frame_id = ?")
+
+(s/def ::file-frame-thumbnail
+ (s/keys :req-un [::profile-id ::file-id ::frame-id]))
+
+(sv/defmethod ::file-frame-thumbnail
+ [{:keys [pool]} {:keys [profile-id file-id frame-id]}]
+ (with-open [conn (db/open pool)]
+ (check-read-permissions! conn profile-id file-id)
+ (db/exec-one! conn [sql:file-frame-thumbnail file-id frame-id])))
+
+
;; --- Helpers
(defn decode-row
diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj
index 5c4c338767..c1ed702444 100644
--- a/backend/src/app/rpc/queries/profile.clj
+++ b/backend/src/app/rpc/queries/profile.clj
@@ -35,7 +35,8 @@
(s/def ::profile
(s/keys :opt-un [::profile-id]))
-(sv/defmethod ::profile {:auth false}
+(sv/defmethod ::profile
+ {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other
diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj
new file mode 100644
index 0000000000..471bc526dc
--- /dev/null
+++ b/backend/src/app/rpc/retry.clj
@@ -0,0 +1,45 @@
+;; 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.retry
+ "A fault tolerance helpers. Allow retry some operations that we know
+ we can retry."
+ (:require
+ [app.common.logging :as l]
+ [app.util.services :as sv]
+ [promesa.core :as p]))
+
+(defn conflict-db-insert?
+ "Check if exception matches a insertion conflict on postgresql."
+ [e]
+ (and (instance? org.postgresql.util.PSQLException e)
+ (= "23505" (.getSQLState e))))
+
+(defn wrap-retry
+ [_ f {:keys [::matches ::sv/name]
+ :or {matches (constantly false)}
+ :as mdata}]
+
+ (when (::enabled mdata)
+ (l/debug :hint "wrapping retry" :name name))
+
+ (if-let [max-retries (::max-retries mdata)]
+ (fn [cfg params]
+ (letfn [(run [retry]
+ (-> (f cfg params)
+ (p/catch (partial handle-error retry))))
+
+ (handle-error [retry cause]
+ (if (matches cause)
+ (let [current-retry (inc retry)]
+ (l/trace :hint "running retry algorithm" :retry current-retry)
+ (if (<= current-retry max-retries)
+ (run current-retry)
+ (throw cause)))
+ (throw cause)))]
+ (run 0)))
+ f))
+
diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj
new file mode 100644
index 0000000000..1b70b2da62
--- /dev/null
+++ b/backend/src/app/rpc/rlimit.clj
@@ -0,0 +1,67 @@
+;; 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.rlimit
+ "Resource usage limits (in other words: semaphores)."
+ (:require
+ [app.common.data :as d]
+ [app.common.logging :as l]
+ [app.metrics :as mtx]
+ [app.util.services :as sv]
+ [promesa.core :as p]))
+
+(defprotocol IAsyncSemaphore
+ (acquire! [_])
+ (release! [_]))
+
+(defn semaphore
+ [{:keys [permits metrics name]}]
+ (let [name (d/name name)
+ used (volatile! 0)
+ queue (volatile! (d/queue))
+ labels (into-array String [name])]
+ (reify IAsyncSemaphore
+ (acquire! [this]
+ (let [d (p/deferred)]
+ (locking this
+ (if (< @used permits)
+ (do
+ (vswap! used inc)
+ (p/resolve! d))
+ (vswap! queue conj d)))
+
+ (mtx/run! metrics {:id :rlimit-used-permits :val @used :labels labels })
+ (mtx/run! metrics {:id :rlimit-queued-submissions :val (count @queue) :labels labels})
+ (mtx/run! metrics {:id :rlimit-acquires-total :inc 1 :labels labels})
+ d))
+
+ (release! [this]
+ (locking this
+ (if-let [item (peek @queue)]
+ (do
+ (vswap! queue pop)
+ (p/resolve! item))
+ (when (pos? @used)
+ (vswap! used dec))))
+
+ (mtx/run! metrics {:id :rlimit-used-permits :val @used :labels labels})
+ (mtx/run! metrics {:id :rlimit-queued-submissions :val (count @queue) :labels labels})
+ ))))
+
+(defn wrap-rlimit
+ [{:keys [metrics] :as cfg} f mdata]
+ (if-let [permits (::permits mdata)]
+ (let [sem (semaphore {:permits permits
+ :metrics metrics
+ :name (::sv/name mdata)})]
+ (l/debug :hint "wrapping rlimit" :handler (::sv/name mdata) :permits permits)
+ (fn [cfg params]
+ (-> (acquire! sem)
+ (p/then (fn [_] (f cfg params)))
+ (p/finally (fn [_ _] (release! sem))))))
+ f))
+
+
diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj
index 610e9e9ff6..b1b5e93138 100644
--- a/backend/src/app/setup.clj
+++ b/backend/src/app/setup.clj
@@ -7,6 +7,7 @@
(ns app.setup
"Initial data setup of instance."
(:require
+ [app.common.logging :as l]
[app.common.uuid :as uuid]
[app.db :as db]
[buddy.core.codecs :as bc]
@@ -14,55 +15,49 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
-(declare initialize-instance-id!)
-(declare initialize-secret-key!)
-(declare retrieve-all)
+(defn- generate-random-key
+ []
+ (-> (bn/random-bytes 64)
+ (bc/bytes->b64u)
+ (bc/bytes->str)))
+
+(defn- retrieve-all
+ [conn]
+ (->> (db/query conn :server-prop {:preload true})
+ (filter #(not= "secret-key" (:id %)))
+ (map (fn [row]
+ [(keyword (:id row))
+ (db/decode-transit-pgobject (:content row))]))
+ (into {})))
+
+(defn- handle-instance-id
+ [instance-id conn read-only?]
+ (or instance-id
+ (let [instance-id (uuid/random)]
+ (when-not read-only?
+ (try
+ (db/insert! conn :server-prop
+ {:id "instance-id"
+ :preload true
+ :content (db/tjson instance-id)})
+ (catch Throwable cause
+ (l/warn :hint "unable to persist instance-id"
+ :instance-id instance-id
+ :cause cause))))
+ instance-id)))
(defmethod ig/pre-init-spec ::props [_]
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::props
- [_ {:keys [pool] :as cfg}]
+ [_ {:keys [pool key] :as cfg}]
(db/with-atomic [conn pool]
- (let [cfg (assoc cfg :conn conn)]
- (initialize-secret-key! cfg)
- (initialize-instance-id! cfg)
- (retrieve-all cfg))))
+ (db/xact-lock! conn 0)
+ (when-not key
+ (l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
+ "all sessions on each restart, it is hightly recommeded setting up the "
+ "PENPOT_SECRET_KEY environment variable")))
-(def sql:upsert-secret-key
- "insert into server_prop (id, preload, content)
- values ('secret-key', true, ?::jsonb)
- on conflict (id) do update set content = ?::jsonb")
-
-(def sql:insert-secret-key
- "insert into server_prop (id, preload, content)
- values ('secret-key', true, ?::jsonb)
- on conflict (id) do nothing")
-
-(defn- initialize-secret-key!
- [{:keys [conn key] :as cfg}]
- (if key
- (let [key (db/tjson key)]
- (db/exec-one! conn [sql:upsert-secret-key key key]))
- (let [key (-> (bn/random-bytes 64)
- (bc/bytes->b64u)
- (bc/bytes->str))
- key (db/tjson key)]
- (db/exec-one! conn [sql:insert-secret-key key]))))
-
-(defn- initialize-instance-id!
- [{:keys [conn] :as cfg}]
- (let [iid (uuid/random)]
-
- (db/insert! conn :server-prop
- {:id "instance-id"
- :preload true
- :content (db/tjson iid)}
- {:on-conflict-do-nothing true})))
-
-(defn- retrieve-all
- [{:keys [conn] :as cfg}]
- (reduce (fn [acc row]
- (assoc acc (keyword (:id row)) (db/decode-transit-pgobject (:content row))))
- {}
- (db/query conn :server-prop {:preload true})))
+ (let [stored (-> (retrieve-all conn)
+ (assoc :secret-key (or key (generate-random-key))))]
+ (update stored :instance-id handle-instance-id conn (db/read-only? pool)))))
diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj
index 111f46e320..11d6116ad3 100644
--- a/backend/src/app/srepl/main.clj
+++ b/backend/src/app/srepl/main.clj
@@ -7,7 +7,7 @@
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.pages.migrations :as pmg]
- [app.common.pages.spec :as spec]
+ [app.common.spec.file :as spec.file]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
@@ -86,7 +86,7 @@
(validate-item [{:keys [id data modified-at] :as file}]
(let [data (blob/decode data)
- valid? (s/valid? ::spec/data data)]
+ valid? (s/valid? ::spec.file/data data)]
(l/debug :hint "validated file"
:file-id id
@@ -98,7 +98,7 @@
:valid valid?)
(when (and (not valid?) verbose?)
- (let [edata (-> (s/explain-data ::spec/data data)
+ (let [edata (-> (s/explain-data ::spec.file/data data)
(update ::s/problems #(take 5 %)))]
(binding [s/*explain-out* expound/printer]
(l/warn ::l/raw (with-out-str (s/explain-out edata))))))
diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj
index eeee57f465..e084c46332 100644
--- a/backend/src/app/storage.clj
+++ b/backend/src/app/storage.clj
@@ -18,11 +18,9 @@
[app.storage.impl :as impl]
[app.storage.s3 :as ss3]
[app.util.time :as dt]
- [app.worker :as wrk]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
- [integrant.core :as ig]
- [promesa.exec :as px]))
+ [integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Storage Module State
@@ -40,7 +38,7 @@
:db ::sdb/backend))))
(defmethod ig/pre-init-spec ::storage [_]
- (s/keys :req-un [::wrk/executor ::db/pool ::backends]))
+ (s/keys :req-un [::db/pool ::backends]))
(defmethod ig/prep-key ::storage
[_ {:keys [backends] :as cfg}]
@@ -53,78 +51,74 @@
(assoc :backends (d/without-nils backends))))
(s/def ::storage
- (s/keys :req-un [::backends ::wrk/executor ::db/pool]))
+ (s/keys :req-un [::backends ::db/pool]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Database Objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defrecord StorageObject [id size created-at expired-at backend])
+(defrecord StorageObject [id size created-at expired-at touched-at backend])
(defn storage-object?
[v]
(instance? StorageObject v))
-(def ^:private
- sql:insert-storage-object
- "insert into storage_object (id, size, backend, metadata)
- values (?, ?, ?, ?::jsonb)
- returning *")
+(s/def ::storage-object storage-object?)
+(s/def ::storage-content impl/content?)
-(def ^:private
- sql:insert-storage-object-with-expiration
- "insert into storage_object (id, size, backend, metadata, deleted_at)
- values (?, ?, ?, ?::jsonb, ?)
- returning *")
-(defn- insert-object
- [conn id size backend mdata expiration]
- (if expiration
- (db/exec-one! conn [sql:insert-storage-object-with-expiration id size backend mdata expiration])
- (db/exec-one! conn [sql:insert-storage-object id size backend mdata])))
+(defn- clone-database-object
+ ;; If we in this condition branch, this means we come from the
+ ;; clone-object, so we just need to clone it with a new backend.
+ [{:keys [conn backend]} object]
+ (let [id (uuid/random)
+ mdata (meta object)
+ result (db/insert! conn :storage-object
+ {:id id
+ :size (:size object)
+ :backend (name backend)
+ :metadata (db/tjson mdata)
+ :deleted-at (:expired-at object)
+ :touched-at (:touched-at object)})]
+ (assoc object
+ :id (:id result)
+ :backend backend
+ :created-at (:created-at result)
+ :touched-at (:touched-at result))))
(defn- create-database-object
[{:keys [conn backend]} {:keys [content] :as object}]
- (if (instance? StorageObject object)
- ;; If we in this condition branch, this means we come from the
- ;; clone-object, so we just need to clone it with a new backend.
- (let [id (uuid/random)
- mdata (meta object)
- result (insert-object conn
- id
- (:size object)
- (name backend)
- (db/tjson mdata)
- (:expired-at object))]
- (assoc object
- :id (:id result)
- :backend backend
- :created-at (:created-at result)))
- (let [id (uuid/random)
- mdata (dissoc object :content :expired-at)
- result (insert-object conn
- id
- (count content)
- (name backend)
- (db/tjson mdata)
- (:expired-at object))]
- (StorageObject. (:id result)
- (:size result)
- (:created-at result)
- (:deleted-at result)
- backend
- mdata
- nil))))
+ (us/assert ::storage-content content)
+ (let [id (uuid/random)
+ mdata (dissoc object :content :expired-at :touched-at)
+
+ result (db/insert! conn :storage-object
+ {:id id
+ :size (count content)
+ :backend (name backend)
+ :metadata (db/tjson mdata)
+ :deleted-at (:expired-at object)
+ :touched-at (:touched-at object)})]
+
+ (StorageObject. (:id result)
+ (:size result)
+ (:created-at result)
+ (:deleted-at result)
+ (:touched-at result)
+ backend
+ mdata
+ nil)))
(def ^:private sql:retrieve-storage-object
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
(defn row->storage-object [res]
- (let [mdata (some-> (:metadata res) (db/decode-transit-pgobject))]
+ (let [mdata (or (some-> (:metadata res) (db/decode-transit-pgobject)) {})]
(StorageObject. (:id res)
(:size res)
(:created-at res)
(:deleted-at res)
+ (:touched-at res)
(keyword (:backend res))
mdata
nil)))
@@ -142,10 +136,6 @@
(let [result (db/exec-one! conn [sql:delete-storage-object id])]
(pos? (:next.jdbc/update-count result))))
-(defn- register-recheck
- [{:keys [pool] :as storage} backend id]
- (db/insert! pool :storage-pending {:id id :backend (name backend)}))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -170,17 +160,13 @@
(defn put-object
"Creates a new object with the provided content."
- [{:keys [pool conn backend executor] :as storage} {:keys [content] :as params}]
+ [{:keys [pool conn backend] :as storage} {:keys [content] :as params}]
(us/assert ::storage storage)
- (us/assert impl/content? content)
+ (us/assert ::storage-content content)
+ (us/assert ::us/keyword backend)
(let [storage (assoc storage :conn (or conn pool))
object (create-database-object storage params)]
- ;; Schedule to execute in background; in an other transaction and
- ;; register the currently created storage object id for a later
- ;; recheck.
- (px/run! executor #(register-recheck storage backend (:id object)))
-
;; Store the data finally on the underlying storage subsystem.
(-> (impl/resolve-backend storage backend)
(impl/put-object object content))
@@ -190,10 +176,12 @@
(defn clone-object
"Creates a clone of the provided object using backend based efficient
method. Always clones objects to the configured default."
- [{:keys [pool conn] :as storage} object]
+ [{:keys [pool conn backend] :as storage} object]
(us/assert ::storage storage)
+ (us/assert ::storage-object object)
+ (us/assert ::us/keyword backend)
(let [storage (assoc storage :conn (or conn pool))
- object* (create-database-object storage object)]
+ object* (clone-database-object storage object)]
(if (= (:backend object) (:backend storage))
;; if the source and destination backends are the same, we
;; proceed to use the fast path with specific copy
@@ -269,7 +257,7 @@
;; A task responsible to permanently delete already marked as deleted
;; storage files.
-(declare sql:retrieve-deleted-objects)
+(declare sql:retrieve-deleted-objects-chunk)
(s/def ::min-age ::dt/duration)
@@ -278,44 +266,46 @@
(defmethod ig/init-key ::gc-deleted-task
[_ {:keys [pool storage min-age] :as cfg}]
- (letfn [(group-by-backend [rows]
- (let [conj (fnil conj [])]
- [(reduce (fn [acc {:keys [id backend]}]
- (update acc (keyword backend) conj id))
- {}
- rows)
- (count rows)]))
+ (letfn [(retrieve-deleted-objects-chunk [conn cursor]
+ (let [min-age (db/interval min-age)
+ rows (db/exec! conn [sql:retrieve-deleted-objects-chunk min-age cursor])]
+ [(some-> rows peek :created-at)
+ (some->> (seq rows) (d/group-by' #(-> % :backend keyword) :id) seq)]))
(retrieve-deleted-objects [conn]
- (let [min-age (db/interval min-age)
- rows (db/exec! conn [sql:retrieve-deleted-objects min-age])]
- (some-> (seq rows) (group-by-backend))))
+ (->> (d/iteration (fn [cursor]
+ (retrieve-deleted-objects-chunk conn cursor))
+ :initk (dt/now)
+ :vf second
+ :kf first)
+ (sequence cat)))
- (delete-in-bulk [conn [backend ids]]
+ (delete-in-bulk [conn backend ids]
(let [backend (impl/resolve-backend storage backend)
backend (assoc backend :conn conn)]
(impl/del-objects-in-bulk backend ids)))]
(fn [_]
(db/with-atomic [conn pool]
- (loop [n 0]
- (if-let [[groups total] (retrieve-deleted-objects conn)]
+ (loop [total 0
+ groups (retrieve-deleted-objects conn)]
+ (if-let [[backend ids] (first groups)]
(do
- (run! (partial delete-in-bulk conn) groups)
- (recur (+ n ^long total)))
+ (delete-in-bulk conn backend ids)
+ (recur (+ total (count ids))
+ (rest groups)))
(do
- (l/info :task "gc-deleted"
- :hint "permanently delete items"
- :count n)
- {:deleted n})))))))
+ (l/info :task "gc-deleted" :count total)
+ {:deleted total})))))))
-(def sql:retrieve-deleted-objects
+(def sql:retrieve-deleted-objects-chunk
"with items_part as (
select s.id
from storage_object as s
where s.deleted_at is not null
and s.deleted_at < (now() - ?::interval)
- order by s.deleted_at
+ and s.created_at < ?
+ order by s.created_at desc
limit 100
)
delete from storage_object
@@ -326,157 +316,105 @@
;; Garbage Collection: Analyze touched objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; This task is part of the garbage collection of storage objects and
-;; is responsible on analyzing the touched objects and mark them for deletion
-;; if corresponds.
+;; This task is part of the garbage collection of storage objects and is responsible on analyzing the touched
+;; objects and mark them for deletion if corresponds.
;;
-;; When file_media_object is deleted, the depending storage_object are
-;; marked as touched. This means that some files that depend on a
-;; concrete storage_object are no longer exists and maybe this
-;; storage_object is no longer necessary and can be eligible for
-;; elimination. This task periodically analyzes touched objects and
-;; mark them as freeze (means that has other references and the object
-;; is still valid) or deleted (no more references to this object so is
-;; ready to be deleted).
+;; For example: when file_media_object is deleted, the depending storage_object are marked as touched. This
+;; means that some files that depend on a concrete storage_object are no longer exists and maybe this
+;; storage_object is no longer necessary and can be eligible for elimination. This task periodically analyzes
+;; touched objects and mark them as freeze (means that has other references and the object is still valid) or
+;; deleted (no more references to this object so is ready to be deleted).
-(declare sql:retrieve-touched-objects)
+(declare sql:retrieve-touched-objects-chunk)
+(declare sql:retrieve-file-media-object-nrefs)
+(declare sql:retrieve-team-font-variant-nrefs)
(defmethod ig/pre-init-spec ::gc-touched-task [_]
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::gc-touched-task
[_ {:keys [pool] :as cfg}]
- (letfn [(group-results [rows]
- (let [conj (fnil conj [])]
- (reduce (fn [acc {:keys [id nrefs]}]
- (if (pos? nrefs)
- (update acc :to-freeze conj id)
- (update acc :to-delete conj id)))
- {}
- rows)))
+ (letfn [(has-team-font-variant-nrefs? [conn id]
+ (-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs pos?))
- (retrieve-touched [conn]
- (let [rows (db/exec! conn [sql:retrieve-touched-objects])]
- (some-> (seq rows) (group-results))))
-
- (mark-delete-in-bulk [conn ids]
- (db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
- (db/create-array conn "uuid" (into-array java.util.UUID ids))]))
+ (has-file-media-object-nrefs? [conn id]
+ (-> (db/exec-one! conn [sql:retrieve-file-media-object-nrefs id id]) :nrefs pos?))
(mark-freeze-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set touched_at=null where id = ANY(?)"
- (db/create-array conn "uuid" (into-array java.util.UUID ids))]))]
+ (db/create-array conn "uuid" ids)]))
+
+ (mark-delete-in-bulk [conn ids]
+ (db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
+ (db/create-array conn "uuid" ids)]))
+
+ (retrieve-touched-chunk [conn cursor]
+ (let [rows (->> (db/exec! conn [sql:retrieve-touched-objects-chunk cursor])
+ (mapv #(d/update-when % :metadata db/decode-transit-pgobject)))
+ kw (fn [o] (if (keyword? o) o (keyword o)))]
+ (when (seq rows)
+ [(-> rows peek :created-at)
+ ;; NOTE: we use the :file-media-object as default value for backward compatibility because when we
+ ;; deploy it we can have old backend instances running in the same time as the new one and we can
+ ;; still have storage-objects created without reference value. And we know that if it does not
+ ;; have value, it means :file-media-object.
+ (d/group-by' #(or (some-> % :metadata :reference kw) :file-media-object) :id rows)])))
+
+ (retrieve-touched [conn]
+ (->> (d/iteration (fn [cursor]
+ (retrieve-touched-chunk conn cursor))
+ :initk (dt/now)
+ :vf second
+ :kf first)
+ (sequence cat)))
+
+ (process-objects! [conn pred-fn ids]
+ (loop [to-freeze #{}
+ to-delete #{}
+ ids (seq ids)]
+ (if-let [id (first ids)]
+ (if (pred-fn conn id)
+ (recur (conj to-freeze id) to-delete (rest ids))
+ (recur to-freeze (conj to-delete id) (rest ids)))
+
+ (do
+ (some->> (seq to-freeze) (mark-freeze-in-bulk conn))
+ (some->> (seq to-delete) (mark-delete-in-bulk conn))
+ [(count to-freeze) (count to-delete)]))))
+ ]
(fn [_]
(db/with-atomic [conn pool]
- (loop [cntf 0
- cntd 0]
- (if-let [{:keys [to-delete to-freeze]} (retrieve-touched conn)]
+ (loop [to-freeze 0
+ to-delete 0
+ groups (retrieve-touched conn)]
+ (if-let [[reference ids] (first groups)]
+ (let [[f d] (case reference
+ :file-media-object (process-objects! conn has-file-media-object-nrefs? ids)
+ :team-font-variant (process-objects! conn has-team-font-variant-nrefs? ids)
+ (ex/raise :type :internal
+ :code :unexpected-unknown-reference
+ :hint (format "unknown reference %s" (pr-str reference))))]
+ (recur (+ to-freeze f)
+ (+ to-delete d)
+ (rest groups)))
(do
- (when (seq to-delete) (mark-delete-in-bulk conn to-delete))
- (when (seq to-freeze) (mark-freeze-in-bulk conn to-freeze))
- (recur (+ cntf (count to-freeze))
- (+ cntd (count to-delete))))
- (do
- (l/info :task "gc-touched"
- :hint "mark freeze"
- :count cntf)
- (l/info :task "gc-touched"
- :hint "mark for deletion"
- :count cntd)
- {:freeze cntf :delete cntd})))))))
+ (l/info :task "gc-touched" :to-freeze to-freeze :to-delete to-delete)
+ {:freeze to-freeze :delete to-delete})))))))
-(def sql:retrieve-touched-objects
- "select so.id,
- ((select count(*) from file_media_object where media_id = so.id) +
- (select count(*) from file_media_object where thumbnail_id = so.id)) as nrefs
- from storage_object as so
+(def sql:retrieve-touched-objects-chunk
+ "select so.* from storage_object as so
where so.touched_at is not null
- order by so.touched_at
- limit 100;")
+ and so.created_at < ?
+ order by so.created_at desc
+ limit 500;")
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Recheck Stalled Task
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(def sql:retrieve-file-media-object-nrefs
+ "select ((select count(*) from file_media_object where media_id = ?) +
+ (select count(*) from file_media_object where thumbnail_id = ?)) as nrefs")
-;; Because the physical storage (filesystem, s3, ... except db) is not
-;; transactional, in some situations we can found physical object
-;; leakage. That situations happens when the transaction that writes
-;; the file aborts, leaving the file written to the underlying storage
-;; but the reference on the database is lost with the rollback.
-;;
-;; For this situations we need to write a "log" of inserted files that
-;; are checked in some time in future. If physical file exists but the
-;; database refence does not exists means that leaked file is found
-;; and is immediately deleted. The responsibility of this task is
-;; check that write log for possible leaked files.
-
-(def recheck-min-age (dt/duration {:hours 1}))
-
-(declare sql:retrieve-pending-to-recheck)
-(declare sql:exists-storage-object)
-
-(defmethod ig/pre-init-spec ::recheck-task [_]
- (s/keys :req-un [::storage ::db/pool]))
-
-(defmethod ig/init-key ::recheck-task
- [_ {:keys [pool storage] :as cfg}]
- (letfn [(group-results [rows]
- (let [conj (fnil conj [])]
- (reduce (fn [acc {:keys [id exist] :as row}]
- (cond-> (update acc :all conj id)
- (false? exist)
- (update :to-delete conj (dissoc row :exist))))
- {}
- rows)))
-
- (group-by-backend [rows]
- (let [conj (fnil conj [])]
- (reduce (fn [acc {:keys [id backend]}]
- (update acc (keyword backend) conj id))
- {}
- rows)))
-
- (retrieve-pending [conn]
- (let [rows (db/exec! conn [sql:retrieve-pending-to-recheck (db/interval recheck-min-age)])]
- (some-> (seq rows) (group-results))))
-
- (delete-group [conn [backend ids]]
- (let [backend (impl/resolve-backend storage backend)
- backend (assoc backend :conn conn)]
- (impl/del-objects-in-bulk backend ids)))
-
- (delete-all [conn ids]
- (let [ids (db/create-array conn "uuid" (into-array java.util.UUID ids))]
- (db/exec-one! conn ["delete from storage_pending where id = ANY(?)" ids])))]
-
- (fn [_]
- (db/with-atomic [conn pool]
- (loop [n 0 d 0]
- (if-let [{:keys [all to-delete]} (retrieve-pending conn)]
- (let [groups (group-by-backend to-delete)]
- (run! (partial delete-group conn) groups)
- (delete-all conn all)
- (recur (+ n (count all))
- (+ d (count to-delete))))
- (do
- (l/info :task "recheck"
- :hint "recheck items"
- :processed n
- :deleted d)
- {:processed n :deleted d})))))))
-
-(def sql:retrieve-pending-to-recheck
- "select sp.id,
- sp.backend,
- sp.created_at,
- (case when count(so.id) > 0 then true
- else false
- end) as exist
- from storage_pending as sp
- left join storage_object as so
- on (so.id = sp.id)
- where sp.created_at < now() - ?::interval
- group by 1,2,3
- order by sp.created_at asc
- limit 100")
+(def sql:retrieve-team-font-variant-nrefs
+ "select ((select count(*) from team_font_variant where woff1_file_id = ?) +
+ (select count(*) from team_font_variant where woff2_file_id = ?) +
+ (select count(*) from team_font_variant where otf_file_id = ?) +
+ (select count(*) from team_font_variant where ttf_file_id = ?)) as nrefs")
diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj
index 10c5710e06..22b3d88bdb 100644
--- a/backend/src/app/storage/s3.clj
+++ b/backend/src/app/storage/s3.clj
@@ -56,9 +56,10 @@
(s/def ::region #{:eu-central-1})
(s/def ::bucket ::us/string)
(s/def ::prefix ::us/string)
+(s/def ::endpoint ::us/string)
(defmethod ig/pre-init-spec ::backend [_]
- (s/keys :opt-un [::region ::bucket ::prefix]))
+ (s/keys :opt-un [::region ::bucket ::prefix ::endpoint]))
(defmethod ig/prep-key ::backend
[_ {:keys [prefix] :as cfg}]
@@ -119,20 +120,31 @@
(defn- ^Region lookup-region
[region]
- (case region
- :eu-central-1 Region/EU_CENTRAL_1))
+ (Region/of (name region)))
(defn build-s3-client
- [{:keys [region]}]
- (.. (S3Client/builder)
- (region (lookup-region region))
- (build)))
+ [{:keys [region endpoint]}]
+ (if (string? endpoint)
+ (let [uri (java.net.URI. endpoint)]
+ (.. (S3Client/builder)
+ (endpointOverride uri)
+ (region (lookup-region region))
+ (build)))
+ (.. (S3Client/builder)
+ (region (lookup-region region))
+ (build))))
(defn build-s3-presigner
- [{:keys [region]}]
- (.. (S3Presigner/builder)
- (region (lookup-region region))
- (build)))
+ [{:keys [region endpoint]}]
+ (if (string? endpoint)
+ (let [uri (java.net.URI. endpoint)]
+ (.. (S3Presigner/builder)
+ (endpointOverride uri)
+ (region (lookup-region region))
+ (build)))
+ (.. (S3Presigner/builder)
+ (region (lookup-region region))
+ (build))))
(defn put-object
[{:keys [client bucket prefix]} {:keys [id] :as object} content]
diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj
index 17d40c9041..40f55f53fc 100644
--- a/backend/src/app/tasks/file_media_gc.clj
+++ b/backend/src/app/tasks/file_media_gc.clj
@@ -10,6 +10,7 @@
after some period of inactivity (the default threshold is 72h)."
(:require
[app.common.logging :as l]
+ [app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.util.blob :as blob]
@@ -52,6 +53,7 @@
limit 10
for update skip locked")
+
(defn- retrieve-candidates
[{:keys [conn max-age] :as cfg}]
(let [interval (db/interval max-age)]
@@ -64,12 +66,11 @@
(comp
(map :objects)
(mapcat vals)
- (map (fn [{:keys [type] :as obj}]
- (case type
- :path (get-in obj [:fill-image :id])
- :image (get-in obj [:metadata :id])
- nil)))
- (filter uuid?)))
+ (keep (fn [{:keys [type] :as obj}]
+ (case type
+ :path (get-in obj [:fill-image :id])
+ :image (get-in obj [:metadata :id])
+ nil)))))
(defn- collect-used-media
[data]
@@ -80,37 +81,59 @@
(into collect-media-xf pages)
(into (keys (:media data))))))
+(def ^:private
+ collect-frames-xf
+ (comp
+ (map :objects)
+ (mapcat vals)
+ (filter cph/frame-shape?)
+ (keep :id)))
+
+(defn- collect-frames
+ [data]
+ (let [pages (concat
+ (vals (:pages-index data))
+ (vals (:components data)))]
+ (into #{} collect-frames-xf pages)))
+
(defn- process-file
[{:keys [conn] :as cfg} {:keys [id data age] :as file}]
- (let [data (-> (blob/decode data)
- (assoc :id id)
- (pmg/migrate-data))
+ (let [data (-> (blob/decode data)
+ (assoc :id id)
+ (pmg/migrate-data))]
- used (collect-used-media data)
- unused (->> (db/query conn :file-media-object {:file-id id})
- (remove #(contains? used (:id %))))]
+ (let [used (collect-used-media data)
+ unused (->> (db/query conn :file-media-object {:file-id id})
+ (remove #(contains? used (:id %))))]
- (l/debug :hint "processing file"
- :id id
- :age age
- :to-delete (count unused))
+ (l/debug :hint "processing file"
+ :id id
+ :age age
+ :to-delete (count unused))
- ;; Mark file as trimmed
- (db/update! conn :file
- {:has-media-trimmed true}
- {:id id})
+ ;; Mark file as trimmed
+ (db/update! conn :file
+ {:has-media-trimmed true}
+ {:id id})
- (doseq [mobj unused]
- (l/debug :hint "deleting media object"
- :id (:id mobj)
- :media-id (:media-id mobj)
- :thumbnail-id (:thumbnail-id mobj))
+ (doseq [mobj unused]
+ (l/debug :hint "deleting media object"
+ :id (:id mobj)
+ :media-id (:media-id mobj)
+ :thumbnail-id (:thumbnail-id mobj))
- ;; NOTE: deleting the file-media-object in the database
- ;; automatically marks as touched the referenced storage
- ;; objects. The touch mechanism is needed because many files can
- ;; point to the same storage objects and we can't just delete
- ;; them.
- (db/delete! conn :file-media-object {:id (:id mobj)}))
+ ;; NOTE: deleting the file-media-object in the database
+ ;; automatically marks as touched the referenced storage
+ ;; objects. The touch mechanism is needed because many files can
+ ;; point to the same storage objects and we can't just delete
+ ;; them.
+ (db/delete! conn :file-media-object {:id (:id mobj)})))
+
+ (let [sql (str "delete from file_frame_thumbnail "
+ " where file_id = ? and not (frame_id = ANY(?))")
+ ids (->> (collect-frames data)
+ (db/create-array conn "uuid"))]
+ ;; delete the unused frame thumbnails
+ (db/exec! conn [sql (:id file) ids]))
nil))
diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj
index 812e36f975..7b208cb00f 100644
--- a/backend/src/app/tasks/telemetry.clj
+++ b/backend/src/app/tasks/telemetry.clj
@@ -38,14 +38,17 @@
(defmethod ig/init-key ::handler
[_ {:keys [pool sprops version] :as cfg}]
- (fn [_]
+ (fn [{:keys [send?] :or {send? true}}]
;; Sleep randomly between 0 to 10s
- (thread-sleep (rand-int 10000))
+ (when send?
+ (thread-sleep (rand-int 10000)))
- (let [instance-id (:instance-id sprops)]
- (-> (get-stats pool version)
- (assoc :instance-id instance-id)
- (send! cfg)))))
+ (let [instance-id (:instance-id sprops)
+ stats (-> (get-stats pool version)
+ (assoc :instance-id instance-id))]
+ (when send?
+ (send! stats cfg))
+ stats)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
@@ -137,12 +140,28 @@
(->> [sql:team-averages]
(db/exec-one! conn)))
+(defn- retrieve-enabled-auth-providers
+ [conn]
+ (let [sql (str "select auth_backend as backend, count(*) as total "
+ " from profile group by 1")
+ rows (db/exec! conn [sql])]
+ (->> rows
+ (map (fn [{:keys [backend total]}]
+ (let [backend (or backend "penpot")]
+ [(keyword (str "auth-backend-" backend))
+ total])))
+ (into {}))))
+
(defn- retrieve-jvm-stats
[]
(let [^Runtime runtime (Runtime/getRuntime)]
{:jvm-heap-current (.totalMemory runtime)
:jvm-heap-max (.maxMemory runtime)
- :jvm-cpus (.availableProcessors runtime)}))
+ :jvm-cpus (.availableProcessors runtime)
+ :os-arch (System/getProperty "os.arch")
+ :os-name (System/getProperty "os.name")
+ :os-version (System/getProperty "os.version")
+ :user-tz (System/getProperty "user.timezone")}))
(defn get-stats
[conn version]
@@ -161,6 +180,7 @@
:total-touched-files (retrieve-num-touched-files conn)}
(d/merge
(retrieve-team-averages conn)
- (retrieve-jvm-stats))
+ (retrieve-jvm-stats)
+ (retrieve-enabled-auth-providers conn))
(d/without-nils))))
diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj
index efff646d1e..532055c90e 100644
--- a/backend/src/app/tokens.clj
+++ b/backend/src/app/tokens.clj
@@ -7,6 +7,7 @@
(ns app.tokens
"Tokens generation service."
(:require
+ [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.transit :as t]
@@ -17,7 +18,7 @@
(defn- generate
[cfg claims]
- (let [payload (t/encode claims)]
+ (let [payload (-> claims d/without-nils t/encode)]
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm})))
(defn- verify
diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj
index 6193fbe2fd..c04fa891f7 100644
--- a/backend/src/app/util/async.clj
+++ b/backend/src/app/util/async.clj
@@ -7,7 +7,8 @@
(ns app.util.async
(:require
[clojure.core.async :as a]
- [clojure.spec.alpha :as s])
+ [clojure.spec.alpha :as s]
+ [promesa.exec :as px])
(:import
java.util.concurrent.Executor))
@@ -54,13 +55,16 @@
(a/close! c)
c))))
-
(defmacro with-thread
[executor & body]
(if (= executor ::default)
`(a/thread-call (^:once fn* [] (try ~@body (catch Exception e# e#))))
`(thread-call ~executor (^:once fn* [] ~@body))))
+(defmacro with-dispatch
+ [executor & body]
+ `(px/submit! ~executor (^:once fn* [] ~@body)))
+
(defn batch
[in {:keys [max-batch-size
max-batch-age
diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj
deleted file mode 100644
index d0bed166db..0000000000
--- a/backend/src/app/util/retry.clj
+++ /dev/null
@@ -1,43 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.util.retry
- "A fault tolerance helpers. Allow retry some operations that we know
- we can retry."
- (:require
- [app.common.exceptions :as ex]
- [app.common.logging :as l]
- [app.util.async :as aa]
- [app.util.services :as sv]))
-
-(defn conflict-db-insert?
- "Check if exception matches a insertion conflict on postgresql."
- [e]
- (and (instance? org.postgresql.util.PSQLException e)
- (= "23505" (.getSQLState e))))
-
-(defn wrap-retry
- [_ f {:keys [::max-retries ::matches ::sv/name]
- :or {max-retries 3
- matches (constantly false)}
- :as mdata}]
- (when (::enabled mdata)
- (l/debug :hint "wrapping retry" :name name))
- (if (::enabled mdata)
- (fn [cfg params]
- (loop [retry 1]
- (when (> retry 1)
- (l/debug :hint "retrying controlled function" :retry retry :name name))
- (let [res (ex/try (f cfg params))]
- (if (ex/exception? res)
- (if (and (matches res) (< retry max-retries))
- (do
- (aa/thread-sleep (* 100 retry))
- (recur (inc retry)))
- (throw res))
- res))))
- f))
-
diff --git a/backend/src/app/util/rlimit.clj b/backend/src/app/util/rlimit.clj
deleted file mode 100644
index 8398237c19..0000000000
--- a/backend/src/app/util/rlimit.clj
+++ /dev/null
@@ -1,36 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.util.rlimit
- "Resource usage limits (in other words: semaphores)."
- (:require
- [app.common.logging :as l]
- [app.util.services :as sv])
- (:import
- java.util.concurrent.Semaphore))
-
-(defn acquire!
- [sem]
- (.acquire ^Semaphore sem))
-
-(defn release!
- [sem]
- (.release ^Semaphore sem))
-
-(defn wrap-rlimit
- [_cfg f mdata]
- (if-let [permits (::permits mdata)]
- (let [sem (Semaphore. permits)]
- (l/debug :hint "wrapping rlimit" :handler (::sv/name mdata) :permits permits)
- (fn [cfg params]
- (try
- (acquire! sem)
- (f cfg params)
- (finally
- (release! sem)))))
- f))
-
-
diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj
index b82783e3cf..6acbd9e363 100644
--- a/backend/src/app/util/websocket.clj
+++ b/backend/src/app/util/websocket.clj
@@ -27,11 +27,6 @@
(declare ws-ping!)
(declare ws-send!)
-(defmacro call-mtx
- [definitions name & args]
- `(when-let [mtx-fn# (some-> ~definitions ~name ::mtx/fn)]
- (mtx-fn# ~@args)))
-
(def noop (constantly nil))
(defn handler
@@ -49,7 +44,7 @@
([handle-message {:keys [::input-buff-size
::output-buff-size
::idle-timeout
- ::metrics]
+ metrics]
:or {input-buff-size 64
output-buff-size 64
idle-timeout 30000}
@@ -71,8 +66,8 @@
on-terminate
(fn [& _args]
(when (compare-and-set! terminated false true)
- (call-mtx metrics :connections {:cmd :dec :by 1})
- (call-mtx metrics :sessions {:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)})
+ (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)})
(a/close! close-ch)
(a/close! pong-ch)
@@ -88,7 +83,7 @@
on-connect
(fn [conn]
- (call-mtx metrics :connections {:cmd :inc :by 1})
+ (mtx/run! metrics {:id :websocket-active-connections :inc 1})
(let [wsp (atom (assoc options ::conn conn))]
;; Handle heartbeat
@@ -102,7 +97,7 @@
;; connection
(a/go-loop []
(when-let [val (a/> schedule
- (filter some?)
- ;; If id is not defined, use the task as id.
- (map (fn [{:keys [id task] :as item}]
- (if (some? id)
- (assoc item :id (d/name id))
- (assoc item :id (d/name task)))))
- (map (fn [{:keys [task] :as item}]
- (let [f (get tasks task)]
- (when-not f
- (ex/raise :type :internal
- :code :task-not-found
- :hint (str/fmt "task %s not configured" task)))
- (-> item
- (dissoc :task)
- (assoc :fn f))))))
- cfg (assoc cfg
- :scheduler scheduler
- :schedule schedule)]
+ [_ {:keys [schedule tasks pool] :as cfg}]
+ (let [scheduler (Executors/newScheduledThreadPool (int 1))]
+ (if (db/read-only? pool)
+ (l/warn :hint "scheduler not started, db is read-only")
+ (let [schedule (->> schedule
+ (filter some?)
+ ;; If id is not defined, use the task as id.
+ (map (fn [{:keys [id task] :as item}]
+ (if (some? id)
+ (assoc item :id (d/name id))
+ (assoc item :id (d/name task)))))
+ (map (fn [{:keys [task] :as item}]
+ (let [f (get tasks task)]
+ (when-not f
+ (ex/raise :type :internal
+ :code :task-not-found
+ :hint (str/fmt "task %s not configured" task)))
+ (-> item
+ (dissoc :task)
+ (assoc :fn f))))))
+ cfg (assoc cfg
+ :scheduler scheduler
+ :schedule schedule)]
+ (l/info :hint "scheduler started"
+ :registred-tasks (count schedule))
- (synchronize-schedule cfg)
- (run! (partial schedule-task cfg)
- (filter some? schedule))
+ (synchronize-schedule cfg)
+ (run! (partial schedule-task cfg)
+ (filter some? schedule))))
(reify
java.lang.AutoCloseable
@@ -405,11 +475,6 @@
(def sql:lock-scheduled-task
"select id from scheduled_task where id=? for update skip locked")
-(defn exception->string
- [error]
- (with-out-str
- (.printStackTrace ^Throwable error (java.io.PrintWriter. *out*))))
-
(defn- execute-scheduled-task
[{:keys [executor pool] :as cfg} {:keys [id] :as task}]
(letfn [(run-task [conn]
@@ -445,59 +510,27 @@
;; --- INSTRUMENTATION
-(defn instrument!
- [registry]
- (mtx/instrument-vars!
- [#'submit!]
- {:registry registry
- :type :counter
- :labels ["name"]
- :name "tasks_submit_total"
- :help "A counter of task submissions."
- :wrap (fn [rootf mobj]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn [conn params]
- (let [tname (:name params)]
- (mobj :inc [tname])
- (origf conn params)))
- {::original origf})))})
-
- (mtx/instrument-vars!
- [#'app.worker/run-task]
- {:registry registry
- :type :summary
- :quantiles []
- :name "tasks_checkout_timing"
- :help "Latency measured between scheduled_at and execution time."
- :wrap (fn [rootf mobj]
- (let [mdata (meta rootf)
- origf (::original mdata rootf)]
- (with-meta
- (fn [tasks item]
- (let [now (inst-ms (dt/now))
- sat (inst-ms (:scheduled-at item))]
- (mobj :observe (- now sat))
- (origf tasks item)))
- {::original origf})))}))
-
+(defn- wrap-task-handler
+ [metrics tname f]
+ (let [labels (into-array String [tname])]
+ (fn [params]
+ (let [start (System/nanoTime)]
+ (try
+ (f params)
+ (finally
+ (mtx/run! metrics
+ {:id :tasks-timing
+ :val (/ (- (System/nanoTime) start) 1000000)
+ :labels labels})))))))
(defmethod ig/pre-init-spec ::registry [_]
(s/keys :req-un [::mtx/metrics ::tasks]))
(defmethod ig/init-key ::registry
[_ {:keys [metrics tasks]}]
- (let [mobj (mtx/create
- {:registry (:registry metrics)
- :type :summary
- :labels ["name"]
- :quantiles []
- :name "tasks_timing"
- :help "Background task execution timing."})]
- (reduce-kv (fn [res k v]
- (let [tname (name k)]
- (l/debug :action "register task" :name tname)
- (assoc res k (mtx/wrap-summary v mobj [tname]))))
- {}
- tasks)))
+ (reduce-kv (fn [res k v]
+ (let [tname (name k)]
+ (l/debug :hint "register task" :name tname)
+ (assoc res k (wrap-task-handler metrics tname v))))
+ {}
+ tasks))
diff --git a/backend/test/app/services_files_test.clj b/backend/test/app/services_files_test.clj
index c0dc39e322..dc96c2cbac 100644
--- a/backend/test/app/services_files_test.clj
+++ b/backend/test/app/services_files_test.clj
@@ -174,6 +174,14 @@
:type :image
:metadata {:id (:id fmo1)}}}]})]
+
+
+ ;; If we launch gc-touched-task, we should have 4 items to freeze.
+ (let [task (:app.storage/gc-touched-task th/*system*)
+ res (task {})]
+ (t/is (= 4 (:freeze res)))
+ (t/is (= 0 (:delete res))))
+
;; run the task immediately
(let [task (:app.tasks.file-media-gc/handler th/*system*)
res (task {})]
@@ -202,16 +210,22 @@
(t/is (some? (sto/get-object storage (:media-id fmo1))))
(t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))
- ;; but if we pass the touched gc task two of them should disappear
+ ;; now, we have deleted the unused file-media-object, if we
+ ;; execute the touched-gc task, we should see that two of them
+ ;; are marked to be deleted.
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
(t/is (= 0 (:freeze res)))
- (t/is (= 2 (:delete res)))
+ (t/is (= 2 (:delete res))))
- (t/is (nil? (sto/get-object storage (:media-id fmo2))))
- (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
- (t/is (some? (sto/get-object storage (:media-id fmo1))))
- (t/is (some? (sto/get-object storage (:thumbnail-id fmo1)))))
+
+ ;; Finally, check that some of the objects that are marked as
+ ;; deleted we are unable to retrieve them using standard storage
+ ;; public api.
+ (t/is (nil? (sto/get-object storage (:media-id fmo2))))
+ (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
+ (t/is (some? (sto/get-object storage (:media-id fmo1))))
+ (t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))
)))
@@ -389,3 +403,73 @@
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))
))
+
+(t/deftest query-frame-thumbnails
+ (let [prof (th/create-profile* 1 {:is-active true})
+ file (th/create-file* 1 {:profile-id (:id prof)
+ :project-id (:default-project-id prof)
+ :is-shared false})
+ data {::th/type :file-frame-thumbnail
+ :profile-id (:id prof)
+ :file-id (:id file)
+ :frame-id (uuid/next)}]
+
+ ;;insert an entry on the database with a test value for the thumbnail of this frame
+ (db/exec-one! th/*pool*
+ ["insert into file_frame_thumbnail(file_id, frame_id, data) values (?, ?, ?)"
+ (:file-id data) (:frame-id data) "testvalue"])
+
+ (let [out (th/query! data)]
+ (t/is (nil? (:error out)))
+ (let [result (:result out)]
+ (t/is (= 1 (count result)))
+ (t/is (= "testvalue" (:data result)))))))
+
+(t/deftest insert-frame-thumbnails
+ (let [prof (th/create-profile* 1 {:is-active true})
+ file (th/create-file* 1 {:profile-id (:id prof)
+ :project-id (:default-project-id prof)
+ :is-shared false})
+ data {::th/type :upsert-frame-thumbnail
+ :profile-id (:id prof)
+ :file-id (:id file)
+ :frame-id (uuid/next)
+ :data "test insert new value"}
+ out (th/mutation! data)]
+
+ (t/is (nil? (:error out)))
+ (t/is (nil? (:result out)))
+
+ ;;retrieve the value from the database and check its content
+ (let [result (db/exec-one!
+ th/*pool*
+ ["select data from file_frame_thumbnail where file_id = ? and frame_id = ?"
+ (:file-id data) (:frame-id data)])]
+ (t/is (= "test insert new value" (:data result))))))
+
+(t/deftest frame-thumbnails
+ (let [prof (th/create-profile* 1 {:is-active true})
+ file (th/create-file* 1 {:profile-id (:id prof)
+ :project-id (:default-project-id prof)
+ :is-shared false})
+ data {::th/type :upsert-frame-thumbnail
+ :profile-id (:id prof)
+ :file-id (:id file)
+ :frame-id (uuid/next)
+ :data "updated value"}]
+
+ ;;insert an entry on the database with and old value for the thumbnail of this frame
+ (db/exec-one! th/*pool*
+ ["insert into file_frame_thumbnail(file_id, frame_id, data) values (?, ?, ?)"
+ (:file-id data) (:frame-id data) "old value"])
+
+ (let [out (th/mutation! data)]
+ (t/is (nil? (:error out)))
+ (t/is (nil? (:result out)))
+
+ ;;retrieve the value from the database and check its content
+ (let [result (db/exec-one!
+ th/*pool*
+ ["select data from file_frame_thumbnail where file_id = ? and frame_id = ?"
+ (:file-id data) (:frame-id data)])]
+ (t/is (= "updated value" (:data result)))))))
diff --git a/backend/test/app/services_management_test.clj b/backend/test/app/services_management_test.clj
index 7d237b5d15..eb9c28f73d 100644
--- a/backend/test/app/services_management_test.clj
+++ b/backend/test/app/services_management_test.clj
@@ -11,6 +11,7 @@
[app.http :as http]
[app.storage :as sto]
[app.test-helpers :as th]
+ [app.storage-test :refer [configure-storage-backend]]
[clojure.test :as t]
[buddy.core.bytes :as b]
[datoteka.core :as fs]))
@@ -19,7 +20,9 @@
(t/use-fixtures :each th/database-reset)
(t/deftest duplicate-file
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
+
sobject (sto/put-object storage {:content (sto/content "content")
:content-type "text/plain"
:other "data"})
@@ -90,7 +93,8 @@
))))
(t/deftest duplicate-file-with-deleted-rels
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
sobject (sto/put-object storage {:content (sto/content "content")
:content-type "text/plain"
:other "data"})
@@ -151,7 +155,9 @@
))))
(t/deftest duplicate-project
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
+
sobject (sto/put-object storage {:content (sto/content "content")
:content-type "text/plain"
:other "data"})
@@ -221,7 +227,8 @@
)))))
(t/deftest duplicate-project-with-deleted-files
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
sobject (sto/put-object storage {:content (sto/content "content")
:content-type "text/plain"
:other "data"})
diff --git a/backend/test/app/services_profile_test.clj b/backend/test/app/services_profile_test.clj
index ba82c0f0e8..78c423872c 100644
--- a/backend/test/app/services_profile_test.clj
+++ b/backend/test/app/services_profile_test.clj
@@ -240,6 +240,16 @@
(t/is (nil? error))
(t/is (string? (:token result))))))
+(t/deftest test-register-profile-with-email-as-password
+ (let [data {::th/type :prepare-register-profile
+ :email "user@example.com"
+ :password "USER@example.com"}]
+
+ (let [{:keys [result error] :as out} (th/mutation! data)]
+ (t/is (th/ex-info? error))
+ (t/is (th/ex-of-type? error :validation))
+ (t/is (th/ex-of-code? error :email-as-password)))))
+
(t/deftest test-email-change-request
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
cfg-get-mock {:target 'app.config/get
@@ -345,3 +355,39 @@
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))
)))
+
+
+(t/deftest update-profile-password
+ (let [profile (th/create-profile* 1)
+ data {::th/type :update-profile-password
+ :profile-id (:id profile)
+ :old-password "123123"
+ :password "foobarfoobar"}
+ out (th/mutation! data)]
+ (t/is (nil? (:error out)))
+ (t/is (nil? (:result out)))
+ ))
+
+
+(t/deftest update-profile-password-bad-old-password
+ (let [profile (th/create-profile* 1)
+ data {::th/type :update-profile-password
+ :profile-id (:id profile)
+ :old-password "badpassword"
+ :password "foobarfoobar"}
+ {:keys [result error] :as out} (th/mutation! data)]
+ (t/is (th/ex-info? error))
+ (t/is (th/ex-of-type? error :validation))
+ (t/is (th/ex-of-code? error :old-password-not-match))))
+
+
+(t/deftest update-profile-password-email-as-password
+ (let [profile (th/create-profile* 1)
+ data {::th/type :update-profile-password
+ :profile-id (:id profile)
+ :old-password "123123"
+ :password "profile1.test@nodomain.com"}
+ {:keys [result error] :as out} (th/mutation! data)]
+ (t/is (th/ex-info? error))
+ (t/is (th/ex-of-type? error :validation))
+ (t/is (th/ex-of-code? error :email-as-password))))
diff --git a/backend/test/app/storage_test.clj b/backend/test/app/storage_test.clj
index 23777215ba..a7353a65d8 100644
--- a/backend/test/app/storage_test.clj
+++ b/backend/test/app/storage_test.clj
@@ -7,6 +7,7 @@
(ns app.storage-test
(:require
[app.common.exceptions :as ex]
+ [app.common.uuid :as uuid]
[app.db :as db]
[app.storage :as sto]
[app.test-helpers :as th]
@@ -22,9 +23,19 @@
th/database-reset
th/clean-storage))
+(defn configure-storage-backend
+ "Given storage map, returns a storage configured with the appropriate
+ backend for assets."
+ ([storage]
+ (assoc storage :backend :tmp))
+ ([storage conn]
+ (-> storage
+ (assoc :conn conn)
+ (assoc :backend :tmp))))
(t/deftest put-and-retrieve-object
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
content (sto/content "content")
object (sto/put-object storage {:content content
:content-type "text/plain"
@@ -39,9 +50,9 @@
(t/is (= "content" (slurp (sto/get-object-path storage object))))
))
-
(t/deftest put-and-retrieve-expired-object
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
content (sto/content "content")
object (sto/put-object storage {:content content
:content-type "text/plain"
@@ -59,7 +70,8 @@
))
(t/deftest put-and-delete-object
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
content (sto/content "content")
object (sto/put-object storage {:content content
:content-type "text/plain"
@@ -79,7 +91,8 @@
))
(t/deftest test-deleted-gc-task
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
content (sto/content "content")
object1 (sto/put-object storage {:content content
:content-type "text/plain"
@@ -96,14 +109,17 @@
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object;"])]
(t/is (= 1 (:count res))))))
-(t/deftest test-touched-gc-task
- (let [storage (:app.storage/storage th/*system*)
+(t/deftest test-touched-gc-task-1
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
+
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
+
mfile {:filename "sample.jpg"
:tempfile (th/tempfile "app/test_files/sample.jpg")
:content-type "image/jpeg"
@@ -140,12 +156,12 @@
;; now check if the storage objects are touched
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])]
- (t/is (= 2 (:count res))))
+ (t/is (= 4 (:count res))))
;; run the touched gc task
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {})]
- (t/is (= 0 (:freeze res)))
+ (t/is (= 2 (:freeze res)))
(t/is (= 2 (:delete res))))
;; now check that there are no touched objects
@@ -157,8 +173,85 @@
(t/is (= 2 (:count res))))
)))
+
+(t/deftest test-touched-gc-task-2
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
+ prof (th/create-profile* 1 {:is-active true})
+ team-id (:default-team-id prof)
+ proj-id (:default-project-id prof)
+ font-id (uuid/custom 10 1)
+
+ proj (th/create-project* 1 {:profile-id (:id prof)
+ :team-id team-id})
+
+ file (th/create-file* 1 {:profile-id (:id prof)
+ :project-id proj-id
+ :is-shared false})
+
+ ttfdata (-> (io/resource "app/test_files/font-1.ttf")
+ (fs/slurp-bytes))
+
+ mfile {:filename "sample.jpg"
+ :tempfile (th/tempfile "app/test_files/sample.jpg")
+ :content-type "image/jpeg"
+ :size 312043}
+
+ params1 {::th/type :upload-file-media-object
+ :profile-id (:id prof)
+ :file-id (:id file)
+ :is-local true
+ :name "testfile"
+ :content mfile}
+
+ params2 {::th/type :create-font-variant
+ :profile-id (:id prof)
+ :team-id team-id
+ :font-id font-id
+ :font-family "somefont"
+ :font-weight 400
+ :font-style "normal"
+ :data {"font/ttf" ttfdata}}
+
+ out1 (th/mutation! params1)
+ out2 (th/mutation! params2)]
+
+ ;; (th/print-result! out)
+
+ (t/is (nil? (:error out1)))
+ (t/is (nil? (:error out2)))
+
+ ;; run the touched gc task
+ (let [task (:app.storage/gc-touched-task th/*system*)
+ res (task {})]
+ (t/is (= 6 (:freeze res)))
+ (t/is (= 0 (:delete res)))
+
+ (let [result-1 (:result out1)
+ result-2 (:result out2)]
+
+ ;; now we proceed to manually delete one team-font-variant
+ (db/exec-one! th/*pool* ["delete from team_font_variant where id = ?" (:id result-2)])
+
+ ;; revert touched state to all storage objects
+ (db/exec-one! th/*pool* ["update storage_object set touched_at=now()"])
+
+ ;; Run the task again
+ (let [res (task {})]
+ (t/is (= 2 (:freeze res)))
+ (t/is (= 4 (:delete res))))
+
+ ;; now check that there are no touched objects
+ (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where touched_at is not null"])]
+ (t/is (= 0 (:count res))))
+
+ ;; now check that all objects are marked to be deleted
+ (let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is not null"])]
+ (t/is (= 4 (:count res))))))))
+
(t/deftest test-touched-gc-task-without-delete
- (let [storage (:app.storage/storage th/*system*)
+ (let [storage (-> (:app.storage/storage th/*system*)
+ (configure-storage-backend))
prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
@@ -198,72 +291,3 @@
;; check that we have all object in the db
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object where deleted_at is null"])]
(t/is (= 4 (:count res)))))))
-
-
-;; Recheck is the mechanism for delete leaked resources on
-;; transaction failure.
-
-(t/deftest test-recheck
- (let [storage (:app.storage/storage th/*system*)
- content (sto/content "content")
- object (sto/put-object storage {:content content
- :content-type "text/plain"})]
- ;; Sleep fo 50ms
- (th/sleep 50)
-
- (let [rows (db/exec! th/*pool* ["select * from storage_pending"])]
- (t/is (= 1 (count rows)))
- (t/is (= (:id object) (:id (first rows)))))
-
- ;; Artificially make all storage_pending object 1 hour older.
- (db/exec-one! th/*pool* ["update storage_pending set created_at = created_at - '1 hour'::interval"])
-
- ;; Sleep fo 50ms
- (th/sleep 50)
-
- ;; Run recheck task
- (let [task (:app.storage/recheck-task th/*system*)
- res (task {})]
- (t/is (= 1 (:processed res)))
- (t/is (= 0 (:deleted res))))
-
- ;; After recheck task, storage-pending table should be empty
- (let [rows (db/exec! th/*pool* ["select * from storage_pending"])]
- (t/is (= 0 (count rows))))))
-
-(t/deftest test-recheck-with-rollback
- (let [storage (:app.storage/storage th/*system*)
- content (sto/content "content")]
-
- ;; check with aborted transaction
- (ex/ignoring
- (db/with-atomic [conn th/*pool*]
- (let [storage (assoc storage :conn conn)] ; make participate storage in the transaction
- (sto/put-object storage {:content content
- :content-type "text/plain"})
- (throw (ex-info "expected" {})))))
-
- ;; let a 200ms window for recheck registration thread
- ;; completion before proceed.
- (th/sleep 200)
-
- ;; storage_pending table should have the object
- ;; registered independently of the aborted transaction.
- (let [rows (db/exec! th/*pool* ["select * from storage_pending"])]
- (t/is (= 1 (count rows))))
-
- ;; Artificially make all storage_pending object 1 hour older.
- (db/exec-one! th/*pool* ["update storage_pending set created_at = created_at - '1 hour'::interval"])
-
- ;; Sleep fo 50ms
- (th/sleep 50)
-
- ;; Run recheck task
- (let [task (:app.storage/recheck-task th/*system*)
- res (task {})]
- (t/is (= 1 (:processed res)))
- (t/is (= 1 (:deleted res))))
-
- ;; After recheck task, storage-pending table should be empty
- (let [rows (db/exec! th/*pool* ["select * from storage_pending"])]
- (t/is (= 0 (count rows))))))
diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj
index 9161296d18..9f380551a4 100644
--- a/backend/test/app/test_helpers.clj
+++ b/backend/test/app/test_helpers.clj
@@ -52,7 +52,6 @@
(assoc-in [:app.db/pool :uri] (:database-uri config))
(assoc-in [:app.db/pool :username] (:database-username config))
(assoc-in [:app.db/pool :password] (:database-password config))
- (assoc-in [[:app.main/main :app.storage.fs/backend] :directory] "/tmp/app/storage")
(dissoc :app.srepl/server
:app.http/server
:app.http/router
@@ -65,8 +64,7 @@
:app.worker/scheduler
:app.worker/worker)
(d/deep-merge
- {:app.storage/storage {:backend :tmp}
- :app.tasks.file-media-gc/handler {:max-age (dt/duration 300)}}))
+ {:app.tasks.file-media-gc/handler {:max-age (dt/duration 300)}}))
_ (ig/load-namespaces config)
system (-> (ig/prep config)
(ig/init))]
@@ -250,7 +248,7 @@
[expr]
`(try
{:error nil
- :result ~expr}
+ :result (deref ~expr)}
(catch Exception e#
{:error (handle-error e#)
:result nil})))
diff --git a/common/deps.edn b/common/deps.edn
index 10b2415878..082926005b 100644
--- a/common/deps.edn
+++ b/common/deps.edn
@@ -13,7 +13,7 @@
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
- selmer/selmer {:mvn/version "1.12.49"}
+ selmer/selmer {:mvn/version "1.12.50"}
criterium/criterium {:mvn/version "0.4.6"}
expound/expound {:mvn/version "0.9.0"}
@@ -21,10 +21,10 @@
com.cognitect/transit-cljs {:mvn/version "0.8.269"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
- funcool/promesa {:mvn/version "6.0.2"}
+ funcool/promesa {:mvn/version "7.0.444"}
funcool/cuerdas {:mvn/version "2022.01.14-391"}
- lambdaisland/uri {:mvn/version "1.12.89"
+ lambdaisland/uri {:mvn/version "1.13.95"
:exclusions [org.clojure/data.json]}
frankiesardo/linked {:mvn/version "1.3.0"}
@@ -42,9 +42,8 @@
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
org.clojure/test.check {:mvn/version "RELEASE"}
- org.clojure/tools.deps.alpha {:mvn/version "RELEASE"}
+ thheller/shadow-cljs {:mvn/version "2.17.3"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
- thheller/shadow-cljs {:mvn/version "2.16.12"}
criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
diff --git a/common/package.json b/common/package.json
index b989def6c1..d1aa28e86d 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.16.12",
+ "shadow-cljs": "2.17.3",
"source-map-support": "^0.5.19",
"ws": "^7.4.6"
}
diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc
index f8acee0d3f..a8db384076 100644
--- a/common/src/app/common/colors.cljc
+++ b/common/src/app/common/colors.cljc
@@ -15,4 +15,5 @@
(def info "#59B9E2")
(def test "#fabada")
(def white "#FFFFFF")
+(def primary "#31EFB8")
diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc
index ad167a3211..69e30c6a56 100644
--- a/common/src/app/common/data.cljc
+++ b/common/src/app/common/data.cljc
@@ -6,7 +6,7 @@
(ns app.common.data
"Data manipulation and query helper functions."
- (:refer-clojure :exclude [read-string hash-map merge name parse-double])
+ (:refer-clojure :exclude [read-string hash-map merge name parse-double group-by iteration])
#?(:cljs
(:require-macros [app.common.data]))
(:require
@@ -37,6 +37,22 @@
#?(:cljs (instance? lks/LinkedSet o)
:clj (instance? LinkedSet o)))
+#?(:clj
+ (defmethod print-method clojure.lang.PersistentQueue [q, w]
+ ;; Overload the printer for queues so they look like fish
+ (print-method '<- w)
+ (print-method (seq q) w)
+ (print-method '-< w)))
+
+(defn queue
+ ([] #?(:clj clojure.lang.PersistentQueue/EMPTY :cljs #queue []))
+ ([a] (into (queue) [a]))
+ ([a & more] (into (queue) (cons a more))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Data Structures Manipulation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(defn deep-merge
([a b]
(if (map? a)
@@ -45,10 +61,6 @@
([a b & rest]
(reduce deep-merge a (cons b rest))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Data Structures Manipulation
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
(defn dissoc-in
[m [k & ks]]
(if ks
@@ -151,7 +163,11 @@
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]
- (into {} (remove (comp nil? second) data)))
+ (into {} (remove (comp nil? second)) data))
+
+(defn without-qualified
+ [data]
+ (into {} (remove (comp qualified-keyword? first)) data))
(defn without-keys
"Return a map without the keys provided
@@ -609,3 +625,71 @@
(if (or (keyword? k) (string? k))
[(keyword (str/kebab (name k))) v]
[k v])))))
+
+
+(defn group-by
+ ([kf coll] (group-by kf identity coll))
+ ([kf vf coll]
+ (let [conj (fnil conj [])]
+ (reduce (fn [result item]
+ (update result (kf item) conj (vf item)))
+ {}
+ coll))))
+
+(defn group-by'
+ "A variant of group-by that uses a set for collecting results."
+ ([kf coll] (group-by kf identity coll))
+ ([kf vf coll]
+ (let [conj (fnil conj #{})]
+ (reduce (fn [result item]
+ (update result (kf item) conj (vf item)))
+ {}
+ coll))))
+
+;; TEMPORAL COPY of clojure-1.11 iteration function, should be
+;; replaced with the builtin on when stable version is released.
+
+#?(:clj
+ (defn iteration
+ "Creates a seqable/reducible via repeated calls to step,
+ a function of some (continuation token) 'k'. The first call to step
+ will be passed initk, returning 'ret'. Iff (somef ret) is true,
+ (vf ret) will be included in the iteration, else iteration will
+ terminate and vf/kf will not be called. If (kf ret) is non-nil it
+ will be passed to the next step call, else iteration will terminate.
+ This can be used e.g. to consume APIs that return paginated or batched data.
+ step - (possibly impure) fn of 'k' -> 'ret'
+ :somef - fn of 'ret' -> logical true/false, default 'some?'
+ :vf - fn of 'ret' -> 'v', a value produced by the iteration, default 'identity'
+ :kf - fn of 'ret' -> 'next-k' or nil (signaling 'do not continue'), default 'identity'
+ :initk - the first value passed to step, default 'nil'
+ It is presumed that step with non-initk is unreproducible/non-idempotent.
+ If step with initk is unreproducible it is on the consumer to not consume twice."
+ {:added "1.11"}
+ [step & {:keys [somef vf kf initk]
+ :or {vf identity
+ kf identity
+ somef some?
+ initk nil}}]
+ (reify
+ clojure.lang.Seqable
+ (seq [_]
+ ((fn next [ret]
+ (when (somef ret)
+ (cons (vf ret)
+ (when-some [k (kf ret)]
+ (lazy-seq (next (step k)))))))
+ (step initk)))
+ clojure.lang.IReduceInit
+ (reduce [_ rf init]
+ (loop [acc init
+ ret (step initk)]
+ (if (somef ret)
+ (let [acc (rf acc (vf ret))]
+ (if (reduced? acc)
+ @acc
+ (if-some [k (kf ret)]
+ (recur acc (step k))
+ acc)))
+ acc))))))
+
diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc
index 0d6c96ff4a..d58abedd79 100644
--- a/common/src/app/common/file_builder.cljc
+++ b/common/src/app/common/file_builder.cljc
@@ -13,6 +13,7 @@
[app.common.pages.changes :as ch]
[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]))
@@ -38,9 +39,9 @@
:frame-id (:current-frame-id file)))]
(when fail-on-spec?
- (us/verify :app.common.pages.spec/change change))
+ (us/verify ::spec.change/change change))
- (let [valid? (us/valid? :app.common.pages.spec/change change)]
+ (let [valid? (us/valid? ::spec.change/change change)]
#?(:cljs
(when-not valid? (.warn js/console "Invalid shape" (clj->js change))))
@@ -568,4 +569,78 @@
(dissoc :current-component-id)
(update :parent-stack pop))))
+(defn delete-object
+ [file id]
+ (let [page-id (:current-page-id file)]
+ (commit-change
+ file
+ {:type :del-obj
+ :page-id page-id
+ :id id})))
+(defn update-object
+ [file old-obj new-obj]
+ (let [page-id (:current-page-id file)
+ new-obj (setup-selrect new-obj)
+ attrs (d/concat-set (keys old-obj) (keys new-obj))
+ generate-operation
+ (fn [changes attr]
+ (let [old-val (get old-obj attr)
+ new-val (get new-obj attr)]
+ (if (= old-val new-val)
+ changes
+ (conj changes {:type :set :attr attr :val new-val}))))]
+ (-> file
+ (commit-change
+ {:type :mod-obj
+ :operations (reduce generate-operation [] attrs)
+ :page-id page-id
+ :id (:id old-obj)}))))
+
+(defn get-current-page
+ [file]
+ (let [page-id (:current-page-id file)]
+ (-> file (get-in [:data :pages-index page-id]))))
+
+(defn add-guide
+ [file guide]
+
+ (let [guide (cond-> guide
+ (nil? (:id guide))
+ (assoc :id (uuid/next)))
+ page-id (:current-page-id file)
+ old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
+ new-guides (assoc old-guides (:id guide) guide)]
+ (-> file
+ (commit-change
+ {:type :set-option
+ :page-id page-id
+ :option :guides
+ :value new-guides})
+ (assoc :last-id (:id guide)))))
+
+(defn delete-guide
+ [file id]
+
+ (let [page-id (:current-page-id file)
+ old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
+ new-guides (dissoc old-guides id)]
+ (-> file
+ (commit-change
+ {:type :set-option
+ :page-id page-id
+ :option :guides
+ :value new-guides}))))
+
+(defn update-guide
+ [file guide]
+
+ (let [page-id (:current-page-id file)
+ old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {})
+ new-guides (assoc old-guides (:id guide) guide)]
+ (-> file
+ (commit-change
+ {:type :set-option
+ :page-id page-id
+ :option :guides
+ :value new-guides}))))
diff --git a/common/src/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc
index 11c8422c6c..19d91e7b27 100644
--- a/common/src/app/common/geom/align.cljc
+++ b/common/src/app/common/geom/align.cljc
@@ -6,7 +6,6 @@
(ns app.common.geom.align
(:require
- [app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :refer [get-children]]
[clojure.spec.alpha :as s]))
@@ -20,8 +19,7 @@
(defn- recursive-move
"Move the shape and all its recursive children."
[shape dpoint objects]
- (->> (get-children (:id shape) objects)
- (map (d/getf objects))
+ (->> (get-children objects (:id shape))
(cons shape)
(map #(gsh/move % dpoint))))
diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc
index b03cba0d4e..88ba2ed5d6 100644
--- a/common/src/app/common/geom/matrix.cljc
+++ b/common/src/app/common/geom/matrix.cljc
@@ -10,7 +10,9 @@
:clj [clojure.pprint :as pp])
[app.common.data :as d]
[app.common.geom.point :as gpt]
- [app.common.math :as mth]))
+ [app.common.math :as mth]
+ [app.common.spec :as us]
+ [clojure.spec.alpha :as s]))
;; --- Matrix Impl
@@ -24,6 +26,21 @@
(toString [_]
(str "matrix(" a "," b "," c "," d "," e "," f ")")))
+(defn ^boolean matrix?
+ "Return true if `v` is Matrix instance."
+ [v]
+ (instance? Matrix v))
+
+(s/def ::a ::us/safe-number)
+(s/def ::b ::us/safe-number)
+(s/def ::c ::us/safe-number)
+(s/def ::d ::us/safe-number)
+(s/def ::e ::us/safe-number)
+(s/def ::f ::us/safe-number)
+
+(s/def ::matrix
+ (s/and (s/keys :req-un [::a ::b ::c ::d ::e ::f]) matrix?))
+
(defn matrix
"Create a new matrix instance."
([]
@@ -84,11 +101,6 @@
(- m1a m2a) (- m1b m2b) (- m1c m2c)
(- m1d m2d) (- m1e m2e) (- m1f m2f)))
-(defn ^boolean matrix?
- "Return true if `v` is Matrix instance."
- [v]
- (instance? Matrix v))
-
(def base (matrix))
(defn base?
diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc
index 21fecc68b3..c2b81b2482 100644
--- a/common/src/app/common/geom/point.cljc
+++ b/common/src/app/common/geom/point.cljc
@@ -11,7 +11,9 @@
:clj [clojure.pprint :as pp])
#?(:cljs [cljs.core :as c]
:clj [clojure.core :as c])
- [app.common.math :as mth]))
+ [app.common.math :as mth]
+ [app.common.spec :as us]
+ [clojure.spec.alpha :as s]))
;; --- Point Impl
@@ -25,6 +27,13 @@
(or (instance? Point v)
(and (map? v) (contains? v :x) (contains? v :y))))
+(s/def ::x ::us/safe-number)
+(s/def ::y ::us/safe-number)
+
+(s/def ::point
+ (s/and (s/keys :req-un [::x ::y]) point?))
+
+
(defn ^boolean point-like?
[{:keys [x y] :as v}]
(and (map? v)
diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc
index 6df3f748ea..bf63582e50 100644
--- a/common/src/app/common/geom/shapes.cljc
+++ b/common/src/app/common/geom/shapes.cljc
@@ -185,3 +185,7 @@
;; Bool
(d/export gsb/update-bool-selrect)
(d/export gsb/calc-bool-content)
+
+;; Constraints
+(d/export gct/default-constraints-h)
+(d/export gct/default-constraints-v)
diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc
index 4ead68b4e0..8732be74b6 100644
--- a/common/src/app/common/geom/shapes/constraints.cljc
+++ b/common/src/app/common/geom/shapes/constraints.cljc
@@ -11,7 +11,7 @@
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.transforms :as gtr]
[app.common.math :as mth]
- [app.common.pages.spec :as spec]))
+ [app.common.uuid :as uuid]))
;; Auxiliary methods to work in an specifica axis
(defn get-delta-start [axis rect tr-rect]
@@ -117,7 +117,7 @@
(not (mth/close? (:y resize-vector-2) 1)))
(assoc :resize-origin (:resize-origin-2 modifiers)
:resize-vector (gpt/point 1 (:y resize-vector-2)))
-
+
(some? displacement)
(assoc :displacement
(get-displacement axis (-> (gpt/point 0 0)
@@ -138,16 +138,32 @@
:center :center
:scale :scale})
+(defn default-constraints-h
+ [shape]
+ (if (= (:parent-id shape) uuid/zero)
+ nil
+ (if (= (:parent-id shape) (:frame-id shape))
+ :left
+ :scale)))
+
+(defn default-constraints-v
+ [shape]
+ (if (= (:parent-id shape) uuid/zero)
+ nil
+ (if (= (:parent-id shape) (:frame-id shape))
+ :top
+ :scale)))
+
(defn calc-child-modifiers
[parent child modifiers ignore-constraints transformed-parent-rect]
(let [constraints-h
(if-not ignore-constraints
- (:constraints-h child (spec/default-constraints-h child))
+ (:constraints-h child (default-constraints-h child))
:scale)
constraints-v
(if-not ignore-constraints
- (:constraints-v child (spec/default-constraints-v child))
+ (:constraints-v child (default-constraints-v child))
:scale)
modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect)
diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc
index 01de122190..fa29f33b6b 100644
--- a/common/src/app/common/geom/shapes/transforms.cljc
+++ b/common/src/app/common/geom/shapes/transforms.cljc
@@ -545,7 +545,6 @@
(defn transform-selrect
[selrect {:keys [displacement resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}]
-
;; FIXME: Improve Performance
(let [resize-transform-inverse (or resize-transform-inverse (gmt/matrix))
diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc
index 67a327da8c..6b61b3d731 100644
--- a/common/src/app/common/math.cljc
+++ b/common/src/app/common/math.cljc
@@ -6,6 +6,7 @@
(ns app.common.math
"A collection of math utils."
+ (:refer-clojure :exclude [abs])
#?(:cljs
(:require [goog.math :as math])))
diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc
index 4c6248b86a..3c10910d42 100644
--- a/common/src/app/common/pages.cljc
+++ b/common/src/app/common/pages.cljc
@@ -10,11 +10,8 @@
[app.common.data :as d]
[app.common.pages.changes :as changes]
[app.common.pages.common :as common]
- [app.common.pages.helpers :as helpers]
[app.common.pages.indices :as indices]
- [app.common.pages.init :as init]
- [app.common.pages.spec :as spec]
- [clojure.spec.alpha :as s]))
+ [app.common.pages.init :as init]))
;; Common
(d/export common/root)
@@ -22,55 +19,6 @@
(d/export common/default-color)
(d/export common/component-sync-attrs)
-;; Helpers
-
-(d/export helpers/walk-pages)
-(d/export helpers/select-objects)
-(d/export helpers/update-object-list)
-(d/export helpers/get-component-shape)
-(d/export helpers/get-root-shape)
-(d/export helpers/make-container)
-(d/export helpers/page?)
-(d/export helpers/component?)
-(d/export helpers/get-container)
-(d/export helpers/get-shape)
-(d/export helpers/get-component)
-(d/export helpers/is-main-of)
-(d/export helpers/get-component-root)
-(d/export helpers/get-children)
-(d/export helpers/get-children-objects)
-(d/export helpers/get-object-with-children)
-(d/export helpers/select-children)
-(d/export helpers/is-shape-grouped)
-(d/export helpers/get-parent)
-(d/export helpers/get-parents)
-(d/export helpers/get-frame)
-(d/export helpers/clean-loops)
-(d/export helpers/calculate-invalid-targets)
-(d/export helpers/valid-frame-target)
-(d/export helpers/position-on-parent)
-(d/export helpers/insert-at-index)
-(d/export helpers/append-at-the-end)
-(d/export helpers/select-toplevel-shapes)
-(d/export helpers/select-frames)
-(d/export helpers/clone-object)
-(d/export helpers/indexed-shapes)
-(d/export helpers/expand-region-selection)
-(d/export helpers/frame-id-by-position)
-(d/export helpers/set-touched-group)
-(d/export helpers/touched-group?)
-(d/export helpers/get-base-shape)
-(d/export helpers/is-parent?)
-(d/export helpers/get-index-in-parent)
-(d/export helpers/split-path)
-(d/export helpers/join-path)
-(d/export helpers/parse-path-name)
-(d/export helpers/merge-path-item)
-(d/export helpers/compact-path)
-(d/export helpers/compact-name)
-(d/export helpers/unframed-shape?)
-(d/export helpers/children-seq)
-
;; Indices
(d/export indices/calculate-z-index)
(d/export indices/update-z-index)
@@ -88,15 +36,3 @@
(d/export init/make-minimal-shape)
(d/export init/make-minimal-group)
(d/export init/empty-file-data)
-
-;; Specs
-
-(s/def ::changes ::spec/changes)
-(s/def ::color ::spec/color)
-(s/def ::data ::spec/data)
-(s/def ::media-object ::spec/media-object)
-(s/def ::page ::spec/page)
-(s/def ::recent-color ::spec/recent-color)
-(s/def ::shape-attrs ::spec/shape-attrs)
-(s/def ::typography ::spec/typography)
-
diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc
index f8539e038e..e950bc7ccb 100644
--- a/common/src/app/common/pages/changes.cljc
+++ b/common/src/app/common/pages/changes.cljc
@@ -5,6 +5,7 @@
;; Copyright (c) UXBOX Labs SL
(ns app.common.pages.changes
+ #_:clj-kondo/ignore
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
@@ -13,9 +14,9 @@
[app.common.pages.common :refer [component-sync-attrs]]
[app.common.pages.helpers :as cph]
[app.common.pages.init :as init]
- [app.common.pages.spec :as spec]
- [app.common.spec :as us]))
-
+ [app.common.spec :as us]
+ [app.common.spec.change :as spec.change]
+ [app.common.spec.shape :as spec.shape]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Specific helpers
@@ -47,7 +48,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/changes items))
+ (us/assert ::spec.change/changes items))
(let [result (reduce #(or (process-change %1 %2) %1) data items)]
;; Validate result shapes (only on the backend)
@@ -57,7 +58,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))))))
+ (us/verify ::spec.shape/shape shape))))))
result)))
@@ -159,10 +160,8 @@
(let [lookup (d/getf objects)
update-fn #(d/update-when %1 %2 update-group %1)
xform (comp
- (mapcat #(cons % (cph/get-parents % objects)))
- (map lookup)
- (filter #(contains? #{:group :bool} (:type %)))
- (map :id)
+ (mapcat #(cons % (cph/get-parent-ids objects %)))
+ (filter #(contains? #{:group :bool} (-> % lookup :type)))
(distinct))]
(->> (sequence xform shapes)
@@ -203,11 +202,16 @@
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id component-id ignore-touched]}]
- (letfn [(is-valid-move? [objects shape-id]
- (let [invalid-targets (cph/calculate-invalid-targets shape-id objects)]
+ (letfn [(calculate-invalid-targets [objects shape-id]
+ (let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
+ (->> (get-in objects [shape-id :shapes])
+ (reduce reduce-fn #{shape-id}))))
+
+ (is-valid-move? [objects shape-id]
+ (let [invalid-targets (calculate-invalid-targets objects shape-id)]
(and (contains? objects shape-id)
(not (invalid-targets parent-id))
- (cph/valid-frame-target shape-id parent-id objects))))
+ (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/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc
index ddc5dcbaba..e0c3b27154 100644
--- a/common/src/app/common/pages/changes_builder.cljc
+++ b/common/src/app/common/pages/changes_builder.cljc
@@ -7,18 +7,26 @@
(ns app.common.pages.changes-builder
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
- [app.common.pages.helpers :as h]))
+ [app.common.pages.helpers :as cph]))
;; Auxiliary functions to help create a set of changes (undo + redo)
(defn empty-changes
- [origin page-id]
- (let [changes {:redo-changes []
- :undo-changes []
- :origin origin}]
- (with-meta changes
- {::page-id page-id})))
+ ([origin page-id]
+ (let [changes (empty-changes origin)]
+ (with-meta changes
+ {::page-id page-id})))
+
+ ([origin]
+ {:redo-changes []
+ :undo-changes []
+ :origin origin}))
+
+(defn with-page [changes page]
+ (vary-meta changes assoc
+ ::page page
+ ::page-id (:id page)
+ ::objects (:objects page)))
(defn with-objects [changes objects]
(vary-meta changes assoc ::objects objects))
@@ -69,7 +77,7 @@
:page-id (::page-id (meta changes))
:parent-id (:parent-id shape)
:shapes [(:id shape)]
- :index (cp/position-on-parent (:id shape) objects)}))]
+ :index (cph/get-position-on-parent objects (:id shape))}))]
(-> changes
(update :redo-changes conj set-parent-change)
@@ -162,7 +170,7 @@
:page-id page-id
:parent-id (:parent-id shape)
:shapes [id]
- :index (h/position-on-parent id objects)
+ :index (cph/get-position-on-parent objects id)
:ignore-touched true})))]
(-> changes
@@ -171,10 +179,25 @@
(reduce add-undo-change-parent $ ids)
(reduce add-undo-change-shape $ ids))))))
-
(defn move-page
[chdata index prev-index]
(let [page-id (::page-id (meta chdata))]
(-> chdata
(update :redo-changes conj {:type :mov-page :id page-id :index index})
(update :undo-changes conj {:type :mov-page :id page-id :index prev-index}))))
+
+(defn set-page-option
+ [chdata option-key option-val]
+ (let [page-id (::page-id (meta chdata))
+ page (::page (meta chdata))
+ old-val (get-in page [:options option-key])]
+
+ (-> chdata
+ (update :redo-changes conj {:type :set-option
+ :page-id page-id
+ :option option-key
+ :value option-val})
+ (update :undo-changes conj {:type :set-option
+ :page-id page-id
+ :option option-key
+ :value old-val}))))
diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc
index 6356f4bbec..43cd9371cf 100644
--- a/common/src/app/common/pages/common.cljc
+++ b/common/src/app/common/pages/common.cljc
@@ -9,7 +9,7 @@
[app.common.colors :as clr]
[app.common.uuid :as uuid]))
-(def file-version 12)
+(def file-version 13)
(def default-color clr/gray-20)
(def root uuid/zero)
diff --git a/common/src/app/common/pages/diff.cljc b/common/src/app/common/pages/diff.cljc
new file mode 100644
index 0000000000..46ba2f46bd
--- /dev/null
+++ b/common/src/app/common/pages/diff.cljc
@@ -0,0 +1,170 @@
+;; 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.pages.diff
+ "Given a page in its old version and the new will retrieve a map with
+ the differences that will have an impact in the snap data"
+ (:require
+ [app.common.data :as d]
+ [clojure.set :as set]))
+
+(defn calculate-page-diff
+ [old-page page check-attrs]
+
+ (let [old-objects (get old-page :objects)
+ old-guides (or (get-in old-page [:options :guides]) [])
+
+ new-objects (get page :objects)
+ new-guides (or (get-in page [:options :guides]) [])
+
+ changed-object?
+ (fn [id]
+ (let [oldv (get old-objects id)
+ newv (get new-objects id)]
+ ;; Check first without select-keys because is faster if they are
+ ;; the same reference
+ (and (not= oldv newv)
+ (not= (select-keys oldv check-attrs)
+ (select-keys newv check-attrs)))))
+
+ frame?
+ (fn [id]
+ (or (= :frame (get-in new-objects [id :type]))
+ (= :frame (get-in old-objects [id :type]))))
+
+ changed-guide?
+ (fn [id]
+ (not= (get old-guides id)
+ (get new-guides id)))
+
+ deleted-object?
+ #(and (contains? old-objects %)
+ (not (contains? new-objects %)))
+
+ deleted-guide?
+ #(and (contains? old-guides %)
+ (not (contains? new-guides %)))
+
+ new-object?
+ #(and (not (contains? old-objects %))
+ (contains? new-objects %))
+
+ new-guide?
+ #(and (not (contains? old-guides %))
+ (contains? new-guides %))
+
+ changed-frame-object?
+ #(and (contains? new-objects %)
+ (contains? old-objects %)
+ (not= (get-in old-objects [% :frame-id])
+ (get-in new-objects [% :frame-id])))
+
+ changed-frame-guide?
+ #(and (contains? new-guides %)
+ (contains? old-guides %)
+ (not= (get-in old-objects [% :frame-id])
+ (get-in new-objects [% :frame-id])))
+
+ changed-attrs-object?
+ #(and (contains? new-objects %)
+ (contains? old-objects %)
+ (= (get-in old-objects [% :frame-id])
+ (get-in new-objects [% :frame-id])))
+
+ changed-attrs-guide?
+ #(and (contains? new-guides %)
+ (contains? old-guides %)
+ (= (get-in old-objects [% :frame-id])
+ (get-in new-objects [% :frame-id])))
+
+ changed-object-ids
+ (into #{}
+ (filter changed-object?)
+ (set/union (set (keys old-objects))
+ (set (keys new-objects))))
+
+ changed-guides-ids
+ (into #{}
+ (filter changed-guide?)
+ (set/union (set (keys old-guides))
+ (set (keys new-guides))))
+
+ get-diff-object (fn [id] [(get old-objects id) (get new-objects id)])
+ get-diff-guide (fn [id] [(get old-guides id) (get new-guides id)])
+
+ ;; Shapes with different frame owner
+ change-frame-shapes
+ (->> changed-object-ids
+ (into [] (comp (filter changed-frame-object?)
+ (map get-diff-object))))
+
+ ;; Guides that changed frames
+ change-frame-guides
+ (->> changed-guides-ids
+ (into [] (comp (filter changed-frame-guide?)
+ (map get-diff-guide))))
+
+ removed-frames
+ (->> changed-object-ids
+ (into [] (comp (filter frame?)
+ (filter deleted-object?)
+ (map (d/getf old-objects)))))
+
+ removed-shapes
+ (->> changed-object-ids
+ (into [] (comp (remove frame?)
+ (filter deleted-object?)
+ (map (d/getf old-objects)))))
+
+ removed-guides
+ (->> changed-guides-ids
+ (into [] (comp (filter deleted-guide?)
+ (map (d/getf old-guides)))))
+
+ updated-frames
+ (->> changed-object-ids
+ (into [] (comp (filter frame?)
+ (filter changed-attrs-object?)
+ (map get-diff-object))))
+
+ updated-shapes
+ (->> changed-object-ids
+ (into [] (comp (remove frame?)
+ (filter changed-attrs-object?)
+ (map get-diff-object))))
+
+ updated-guides
+ (->> changed-guides-ids
+ (into [] (comp (filter changed-attrs-guide?)
+ (map get-diff-guide))))
+
+ new-frames
+ (->> changed-object-ids
+ (into [] (comp (filter frame?)
+ (filter new-object?)
+ (map (d/getf new-objects)))))
+
+ new-shapes
+ (->> changed-object-ids
+ (into [] (comp (remove frame?)
+ (filter new-object?)
+ (map (d/getf new-objects)))))
+
+ new-guides
+ (->> changed-guides-ids
+ (into [] (comp (filter new-guide?)
+ (map (d/getf new-guides)))))]
+ {:change-frame-shapes change-frame-shapes
+ :change-frame-guides change-frame-guides
+ :removed-frames removed-frames
+ :removed-shapes removed-shapes
+ :removed-guides removed-guides
+ :updated-frames updated-frames
+ :updated-shapes updated-shapes
+ :updated-guides updated-guides
+ :new-frames new-frames
+ :new-shapes new-shapes
+ :new-guides new-guides}))
diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc
index 4fd822eca4..8fef5abc85 100644
--- a/common/src/app/common/pages/helpers.cljc
+++ b/common/src/app/common/pages/helpers.cljc
@@ -9,87 +9,211 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.spec :as us]
- [app.common.types.interactions :as cti]
+ [app.common.spec.page :as spec.page]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
-(defn walk-pages
- "Go through all pages of a file and apply a function to each one"
- ;; The function receives two parameters (page-id and page), and
- ;; returns the updated page.
- [f data]
- (update data :pages-index #(d/mapm f %)))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; GENERIC SHAPE SELECTORS AND PREDICATES
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defn select-objects
- "Get a list of all objects in a container (a page or a component) that
- satisfy a condition"
- [f container]
- (filter f (vals (get container :objects))))
+(defn ^boolean root-frame?
+ [{:keys [id type]}]
+ (and (= type :frame)
+ (= id uuid/zero)))
-(defn update-object-list
- "Update multiple objects in a page at once"
- [page objects-list]
- (update page :objects
- #(into % (d/index-by :id objects-list))))
+(defn ^boolean frame-shape?
+ [{:keys [type]}]
+ (= type :frame))
-(defn get-component-shape
- "Get the parent shape linked to a component for this shape, if any"
- [shape objects]
- (if-not (:shape-ref shape)
- nil
- (if (:component-id shape)
- shape
- (if-let [parent-id (:parent-id shape)]
- (get-component-shape (get objects parent-id) objects)
- nil))))
+(defn ^boolean group-shape?
+ [{:keys [type]}]
+ (= type :group))
-(defn get-root-shape
- "Get the root shape linked to a component for this shape, if any"
- [shape objects]
+(defn ^boolean text-shape?
+ [{:keys [type]}]
+ (= type :text))
- (cond
- (some? (:component-root? shape))
- shape
-
- (some? (:shape-ref shape))
- (recur (get objects (:parent-id shape))
- objects)))
-
-(defn make-container
- [page-or-component type]
- (assoc page-or-component
- :type type))
-
-(defn page?
- [container]
- (us/assert some? (:type container))
- (= (:type container) :page))
-
-(defn component?
- [container]
- (= (:type container) :component))
-
-(defn get-container
- [id type local-file]
- (assert (some? type))
- (-> (if (= type :page)
- (get-in local-file [:pages-index id])
- (get-in local-file [:components id]))
- (assoc :type type)))
+(defn ^boolean unframed-shape?
+ "Checks if it's a non-frame shape in the top level."
+ [shape]
+ (and (not (frame-shape? shape))
+ (= (:frame-id shape) uuid/zero)))
(defn get-shape
[container shape-id]
- (get-in container [:objects shape-id]))
+ (us/assert ::spec.page/container container)
+ (us/assert ::us/uuid shape-id)
+ (-> container
+ (get :objects)
+ (get shape-id)))
+
+(defn get-children-ids
+ [objects id]
+ (if-let [shapes (-> (get objects id) :shapes (some-> vec))]
+ (into shapes (mapcat #(get-children-ids objects %)) shapes)
+ []))
+
+(defn get-children
+ [objects id]
+ (mapv (d/getf objects) (get-children-ids objects id)))
+
+(defn get-children-with-self
+ [objects id]
+ (let [lookup (d/getf objects)]
+ (into [(lookup id)] (map lookup) (get-children-ids objects id))))
+
+(defn get-parent
+ "Retrieve the id of the parent for the shape-id (if exists)"
+ [objects id]
+ (let [lookup (d/getf objects)]
+ (-> id lookup :parent-id lookup)))
+
+(defn get-parent-id
+ "Retrieve the id of the parent for the shape-id (if exists)"
+ [objects id]
+ (-> objects (get id) :parent-id))
+
+(defn get-parent-ids
+ "Returns a vector of parents of the specified shape."
+ [objects shape-id]
+ (loop [result [] id shape-id]
+ (if-let [parent-id (->> id (get objects) :parent-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
+ frame, get itself. If no shape is provided, returns the root frame."
+ ([objects]
+ (get objects uuid/zero))
+ ([objects shape-or-id]
+ (cond
+ (map? shape-or-id)
+ (if (frame-shape? shape-or-id)
+ shape-or-id
+ (get objects (:frame-id shape-or-id)))
+
+ (= uuid/zero shape-or-id)
+ (get objects uuid/zero)
+
+ :else
+ (some->> shape-or-id
+ (get objects)
+ (get-frame objects)))))
+
+(defn valid-frame-target?
+ [objects parent-id shape-id]
+ (let [shape (get objects shape-id)]
+ (or (not (frame-shape? shape))
+ (= parent-id uuid/zero))))
+
+(defn get-position-on-parent
+ [objects id]
+ (let [obj (get objects id)
+ pid (:parent-id obj)
+ prt (get objects pid)]
+ (d/index-of (:shapes prt) id)))
+
+(defn get-immediate-children
+ "Retrieve resolved shape objects that are immediate children
+ of the specified shape-id"
+ ([objects] (get-immediate-children objects uuid/zero))
+ ([objects shape-id]
+ (let [lookup (d/getf objects)]
+ (->> (lookup shape-id)
+ (:shapes)
+ (keep lookup)))))
+
+(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 (keep lookup)
+ (filter frame-shape?))]
+ (->> (:shapes (lookup uuid/zero))
+ (into [] xform))))
+
+(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)))
+
+(declare indexed-shapes)
+
+(defn get-base-shape
+ "Selects the shape that will be the base to add the shapes over"
+ [objects selected]
+ (let [;; Gets the tree-index for all the shapes
+ indexed-shapes (indexed-shapes objects)
+
+ ;; Filters the selected and retrieve a list of ids
+ sorted-ids (->> indexed-shapes
+ (filter (comp selected second))
+ (map second))]
+
+ ;; The first id will be the top-most
+ (get objects (first sorted-ids))))
+
+(defn is-parent?
+ "Check if `parent-candidate` is parent of `shape-id`"
+ [objects shape-id parent-candidate]
+
+ (loop [current (get objects parent-candidate)
+ done #{}
+ pending (:shapes current)]
+
+ (cond
+ (contains? done (:id current))
+ (recur (get objects (first pending))
+ done
+ (rest pending))
+
+ (empty? pending) false
+ (and current (contains? (set (:shapes current)) shape-id)) true
+
+ :else
+ (recur (get objects (first pending))
+ (conj done (:id current))
+ (concat (rest pending) (:shapes current))))))
+
+(defn get-index-in-parent
+ "Retrieves the index in the parent"
+ [objects shape-id]
+ (let [shape (get objects shape-id)
+ parent (get objects (:parent-id shape))
+ [parent-idx _] (d/seek (fn [[_idx child-id]] (= child-id shape-id))
+ (d/enumerate (:shapes parent)))]
+ parent-idx))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; COMPONENTS HELPERS
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn set-touched-group
+ [touched group]
+ (conj (or touched #{}) group))
+
+(defn touched-group?
+ [shape group]
+ ((or (:touched shape) #{}) group))
(defn get-component
- [component-id library-id local-library libraries]
- (assert (some? (:id local-library)))
- (let [file (if (= library-id (:id local-library))
- local-library
- (get-in libraries [library-id :data]))]
- (get-in file [:components component-id])))
+ "Retrieve a component from libraries, if no library-id is provided, we
+ iterate over all libraries and find the component on it."
+ ([libraries component-id]
+ (some #(-> % :data :components (get component-id)) (vals libraries)))
+ ([libraries library-id component-id]
+ (get-in libraries [library-id :data :components component-id])))
-(defn is-main-of
+(defn ^boolean is-main-of?
[shape-main shape-inst]
(and (:shape-ref shape-inst)
(or (= (:shape-ref shape-inst) (:id shape-main))
@@ -99,92 +223,67 @@
[component]
(get-in component [:objects (:id component)]))
-(defn get-children [id objects]
- (if-let [shapes (-> (get objects id) :shapes (some-> vec))]
- (into shapes (mapcat #(get-children % objects)) shapes)
- []))
+(defn get-component-shape
+ "Get the parent shape linked to a component for this shape, if any"
+ [objects shape]
+ (if-not (:shape-ref shape)
+ nil
+ (if (:component-id shape)
+ shape
+ (if-let [parent-id (:parent-id shape)]
+ (get-component-shape objects (get objects parent-id))
+ nil))))
-(defn get-children-objects
- "Retrieve all children objects recursively for a given object"
- [id objects]
- (mapv #(get objects %) (get-children id objects)))
+(defn get-root-shape
+ "Get the root shape linked to a component for this shape, if any."
+ [objects shape]
-(defn get-object-with-children
- "Retrieve a vector with an object and all of its children"
- [id objects]
- (mapv #(get objects %) (cons id (get-children id objects))))
-
-(defn select-children [id objects]
- (->> (get-children id objects)
- (select-keys objects)))
-
-(defn is-shape-grouped
- "Checks if a shape is inside a group"
- [shape-id objects]
- (let [contains-shape-fn (fn [{:keys [shapes]}] ((set shapes) shape-id))
- shapes (remove #(= (:type %) :frame) (vals objects))]
- (some contains-shape-fn shapes)))
-
-(defn get-top-frame
- [objects]
- (get objects uuid/zero))
-
-(defn get-parent
- "Retrieve the id of the parent for the shape-id (if exists)"
- [shape-id objects]
- (let [obj (get objects shape-id)]
- (:parent-id obj)))
-
-(defn get-parents
- [shape-id objects]
- (let [{:keys [parent-id]} (get objects shape-id)]
- (when parent-id
- (lazy-seq (cons parent-id (get-parents parent-id objects))))))
-
-(defn get-frame
- "Get the frame that contains the shape. If the shape is already a frame, get itself."
- [shape objects]
- (if (= (:type shape) :frame)
+ (cond
+ (some? (:component-root? shape))
shape
- (get objects (:frame-id shape))))
-(defn clean-loops
- "Clean a list of ids from circular references."
- [objects ids]
+ (some? (:shape-ref shape))
+ (recur objects (get objects (:parent-id shape)))))
- (let [parent-selected?
- (fn [id]
- (let [parents (get-parents id objects)]
- (some ids parents)))
+(defn make-container
+ [page-or-component type]
+ (assoc page-or-component :type type))
- add-element
- (fn [result id]
- (cond-> result
- (not (parent-selected? id))
- (conj id)))]
+(defn page?
+ [container]
+ (= (:type container) :page))
- (reduce add-element (d/ordered-set) ids)))
+(defn component?
+ [container]
+ (= (:type container) :component))
-(defn calculate-invalid-targets
- [shape-id objects]
- (let [result #{shape-id}
- children (get-in objects [shape-id :shapes])
- reduce-fn (fn [result child-id]
- (into result (calculate-invalid-targets child-id objects)))]
- (reduce reduce-fn result children)))
+(defn get-container
+ [file type id]
+ (us/assert map? file)
+ (us/assert keyword? type)
+ (us/assert uuid? id)
-(defn valid-frame-target
- [shape-id parent-id objects]
- (let [shape (get objects shape-id)]
- (or (not= (:type shape) :frame)
- (= parent-id uuid/zero))))
+ (-> (if (= type :page)
+ (get-in file [:pages-index id])
+ (get-in file [:components id]))
+ (assoc :type type)))
-(defn position-on-parent
- [id objects]
- (let [obj (get objects id)
- pid (:parent-id obj)
- prt (get objects pid)]
- (d/index-of (:shapes prt) id)))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; ALGORITHMS & TRANSFORMATIONS FOR SHAPES
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn walk-pages
+ "Go through all pages of a file and apply a function to each one"
+ ;; The function receives two parameters (page-id and page), and
+ ;; returns the updated page.
+ [f data]
+ (update data :pages-index #(d/mapm f %)))
+
+(defn update-object-list
+ "Update multiple objects in a page at once"
+ [page objects-list]
+ (update page :objects
+ #(into % (d/index-by :id objects-list))))
(defn insert-at-index
[objects index ids]
@@ -204,41 +303,22 @@
(vec prev-ids)
ids))
-(defn select-toplevel-shapes
- ([objects] (select-toplevel-shapes objects nil))
- ([objects {:keys [include-frames? include-frame-children?]
- :or {include-frames? false
- include-frame-children? true}}]
+(defn clean-loops
+ "Clean a list of ids from circular references."
+ [objects ids]
- (let [lookup #(get objects %)
- root (lookup uuid/zero)
- root-children (:shapes root)
+ (let [parent-selected?
+ (fn [id]
+ (let [parents (get-parent-ids objects id)]
+ (some ids parents)))
- lookup-shapes
- (fn [result id]
- (if (nil? id)
- result
- (let [obj (lookup id)
- typ (:type obj)
- children (:shapes obj)]
+ add-element
+ (fn [result id]
+ (cond-> result
+ (not (parent-selected? id))
+ (conj id)))]
- (cond-> result
- (or (not= :frame typ) include-frames?)
- (conj obj)
-
- (and (= :frame typ) include-frame-children?)
- (into (map lookup) children)))))]
-
- (reduce lookup-shapes [] root-children))))
-
-(defn select-frames
- [objects]
- (let [lookup #(get objects %)
- frame? #(= :frame (:type %))
- xform (comp (map lookup)
- (filter frame?))]
- (->> (:shapes (lookup uuid/zero))
- (into [] xform))))
+ (reduce add-element (d/ordered-set) ids)))
(defn clone-object
"Gets a copy of the object and all its children, with new ids
@@ -305,8 +385,6 @@
(reduce red-fn cur-idx (reverse (:shapes object)))))]
(into {} (rec-index '() uuid/zero))))
-
-
(defn expand-region-selection
"Given a selection selects all the shapes between the first and last in
an indexed manner (shift selection)"
@@ -323,67 +401,9 @@
(map second)
(into #{}))))
-(defn frame-id-by-position [objects position]
- (let [frames (select-frames objects)]
- (or
- (->> frames
- (reverse)
- (d/seek #(and position (gsh/has-point? % position)))
- :id)
- uuid/zero)))
-
-(defn set-touched-group
- [touched group]
- (conj (or touched #{}) group))
-
-(defn touched-group?
- [shape group]
- ((or (:touched shape) #{}) group))
-
-(defn get-base-shape
- "Selects the shape that will be the base to add the shapes over"
- [objects selected]
- (let [;; Gets the tree-index for all the shapes
- indexed-shapes (indexed-shapes objects)
-
- ;; Filters the selected and retrieve a list of ids
- sorted-ids (->> indexed-shapes
- (filter (comp selected second))
- (map second))]
-
- ;; The first id will be the top-most
- (get objects (first sorted-ids))))
-
-(defn is-parent?
- "Check if `parent-candidate` is parent of `shape-id`"
- [objects shape-id parent-candidate]
-
- (loop [current (get objects parent-candidate)
- done #{}
- pending (:shapes current)]
-
- (cond
- (contains? done (:id current))
- (recur (get objects (first pending))
- done
- (rest pending))
-
- (empty? pending) false
- (and current (contains? (set (:shapes current)) shape-id)) true
-
- :else
- (recur (get objects (first pending))
- (conj done (:id current))
- (concat (rest pending) (:shapes current))))))
-
-(defn get-index-in-parent
- "Retrieves the index in the parent"
- [objects shape-id]
- (let [shape (get objects shape-id)
- parent (get objects (:parent-id shape))
- [parent-idx _] (d/seek (fn [[_idx child-id]] (= child-id shape-id))
- (d/enumerate (:shapes parent)))]
- parent-idx))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; SHAPES ORGANIZATION (PATH MANAGEMENT)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-path
"Decompose a string in the form 'one / two / three' into
@@ -443,25 +463,3 @@
[path name]
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
-
-(defn connected-frame?
- "Check if some frame is origin or destination of any navigate interaction
- in the page"
- [frame-id objects]
- (let [children (get-object-with-children frame-id objects)]
- (or (some cti/flow-origin? (map :interactions children))
- (some #(cti/flow-to? % frame-id) (map :interactions (vals objects))))))
-
-(defn unframed-shape?
- "Checks if it's a non-frame shape in the top level."
- [shape]
- (and (not= (:type shape) :frame)
- (= (:frame-id shape) uuid/zero)))
-
-(defn children-seq
- "Creates a sequence of shapes through the objects tree"
- [shape objects]
- (let [getter (partial get objects)]
- (tree-seq #(d/not-empty? (get shape :shapes))
- #(->> (get % :shapes) (map getter))
- shape)))
diff --git a/common/src/app/common/pages/indices.cljc b/common/src/app/common/pages/indices.cljc
index 8145d2f0ae..2ec5124784 100644
--- a/common/src/app/common/pages/indices.cljc
+++ b/common/src/app/common/pages/indices.cljc
@@ -7,7 +7,7 @@
(ns app.common.pages.indices
(:require
[app.common.data :as d]
- [app.common.pages.helpers :as helpers]
+ [app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[clojure.set :as set]))
@@ -45,7 +45,7 @@
means is displayed over other shapes with less index."
[objects]
- (let [frames (helpers/select-frames objects)
+ (let [frames (cph/get-frames objects)
z-index (calculate-frame-z-index {} uuid/zero objects)]
(->> frames
(map :id)
@@ -61,7 +61,7 @@
changed-frames (set/union old-frames new-frames)
- frames (->> (helpers/select-frames new-objects)
+ frames (->> (cph/get-frames new-objects)
(map :id)
(filter #(contains? changed-frames %)))
@@ -84,13 +84,10 @@
(generate-child-all-parents-index objects (vals objects)))
([objects shapes]
- (let [shape->parents
- (fn [shape]
- (->> (helpers/get-parents (:id shape) objects)
- (into [])))]
- (->> shapes
- (map #(vector (:id %) (shape->parents %)))
- (into {})))))
+ (let [xf-parents (comp
+ (map :id)
+ (map #(vector % (cph/get-parent-ids objects %))))]
+ (into {} xf-parents shapes))))
(defn create-clip-index
"Retrieves the mask information for an object"
diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc
index 8e3b5bb92f..21a209c359 100644
--- a/common/src/app/common/pages/init.cljc
+++ b/common/src/app/common/pages/init.cljc
@@ -51,7 +51,9 @@
:rx 0
:ry 0}
- {:type :image}
+ {:type :image
+ :rx 0
+ :ry 0}
{:type :circle
:name "Circle-1"
diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc
index dd7d232681..d87f2156e8 100644
--- a/common/src/app/common/pages/migrations.cljc
+++ b/common/src/app/common/pages/migrations.cljc
@@ -281,3 +281,22 @@
(d/update-in-when page [:options :saved-grids] #(d/mapm update-grid %)))]
(update data :pages-index #(d/mapm update-page %))))
+
+;; Add rx and ry to images
+(defmethod migrate 13
+ [data]
+ (letfn [(fix-radius [shape]
+ (if-not (or (contains? shape :rx) (contains? shape :r1))
+ (-> shape
+ (assoc :rx 0)
+ (assoc :ry 0))
+ shape))
+ (update-object [_ object]
+ (cond-> object
+ (= :image (:type object))
+ (fix-radius)))
+
+ (update-page [_ page]
+ (update page :objects #(d/mapm update-object %)))]
+
+ (update data :pages-index #(d/mapm update-page %))))
diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc
deleted file mode 100644
index 59ded8f62d..0000000000
--- a/common/src/app/common/pages/spec.cljc
+++ /dev/null
@@ -1,623 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.common.pages.spec
- (:require
- [app.common.geom.matrix :as gmt]
- [app.common.geom.point :as gpt]
- [app.common.spec :as us]
- [app.common.types.interactions :as cti]
- [app.common.types.page-options :as cto]
- [app.common.types.radius :as ctr]
- [app.common.uuid :as uuid]
- [clojure.set :as set]
- [clojure.spec.alpha :as s]))
-
-;; --- Specs
-
-(s/def ::frame-id uuid?)
-(s/def ::id uuid?)
-(s/def ::name string?)
-(s/def ::path (s/nilable string?))
-(s/def ::page-id uuid?)
-(s/def ::parent-id uuid?)
-(s/def ::string string?)
-(s/def ::type keyword?)
-(s/def ::uuid uuid?)
-
-(s/def ::component-id uuid?)
-(s/def ::component-file uuid?)
-(s/def ::component-root? boolean?)
-(s/def ::shape-ref uuid?)
-
-(s/def :internal.matrix/a ::us/safe-number)
-(s/def :internal.matrix/b ::us/safe-number)
-(s/def :internal.matrix/c ::us/safe-number)
-(s/def :internal.matrix/d ::us/safe-number)
-(s/def :internal.matrix/e ::us/safe-number)
-(s/def :internal.matrix/f ::us/safe-number)
-
-(s/def ::matrix
- (s/and (s/keys :req-un [:internal.matrix/a
- :internal.matrix/b
- :internal.matrix/c
- :internal.matrix/d
- :internal.matrix/e
- :internal.matrix/f])
- gmt/matrix?))
-
-
-(s/def :internal.point/x ::us/safe-number)
-(s/def :internal.point/y ::us/safe-number)
-
-(s/def ::point
- (s/and (s/keys :req-un [:internal.point/x
- :internal.point/y])
- gpt/point?))
-
-;; GRADIENTS
-
-(s/def :internal.gradient.stop/color ::string)
-(s/def :internal.gradient.stop/opacity ::us/safe-number)
-(s/def :internal.gradient.stop/offset ::us/safe-number)
-
-(s/def :internal.gradient/type #{:linear :radial})
-(s/def :internal.gradient/start-x ::us/safe-number)
-(s/def :internal.gradient/start-y ::us/safe-number)
-(s/def :internal.gradient/end-x ::us/safe-number)
-(s/def :internal.gradient/end-y ::us/safe-number)
-(s/def :internal.gradient/width ::us/safe-number)
-
-(s/def :internal.gradient/stop
- (s/keys :req-un [:internal.gradient.stop/color
- :internal.gradient.stop/opacity
- :internal.gradient.stop/offset]))
-
-(s/def :internal.gradient/stops
- (s/coll-of :internal.gradient/stop :kind vector?))
-
-(s/def ::gradient
- (s/keys :req-un [:internal.gradient/type
- :internal.gradient/start-x
- :internal.gradient/start-y
- :internal.gradient/end-x
- :internal.gradient/end-y
- :internal.gradient/width
- :internal.gradient/stops]))
-
-
-;;; COLORS
-
-(s/def :internal.color/name ::string)
-(s/def :internal.color/path (s/nilable ::string))
-(s/def :internal.color/value (s/nilable ::string))
-(s/def :internal.color/color (s/nilable ::string))
-(s/def :internal.color/opacity (s/nilable ::us/safe-number))
-(s/def :internal.color/gradient (s/nilable ::gradient))
-
-(s/def ::color
- (s/keys :opt-un [::id
- :internal.color/name
- :internal.color/path
- :internal.color/value
- :internal.color/color
- :internal.color/opacity
- :internal.color/gradient]))
-
-
-;;; SHADOW EFFECT
-
-(s/def :internal.shadow/id uuid?)
-(s/def :internal.shadow/style #{:drop-shadow :inner-shadow})
-(s/def :internal.shadow/color ::color)
-(s/def :internal.shadow/offset-x ::us/safe-number)
-(s/def :internal.shadow/offset-y ::us/safe-number)
-(s/def :internal.shadow/blur ::us/safe-number)
-(s/def :internal.shadow/spread ::us/safe-number)
-(s/def :internal.shadow/hidden boolean?)
-
-(s/def :internal.shadow/shadow
- (s/keys :req-un [:internal.shadow/id
- :internal.shadow/style
- :internal.shadow/color
- :internal.shadow/offset-x
- :internal.shadow/offset-y
- :internal.shadow/blur
- :internal.shadow/spread
- :internal.shadow/hidden]))
-
-(s/def ::shadow
- (s/coll-of :internal.shadow/shadow :kind vector?))
-
-
-;;; BLUR EFFECT
-
-(s/def :internal.blur/id uuid?)
-(s/def :internal.blur/type #{:layer-blur})
-(s/def :internal.blur/value ::us/safe-number)
-(s/def :internal.blur/hidden boolean?)
-
-(s/def ::blur
- (s/keys :req-un [:internal.blur/id
- :internal.blur/type
- :internal.blur/value
- :internal.blur/hidden]))
-
-;; Size constraints
-
-(s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale})
-(s/def :internal.shape/constraints-v #{:top :bottom :topbottom :center :scale})
-(s/def :internal.shape/fixed-scroll boolean?)
-
-; Shapes in the top frame have no constraints. Shapes directly below some
-; frame are left-top constrained. Else (shapes in a group) are scaled.
-(defn default-constraints-h
- [shape]
- (if (= (:parent-id shape) uuid/zero)
- nil
- (if (= (:parent-id shape) (:frame-id shape))
- :left
- :scale)))
-
-(defn default-constraints-v
- [shape]
- (if (= (:parent-id shape) uuid/zero)
- nil
- (if (= (:parent-id shape) (:frame-id shape))
- :top
- :scale)))
-
-;; Page Data related
-(s/def :internal.shape/blocked boolean?)
-(s/def :internal.shape/collapsed boolean?)
-
-(s/def :internal.shape/fill-color string?)
-(s/def :internal.shape/fill-opacity ::us/safe-number)
-(s/def :internal.shape/fill-color-gradient (s/nilable ::gradient))
-(s/def :internal.shape/fill-color-ref-file (s/nilable uuid?))
-(s/def :internal.shape/fill-color-ref-id (s/nilable uuid?))
-(s/def :internal.shape/hide-fill-on-export boolean?)
-
-(s/def :internal.shape/font-family string?)
-(s/def :internal.shape/font-size ::us/safe-integer)
-(s/def :internal.shape/font-style string?)
-(s/def :internal.shape/font-weight string?)
-(s/def :internal.shape/hidden boolean?)
-(s/def :internal.shape/letter-spacing ::us/safe-number)
-(s/def :internal.shape/line-height ::us/safe-number)
-(s/def :internal.shape/locked boolean?)
-(s/def :internal.shape/page-id uuid?)
-(s/def :internal.shape/proportion ::us/safe-number)
-(s/def :internal.shape/proportion-lock boolean?)
-(s/def :internal.shape/stroke-color string?)
-(s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient))
-(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
-(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
-(s/def :internal.shape/stroke-opacity ::us/safe-number)
-(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
-
-(def stroke-caps-line #{:round :square})
-(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker})
-(def stroke-caps (set/union stroke-caps-line stroke-caps-marker))
-(s/def :internal.shape/stroke-cap-start stroke-caps)
-(s/def :internal.shape/stroke-cap-end stroke-caps)
-
-(defn has-caps?
- [shape]
- (= (:type shape) :path))
-
-(s/def :internal.shape/stroke-width ::us/safe-number)
-(s/def :internal.shape/stroke-alignment #{:center :inner :outer})
-(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
-(s/def :internal.shape/x ::us/safe-number)
-(s/def :internal.shape/y ::us/safe-number)
-(s/def :internal.shape/cx ::us/safe-number)
-(s/def :internal.shape/cy ::us/safe-number)
-(s/def :internal.shape/width ::us/safe-number)
-(s/def :internal.shape/height ::us/safe-number)
-(s/def :internal.shape/index integer?)
-(s/def :internal.shape/shadow ::shadow)
-(s/def :internal.shape/blur ::blur)
-
-(s/def :internal.shape/x1 ::us/safe-number)
-(s/def :internal.shape/y1 ::us/safe-number)
-(s/def :internal.shape/x2 ::us/safe-number)
-(s/def :internal.shape/y2 ::us/safe-number)
-
-(s/def :internal.shape.export/suffix string?)
-(s/def :internal.shape.export/scale ::us/safe-number)
-(s/def :internal.shape/export
- (s/keys :req-un [::type
- :internal.shape.export/suffix
- :internal.shape.export/scale]))
-
-(s/def :internal.shape/exports
- (s/coll-of :internal.shape/export :kind vector?))
-
-(s/def :internal.shape/selrect
- (s/keys :req-un [:internal.shape/x
- :internal.shape/y
- :internal.shape/x1
- :internal.shape/y1
- :internal.shape/x2
- :internal.shape/y2
- :internal.shape/width
- :internal.shape/height]))
-
-(s/def :internal.shape/points
- (s/every ::point :kind vector?))
-
-(s/def :internal.shape/shapes
- (s/every uuid? :kind vector?))
-
-(s/def :internal.shape/transform ::matrix)
-(s/def :internal.shape/transform-inverse ::matrix)
-
-(s/def :internal.shape/opacity ::us/safe-number)
-(s/def :internal.shape/blend-mode
- #{:normal
- :darken
- :multiply
- :color-burn
- :lighten
- :screen
- :color-dodge
- :overlay
- :soft-light
- :hard-light
- :difference
- :exclusion
- :hue
- :saturation
- :color
- :luminosity})
-
-(s/def ::shape-attrs
- (s/keys :opt-un [::id
- ::type
- ::name
- ::component-id
- ::component-file
- ::component-root?
- ::shape-ref
- :internal.shape/selrect
- :internal.shape/points
- :internal.shape/blocked
- :internal.shape/collapsed
- :internal.shape/fill-color
- :internal.shape/fill-opacity
- :internal.shape/fill-color-gradient
- :internal.shape/fill-color-ref-file
- :internal.shape/fill-color-ref-id
- :internal.shape/font-family
- :internal.shape/font-size
- :internal.shape/font-style
- :internal.shape/font-weight
- :internal.shape/hidden
- :internal.shape/letter-spacing
- :internal.shape/line-height
- :internal.shape/locked
- :internal.shape/proportion
- :internal.shape/proportion-lock
- :internal.shape/constraints-h
- :internal.shape/constraints-v
- :internal.shape/fixed-scroll
- ::ctr/rx
- ::ctr/ry
- ::ctr/r1
- ::ctr/r2
- ::ctr/r3
- ::ctr/r4
- :internal.shape/x
- :internal.shape/y
- :internal.shape/exports
- :internal.shape/shapes
- :internal.shape/stroke-color
- :internal.shape/stroke-color-ref-file
- :internal.shape/stroke-color-ref-id
- :internal.shape/stroke-opacity
- :internal.shape/stroke-style
- :internal.shape/stroke-width
- :internal.shape/stroke-alignment
- :internal.shape/stroke-cap-start
- :internal.shape/stroke-cap-end
- :internal.shape/text-align
- :internal.shape/transform
- :internal.shape/transform-inverse
- :internal.shape/width
- :internal.shape/height
- ::cti/interactions
- :internal.shape/masked-group?
- :internal.shape/shadow
- :internal.shape/blur
- :internal.shape/opacity
- :internal.shape/blend-mode]))
-
-(s/def :internal.shape.text/type #{"root" "paragraph-set" "paragraph"})
-(s/def :internal.shape.text/children
- (s/coll-of :internal.shape.text/content
- :kind vector?
- :min-count 1))
-
-(s/def :internal.shape.text/text string?)
-(s/def :internal.shape.text/key string?)
-
-(s/def :internal.shape.text/content
- (s/nilable
- (s/or :text-container
- (s/keys :req-un [:internal.shape.text/type
- :internal.shape.text/children]
- :opt-un [:internal.shape.text/key])
- :text-content
- (s/keys :req-un [:internal.shape.text/text]))))
-
-(s/def :internal.shape.path/command keyword?)
-(s/def :internal.shape.path/params
- (s/nilable (s/map-of keyword? any?)))
-
-(s/def :internal.shape.path/command-item
- (s/keys :req-un [:internal.shape.path/command]
- :opt-un [:internal.shape.path/params]))
-
-(s/def :internal.shape.path/content
- (s/coll-of :internal.shape.path/command-item :kind vector?))
-
-(defmulti shape-spec :type)
-
-(defmethod shape-spec :default [_]
- (s/spec ::shape-attrs))
-
-(defmethod shape-spec :text [_]
- (s/and ::shape-attrs
- (s/keys :opt-un [:internal.shape.text/content])))
-
-(defmethod shape-spec :path [_]
- (s/and ::shape-attrs
- (s/keys :opt-un [:internal.shape.path/content])))
-
-(defmethod shape-spec :frame [_]
- (s/and ::shape-attrs
- (s/keys :opt-un [:internal.shape/hide-fill-on-export])))
-
-(s/def ::shape
- (s/and (s/multi-spec shape-spec :type)
- #(contains? % :name)
- #(contains? % :type)))
-
-(s/def :internal.page/objects (s/map-of uuid? ::shape))
-
-(s/def ::page
- (s/keys :req-un [::id
- ::name
- ::cto/options
- :internal.page/objects]))
-
-(s/def ::recent-color
- (s/keys :opt-un [:internal.color/value
- :internal.color/color
- :internal.color/opacity
- :internal.color/gradient]))
-
-(s/def :internal.media-object/name ::string)
-(s/def :internal.media-object/width ::us/safe-integer)
-(s/def :internal.media-object/height ::us/safe-integer)
-(s/def :internal.media-object/mtype ::string)
-
-(s/def ::media-object
- (s/keys :req-un [::id
- ::name
- :internal.media-object/width
- :internal.media-object/height
- :internal.media-object/mtype]))
-
-(s/def ::media-object-update
- (s/keys :req-un [::id]
- :opt-un [::name
- :internal.media-object/width
- :internal.media-object/height
- :internal.media-object/mtype]))
-
-(s/def :internal.file/colors
- (s/map-of ::uuid ::color))
-
-(s/def :internal.file/recent-colors
- (s/coll-of ::recent-color :kind vector?))
-
-(s/def :internal.typography/id ::id)
-(s/def :internal.typography/name ::string)
-(s/def :internal.typography/path (s/nilable ::string))
-(s/def :internal.typography/font-id ::string)
-(s/def :internal.typography/font-family ::string)
-(s/def :internal.typography/font-variant-id ::string)
-(s/def :internal.typography/font-size ::string)
-(s/def :internal.typography/font-weight ::string)
-(s/def :internal.typography/font-style ::string)
-(s/def :internal.typography/line-height ::string)
-(s/def :internal.typography/letter-spacing ::string)
-(s/def :internal.typography/text-transform ::string)
-
-(s/def ::typography
- (s/keys :req-un [:internal.typography/id
- :internal.typography/name
- :internal.typography/font-id
- :internal.typography/font-family
- :internal.typography/font-variant-id
- :internal.typography/font-size
- :internal.typography/font-weight
- :internal.typography/font-style
- :internal.typography/line-height
- :internal.typography/letter-spacing
- :internal.typography/text-transform]
- :opt-un [:internal.typography/path]))
-
-(s/def :internal.file/pages
- (s/coll-of ::uuid :kind vector?))
-
-(s/def :internal.file/media
- (s/map-of ::uuid ::media-object))
-
-(s/def :internal.file/pages-index
- (s/map-of ::uuid ::page))
-
-(s/def ::data
- (s/keys :req-un [:internal.file/pages-index
- :internal.file/pages]
- :opt-un [:internal.file/colors
- :internal.file/recent-colors
- :internal.file/media]))
-
-(s/def :internal.container/type #{:page :component})
-
-(s/def ::container
- (s/keys :req-un [:internal.container/type
- ::id
- ::name
- :internal.page/objects]))
-
-(defmulti operation-spec :type)
-
-(s/def :internal.operations.set/attr keyword?)
-(s/def :internal.operations.set/val any?)
-(s/def :internal.operations.set/touched
- (s/nilable (s/every keyword? :kind set?)))
-(s/def :internal.operations.set/remote-synced?
- (s/nilable boolean?))
-
-(defmethod operation-spec :set [_]
- (s/keys :req-un [:internal.operations.set/attr
- :internal.operations.set/val]))
-
-(defmethod operation-spec :set-touched [_]
- (s/keys :req-un [:internal.operations.set/touched]))
-
-(defmethod operation-spec :set-remote-synced [_]
- (s/keys :req-un [:internal.operations.set/remote-synced?]))
-
-(defmulti change-spec :type)
-
-(s/def :internal.changes.set-option/option any?)
-(s/def :internal.changes.set-option/value any?)
-
-(defmethod change-spec :set-option [_]
- (s/keys :req-un [:internal.changes.set-option/option
- :internal.changes.set-option/value]))
-
-(s/def :internal.changes.add-obj/obj ::shape)
-
-(defn- valid-container-id-frame?
- [o]
- (or (and (contains? o :page-id)
- (not (contains? o :component-id))
- (some? (:frame-id o)))
- (and (contains? o :component-id)
- (not (contains? o :page-id))
- (nil? (:frame-id o)))))
-
-(defn- valid-container-id?
- [o]
- (or (and (contains? o :page-id)
- (not (contains? o :component-id)))
- (and (contains? o :component-id)
- (not (contains? o :page-id)))))
-
-(defmethod change-spec :add-obj [_]
- (s/and (s/keys :req-un [::id :internal.changes.add-obj/obj]
- :opt-un [::page-id ::component-id ::parent-id ::frame-id])
- valid-container-id-frame?))
-
-(s/def ::operation (s/multi-spec operation-spec :type))
-(s/def ::operations (s/coll-of ::operation))
-
-(defmethod change-spec :mod-obj [_]
- (s/and (s/keys :req-un [::id ::operations]
- :opt-un [::page-id ::component-id])
- valid-container-id?))
-
-(defmethod change-spec :del-obj [_]
- (s/and (s/keys :req-un [::id]
- :opt-un [::page-id ::component-id])
- valid-container-id?))
-
-(s/def :internal.changes.reg-objects/shapes
- (s/coll-of uuid? :kind vector?))
-
-(defmethod change-spec :reg-objects [_]
- (s/and (s/keys :req-un [:internal.changes.reg-objects/shapes]
- :opt-un [::page-id ::component-id])
- valid-container-id?))
-
-(defmethod change-spec :mov-objects [_]
- (s/and (s/keys :req-un [::parent-id :internal.shape/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])))
-
-(defmethod change-spec :mod-page [_]
- (s/keys :req-un [::id ::name]))
-
-(defmethod change-spec :del-page [_]
- (s/keys :req-un [::id]))
-
-(defmethod change-spec :mov-page [_]
- (s/keys :req-un [::id ::index]))
-
-(defmethod change-spec :add-color [_]
- (s/keys :req-un [::color]))
-
-(defmethod change-spec :mod-color [_]
- (s/keys :req-un [::color]))
-
-(defmethod change-spec :del-color [_]
- (s/keys :req-un [::id]))
-
-(s/def :internal.changes.add-recent-color/color ::recent-color)
-
-(defmethod change-spec :add-recent-color [_]
- (s/keys :req-un [:internal.changes.add-recent-color/color]))
-
-(s/def :internal.changes.media/object ::media-object)
-
-(defmethod change-spec :add-media [_]
- (s/keys :req-un [:internal.changes.media/object]))
-
-(s/def :internal.changes.media.mod/object ::media-object-update)
-
-(defmethod change-spec :mod-media [_]
- (s/keys :req-un [:internal.changes.media.mod/object]))
-
-(defmethod change-spec :del-media [_]
- (s/keys :req-un [::id]))
-
-(s/def :internal.changes.add-component/shapes
- (s/coll-of ::shape))
-
-(defmethod change-spec :add-component [_]
- (s/keys :req-un [::id ::name :internal.changes.add-component/shapes]
- :opt-un [::path]))
-
-(defmethod change-spec :mod-component [_]
- (s/keys :req-un [::id]
- :opt-un [::name :internal.changes.add-component/shapes]))
-
-(defmethod change-spec :del-component [_]
- (s/keys :req-un [::id]))
-
-(s/def :internal.changes.typography/typography ::typography)
-
-(defmethod change-spec :add-typography [_]
- (s/keys :req-un [:internal.changes.typography/typography]))
-
-(defmethod change-spec :mod-typography [_]
- (s/keys :req-un [:internal.changes.typography/typography]))
-
-(defmethod change-spec :del-typography [_]
- (s/keys :req-un [:internal.typography/id]))
-
-(s/def ::change (s/multi-spec change-spec :type))
-(s/def ::changes (s/coll-of ::change))
diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc
index 1742b6f62b..15b0cd1e7b 100644
--- a/common/src/app/common/spec.cljc
+++ b/common/src/app/common/spec.cljc
@@ -16,7 +16,6 @@
;; because of some strange interaction with cljs.spec.alpha and
;; modules splitting.
[app.common.exceptions :as ex]
- [app.common.geom.point :as gpt]
[app.common.uuid :as uuid]
[cuerdas.core :as str]
[expound.alpha :as expound]))
@@ -110,7 +109,6 @@
(s/def ::not-empty-string (s/and string? #(not (str/empty? %))))
(s/def ::url string?)
(s/def ::fn fn?)
-(s/def ::point gpt/point?)
(s/def ::id ::uuid)
(defn bytes?
@@ -279,5 +277,3 @@
(binding [s/*explain-out* expound/printer]
(with-out-str
(s/explain-out (update data ::s/problems #(take max-problems %))))))))
-
-
diff --git a/common/src/app/common/spec/blur.cljc b/common/src/app/common/spec/blur.cljc
new file mode 100644
index 0000000000..04b643e896
--- /dev/null
+++ b/common/src/app/common/spec/blur.cljc
@@ -0,0 +1,19 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.common.spec.blur
+ (:require
+ [app.common.spec :as us]
+ [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
+(s/def ::type #{:layer-blur})
+(s/def ::value ::us/safe-number)
+(s/def ::hidden boolean?)
+
+(s/def ::blur
+ (s/keys :req-un [::id ::type ::value ::hidden]))
+
diff --git a/common/src/app/common/spec/change.cljc b/common/src/app/common/spec/change.cljc
new file mode 100644
index 0000000000..b38aa3e85f
--- /dev/null
+++ b/common/src/app/common/spec/change.cljc
@@ -0,0 +1,165 @@
+;; 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.spec.change
+ (:require
+ [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]
+ [clojure.spec.alpha :as s]))
+
+(s/def ::index integer?)
+(s/def ::id uuid?)
+(s/def ::parent-id uuid?)
+(s/def ::frame-id uuid?)
+(s/def ::page-id uuid?)
+(s/def ::component-id uuid?)
+(s/def ::name string?)
+
+(defmulti operation-spec :type)
+
+(s/def :internal.operations.set/attr keyword?)
+(s/def :internal.operations.set/val any?)
+
+(s/def :internal.operations.set/touched
+ (s/nilable (s/every keyword? :kind set?)))
+
+(s/def :internal.operations.set/remote-synced?
+ (s/nilable boolean?))
+
+(defmethod operation-spec :set [_]
+ (s/keys :req-un [:internal.operations.set/attr
+ :internal.operations.set/val]))
+
+(defmethod operation-spec :set-touched [_]
+ (s/keys :req-un [:internal.operations.set/touched]))
+
+(defmethod operation-spec :set-remote-synced [_]
+ (s/keys :req-un [:internal.operations.set/remote-synced?]))
+
+(defmulti change-spec :type)
+
+(s/def :internal.changes.set-option/option any?)
+(s/def :internal.changes.set-option/value any?)
+
+(defmethod change-spec :set-option [_]
+ (s/keys :req-un [:internal.changes.set-option/option
+ :internal.changes.set-option/value]))
+
+(s/def :internal.changes.add-obj/obj ::shape/shape)
+
+(defn- valid-container-id-frame?
+ [o]
+ (or (and (contains? o :page-id)
+ (not (contains? o :component-id))
+ (some? (:frame-id o)))
+ (and (contains? o :component-id)
+ (not (contains? o :page-id))
+ (nil? (:frame-id o)))))
+
+(defn- valid-container-id?
+ [o]
+ (or (and (contains? o :page-id)
+ (not (contains? o :component-id)))
+ (and (contains? o :component-id)
+ (not (contains? o :page-id)))))
+
+(defmethod change-spec :add-obj [_]
+ (s/and (s/keys :req-un [::id :internal.changes.add-obj/obj]
+ :opt-un [::page-id ::component-id ::parent-id ::frame-id])
+ valid-container-id-frame?))
+
+(s/def ::operation (s/multi-spec operation-spec :type))
+(s/def ::operations (s/coll-of ::operation))
+
+(defmethod change-spec :mod-obj [_]
+ (s/and (s/keys :req-un [::id ::operations]
+ :opt-un [::page-id ::component-id])
+ valid-container-id?))
+
+(defmethod change-spec :del-obj [_]
+ (s/and (s/keys :req-un [::id]
+ :opt-un [::page-id ::component-id])
+ valid-container-id?))
+
+(defmethod change-spec :reg-objects [_]
+ (s/and (s/keys :req-un [::shape/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]
+ :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])))
+
+(defmethod change-spec :mod-page [_]
+ (s/keys :req-un [::id ::name]))
+
+(defmethod change-spec :del-page [_]
+ (s/keys :req-un [::id]))
+
+(defmethod change-spec :mov-page [_]
+ (s/keys :req-un [::id ::index]))
+
+(defmethod change-spec :add-color [_]
+ (s/keys :req-un [::color/color]))
+
+(defmethod change-spec :mod-color [_]
+ (s/keys :req-un [::color/color]))
+
+(defmethod change-spec :del-color [_]
+ (s/keys :req-un [::id]))
+
+(s/def :internal.changes.add-recent-color/color ::color/recent-color)
+
+(defmethod change-spec :add-recent-color [_]
+ (s/keys :req-un [:internal.changes.add-recent-color/color]))
+
+(s/def :internal.changes.media/object ::file/media-object)
+
+(defmethod change-spec :add-media [_]
+ (s/keys :req-un [:internal.changes.media/object]))
+
+(s/def :internal.changes.media.mod/object
+ (s/and ::file/media-object #(contains? % :id)))
+
+(defmethod change-spec :mod-media [_]
+ (s/keys :req-un [:internal.changes.media.mod/object]))
+
+(defmethod change-spec :del-media [_]
+ (s/keys :req-un [::id]))
+
+(s/def :internal.changes.add-component/shapes
+ (s/coll-of ::shape/shape))
+
+(defmethod change-spec :add-component [_]
+ (s/keys :req-un [::id ::name :internal.changes.add-component/shapes]
+ :opt-un [::path]))
+
+(defmethod change-spec :mod-component [_]
+ (s/keys :req-un [::id]
+ :opt-un [::name :internal.changes.add-component/shapes]))
+
+(defmethod change-spec :del-component [_]
+ (s/keys :req-un [::id]))
+
+(defmethod change-spec :add-typography [_]
+ (s/keys :req-un [::typg/typography]))
+
+(defmethod change-spec :mod-typography [_]
+ (s/keys :req-un [::typg/typography]))
+
+(defmethod change-spec :del-typography [_]
+ (s/keys :req-un [::typg/id]))
+
+(s/def ::change (s/multi-spec change-spec :type))
+(s/def ::changes (s/coll-of ::change))
diff --git a/common/src/app/common/spec/color.cljc b/common/src/app/common/spec/color.cljc
new file mode 100644
index 0000000000..627094a990
--- /dev/null
+++ b/common/src/app/common/spec/color.cljc
@@ -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.common.spec.color
+ (:require
+ [app.common.spec :as us]
+ [clojure.spec.alpha :as s]))
+
+;; TODO: waiting clojure 1.11 to rename this all :internal.stuff to a
+;; more consistent name.
+
+;; TODO: maybe define ::color-hex-string with proper hex color spec?
+
+;; --- GRADIENTS
+
+(s/def ::id uuid?)
+
+(s/def :internal.gradient.stop/color string?)
+(s/def :internal.gradient.stop/opacity ::us/safe-number)
+(s/def :internal.gradient.stop/offset ::us/safe-number)
+
+(s/def :internal.gradient/type #{:linear :radial})
+(s/def :internal.gradient/start-x ::us/safe-number)
+(s/def :internal.gradient/start-y ::us/safe-number)
+(s/def :internal.gradient/end-x ::us/safe-number)
+(s/def :internal.gradient/end-y ::us/safe-number)
+(s/def :internal.gradient/width ::us/safe-number)
+
+(s/def :internal.gradient/stop
+ (s/keys :req-un [:internal.gradient.stop/color
+ :internal.gradient.stop/opacity
+ :internal.gradient.stop/offset]))
+
+(s/def :internal.gradient/stops
+ (s/coll-of :internal.gradient/stop :kind vector?))
+
+(s/def ::gradient
+ (s/keys :req-un [:internal.gradient/type
+ :internal.gradient/start-x
+ :internal.gradient/start-y
+ :internal.gradient/end-x
+ :internal.gradient/end-y
+ :internal.gradient/width
+ :internal.gradient/stops]))
+
+;;; --- COLORS
+
+(s/def :internal.color/name string?)
+(s/def :internal.color/path (s/nilable string?))
+(s/def :internal.color/value (s/nilable string?))
+(s/def :internal.color/color (s/nilable string?))
+(s/def :internal.color/opacity (s/nilable ::us/safe-number))
+(s/def :internal.color/gradient (s/nilable ::gradient))
+
+(s/def ::color
+ (s/keys :opt-un [::id
+ :internal.color/name
+ :internal.color/path
+ :internal.color/value
+ :internal.color/color
+ :internal.color/opacity
+ :internal.color/gradient]))
+
+(s/def ::recent-color
+ (s/keys :opt-un [:internal.color/value
+ :internal.color/color
+ :internal.color/opacity
+ :internal.color/gradient]))
+
+
+
+
diff --git a/common/src/app/common/spec/export.cljc b/common/src/app/common/spec/export.cljc
new file mode 100644
index 0000000000..acfe7b2791
--- /dev/null
+++ b/common/src/app/common/spec/export.cljc
@@ -0,0 +1,22 @@
+;; 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.spec.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?)
+
+(s/def ::export
+ (s/keys :req-un [::type
+ ::suffix
+ ::scale]))
+
+
diff --git a/common/src/app/common/spec/file.cljc b/common/src/app/common/spec/file.cljc
new file mode 100644
index 0000000000..a1e538ecc2
--- /dev/null
+++ b/common/src/app/common/spec/file.cljc
@@ -0,0 +1,54 @@
+;; 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.spec.file
+ (:require
+ [app.common.spec :as us]
+ [app.common.spec.color :as color]
+ [app.common.spec.page :as page]
+ [app.common.spec.typography]
+ [clojure.spec.alpha :as s]))
+
+(s/def :internal.media-object/name string?)
+(s/def :internal.media-object/width ::us/safe-integer)
+(s/def :internal.media-object/height ::us/safe-integer)
+(s/def :internal.media-object/mtype string?)
+
+(s/def ::media-object
+ (s/keys :req-un [::id
+ ::name
+ :internal.media-object/width
+ :internal.media-object/height
+ :internal.media-object/mtype]))
+
+(s/def ::colors
+ (s/map-of uuid? ::color/color))
+
+(s/def ::recent-colors
+ (s/coll-of ::color/recent-color :kind vector?))
+
+(s/def ::typographies
+ (s/map-of uuid? :app.common.spec.typography/typography))
+
+(s/def ::pages
+ (s/coll-of uuid? :kind vector?))
+
+(s/def ::media
+ (s/map-of uuid? ::media-object))
+
+(s/def ::pages-index
+ (s/map-of uuid? ::page/page))
+
+(s/def ::components
+ (s/map-of uuid? ::page/container))
+
+(s/def ::data
+ (s/keys :req-un [::pages-index
+ ::pages]
+ :opt-un [::colors
+ ::recent-colors
+ ::typographies
+ ::media]))
diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/spec/interactions.cljc
similarity index 99%
rename from common/src/app/common/types/interactions.cljc
rename to common/src/app/common/spec/interactions.cljc
index 5903662c34..eb3c2171dc 100644
--- a/common/src/app/common/types/interactions.cljc
+++ b/common/src/app/common/spec/interactions.cljc
@@ -4,7 +4,7 @@
;;
;; Copyright (c) UXBOX Labs SL
-(ns app.common.types.interactions
+(ns app.common.spec.interactions
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
@@ -100,7 +100,7 @@
:bottom-left
:bottom-right
:bottom-center})
-(s/def ::overlay-position ::us/point)
+(s/def ::overlay-position ::gpt/point)
(s/def ::url ::us/string)
(s/def ::close-click-outside ::us/boolean)
(s/def ::background-overlay ::us/boolean)
diff --git a/common/src/app/common/spec/page.cljc b/common/src/app/common/spec/page.cljc
new file mode 100644
index 0000000000..135eadb1eb
--- /dev/null
+++ b/common/src/app/common/spec/page.cljc
@@ -0,0 +1,128 @@
+;; 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.spec.page
+ (:require
+ [app.common.data :as d]
+ [app.common.spec :as us]
+ [app.common.spec.shape :as shape]
+ [clojure.spec.alpha :as s]))
+
+;; --- Grid options
+
+(s/def :internal.grid.color/color string?)
+(s/def :internal.grid.color/opacity ::us/safe-number)
+
+(s/def :internal.grid/size (s/nilable ::us/safe-integer))
+(s/def :internal.grid/item-length (s/nilable ::us/safe-number))
+
+(s/def :internal.grid/color (s/keys :req-un [:internal.grid.color/color
+ :internal.grid.color/opacity]))
+(s/def :internal.grid/type #{:stretch :left :center :right})
+(s/def :internal.grid/gutter (s/nilable ::us/safe-integer))
+(s/def :internal.grid/margin (s/nilable ::us/safe-integer))
+
+(s/def :internal.grid/square
+ (s/keys :req-un [:internal.grid/size
+ :internal.grid/color]))
+
+(s/def :internal.grid/column
+ (s/keys :req-un [:internal.grid/color]
+ :opt-un [:internal.grid/size
+ :internal.grid/type
+ :internal.grid/item-length
+ :internal.grid/margin
+ :internal.grid/gutter]))
+
+(s/def :internal.grid/row :internal.grid/column)
+
+(s/def ::saved-grids
+ (s/keys :opt-un [:internal.grid/square
+ :internal.grid/row
+ :internal.grid/column]))
+
+;; --- Background options
+
+(s/def ::background string?)
+
+;; --- Flow options
+
+(s/def :internal.flow/id uuid?)
+(s/def :internal.flow/name string?)
+(s/def :internal.flow/starting-frame uuid?)
+
+(s/def ::flow
+ (s/keys :req-un [:internal.flow/id
+ :internal.flow/name
+ :internal.flow/starting-frame]))
+
+(s/def ::flows
+ (s/coll-of ::flow :kind vector?))
+
+;; --- Guides
+
+(s/def :internal.guides/id uuid?)
+(s/def :internal.guides/axis #{:x :y})
+(s/def :internal.guides/position ::us/safe-number)
+(s/def :internal.guides/frame-id (s/nilable uuid?))
+
+(s/def ::guide
+ (s/keys :req-un [:internal.guides/id
+ :internal.guides/axis
+ :internal.guides/position]
+ :opt-un [:internal.guides/frame-id]))
+
+(s/def ::guides
+ (s/map-of uuid? ::guide))
+
+;; --- Page Options
+
+(s/def ::options
+ (s/keys :opt-un [::background
+ ::saved-grids
+ ::flows
+ ::guides]))
+
+;; --- Page
+
+(s/def ::id uuid?)
+(s/def ::name string?)
+(s/def ::objects (s/map-of uuid? ::shape/shape))
+
+(s/def ::page
+ (s/keys :req-un [::id ::name ::objects ::options]))
+
+(s/def ::type #{:page :component})
+(s/def ::path (s/nilable string?))
+(s/def ::container
+ (s/keys :req-un [::id ::name ::objects]
+ :opt-un [::type ::path]))
+
+;; --- Helpers for flow
+
+(defn rename-flow
+ [flow name]
+ (assoc flow :name name))
+
+(defn add-flow
+ [flows flow]
+ (conj (or flows []) flow))
+
+(defn remove-flow
+ [flows flow-id]
+ (d/removev #(= (:id %) flow-id) flows))
+
+(defn update-flow
+ [flows flow-id update-fn]
+ (let [index (d/index-of-pred flows #(= (:id %) flow-id))]
+ (update flows index update-fn)))
+
+(defn get-frame-flow
+ [flows frame-id]
+ (d/seek #(= (:starting-frame %) frame-id) flows))
+
+
+
diff --git a/common/src/app/common/types/radius.cljc b/common/src/app/common/spec/radius.cljc
similarity index 98%
rename from common/src/app/common/types/radius.cljc
rename to common/src/app/common/spec/radius.cljc
index 308de6c2e3..91f0eb78b6 100644
--- a/common/src/app/common/types/radius.cljc
+++ b/common/src/app/common/spec/radius.cljc
@@ -4,7 +4,7 @@
;;
;; Copyright (c) UXBOX Labs SL
-(ns app.common.types.radius
+(ns app.common.spec.radius
(:require
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
diff --git a/common/src/app/common/spec/shadow.cljc b/common/src/app/common/spec/shadow.cljc
new file mode 100644
index 0000000000..b7c61a7ee9
--- /dev/null
+++ b/common/src/app/common/spec/shadow.cljc
@@ -0,0 +1,37 @@
+;; 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.spec.shadow
+ (:require
+ [app.common.spec :as us]
+ [app.common.spec.color :as color]
+ [clojure.spec.alpha :as s]))
+
+
+;;; SHADOW EFFECT
+
+(s/def ::id uuid?)
+(s/def ::style #{:drop-shadow :inner-shadow})
+(s/def ::color ::color/color)
+(s/def ::offset-x ::us/safe-number)
+(s/def ::offset-y ::us/safe-number)
+(s/def ::blur ::us/safe-number)
+(s/def ::spread ::us/safe-number)
+(s/def ::hidden boolean?)
+
+(s/def ::shadow-props
+ (s/keys :req-un [:internal.shadow/id
+ :internal.shadow/style
+ :internal.shadow/color
+ :internal.shadow/offset-x
+ :internal.shadow/offset-y
+ :internal.shadow/blur
+ :internal.shadow/spread
+ :internal.shadow/hidden]))
+
+(s/def ::shadow
+ (s/coll-of ::shadow-props :kind vector?))
+
diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc
new file mode 100644
index 0000000000..08ab1b023f
--- /dev/null
+++ b/common/src/app/common/spec/shape.cljc
@@ -0,0 +1,242 @@
+;; 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.spec.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]
+ [clojure.set :as set]
+ [clojure.spec.alpha :as s]))
+
+;; --- Specs
+
+(s/def ::frame-id uuid?)
+(s/def ::id uuid?)
+(s/def ::name string?)
+(s/def ::path (s/nilable string?))
+(s/def ::page-id uuid?)
+(s/def ::parent-id uuid?)
+(s/def ::string string?)
+(s/def ::type keyword?)
+(s/def ::uuid uuid?)
+
+(s/def ::component-id uuid?)
+(s/def ::component-file uuid?)
+(s/def ::component-root? boolean?)
+(s/def ::shape-ref uuid?)
+
+;; Size constraints
+
+(s/def ::constraints-h #{:left :right :leftright :center :scale})
+(s/def ::constraints-v #{:top :bottom :topbottom :center :scale})
+(s/def ::fixed-scroll boolean?)
+
+;; Page Data related
+(s/def ::blocked boolean?)
+(s/def ::collapsed boolean?)
+
+(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-ref-file (s/nilable uuid?))
+(s/def ::fill-color-ref-id (s/nilable uuid?))
+
+(s/def ::hide-fill-on-export boolean?)
+
+(s/def ::masked-group? boolean?)
+(s/def ::font-family string?)
+(s/def ::font-size ::us/safe-integer)
+(s/def ::font-style string?)
+(s/def ::font-weight string?)
+(s/def ::hidden boolean?)
+(s/def ::letter-spacing ::us/safe-number)
+(s/def ::line-height ::us/safe-number)
+(s/def ::locked boolean?)
+(s/def ::page-id uuid?)
+(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-ref-file (s/nilable uuid?))
+(s/def ::stroke-color-ref-id (s/nilable uuid?))
+(s/def ::stroke-opacity ::us/safe-number)
+(s/def ::stroke-style #{:solid :dotted :dashed :mixed :none :svg})
+
+(def stroke-caps-line #{:round :square})
+(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker})
+(def stroke-caps (set/union stroke-caps-line stroke-caps-marker))
+
+(s/def ::stroke-cap-start stroke-caps)
+(s/def ::stroke-cap-end stroke-caps)
+
+(s/def ::stroke-width ::us/safe-number)
+(s/def ::stroke-alignment #{:center :inner :outer})
+(s/def ::text-align #{"left" "right" "center" "justify"})
+(s/def ::x ::us/safe-number)
+(s/def ::y ::us/safe-number)
+(s/def ::cx ::us/safe-number)
+(s/def ::cy ::us/safe-number)
+(s/def ::width ::us/safe-number)
+(s/def ::height ::us/safe-number)
+(s/def ::index integer?)
+
+(s/def ::x1 ::us/safe-number)
+(s/def ::y1 ::us/safe-number)
+(s/def ::x2 ::us/safe-number)
+(s/def ::y2 ::us/safe-number)
+
+(s/def ::selrect
+ (s/keys :req-un [::x ::y ::x1 ::y1 ::x2 ::y2 ::width ::height]))
+
+(s/def ::exports
+ (s/coll-of ::export/export :kind vector?))
+
+(s/def ::points
+ (s/every ::gpt/point :kind vector?))
+
+(s/def ::shapes
+ (s/every uuid? :kind vector?))
+
+(s/def ::transform ::gmt/matrix)
+(s/def ::transform-inverse ::gmt/matrix)
+(s/def ::opacity ::us/safe-number)
+(s/def ::blend-mode
+ #{:normal
+ :darken
+ :multiply
+ :color-burn
+ :lighten
+ :screen
+ :color-dodge
+ :overlay
+ :soft-light
+ :hard-light
+ :difference
+ :exclusion
+ :hue
+ :saturation
+ :color
+ :luminosity})
+
+(s/def ::shape-attrs
+ (s/keys :opt-un [::id
+ ::type
+ ::name
+ ::component-id
+ ::component-file
+ ::component-root?
+ ::shape-ref
+ ::selrect
+ ::points
+ ::blocked
+ ::collapsed
+ ::fill-color
+ ::fill-opacity
+ ::fill-color-gradient
+ ::fill-color-ref-file
+ ::fill-color-ref-id
+ ::hide-fill-on-export
+ ::font-family
+ ::font-size
+ ::font-style
+ ::font-weight
+ ::hidden
+ ::letter-spacing
+ ::line-height
+ ::locked
+ ::proportion
+ ::proportion-lock
+ ::constraints-h
+ ::constraints-v
+ ::fixed-scroll
+ ::radius/rx
+ ::radius/ry
+ ::radius/r1
+ ::radius/r2
+ ::radius/r3
+ ::radius/r4
+ ::x
+ ::y
+ ::exports
+ ::shapes
+ ::stroke-color
+ ::stroke-color-ref-file
+ ::stroke-color-ref-id
+ ::stroke-opacity
+ ::stroke-style
+ ::stroke-width
+ ::stroke-alignment
+ ::stroke-cap-start
+ ::stroke-cap-end
+ ::text-align
+ ::transform
+ ::transform-inverse
+ ::width
+ ::height
+ ::masked-group?
+ ::cti/interactions
+ ::shadow/shadow
+ ::blur/blur
+ ::opacity
+ ::blend-mode]))
+
+(s/def :internal.shape.text/type #{"root" "paragraph-set" "paragraph"})
+(s/def :internal.shape.text/children
+ (s/coll-of :internal.shape.text/content
+ :kind vector?
+ :min-count 1))
+
+(s/def :internal.shape.text/text string?)
+(s/def :internal.shape.text/key string?)
+
+(s/def :internal.shape.text/content
+ (s/nilable
+ (s/or :text-container
+ (s/keys :req-un [:internal.shape.text/type
+ :internal.shape.text/children]
+ :opt-un [:internal.shape.text/key])
+ :text-content
+ (s/keys :req-un [:internal.shape.text/text]))))
+
+(s/def :internal.shape.path/command keyword?)
+(s/def :internal.shape.path/params
+ (s/nilable (s/map-of keyword? any?)))
+
+(s/def :internal.shape.path/command-item
+ (s/keys :req-un [:internal.shape.path/command]
+ :opt-un [:internal.shape.path/params]))
+
+(s/def :internal.shape.path/content
+ (s/coll-of :internal.shape.path/command-item :kind vector?))
+
+(defmulti shape-spec :type)
+
+(defmethod shape-spec :default [_]
+ (s/spec ::shape-attrs))
+
+(defmethod shape-spec :text [_]
+ (s/and ::shape-attrs
+ (s/keys :opt-un [:internal.shape.text/content])))
+
+(defmethod shape-spec :path [_]
+ (s/and ::shape-attrs
+ (s/keys :opt-un [:internal.shape.path/content])))
+
+(defmethod shape-spec :frame [_]
+ (s/and ::shape-attrs
+ (s/keys :opt-un [::hide-fill-on-export])))
+
+(s/def ::shape
+ (s/and (s/multi-spec shape-spec :type)
+ #(contains? % :type)
+ #(contains? % :name)))
diff --git a/common/src/app/common/spec/typography.cljc b/common/src/app/common/spec/typography.cljc
new file mode 100644
index 0000000000..51c54a5171
--- /dev/null
+++ b/common/src/app/common/spec/typography.cljc
@@ -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.common.spec.typography
+ (:require
+ [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
+(s/def ::name string?)
+(s/def ::path (s/nilable string?))
+(s/def ::font-id string?)
+(s/def ::font-family string?)
+(s/def ::font-variant-id string?)
+(s/def ::font-size string?)
+(s/def ::font-weight string?)
+(s/def ::font-style string?)
+(s/def ::line-height string?)
+(s/def ::letter-spacing string?)
+(s/def ::text-transform string?)
+
+(s/def ::typography
+ (s/keys :req-un [::id
+ ::name
+ ::font-id
+ ::font-family
+ ::font-variant-id
+ ::font-size
+ ::font-weight
+ ::font-style
+ ::line-height
+ ::letter-spacing
+ ::text-transform]
+ :opt-un [::path]))
+
+
diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc
deleted file mode 100644
index 4901b87222..0000000000
--- a/common/src/app/common/types/page_options.cljc
+++ /dev/null
@@ -1,95 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.common.types.page-options
- (:require
- [app.common.data :as d]
- [app.common.spec :as us]
- [clojure.spec.alpha :as s]))
-
-;; --- Grid options
-
-(s/def :artboard-grid.color/color ::us/string)
-(s/def :artboard-grid.color/opacity ::us/safe-number)
-
-(s/def :artboard-grid/size (s/nilable ::us/safe-integer))
-(s/def :artboard-grid/item-length (s/nilable ::us/safe-number))
-
-(s/def :artboard-grid/color (s/keys :req-un [:artboard-grid.color/color
- :artboard-grid.color/opacity]))
-(s/def :artboard-grid/type #{:stretch :left :center :right})
-(s/def :artboard-grid/gutter (s/nilable ::us/safe-integer))
-(s/def :artboard-grid/margin (s/nilable ::us/safe-integer))
-
-(s/def :artboard-grid/square
- (s/keys :req-un [:artboard-grid/size
- :artboard-grid/color]))
-
-(s/def :artboard-grid/column
- (s/keys :req-un [:artboard-grid/color]
- :opt-un [:artboard-grid/size
- :artboard-grid/type
- :artboard-grid/item-length
- :artboard-grid/margin
- :artboard-grid/gutter]))
-
-(s/def :artboard-grid/row :artboard-grid/column)
-
-(s/def ::saved-grids
- (s/keys :opt-un [:artboard-grid/square
- :artboard-grid/row
- :artboard-grid/column]))
-
-;; --- Background options
-
-(s/def ::background string?)
-
-;; --- Flow options
-
-(s/def :interactions-flow/id ::us/uuid)
-(s/def :interactions-flow/name ::us/string)
-(s/def :interactions-flow/starting-frame ::us/uuid)
-
-(s/def ::flow
- (s/keys :req-un [:interactions-flow/id
- :interactions-flow/name
- :interactions-flow/starting-frame]))
-
-(s/def ::flows
- (s/coll-of ::flow :kind vector?))
-
-;; --- Options
-
-(s/def ::options
- (s/keys :opt-un [::background
- ::saved-grids
- ::flows]))
-
-;; --- Helpers for flow
-
-(defn rename-flow
- [flow name]
- (assoc flow :name name))
-
-;; --- Helpers for flows
-
-(defn add-flow
- [flows flow]
- (conj (or flows []) flow))
-
-(defn remove-flow
- [flows flow-id]
- (d/removev #(= (:id %) flow-id) flows))
-
-(defn update-flow
- [flows flow-id update-fn]
- (let [index (d/index-of-pred flows #(= (:id %) flow-id))]
- (update flows index update-fn)))
-
-(defn get-frame-flow
- [flows frame-id]
- (d/seek #(= (:starting-frame %) frame-id) flows))
-
diff --git a/common/test/app/common/types_interactions_test.cljc b/common/test/app/common/spec_interactions_test.cljc
similarity index 69%
rename from common/test/app/common/types_interactions_test.cljc
rename to common/test/app/common/spec_interactions_test.cljc
index f5df8c448d..d874268955 100644
--- a/common/test/app/common/types_interactions_test.cljc
+++ b/common/test/app/common/spec_interactions_test.cljc
@@ -4,68 +4,68 @@
;;
;; Copyright (c) UXBOX Labs SL
-(ns app.common.types-interactions-test
+(ns app.common.spec-interactions-test
(:require
[clojure.test :as t]
[clojure.pprint :refer [pprint]]
[app.common.exceptions :as ex]
[app.common.pages.init :as cpi]
- [app.common.types.interactions :as cti]
+ [app.common.spec.interactions :as csi]
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]))
(t/deftest set-event-type
- (let [interaction cti/default-interaction
+ (let [interaction csi/default-interaction
shape (cpi/make-minimal-shape :rect)
frame (cpi/make-minimal-shape :frame)]
(t/testing "Set event type unchanged"
(let [new-interaction
- (cti/set-event-type interaction :click shape)]
+ (csi/set-event-type interaction :click shape)]
(t/is (= :click (:event-type new-interaction)))))
(t/testing "Set event type changed"
(let [new-interaction
- (cti/set-event-type interaction :mouse-press shape)]
+ (csi/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
- (cti/set-event-type interaction :after-delay shape))]
+ (csi/set-event-type interaction :after-delay shape))]
(t/is (ex/exception? result))))
(t/testing "Set after delay on frame"
(let [new-interaction
- (cti/set-event-type interaction :after-delay frame)]
+ (csi/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
- (cti/set-event-type interaction :after-delay frame)]
+ (csi/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 cti/default-interaction]
+ (let [interaction csi/default-interaction]
(t/testing "Set action type unchanged"
(let [new-interaction
- (cti/set-action-type interaction :navigate)]
+ (csi/set-action-type interaction :navigate)]
(t/is (= :navigate (:action-type new-interaction)))))
(t/testing "Set action type changed"
(let [new-interaction
- (cti/set-action-type interaction :prev-screen)]
+ (csi/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
- (cti/set-action-type interaction :navigate)]
+ (csi/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
- (cti/set-action-type interaction :navigate)]
+ (csi/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
- (cti/set-action-type interaction :open-overlay)]
+ (csi/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
- (cti/set-action-type interaction :open-overlay)]
+ (csi/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
- (cti/set-action-type interaction :toggle-overlay)]
+ (csi/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
- (cti/set-action-type interaction :toggle-overlay)]
+ (csi/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
- (cti/set-action-type interaction :close-overlay)]
+ (csi/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
- (cti/set-action-type interaction :close-overlay)]
+ (csi/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
- (cti/set-action-type interaction :prev-screen)]
+ (csi/set-action-type interaction :prev-screen)]
(t/is (= :prev-screen (:action-type new-interaction)))))
(t/testing "Set action type open-url"
(let [new-interaction
- (cti/set-action-type interaction :open-url)]
+ (csi/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
- (cti/set-action-type interaction :open-url)]
+ (csi/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 cti/default-interaction
- i2 (cti/set-event-type i1 :after-delay frame)]
+ i1 csi/default-interaction
+ i2 (csi/set-event-type i1 :after-delay frame)]
(t/testing "Has delay"
- (t/is (not (cti/has-delay i1)))
- (t/is (cti/has-delay i2)))
+ (t/is (not (csi/has-delay i1)))
+ (t/is (csi/has-delay i2)))
(t/testing "Set delay"
- (let [new-interaction (cti/set-delay i2 1000)]
+ (let [new-interaction (csi/set-delay i2 1000)]
(t/is (= 1000 (:delay new-interaction)))))))
(t/deftest option-destination
(let [destination (uuid/next)
- i1 cti/default-interaction
- i2 (cti/set-action-type i1 :prev-screen)
- i3 (cti/set-action-type i1 :open-overlay)]
+ i1 csi/default-interaction
+ i2 (csi/set-action-type i1 :prev-screen)
+ i3 (csi/set-action-type i1 :open-overlay)]
(t/testing "Has destination"
- (t/is (cti/has-destination i1))
- (t/is (not (cti/has-destination i2))))
+ (t/is (csi/has-destination i1))
+ (t/is (not (csi/has-destination i2))))
(t/testing "Set destination"
- (let [new-interaction (cti/set-destination i1 destination)]
+ (let [new-interaction (csi/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 (cti/set-destination i3 destination)]
+ (let [new-interaction (csi/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 cti/default-interaction
- i2 (cti/set-action-type i1 :prev-screen)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-action-type i1 :prev-screen)]
(t/testing "Has preserve-scroll"
- (t/is (cti/has-preserve-scroll i1))
- (t/is (not (cti/has-preserve-scroll i2))))
+ (t/is (csi/has-preserve-scroll i1))
+ (t/is (not (csi/has-preserve-scroll i2))))
(t/testing "Set preserve-scroll"
- (let [new-interaction (cti/set-preserve-scroll i1 true)]
+ (let [new-interaction (csi/set-preserve-scroll i1 true)]
(t/is (= true (:preserve-scroll new-interaction)))))))
(t/deftest option-url
- (let [i1 cti/default-interaction
- i2 (cti/set-action-type i1 :open-url)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-action-type i1 :open-url)]
(t/testing "Has url"
- (t/is (not (cti/has-url i1)))
- (t/is (cti/has-url i2)))
+ (t/is (not (csi/has-url i1)))
+ (t/is (csi/has-url i2)))
(t/testing "Set url"
- (let [new-interaction (cti/set-url i2 "https://example.com")]
+ (let [new-interaction (csi/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 cti/default-interaction
- i2 (cti/set-action-type i1 :open-overlay)
+ i1 csi/default-interaction
+ i2 (csi/set-action-type i1 :open-overlay)
i3 (-> i1
- (cti/set-action-type :open-overlay)
- (cti/set-destination (:id overlay-frame)))]
+ (csi/set-action-type :open-overlay)
+ (csi/set-destination (:id overlay-frame)))]
(t/testing "Has overlay options"
- (t/is (not (cti/has-overlay-opts i1)))
- (t/is (cti/has-overlay-opts i2)))
+ (t/is (not (csi/has-overlay-opts i1)))
+ (t/is (csi/has-overlay-opts i2)))
(t/testing "Set overlay-pos-type without destination"
- (let [new-interaction (cti/set-overlay-pos-type i2 :top-right base-frame objects)]
+ (let [new-interaction (csi/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 (cti/set-overlay-pos-type i3 :bottom-right base-frame objects)]
+ (let [new-interaction (csi/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 (cti/set-overlay-pos-type i3 :manual base-frame objects)]
+ (let [new-interaction (csi/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 (cti/toggle-overlay-pos-type i3 :center base-frame objects)
- new-interaction-2 (cti/toggle-overlay-pos-type new-interaction :center base-frame objects)
- new-interaction-3 (cti/toggle-overlay-pos-type new-interaction-2 :top-right base-frame objects)]
+ (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)]
(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 (cti/set-overlay-position i3 (gpt/point 50 60))]
+ (let [new-interaction (csi/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 (cti/set-close-click-outside i3 true)]
+ (let [new-interaction (csi/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 (cti/set-background-overlay i3 true)]
+ (let [new-interaction (csi/set-background-overlay i3 true)]
(t/is (not (:background-overlay i3)))
(t/is (:background-overlay new-interaction))))))
(t/deftest animation-checks
- (let [i1 cti/default-interaction
- i2 (cti/set-action-type i1 :open-overlay)
- i3 (cti/set-action-type i1 :toggle-overlay)
- i4 (cti/set-action-type i1 :close-overlay)
- i5 (cti/set-action-type i1 :prev-screen)
- i6 (cti/set-action-type i1 :open-url)]
+ (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)]
(t/testing "Has animation?"
- (t/is (cti/has-animation? i1))
- (t/is (cti/has-animation? i2))
- (t/is (cti/has-animation? i3))
- (t/is (cti/has-animation? i4))
- (t/is (not (cti/has-animation? i5)))
- (t/is (not (cti/has-animation? i6))))
+ (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/testing "Valid push?"
- (t/is (cti/allow-push? (:action-type i1)))
- (t/is (not (cti/allow-push? (:action-type i2))))
- (t/is (not (cti/allow-push? (:action-type i3))))
- (t/is (not (cti/allow-push? (:action-type i4))))
- (t/is (not (cti/allow-push? (:action-type i5))))
- (t/is (not (cti/allow-push? (:action-type i6)))))))
+ (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/deftest set-animation-type
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type i1 :dissolve)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type i1 :dissolve)]
(t/testing "Set animation type nil"
(let [new-interaction
- (cti/set-animation-type i1 nil)]
+ (csi/set-animation-type i1 nil)]
(t/is (nil? (-> new-interaction :animation :animation-type)))))
(t/testing "Set animation type unchanged"
(let [new-interaction
- (cti/set-animation-type i2 :dissolve)]
+ (csi/set-animation-type i2 :dissolve)]
(t/is (= :dissolve (-> new-interaction :animation :animation-type)))))
(t/testing "Set animation type changed"
(let [new-interaction
- (cti/set-animation-type i2 :slide)]
+ (csi/set-animation-type i2 :slide)]
(t/is (= :slide (-> new-interaction :animation :animation-type)))))
(t/testing "Set animation type reset"
(let [new-interaction
- (cti/set-animation-type i2 nil)]
+ (csi/set-animation-type i2 nil)]
(t/is (nil? (-> new-interaction :animation)))))
(t/testing "Set animation type dissolve"
(let [new-interaction
- (cti/set-animation-type i1 :dissolve)]
+ (csi/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
- (cti/set-animation-type interaction :dissolve)]
+ (csi/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
- (cti/set-animation-type i1 :slide)]
+ (csi/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
- (cti/set-animation-type interaction :slide)]
+ (csi/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
- (cti/set-animation-type i1 :push)]
+ (csi/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
- (cti/set-animation-type interaction :push)]
+ (csi/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 (cti/set-action-type cti/default-interaction :open-overlay)
- i2 (cti/set-action-type cti/default-interaction :close-overlay)
- i3 (cti/set-action-type cti/default-interaction :toggle-overlay)]
+ (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)]
(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 (cti/allowed-animation? (:action-type bad-interaction-1)
+ (t/is (not (csi/allowed-animation? (:action-type bad-interaction-1)
(-> bad-interaction-1 :animation :animation-type))))
- (t/is (not (cti/allowed-animation? (:action-type bad-interaction-2)
+ (t/is (not (csi/allowed-animation? (:action-type bad-interaction-2)
(-> bad-interaction-1 :animation :animation-type))))
- (t/is (not (cti/allowed-animation? (:action-type bad-interaction-3)
+ (t/is (not (csi/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 (cti/set-animation-type cti/default-interaction :push)
- new-interaction (cti/set-action-type interaction :open-overlay)]
+ (let [interaction (csi/set-animation-type csi/default-interaction :push)
+ new-interaction (csi/set-action-type interaction :open-overlay)]
(t/is (nil? (:animation new-interaction)))))))
(t/deftest option-duration
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type cti/default-interaction :dissolve)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type csi/default-interaction :dissolve)]
(t/testing "Has duration?"
- (t/is (not (cti/has-duration? i1)))
- (t/is (cti/has-duration? i2)))
+ (t/is (not (csi/has-duration? i1)))
+ (t/is (csi/has-duration? i2)))
(t/testing "Set duration"
- (let [new-interaction (cti/set-duration i2 1000)]
+ (let [new-interaction (csi/set-duration i2 1000)]
(t/is (= 1000 (-> new-interaction :animation :duration)))))))
(t/deftest option-easing
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type cti/default-interaction :dissolve)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type csi/default-interaction :dissolve)]
(t/testing "Has easing?"
- (t/is (not (cti/has-easing? i1)))
- (t/is (cti/has-easing? i2)))
+ (t/is (not (csi/has-easing? i1)))
+ (t/is (csi/has-easing? i2)))
(t/testing "Set easing"
- (let [new-interaction (cti/set-easing i2 :ease-in)]
+ (let [new-interaction (csi/set-easing i2 :ease-in)]
(t/is (= :ease-in (-> new-interaction :animation :easing)))))))
(t/deftest option-way
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type cti/default-interaction :slide)
- i3 (cti/set-action-type i2 :open-overlay)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type csi/default-interaction :slide)
+ i3 (csi/set-action-type i2 :open-overlay)]
(t/testing "Has way?"
- (t/is (not (cti/has-way? i1)))
- (t/is (cti/has-way? i2))
- (t/is (not (cti/has-way? i3)))
+ (t/is (not (csi/has-way? i1)))
+ (t/is (csi/has-way? i2))
+ (t/is (not (csi/has-way? i3)))
(t/is (some? (-> i3 :animation :way)))) ; <- it exists but is ignored
(t/testing "Set way"
- (let [new-interaction (cti/set-way i2 :out)]
+ (let [new-interaction (csi/set-way i2 :out)]
(t/is (= :out (-> new-interaction :animation :way)))))))
(t/deftest option-direction
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type cti/default-interaction :push)
- i3 (cti/set-animation-type cti/default-interaction :dissolve)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type csi/default-interaction :push)
+ i3 (csi/set-animation-type csi/default-interaction :dissolve)]
(t/testing "Has direction?"
- (t/is (not (cti/has-direction? i1)))
- (t/is (cti/has-direction? i2)))
+ (t/is (not (csi/has-direction? i1)))
+ (t/is (csi/has-direction? i2)))
(t/testing "Set direction"
- (let [new-interaction (cti/set-direction i2 :left)]
+ (let [new-interaction (csi/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' (cti/invert-direction nil)
- a-none' (cti/invert-direction a-none)
- a-right' (cti/invert-direction a-right)
- a-left' (cti/invert-direction a-left)
- a-up' (cti/invert-direction a-up)
- a-down' (cti/invert-direction a-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)]
(t/is (nil? a-nil'))
(t/is (nil? (:direction a-none')))
@@ -499,44 +499,44 @@
(t/deftest option-offset-effect
- (let [i1 cti/default-interaction
- i2 (cti/set-animation-type cti/default-interaction :slide)
- i3 (cti/set-action-type i2 :open-overlay)]
+ (let [i1 csi/default-interaction
+ i2 (csi/set-animation-type csi/default-interaction :slide)
+ i3 (csi/set-action-type i2 :open-overlay)]
(t/testing "Has offset-effect"
- (t/is (not (cti/has-offset-effect? i1)))
- (t/is (cti/has-offset-effect? i2))
- (t/is (not (cti/has-offset-effect? i3)))
+ (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 (some? (-> i3 :animation :offset-effect)))) ; <- it exists but is ignored
(t/testing "Set offset-effect"
- (let [new-interaction (cti/set-offset-effect i2 true)]
+ (let [new-interaction (csi/set-offset-effect i2 true)]
(t/is (= true (-> new-interaction :animation :offset-effect)))))))
(t/deftest modify-interactions
- (let [i1 (cti/set-action-type cti/default-interaction :open-overlay)
- i2 (cti/set-action-type cti/default-interaction :close-overlay)
- i3 (cti/set-action-type cti/default-interaction :prev-screen)
+ (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)
interactions [i1 i2]]
(t/testing "Add interaction to nil"
- (let [new-interactions (cti/add-interaction nil i3)]
+ (let [new-interactions (csi/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 (cti/add-interaction interactions i3)]
+ (let [new-interactions (csi/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 (cti/remove-interaction interactions 0)]
+ (let [new-interactions (csi/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 (cti/update-interaction interactions 1 #(cti/set-action-type % :open-url))]
+ (let [new-interactions (csi/update-interaction interactions 1 #(csi/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 (cti/set-destination cti/default-interaction (:id frame1))
- i2 (cti/set-destination cti/default-interaction (:id frame2))
- i3 (cti/set-destination cti/default-interaction (:id frame3))
- i4 (cti/set-destination cti/default-interaction nil)
- i5 (cti/set-destination cti/default-interaction (:id frame6))
+ 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))
interactions [i1 i2 i3 i4 i5]]
(t/testing "Remap interactions"
- (let [new-interactions (cti/remap-interactions interactions ids-map objects)]
+ (let [new-interactions (csi/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/common/yarn.lock b/common/yarn.lock
index f59d710bf4..22f689b46e 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.16.12:
- version "2.16.12"
- resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.16.12.tgz#8757b3079dadfff15ca09192f81eb69b5d25266d"
- integrity sha512-6JqOhN5X3n0IkxA/gSUcZ1lImwcW1LmpgzlaBDOC/u/pIysdNm0tiOxpOTEnExl9nKZBS/EYS7bXIIInywPJUA==
+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==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"
diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml
index ee6b1d7df8..5b92d0ed4c 100644
--- a/docker/devenv/docker-compose.yaml
+++ b/docker/devenv/docker-compose.yaml
@@ -10,6 +10,7 @@ networks:
volumes:
postgres_data:
user_data:
+ minio_data:
services:
main:
@@ -66,6 +67,22 @@ services:
- PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
+ minio:
+ profiles: ["full"]
+ image: "minio/minio:latest"
+ command: minio server /mnt/data --console-address ":9001"
+
+ volumes:
+ - "minio_data:/mnt/data"
+
+ environment:
+ - MINIO_ROOT_USER=minioadmin
+ - MINIO_ROOT_PASSWORD=minioadmin
+
+ ports:
+ - 9000:9000
+ - 9001:9001
+
backend:
profiles: ["backend"]
privileged: true
@@ -91,6 +108,7 @@ services:
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}
- PENPOT_SECRET_KEY=super-secret-devenv-key
+
# SMTP setup
- PENPOT_SMTP_ENABLED=true
- PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com
diff --git a/exporter/deps.edn b/exporter/deps.edn
index 64cd9f7b33..6a113eebaf 100644
--- a/exporter/deps.edn
+++ b/exporter/deps.edn
@@ -14,7 +14,7 @@
:dev
{:extra-deps
- {thheller/shadow-cljs {:mvn/version "2.16.12"}}}
+ {thheller/shadow-cljs {:mvn/version "2.17.3"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
diff --git a/exporter/package.json b/exporter/package.json
index ce3a25a147..81ef6612a1 100644
--- a/exporter/package.json
+++ b/exporter/package.json
@@ -22,7 +22,7 @@
"xregexp": "^5.0.2"
},
"devDependencies": {
- "shadow-cljs": "^2.16.12",
+ "shadow-cljs": "^2.17.3",
"source-map-support": "^0.5.21"
}
}
diff --git a/exporter/yarn.lock b/exporter/yarn.lock
index 1774a5a148..3a0453162a 100644
--- a/exporter/yarn.lock
+++ b/exporter/yarn.lock
@@ -1010,10 +1010,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.16.12:
- version "2.16.12"
- resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.16.12.tgz#8757b3079dadfff15ca09192f81eb69b5d25266d"
- integrity sha512-6JqOhN5X3n0IkxA/gSUcZ1lImwcW1LmpgzlaBDOC/u/pIysdNm0tiOxpOTEnExl9nKZBS/EYS7bXIIInywPJUA==
+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==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"
diff --git a/frontend/cypress.json b/frontend/cypress.json
index 0967ef424b..53555d954b 100644
--- a/frontend/cypress.json
+++ b/frontend/cypress.json
@@ -1 +1,4 @@
-{}
+{
+ "watchForFileChanges": false,
+ "video": false
+}
diff --git a/frontend/cypress/fixtures/fonts/Viafont.otf b/frontend/cypress/fixtures/fonts/Viafont.otf
new file mode 100644
index 0000000000..40a8470b48
Binary files /dev/null and b/frontend/cypress/fixtures/fonts/Viafont.otf differ
diff --git a/frontend/cypress/fixtures/fonts/blkchcry.ttf b/frontend/cypress/fixtures/fonts/blkchcry.ttf
new file mode 100644
index 0000000000..cca50917c0
Binary files /dev/null and b/frontend/cypress/fixtures/fonts/blkchcry.ttf differ
diff --git a/frontend/cypress/fixtures/test-image-jpg.jpg b/frontend/cypress/fixtures/test-image-jpg.jpg
new file mode 100644
index 0000000000..868bdfdf98
Binary files /dev/null and b/frontend/cypress/fixtures/test-image-jpg.jpg differ
diff --git a/frontend/cypress/fixtures/test-image-png.png b/frontend/cypress/fixtures/test-image-png.png
new file mode 100644
index 0000000000..86c42a5606
Binary files /dev/null and b/frontend/cypress/fixtures/test-image-png.png differ
diff --git a/frontend/cypress/fixtures/validuser-sample.json b/frontend/cypress/fixtures/validuser-sample.json
new file mode 100644
index 0000000000..7301b2b7db
--- /dev/null
+++ b/frontend/cypress/fixtures/validuser-sample.json
@@ -0,0 +1,5 @@
+{
+ "email": "validuser@penpot.app",
+ "password": "password",
+ "team": "test team"
+}
\ No newline at end of file
diff --git a/frontend/cypress/integration/01-auth/create-account.spec.js b/frontend/cypress/integration/01-auth/create-account.spec.js
new file mode 100644
index 0000000000..3cc2b5269c
--- /dev/null
+++ b/frontend/cypress/integration/01-auth/create-account.spec.js
@@ -0,0 +1,50 @@
+/**
+ * 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
+ */
+
+"use strict";
+
+describe("account creation", () => {
+ let validUser;
+
+ beforeEach(() => {
+ cy.fixture("validuser.json").then((user) => {
+ validUser = user;
+ });
+ cy.visit("http://localhost:3449/#/auth/login");
+ cy.getBySel("register-submit").click();
+ });
+
+ it("displays the account creation form", () => {
+ cy.getBySel("register-form-submit").should("exist");
+ });
+
+ it("create an account", () => {
+ let email = "mail" + Date.now() +"@mail.com";
+ cy.get("#email").type(email);
+ cy.get("#password").type("anewpassword");
+ cy.get("input[type=submit]").click();
+ cy.getBySel("register-title").should("exist");
+ cy.get("#fullname").type("Test user")
+ cy.get("input[type=submit]").click();
+ cy.get(".dashboard-layout").should("exist");
+ });
+
+ it("create an account of an existent email fails", () => {
+ cy.get("#email").type(validUser.email);
+ cy.get("#password").type("anewpassword");
+ cy.getBySel("register-form-submit").click();
+ cy.getBySel("email-input-error").should("exist");
+ });
+
+ it("can go back", () => {
+ cy.getBySel("login-here-link").click();
+ cy.getBySel("login-title").should("exist");
+ cy.get("#email").should("exist");
+ cy.get("#password").should("exist");
+ });
+});
diff --git a/frontend/cypress/integration/01-auth/demo-account.spec.js b/frontend/cypress/integration/01-auth/demo-account.spec.js
new file mode 100644
index 0000000000..075ce0d221
--- /dev/null
+++ b/frontend/cypress/integration/01-auth/demo-account.spec.js
@@ -0,0 +1,21 @@
+/**
+ * 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
+ */
+
+"use strict";
+
+describe("demo account", () => {
+ beforeEach(() => {
+ cy.visit("http://localhost:3449/#/auth/login");
+ });
+
+ it("create demo account", () => {
+ cy.getBySel("demo-account-link").should("exist");
+ cy.getBySel("demo-account-link").click();
+ cy.get(".profile").contains("Demo User");
+ });
+});
diff --git a/frontend/cypress/integration/01-auth/login.spec.js b/frontend/cypress/integration/01-auth/login.spec.js
index f3b84e2aff..34decb0e78 100644
--- a/frontend/cypress/integration/01-auth/login.spec.js
+++ b/frontend/cypress/integration/01-auth/login.spec.js
@@ -14,8 +14,25 @@ describe("login", () => {
});
it("displays the login form", () => {
+ cy.getBySel("login-title").should("exist");
cy.get("#email").should("exist");
cy.get("#password").should("exist");
});
-});
+ it("can't login with an invalid user", () => {
+ cy.get("#email").type("bad@mail.com");
+ cy.get("#password").type("badpassword");
+ cy.getBySel("login-submit").click();
+ cy.getBySel("login-banner").should("exist");
+ });
+
+ it("can login with a valid user", () => {
+ cy.fixture("validuser.json").then((user) => {
+ cy.get("#email").type(user.email);
+ cy.get("#password").type(user.password);
+ });
+
+ cy.getBySel("login-submit").click();
+ cy.get(".dashboard-layout").should("exist");
+ });
+});
diff --git a/frontend/cypress/integration/01-auth/recover.spec.js b/frontend/cypress/integration/01-auth/recover.spec.js
new file mode 100644
index 0000000000..91488250f0
--- /dev/null
+++ b/frontend/cypress/integration/01-auth/recover.spec.js
@@ -0,0 +1,41 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Copyright (c) UXBOX Labs SL
+ */
+
+"use strict";
+
+describe("recover password", () => {
+ beforeEach(() => {
+ cy.visit("http://localhost:3449/#/auth/login");
+ cy.getBySel("forgot-password").click();
+ });
+
+ it("displays the recover form", () => {
+ cy.getBySel("recovery-resquest-submit").should("exist");
+ });
+
+ it("recover password with wrong mail works", () => {
+ cy.get("#email").type("bad@mail.com");
+ cy.getBySel("recovery-resquest-submit").click();
+ cy.get(".info").should("exist");
+ });
+
+ it("recover password with good mail works", () => {
+ cy.fixture("validuser.json").then((user) => {
+ cy.get("#email").type(user.email);
+ });
+ cy.getBySel("recovery-resquest-submit").click();
+ cy.get(".info").should("exist");
+ });
+
+ it("can go back", () => {
+ cy.getBySel("go-back-link").click();
+ cy.getBySel("login-title").should("exist");
+ cy.get("#email").should("exist");
+ cy.get("#password").should("exist");
+ });
+});
diff --git a/frontend/cypress/integration/02-onboarding/onboarding-options.spec.js b/frontend/cypress/integration/02-onboarding/onboarding-options.spec.js
new file mode 100644
index 0000000000..69e7b3d55e
--- /dev/null
+++ b/frontend/cypress/integration/02-onboarding/onboarding-options.spec.js
@@ -0,0 +1,71 @@
+/**
+ * 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
+ */
+
+ "use strict";
+
+ describe("onboarding options solo or team", () => {
+ beforeEach(() => {
+ cy.demoLogin();
+ cy.get(".modal-right button").click();
+ cy.get(".onboarding button").click();
+ cy.get(".onboarding .skip").click();
+ });
+
+ it("choose solo option", () => {
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ cy.getBySel("fly-solo-op").click();
+ cy.getBySel("empty-placeholder").should("exist");
+ });
+
+ it("choose team option and cancel", () => {
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ cy.getBySel("team-up-button").click();
+ cy.getBySel("onboarding-choice-team-up").should("exist");
+ cy.get("button").click();
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ });
+
+ it("choose team option, set team name and cancel", () => {
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ cy.getBySel("team-up-button").click();
+ cy.getBySel("onboarding-choice-team-up").should("exist");
+ cy.get("#name").type("test team");
+ cy.get("input[type=submit]").first().click();
+ cy.get("#email").should("exist");
+ cy.get("button").click();
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ });
+
+ it("choose team option, set team name and skip", () => {
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ cy.getBySel("team-up-button").click();
+ cy.getBySel("onboarding-choice-team-up").should("exist");
+ cy.get("#name").type("test team");
+ cy.get("input[type=submit]").first().click();
+ cy.get("#email").should("exist");
+ cy.get(".skip-action").click();
+ cy.getBySel("empty-placeholder").should("exist");
+ });
+
+ it("choose team option, set team name and invite", () => {
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ cy.getBySel("team-up-button").click();
+ cy.getBySel("onboarding-choice-team-up").should("exist");
+ cy.get("#name").type("test team");
+ cy.get("input[type=submit]").first().click();
+ cy.get("#email").should("exist");
+ cy.get("#email").type("test@test.com");
+ cy.get("input[type=submit]").first().click();
+ cy.getBySel("empty-placeholder").should("exist");
+ });
+
+
+
+ });
+
+
\ No newline at end of file
diff --git a/frontend/cypress/integration/02-onboarding/slides.spec.js b/frontend/cypress/integration/02-onboarding/slides.spec.js
new file mode 100644
index 0000000000..79dd6d275b
--- /dev/null
+++ b/frontend/cypress/integration/02-onboarding/slides.spec.js
@@ -0,0 +1,55 @@
+/**
+ * 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
+ */
+
+"use strict";
+import {
+ checkOnboardingSlide,
+ goToSlideByNumber,
+} from "../../support/utils.js";
+
+describe("onboarding slides", () => {
+ beforeEach(() => {
+ cy.demoLogin();
+ });
+
+ it("go trough all the onboarding slides", () => {
+ cy.getBySel("onboarding-welcome").should("exist");
+ cy.getBySel("onboarding-next-btn").should("exist");
+ cy.getBySel("onboarding-next-btn").click();
+
+ cy.getBySel("opsource-next-btn").should("exist");
+ cy.getBySel("skip-btn").should("not.exist");
+ cy.getBySel("opsource-next-btn").click();
+
+ var genArr = Array.from(Array(3).keys());
+ cy.wrap(genArr).each((index) => {
+ checkOnboardingSlide(index, true);
+ });
+ checkOnboardingSlide("3", false);
+
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ });
+
+ it("go to specific onboarding slides", () => {
+ cy.getBySel("onboarding-next-btn").click();
+ cy.getBySel(`opsource-next-btn`).click();
+
+ var genArr = Array.from(Array(4).keys());
+ cy.wrap(genArr).each((index) => {
+ goToSlideByNumber(4 - index);
+ });
+ });
+
+ it("skip onboarding slides", () => {
+ cy.getBySel("onboarding-next-btn").click();
+ cy.getBySel("opsource-next-btn").click();
+ cy.getBySel("skip-btn").click();
+ cy.getBySel("fly-solo-op").click();
+ cy.getBySel("onboarding-welcome-title").should("exist");
+ });
+});
diff --git a/frontend/cypress/integration/03-dashboard/files.spec.js b/frontend/cypress/integration/03-dashboard/files.spec.js
new file mode 100644
index 0000000000..ff385c7a03
--- /dev/null
+++ b/frontend/cypress/integration/03-dashboard/files.spec.js
@@ -0,0 +1,489 @@
+/**
+ * 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
+ */
+
+ "use strict";
+
+ import {
+
+ createProject,
+ deleteFirstProject,
+ deleteFirstFile,
+ createFile
+
+} from '../../support/utils.js';
+
+
+
+ describe("files", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ createProject("test project" + Date.now());
+ });
+
+ });
+
+ afterEach(() => {
+ //cleanup
+ deleteFirstProject();
+ });
+
+
+ it("can create a new file", () => {
+ cy.get(".grid-item").then((files) => {
+ cy.get('.project').first().find("[data-test=project-new-file]").click();
+ cy.get("#workspace").should("exist");
+ cy.get(".project-tree").should("contain", "New File");
+
+ //Go back
+ cy.get(".main-icon a").click();
+ cy.get(".grid-item").should('have.length', files.length + 1);
+ })
+
+ })
+
+ it("can create a new file inside a project", () => {
+ cy.get(".project").first().find("h2").click();
+ cy.get(".grid-item").should('have.length', 0);
+ createFile();
+ cy.get(".grid-item").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can create a new file inside a project with shortcut", () => {
+ cy.get(".project").first().find("h2").click();
+ cy.get(".grid-item").should('have.length', 0);
+ cy.get("body").type("+");
+ cy.get(".grid-item").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can delete a file inside a project", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ cy.get(".grid-item").should('have.length', 1);
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-delete").click();
+ cy.get('.accept-button').click();
+ cy.get(".grid-item").should('have.length', 0);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can cancel a file deletion inside a project", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ cy.get(".grid-item").should('have.length', 1);
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-delete").click();
+ cy.get('.cancel-button').click();
+ cy.get(".grid-item").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can delete a file outside a project", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ //Go back
+ cy.get(".recent-projects").click();
+
+ cy.get(".grid-item").then((files) => {
+ cy.get('.menu')
+ .first()
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-delete").click();
+ cy.get('.accept-button').click();
+ cy.get(".grid-item").should('have.length', files.length-1);
+ });
+ })
+
+ it("can cancel a file deletion outside a project", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ //Go back
+ cy.get(".recent-projects").click();
+
+ cy.get(".grid-item").then((files) => {
+ cy.get('.menu')
+ .first()
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-delete").click();
+ cy.get('.cancel-button').click();
+ cy.get(".grid-item").should('have.length', files.length);
+ });
+ })
+
+ it("can rename a file", () => {
+ const fileName = "test file " + Date.now();
+
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-rename").click();
+ cy.get(".edit-wrapper").should("exist");
+ cy.get(".edit-wrapper").type(fileName + "{enter}");
+
+ cy.get(".grid-item").first().should("contain", fileName);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can duplicate a file", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get(".grid-item").should('have.length', 1);
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-duplicate").click();
+ cy.get(".grid-item").should('have.length', 2);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can move a file to another project", () => {
+ const projectToMoveName = "test project to move " + Date.now();
+ const fileName = "test file " + Date.now();
+
+ createProject(projectToMoveName);
+ cy.get(".project").eq(1).find("h2").click();
+ createFile(fileName);
+
+ //TODO: Bug workaround. When a file is selected, it doesn't open context menu
+ cy.get(".dashboard-grid").click();
+
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.wait(500);
+ cy.getBySel("file-move-to").click();
+
+ cy.get('a').contains(projectToMoveName).click();
+
+ cy.getBySel("project-title").should("contain", projectToMoveName);
+ cy.get(".grid-item").should('have.length', 1);
+ cy.get(".grid-item").first().should("contain", fileName);
+
+
+ //Go back and cleanup: delete project
+ cy.get(".recent-projects").click();
+ deleteFirstProject();
+ });
+
+
+ it("can move a file to another team", () => {
+ const fileName = "test file " + Date.now();
+ cy.get(".project").first().find("h2").click();
+ createFile(fileName);
+
+ //TODO: Bug workaround. When a file is selected, it doesn't open context menu
+ cy.get(".dashboard-grid").click();
+
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.wait(500);
+ cy.getBySel("file-move-to").click();
+
+ cy.getBySel("move-to-other-team").click();
+ cy.fixture('validuser.json').then((user) => {
+ cy.get('a').contains(user.team).click();
+ cy.get('a').contains("Drafts").click();
+ cy.get(".current-team").should("contain", user.team);
+ cy.get(".dashboard-title").should("contain", "Drafts");
+ cy.get(".grid-item").first().should("contain", fileName);
+ });
+
+
+ //cleanup
+ deleteFirstFile();
+ cy.get(".current-team").click();
+ cy.get(".team-name").contains("Your Penpot").click();
+ });
+
+
+ it("can make a file a shared library", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get(".icon-library").should('have.length', 0);
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-add-shared").click();
+ cy.get(".accept-button").click();
+ cy.get(".icon-library").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can cancel make a file a shared library", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get(".icon-library").should('have.length', 0);
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-add-shared").click();
+ cy.get(".modal-close-button").click();
+ cy.get(".icon-library").should('have.length', 0);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can remove a file as shared library", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-add-shared").click();
+ cy.get(".accept-button").click();
+ cy.get(".icon-library").should('have.length', 1);
+
+ //TODO: Bug workaround. When a file is selected, it doesn't open context menu
+ cy.get(".dashboard-grid").click();
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-del-shared").click();
+ cy.get(".accept-button").click();
+ cy.get(".icon-library").should('have.length', 0);
+
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can cancel remove a file as shared library", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-add-shared").click();
+ cy.get(".accept-button").click();
+ cy.get(".icon-library").should('have.length', 1);
+
+ //TODO: Bug workaround. When a file is selected, it doesn't open context menu
+ cy.get(".dashboard-grid").click();
+
+ cy.get('.menu')
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-del-shared").click();
+ cy.get(".modal-close-button").click();
+ cy.get(".icon-library").should('have.length', 1);
+
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can search for a file", () => {
+ const fileName = "test file " + Date.now();
+
+ cy.get(".project").first().find("h2").click();
+ createFile(fileName);
+
+ cy.get("#search-input").type("bad name");
+ cy.get(".grid-item").should('have.length', 0);
+
+ cy.get("#search-input").clear().type(fileName);
+ cy.get(".grid-item").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can multiselect files", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ createFile();
+ createFile();
+
+ cy.get(".selected").should('have.length', 0);
+
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".selected").should('have.length', 1);
+
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+ cy.get(".selected").should('have.length', 2);
+
+ cy.get(".grid-item").eq(1).click({shiftKey: true});
+ cy.get(".selected").should('have.length', 3);
+
+ cy.get(".grid-item").eq(1).click({shiftKey: true});
+ cy.get(".selected").should('have.length', 2);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can delete multiselected files", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ createFile();
+ createFile();
+
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+
+ cy.get(".grid-item").should('have.length', 3);
+ cy.get(".grid-item").eq(0).rightclick();
+ cy.getBySel("delete-multi-files").should("contain", "Delete 2 files");
+ cy.getBySel("delete-multi-files").click();
+ cy.get('.accept-button').click();
+ cy.get(".grid-item").should('have.length', 1);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can cancel delete multiselected files", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ createFile();
+ createFile();
+
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+
+ cy.get(".grid-item").should('have.length', 3);
+ cy.get(".grid-item").eq(0).rightclick();
+ cy.getBySel("delete-multi-files").should("contain", "Delete 2 files");
+ cy.getBySel("delete-multi-files").click();
+ cy.get('.cancel-button').click();
+ cy.get(".grid-item").should('have.length', 3);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+
+ it("can duplicate multiselected files", () => {
+ cy.get(".project").first().find("h2").click();
+ createFile();
+ createFile();
+ createFile();
+
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+
+ cy.get(".grid-item").should('have.length', 3);
+ cy.get(".grid-item").eq(0).rightclick();
+ cy.getBySel("duplicate-multi").should("contain", "Duplicate 2 files");
+ cy.getBySel("duplicate-multi").click();
+ cy.get(".grid-item").should('have.length', 5);
+
+ //Go back
+ cy.get(".recent-projects").click();
+ })
+
+ it("can move multiselected files to another project", () => {
+ const projectToMoveName = "test project to move " + Date.now();
+ createProject(projectToMoveName);
+
+ cy.get(".project").eq(1).find("h2").click();
+ createFile();
+ createFile();
+ createFile();
+
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+
+
+ cy.get(".grid-item").eq(0).rightclick();
+ cy.getBySel("move-to-multi").should("contain", "Move 2 files to");
+ cy.getBySel("move-to-multi").click();
+ cy.get('a').contains(projectToMoveName).click();
+
+ cy.getBySel("project-title").should("contain", projectToMoveName);
+ cy.get(".grid-item").should('have.length', 2);
+
+
+ //Go back
+ cy.get(".recent-projects").click();
+ deleteFirstProject();
+ })
+
+
+ it("can move multiselected files to another team", () => {
+ const fileName1 = "test file " + Date.now();
+ const fileName2 = "test file " + Date.now();
+ const fileName3 = "test file " + Date.now();
+
+ cy.get(".project").first().find("h2").click();
+ createFile(fileName1)
+ createFile(fileName2)
+ createFile(fileName3)
+
+
+ //multiselect first and third file
+ cy.get(".grid-item").eq(0).click({shiftKey: true});
+ cy.get(".grid-item").eq(2).click({shiftKey: true});
+
+
+ cy.get(".grid-item").eq(0).rightclick();
+ cy.getBySel("move-to-multi").should("contain", "Move 2 files to");
+ cy.getBySel("move-to-multi").click();
+ cy.getBySel("move-to-other-team").click();
+ cy.fixture('validuser.json').then((user) => {
+ cy.get('a').contains(user.team).click();
+ cy.get('a').contains("Drafts").click();
+ cy.get(".current-team").should("contain", user.team);
+ cy.get(".dashboard-title").should("contain", "Drafts");
+ cy.get(".grid-item").eq(0).should("contain", fileName1);
+ cy.get(".grid-item").eq(1).should("contain", fileName2);
+ });
+
+
+ //cleanup
+ deleteFirstFile()
+ deleteFirstFile()
+ cy.get(".current-team").click();
+ cy.get(".team-name").contains("Your Penpot").click();
+ })
+
+ });
+
+
diff --git a/frontend/cypress/integration/03-dashboard/misc.spec.js b/frontend/cypress/integration/03-dashboard/misc.spec.js
new file mode 100644
index 0000000000..840d5b2961
--- /dev/null
+++ b/frontend/cypress/integration/03-dashboard/misc.spec.js
@@ -0,0 +1,150 @@
+/**
+ * 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
+ */
+
+ "use strict";
+
+ import {
+
+ deleteFirstFile,
+ deleteFirstFont
+
+} from '../../support/utils.js';
+
+
+
+describe("comments", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ });
+
+ });
+
+ it("can open and close comments popup", () => {
+ cy.get(".comments-section").should("not.exist");
+ cy.getBySel("open-comments").click();
+ cy.get(".comments-section").should("exist");
+ cy.getBySel("open-comments").click();
+ cy.get(".comments-section").should("not.exist");
+ });
+
+});
+
+describe("import and export", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ });
+ });
+
+ it("can export a file", () => {
+ cy.get('.menu')
+ .first()
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-export").click();
+ cy.get('.icon-tick').should("exist");
+ });
+
+ it("can import a file", () => {
+ cy.get(".grid-item").then((files) => {
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.getBySel("file-import").click();
+
+ cy.uploadBinaryFile("input[type=file]", "test-file-import.penpot");
+
+ cy.get(".accept-button").should('not.be.disabled');
+ cy.get(".accept-button").click();
+ cy.get(".accept-button").should('not.be.disabled');
+ cy.get(".accept-button").click();
+ cy.get(".grid-item").should('have.length', files.length+1);
+ });
+
+ //cleanup
+ deleteFirstFile() ;
+ })
+
+})
+
+describe("release notes", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ });
+ });
+
+ it("can show release notes", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.get(".onboarding").should("not.exist");
+ cy.getBySel("release-notes").click();
+ cy.get(".onboarding").should("exist");
+ });
+});
+
+describe("fonts", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ });
+ });
+
+ it("can upload a font file", () => {
+ cy.getBySel("fonts").click();
+ cy.get(".font-item").should('have.length', 0);
+ cy.uploadBinaryFile("#font-upload", "fonts/Viafont.otf");
+ cy.get(".upload-button").click();
+ cy.get(".font-item").should('have.length', 1);
+
+ //cleanup
+ deleteFirstFont();
+
+ });
+
+ it("can upload multiple font files", () => {
+ cy.getBySel("fonts").click();
+ cy.get(".font-item").should('have.length', 0);
+ cy.uploadBinaryFile("#font-upload", "fonts/Viafont.otf");
+ cy.uploadBinaryFile("#font-upload", "fonts/blkchcry.ttf");
+ cy.getBySel("upload-all").click();
+ cy.get(".font-item").should('have.length', 2);
+
+ //cleanup
+ deleteFirstFont();
+ deleteFirstFont();
+ });
+
+ it("can dismiss multiple font files", () => {
+ cy.getBySel("fonts").click();
+ cy.get(".font-item").should('have.length', 0);
+ cy.uploadBinaryFile("#font-upload", "fonts/Viafont.otf");
+ cy.uploadBinaryFile("#font-upload", "fonts/blkchcry.ttf");
+ cy.getBySel("dismiss-all").click();
+ cy.get(".font-item").should('have.length', 0);
+ });
+
+ it("can rename a font", () => {
+ const fontName = "test font " + Date.now();
+
+ //Upload a font
+ cy.getBySel("fonts").click();
+ cy.uploadBinaryFile("#font-upload", "fonts/Viafont.otf");
+ cy.get(".upload-button").click();
+ cy.get(".font-item").should('have.length', 1);
+
+ //Rename font
+ cy.get(".font-item .options").first().click();
+ cy.getBySel("font-edit").click();
+ cy.get(".dashboard-installed-fonts input[value=Viafont]").type(fontName+"{enter}");
+ cy.get(".dashboard-installed-fonts").should("contain", fontName);
+
+ //cleanup
+ deleteFirstFont();
+
+ });
+});
\ No newline at end of file
diff --git a/frontend/cypress/integration/03-dashboard/projects.spec.js b/frontend/cypress/integration/03-dashboard/projects.spec.js
new file mode 100644
index 0000000000..d2b94aac56
--- /dev/null
+++ b/frontend/cypress/integration/03-dashboard/projects.spec.js
@@ -0,0 +1,153 @@
+/**
+ * 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
+ */
+
+ "use strict";
+
+ import {
+
+ createProject,
+ deleteFirstProject
+
+} from '../../support/utils.js';
+
+
+
+ describe("projects", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password)
+ });
+
+ });
+
+ it("displays the projects page", () => {
+ cy.get(".dashboard-title").should("contain", "Projects");
+ });
+
+ it("can create a new project", () => {
+ let projectName = "test project " + Date.now();
+ cy.get(".project").then((projects) => {
+ cy.getBySel("new-project-button").click();
+ cy.get('.project').should('have.length', projects.length + 1);
+ cy.get('.project').first().find(".edit-wrapper").type(projectName + "{enter}")
+ cy.get('.project').first().find("h2").should("contain", projectName);
+
+ //cleanup: delete project
+ deleteFirstProject();
+ })
+
+ })
+
+ it("can rename a project", () => {
+ let projectName = "test project " + Date.now();
+ let projectName2 = "renamed project " + Date.now();
+
+ createProject(projectName);
+
+ cy.get('.project').first().find("h2").should("contain", projectName);
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.get('.project').first().find("[data-test=project-rename]").click();
+ cy.get('.project').first().find(".edit-wrapper").type(projectName2 + "{enter}")
+ cy.get('.project').first().find("h2").should("contain", projectName2)
+
+ //cleanup: delete project
+ deleteFirstProject();
+ });
+
+ it("can delete a project", () => {
+ createProject();
+ cy.get(".project").then((projects) => {
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.wait(500);
+ cy.getBySel("project-delete").click();
+ cy.wait(500);
+ cy.get('.accept-button').click();
+ cy.wait(500);
+
+ cy.get('.project').should('have.length', projects.length - 1);
+ })
+ });
+
+ it("can cancel the deletion of a project", () => {
+ createProject();
+ cy.get(".project").then((projects) => {
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.wait(500);
+ cy.getBySel("project-delete").click();
+ cy.wait(500);
+ cy.get('.cancel-button').click();
+ cy.wait(500);
+
+ cy.get('.project').should('have.length', projects.length);
+
+
+ //cleanup: delete project
+ deleteFirstProject();
+ })
+ });
+
+ it("can duplicate a project", () => {
+ let projectName = "test project " + Date.now();
+ createProject(projectName);
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.wait(500);
+ cy.getBySel("project-duplicate").click();
+ cy.getBySel("project-title").should("exist");
+ cy.getBySel("project-title").should("contain", projectName+" (");
+
+
+ //cleanup: delete project
+ cy.get(".recent-projects").click();
+ deleteFirstProject();
+ deleteFirstProject();
+ });
+
+
+ it("can move a project to a team", () => {
+ let projectName = "test project " + Date.now();
+ createProject(projectName);
+
+ cy.fixture('validuser.json').then((user) => {
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.get('.project').first().find("[data-test=project-move-to]").click();
+ cy.get('a').contains(user.team).click();
+
+ cy.get(".current-team").should("contain", user.team);
+ cy.get(".project").first().should("contain", projectName);
+
+
+ //cleanup: delete project
+ deleteFirstProject();
+ });
+ });
+
+
+ it("pin and unpin project to sidebar", () => {
+ let projectName = "test project " + Date.now();
+ createProject(projectName);
+
+ cy.get(".project").first().find(".icon-pin-fill").should("exist");
+ cy.getBySel("pinned-projects").should("contain", projectName);
+
+ //unpin
+ cy.get(".project").first().find(".pin-icon").click();
+ cy.get(".project").first().find(".icon-pin-fill").should("not.exist");
+ cy.getBySel("pinned-projects").should("not.contain", projectName);
+
+ //pin
+ cy.get(".project").first().find(".pin-icon").click();
+ cy.get(".project").first().find(".icon-pin-fill").should("exist");
+ cy.getBySel("pinned-projects").should("contain", projectName);
+
+ //cleanup: delete project
+ deleteFirstProject();
+ });
+
+ });
+
+
diff --git a/frontend/cypress/integration/03-dashboard/teams.spec.js b/frontend/cypress/integration/03-dashboard/teams.spec.js
new file mode 100644
index 0000000000..98071e3d2f
--- /dev/null
+++ b/frontend/cypress/integration/03-dashboard/teams.spec.js
@@ -0,0 +1,155 @@
+/**
+ * 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
+ */
+
+ "use strict";
+
+ import {
+
+ createTeam,
+ deleteCurrentTeam
+
+ } from '../../support/utils.js';
+
+
+ describe("teams", () => {
+ beforeEach(() => {
+ cy.fixture('validuser.json').then((user) => {
+ cy.login(user.email, user.password);
+ });
+
+ });
+
+ it("can create a new team", () => {
+ const teamName = "test team " + Date.now();
+ cy.get(".current-team").click();
+ cy.getBySel("create-new-team").click();
+ cy.get("#name").type(teamName);
+ cy.get("input[type=submit]").click();
+
+ cy.get(".current-team").should("contain", teamName);
+
+ //cleanup
+ deleteCurrentTeam();
+
+ })
+
+ it("can cancel create a new team", () => {
+ cy.get(".current-team").click();
+ cy.getBySel("create-new-team").click();
+ cy.get(".modal-close-button").click();
+
+ cy.get(".current-team").should("contain", "Your Penpot");
+ })
+
+ it("can delete a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("delete-team").click();
+ cy.get(".accept-button").click();
+ cy.get(".current-team").should("contain", "Your Penpot");
+ })
+
+ it("can cancel the deletion of a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("delete-team").click();
+ cy.get(".cancel-button").click();
+ cy.get(".current-team").should("contain", teamName);
+
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+ it("can see the members page of a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("team-members").click();
+
+ cy.get(".dashboard-title").should("contain", "Members");
+ cy.fixture('validuser.json').then((user) => {
+ cy.get(".dashboard-table").should("contain", user.email);
+ });
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+ it("can invite someone to a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("team-members").click();
+
+ cy.getBySel("invite-member").click();
+ cy.get("#email").type("mail@mail.com");
+ cy.get(".custom-select select").select("admin");
+ cy.get("input[type=submit]").click();
+
+ cy.get(".success").should("exist");
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+ it("can see the settings page of a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("team-settings").click();
+
+ cy.get(".dashboard-title").should("contain", "Settings");
+
+ cy.get(".team-settings .name").should("contain", teamName);
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+ it("can rename team", () => {
+ const teamName = "test team " + Date.now();
+ const newTeamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("rename-team").click();
+ cy.get("#name").type(newTeamName);
+ cy.get("input[type=submit]").click();
+
+ cy.get(".current-team").should("contain", newTeamName);
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+ it("can cancel the rename of a team", () => {
+ const teamName = "test team " + Date.now();
+ createTeam(teamName);
+
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("rename-team").click();
+ cy.get(".modal-close-button").click();
+
+ cy.get(".current-team").should("contain", teamName);
+
+ //cleanup
+ deleteCurrentTeam();
+ })
+
+
+
+
+})
\ No newline at end of file
diff --git a/frontend/cypress/integration/04-profile/profile.spec.js b/frontend/cypress/integration/04-profile/profile.spec.js
new file mode 100644
index 0000000000..5079f6031d
--- /dev/null
+++ b/frontend/cypress/integration/04-profile/profile.spec.js
@@ -0,0 +1,155 @@
+/**
+ * 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
+ */
+"use strict";
+
+describe("profile", () => {
+ beforeEach(() => {
+ cy.fixture("validuser.json").then((user) => {
+ cy.login(user.email, user.password);
+ });
+ });
+
+ it("open profile section", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").should("exist");
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("account-title").should("exist");
+ });
+
+ it("change profile name", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.get("#fullname").should("exist");
+ cy.get("#fullname").clear().type("New name").type("{enter}");
+ cy.get(".banner.success").should("exist");
+ });
+
+ it("change profile image with png", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("profile-image-input").should("exist");
+
+ cy.get(".profile img").then((oldImg) => {
+ cy.getBySel("profile-image-input").attachFile("test-image-png.png");
+ cy.get(".profile img")
+ .invoke("attr", "src")
+ .should("not.eq", oldImg[0].src);
+ });
+ });
+
+ it("change profile image with jpg", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("profile-image-input").should("exist");
+
+ cy.get(".profile img").then((oldImg) => {
+ cy.getBySel("profile-image-input").attachFile("test-image-jpg.jpg");
+ cy.get(".profile img")
+ .invoke("attr", "src")
+ .should("not.eq", oldImg[0].src);
+ });
+ });
+
+ it("change profile email", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.get(".change-email").should("exist");
+ cy.get(".change-email").click();
+ cy.getBySel("change-email-title").should("exist");
+ cy.fixture("validuser.json").then((user) => {
+ cy.get("#email-1").type(user.email);
+ cy.get("#email-2").type(user.email);
+ });
+ cy.getBySel("change-email-submit").click();
+ cy.get(".banner.info").should("exist");
+ });
+
+ it("type wrong email while trying to update should throw an error", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.get(".change-email").click();
+ cy.fixture("validuser.json").then((user) => {
+ cy.get("#email-1").type(user.email);
+ });
+ cy.get("#email-2").type("bad@email.com");
+ cy.getBySel("change-email-submit").click();
+ cy.get(".error").should("exist");
+ });
+
+ it("open password section", () => {
+ cy.get(".profile").click();
+ cy.getBySel("password-profile-opt").click();
+ cy.get(".password-form").should("exist");
+ });
+
+ it("type old password wrong should throw an error", () => {
+ cy.get(".profile").click();
+ cy.getBySel("password-profile-opt").click();
+ cy.get("#password-old").type("badpassword");
+ cy.get("#password-1").type("pretty-new-password");
+ cy.get("#password-2").type("pretty-new-password");
+ cy.getBySel("submit-password").click();
+ cy.get(".error").should("exist");
+ });
+
+ it("type same old password should work", () => {
+ cy.get(".profile").click();
+ cy.getBySel("password-profile-opt").click();
+ cy.fixture("validuser.json").then((user) => {
+ cy.get("#password-old").type(user.password);
+ cy.get("#password-1").type(user.password);
+ cy.get("#password-2").type(user.password);
+ });
+ cy.getBySel("submit-password").click();
+ cy.get(".banner.success").should("exist");
+ });
+
+ it("open settings section", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("settings-profile").should("exist");
+ });
+
+ it("set lang to Spanish and back to english", () => {
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("settings-profile").click();
+ cy.getBySel("setting-lang").should("exist");
+ cy.getBySel("setting-lang").select("es");
+ cy.getBySel("submit-lang-change").should("exist");
+ cy.getBySel("submit-lang-change").click();
+ cy.contains("Tu cuenta").should("exist");
+ cy.getBySel("setting-lang").select("en");
+ cy.getBySel("submit-lang-change").click();
+ cy.contains("Your account").should("exist");
+ });
+
+ it("log out from app", () => {
+ cy.get(".profile").click();
+ cy.getBySel("logout-profile-opt").should("exist");
+ cy.getBySel("logout-profile-opt").click();
+ cy.getBySel("login-title").should("exist");
+ });
+});
+
+describe("remove account", () => {
+ it("create demo account and delete it", () => {
+ cy.visit("http://localhost:3449/#/auth/login");
+ cy.getBySel("demo-account-link").click();
+ cy.getBySel("onboarding-next-btn").click();
+ cy.getBySel("opsource-next-btn").click();
+ cy.getBySel("skip-btn").click();
+ cy.getBySel("fly-solo-op").click();
+ cy.getBySel("close-templates-btn").click();
+ cy.get(".profile").click();
+ cy.getBySel("profile-profile-opt").click();
+ cy.getBySel("remove-acount-btn").click();
+ cy.getBySel("delete-account-btn").click();
+ cy.getBySel("login-title").should("exist");
+ });
+});
diff --git a/frontend/cypress/integration/09-draw/draw-shapes.spec.js b/frontend/cypress/integration/09-draw/draw-shapes.spec.js
new file mode 100644
index 0000000000..46e7970f79
--- /dev/null
+++ b/frontend/cypress/integration/09-draw/draw-shapes.spec.js
@@ -0,0 +1,93 @@
+/**
+ * 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
+ */
+
+"use strict";
+
+describe("draw shapes", () => {
+ beforeEach(() => {
+ cy.fixture("validuser.json").then((user) => {
+ cy.login(user.email, user.password);
+ cy.get(".project-th").first().dblclick();
+ cy.clearViewport();
+ });
+ });
+
+ it("draw an artboard", () => {
+ cy.get(".render-shapes rect").should("not.exist");
+ cy.getBySel("artboard-btn").click();
+ cy.drawInViewport(300, 300, 400, 450);
+ cy.get(".render-shapes rect").first().as("artboard");
+ cy.get("@artboard").should("exist");
+ cy.get("@artboard").invoke("attr", "width").should("eq", "100");
+ cy.get("@artboard").invoke("attr", "height").should("eq", "150");
+ });
+
+ it("draw a square", () => {
+ cy.get(".render-shapes rect").should("not.exist");
+ cy.getBySel("rect-btn").click();
+ cy.drawInViewport(300, 300, 400, 450);
+ cy.get(".render-shapes rect").should("exist");
+ cy.get(".render-shapes rect")
+ .invoke("attr", "width")
+ .should("eq", "100");
+ cy.get(".render-shapes rect")
+ .invoke("attr", "height")
+ .should("eq", "150");
+ });
+
+ it("draw an ellipse", () => {
+ cy.get(".render-shapes ellipse").should("not.exist");
+ cy.getBySel("ellipse-btn").click();
+ cy.drawInViewport(300, 300, 400, 450);
+ cy.get(".render-shapes ellipse").as("ellipse");
+ cy.get("@ellipse").should("exist");
+ cy.get("@ellipse").invoke("attr", "rx").should("eq", "50");
+ cy.get("@ellipse").invoke("attr", "ry").should("eq", "75");
+ });
+
+ it("draw a curve", () => {
+ cy.get(".render-shapes path").should("not.exist");
+ cy.getBySel("curve-btn").click();
+ cy.drawMultiInViewport([
+ { x: 300, y: 300 },
+ { x: 350, y: 300 },
+ { x: 300, y: 350 },
+ { x: 400, y: 450 },
+ ]);
+ cy.get(".render-shapes path").as("curve");
+ cy.get("@curve").should("exist");
+ cy.get("@curve")
+ .invoke("attr", "d")
+ .should("eq", "M300,300L350,300L300,350L400,450");
+ });
+
+ it("draw a path", () => {
+ cy.get(".render-shapes path").should("not.exist");
+ cy.getBySel("path-btn").click();
+ cy.clickMultiInViewport([
+ { x: 300, y: 300 },
+ { x: 350, y: 300 },
+ ]);
+ cy.drawMultiInViewport(
+ [
+ { x: 400, y: 450 },
+ { x: 450, y: 450 },
+ ],
+ true
+ );
+ cy.clickMultiInViewport([{ x: 300, y: 300 }]);
+ cy.get(".render-shapes path").as("curve");
+ cy.get("@curve").should("exist");
+ cy.get("@curve")
+ .invoke("attr", "d")
+ .should(
+ "eq",
+ "M300,300L350,300C350,300,350,450,400,450C450,450,300,300,300,300Z"
+ );
+ });
+});
diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js
index 119ab03f7c..fa5cee7418 100644
--- a/frontend/cypress/support/commands.js
+++ b/frontend/cypress/support/commands.js
@@ -23,3 +23,92 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+import 'cypress-file-upload';
+
+Cypress.Commands.add('login', (email, password) => {
+ cy.visit("http://localhost:3449/#/auth/login");
+ cy.get("#email").type(email);
+ cy.get("#password").type(password);
+ cy.getBySel("login-submit").click();
+ })
+
+ Cypress.Commands.add('demoLogin', () => {
+ cy.visit("http://localhost:3449/#/auth/login");
+ cy.getBySel("demo-account-link").click()
+ })
+
+ Cypress.Commands.add('drawInViewport', (x1, y1, x2, y2) => {
+ cy.get(".viewport-controls")
+ .trigger('mousemove', { x: x1, y: y1 })
+ .trigger('mousedown', {
+ x: x1,
+ y: y1,
+ which: 1
+ })
+ .trigger('mousemove', { x: x2, y: y2 })
+ .trigger('mouseup', { x: x2, y: y2, which: 1 });
+})
+
+Cypress.Commands.add('drawMultiInViewport', (coords, force=false) => {
+ cy.get(".viewport-controls")
+ .trigger('mousemove', { x: coords[0].x, y: coords[0].y, force: force})
+ .trigger('mousedown', {
+ x: coords[0].x,
+ y: coords[0].y,
+ which: 1,
+ force: force
+ });
+
+ for (var i=1; i {
+ for (var i=0; i {
+ cy.get(".viewport-controls").type('{ctrl}a');
+ cy.get(".viewport-controls").type('{del}');
+ cy.window().its("debug").invoke('reset_viewport');
+})
+
+Cypress.Commands.add('getBySel', (selector, ...args) => {
+ return cy.get(`[data-test=${selector}]`, ...args)
+})
+
+Cypress.Commands.add('getBySelLike', (selector, ...args) => {
+ return cy.get(`[data-test*=${selector}]`, ...args)
+})
+
+Cypress.Commands.add('uploadBinaryFile', (fileInputSelector, fileName) => {
+ cy.fixture(fileName, "binary")
+ .then(Cypress.Blob.binaryStringToBlob)
+ .then(fileContent => {
+ cy.get(fileInputSelector).attachFile({
+ fileContent,
+ filePath: fileName,
+ encoding: 'utf-8',
+ lastModified: new Date().getTime()
+ });
+ });
+})
+
diff --git a/frontend/cypress/support/utils.js b/frontend/cypress/support/utils.js
new file mode 100644
index 0000000000..595f71ced8
--- /dev/null
+++ b/frontend/cypress/support/utils.js
@@ -0,0 +1,75 @@
+export const checkOnboardingSlide = (number, checkSkip) => {
+ cy.getBySel(`slide-${number}-title`).should("exist");
+ if(checkSkip){cy.getBySel("skip-btn").should("exist");}
+ cy.get(".onboarding .step-dots").should("exist");
+ cy.getBySel(`slide-${number}-btn`).should("exist");
+ cy.getBySel(`slide-${number}-btn`).click();
+};
+export const deleteFirstProject = () => {
+ cy.get('.project').first().find("[data-test=project-options]").click();
+ cy.wait(500);
+ cy.get('.project').first().find("[data-test=project-delete]").click();
+ cy.wait(500);
+ cy.get('.accept-button').click();
+ }
+
+ export const createProject = (projectName="") => {
+ cy.getBySel("new-project-button").click();
+ cy.wait(500);
+ cy.get('.project').first().find(".edit-wrapper").type(projectName + "{enter}");
+ cy.wait(500);
+ }
+
+
+ export const deleteFirstFile = () => {
+ cy.get('.menu')
+ .first()
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-delete").click();
+ cy.get('.accept-button').click();
+ }
+
+
+ export const createFile = (fileName="", projectNum=0) => {
+ cy.getBySel("new-file").click();
+ cy.wait(500);
+ if (fileName !=""){
+ cy.get('.menu')
+ .first()
+ .trigger('mouseover')
+ .click();
+ cy.getBySel("file-rename").click();
+ cy.get(".edit-wrapper").type(fileName + "{enter}");
+ //TODO: Bug workaround. When a file is selected, it doesn't open context menu
+ cy.get(".dashboard-grid").click();
+ }
+ }
+
+ export const createTeam = (teamName) => {
+ cy.get(".current-team").click();
+ cy.getBySel("create-new-team").click();
+ cy.get("#name").type(teamName);
+ cy.get("input[type=submit]").click();
+ cy.wait(500);
+ }
+
+ export const deleteCurrentTeam = () => {
+ cy.get(".icon-actions").first().click();
+ cy.getBySel("delete-team").click();
+ cy.get(".accept-button").click();
+ }
+
+
+
+
+export const goToSlideByNumber = (number) => {
+ cy.get(`.step-dots li:nth-child(${number})`).click();
+ cy.getBySel(`slide-${number -1}-btn`).should("exist");
+};
+
+export const deleteFirstFont = () => {
+ cy.get(".font-item .options").first().click();
+ cy.getBySel("font-delete").click();
+ cy.get(".accept-button").click();
+};
\ No newline at end of file
diff --git a/frontend/deps.edn b/frontend/deps.edn
index 56225cef59..fefc860715 100644
--- a/frontend/deps.edn
+++ b/frontend/deps.edn
@@ -29,9 +29,9 @@
:dev
{:extra-paths ["dev"]
:extra-deps
- {thheller/shadow-cljs {:mvn/version "2.16.12"}
+ {thheller/shadow-cljs {:mvn/version "2.17.3"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
- cider/cider-nrepl {:mvn/version "0.28.0"}}}
+ cider/cider-nrepl {:mvn/version "0.28.2"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
diff --git a/frontend/package.json b/frontend/package.json
index 5c484d65e1..2f42679ee2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,8 +24,9 @@
"test-e2e-gui": "cypress open"
},
"devDependencies": {
- "autoprefixer": "^10.4.1",
- "cypress": "^9.2.0",
+ "autoprefixer": "^10.4.2",
+ "cypress": "^9.5.0",
+ "cypress-file-upload": "^5.0.8",
"gettext-parser": "^4.2.0",
"gulp": "4.0.2",
"gulp-concat": "^2.6.1",
@@ -33,37 +34,37 @@
"gulp-mustache": "^5.0.0",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
- "gulp-sass": "^5.0.0",
+ "gulp-sass": "^5.1.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^1.5.0",
"map-stream": "0.0.7",
- "marked": "^4.0.8",
+ "marked": "^4.0.12",
"mkdirp": "^1.0.4",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
- "postcss": "^8.4.5",
+ "postcss": "^8.4.6",
"postcss-clean": "^1.2.2",
"prettier": "^2.5.1",
"rimraf": "^3.0.0",
- "sass": "^1.45.1",
- "shadow-cljs": "2.16.12"
+ "sass": "^1.49.7",
+ "shadow-cljs": "2.17.3"
},
"dependencies": {
- "@sentry/browser": "^6.16.1",
- "@sentry/tracing": "^6.16.1",
+ "@sentry/browser": "^6.17.4",
+ "@sentry/tracing": "^6.17.4",
"date-fns": "^2.28.0",
"draft-js": "^0.11.7",
- "highlight.js": "^11.3.1",
+ "highlight.js": "^11.4.0",
"js-beautify": "^1.14.0",
"jszip": "^3.6.0",
- "luxon": "^2.2.0",
+ "luxon": "^2.3.0",
"mousetrap": "^1.6.5",
"opentype.js": "^1.3.4",
"randomcolor": "^0.6.2",
"react": "~17.0.2",
"react-dom": "~17.0.2",
"react-virtualized": "^9.22.3",
- "rxjs": "~7.4.0",
+ "rxjs": "~7.5.2",
"sax": "^1.2.4",
"source-map-support": "^0.5.21",
"tdigest": "^0.1.1",
diff --git a/frontend/resources/images/color-bar-library.png b/frontend/resources/images/color-bar-library.png
deleted file mode 100644
index 70dad5429f..0000000000
Binary files a/frontend/resources/images/color-bar-library.png and /dev/null differ
diff --git a/frontend/resources/images/color-bar-options.png b/frontend/resources/images/color-bar-options.png
deleted file mode 100644
index c2be2595f1..0000000000
Binary files a/frontend/resources/images/color-bar-options.png and /dev/null differ
diff --git a/frontend/resources/images/color-gamma.png b/frontend/resources/images/color-gamma.png
deleted file mode 100644
index f0132e1fe8..0000000000
Binary files a/frontend/resources/images/color-gamma.png and /dev/null differ
diff --git a/frontend/resources/images/colorspecrum-400x300.png b/frontend/resources/images/colorspecrum-400x300.png
deleted file mode 100644
index bbc5b793dc..0000000000
Binary files a/frontend/resources/images/colorspecrum-400x300.png and /dev/null differ
diff --git a/frontend/resources/images/cursors/resize-h-2.svg b/frontend/resources/images/cursors/resize-h-2.svg
new file mode 100644
index 0000000000..5522037b14
--- /dev/null
+++ b/frontend/resources/images/cursors/resize-h-2.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/cursors/zoom-in.svg b/frontend/resources/images/cursors/zoom-in.svg
new file mode 100644
index 0000000000..ecd153448a
--- /dev/null
+++ b/frontend/resources/images/cursors/zoom-in.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/cursors/zoom-out.svg b/frontend/resources/images/cursors/zoom-out.svg
new file mode 100644
index 0000000000..65a5a300f2
--- /dev/null
+++ b/frontend/resources/images/cursors/zoom-out.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/cursors/zoom.svg b/frontend/resources/images/cursors/zoom.svg
new file mode 100644
index 0000000000..9650153fef
--- /dev/null
+++ b/frontend/resources/images/cursors/zoom.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/resources/images/deco-left.png b/frontend/resources/images/deco-left.png
index bd14661c7a..5ccb9aa7a6 100644
Binary files a/frontend/resources/images/deco-left.png and b/frontend/resources/images/deco-left.png differ
diff --git a/frontend/resources/images/deco-right.png b/frontend/resources/images/deco-right.png
index cc108924e5..f89dc14907 100644
Binary files a/frontend/resources/images/deco-right.png and b/frontend/resources/images/deco-right.png differ
diff --git a/frontend/resources/images/email/logo-github.png b/frontend/resources/images/email/logo-github.png
index 9c65f71a4a..e3d3215af3 100644
Binary files a/frontend/resources/images/email/logo-github.png and b/frontend/resources/images/email/logo-github.png differ
diff --git a/frontend/resources/images/email/logo-instagram.png b/frontend/resources/images/email/logo-instagram.png
index c0ba9bb7d8..3b94e53a29 100644
Binary files a/frontend/resources/images/email/logo-instagram.png and b/frontend/resources/images/email/logo-instagram.png differ
diff --git a/frontend/resources/images/email/logo-taiga.png b/frontend/resources/images/email/logo-taiga.png
index 6e29f6bcea..e604e8b15a 100644
Binary files a/frontend/resources/images/email/logo-taiga.png and b/frontend/resources/images/email/logo-taiga.png differ
diff --git a/frontend/resources/images/email/logo-twitter.png b/frontend/resources/images/email/logo-twitter.png
index dd73069bfc..4860be5d82 100644
Binary files a/frontend/resources/images/email/logo-twitter.png and b/frontend/resources/images/email/logo-twitter.png differ
diff --git a/frontend/resources/images/email/logo-uxbox.png b/frontend/resources/images/email/logo-uxbox.png
index 0bde659689..8b3a078296 100644
Binary files a/frontend/resources/images/email/logo-uxbox.png and b/frontend/resources/images/email/logo-uxbox.png differ
diff --git a/frontend/resources/images/email/uxbox-title.png b/frontend/resources/images/email/uxbox-title.png
index 45869e7581..18a878202c 100644
Binary files a/frontend/resources/images/email/uxbox-title.png and b/frontend/resources/images/email/uxbox-title.png differ
diff --git a/frontend/resources/images/favicon-preview.png b/frontend/resources/images/favicon-preview.png
deleted file mode 100644
index bf99637fa5..0000000000
Binary files a/frontend/resources/images/favicon-preview.png and /dev/null differ
diff --git a/frontend/resources/images/favicon.png b/frontend/resources/images/favicon.png
index c185a572e3..1645d8b36a 100644
Binary files a/frontend/resources/images/favicon.png and b/frontend/resources/images/favicon.png differ
diff --git a/frontend/resources/images/features/1.12-guides.gif b/frontend/resources/images/features/1.12-guides.gif
new file mode 100644
index 0000000000..ea6a0264e9
Binary files /dev/null and b/frontend/resources/images/features/1.12-guides.gif differ
diff --git a/frontend/resources/images/features/1.12-nudge.gif b/frontend/resources/images/features/1.12-nudge.gif
new file mode 100644
index 0000000000..b4d226f7c4
Binary files /dev/null and b/frontend/resources/images/features/1.12-nudge.gif differ
diff --git a/frontend/resources/images/features/1.12-scrollbars.gif b/frontend/resources/images/features/1.12-scrollbars.gif
new file mode 100644
index 0000000000..09a8b1444e
Binary files /dev/null and b/frontend/resources/images/features/1.12-scrollbars.gif differ
diff --git a/frontend/resources/images/features/1.12-ui.gif b/frontend/resources/images/features/1.12-ui.gif
new file mode 100644
index 0000000000..20a846e48d
Binary files /dev/null and b/frontend/resources/images/features/1.12-ui.gif differ
diff --git a/frontend/resources/images/form/adobe-xd.png b/frontend/resources/images/form/adobe-xd.png
index f2946ae396..40c0c5fce1 100644
Binary files a/frontend/resources/images/form/adobe-xd.png and b/frontend/resources/images/form/adobe-xd.png differ
diff --git a/frontend/resources/images/form/figma.png b/frontend/resources/images/form/figma.png
index 5e3bccb1a7..01511465da 100644
Binary files a/frontend/resources/images/form/figma.png and b/frontend/resources/images/form/figma.png differ
diff --git a/frontend/resources/images/form/invision.png b/frontend/resources/images/form/invision.png
index 551b8e2c52..c57f8993fe 100644
Binary files a/frontend/resources/images/form/invision.png and b/frontend/resources/images/form/invision.png differ
diff --git a/frontend/resources/images/form/never-used.png b/frontend/resources/images/form/never-used.png
index cda0947ecf..1018899951 100644
Binary files a/frontend/resources/images/form/never-used.png and b/frontend/resources/images/form/never-used.png differ
diff --git a/frontend/resources/images/form/sketch.png b/frontend/resources/images/form/sketch.png
index b923c607c8..d917d08b4e 100644
Binary files a/frontend/resources/images/form/sketch.png and b/frontend/resources/images/form/sketch.png differ
diff --git a/frontend/resources/images/form/uxpin.png b/frontend/resources/images/form/uxpin.png
index 02b5d93314..bbd155d6a4 100644
Binary files a/frontend/resources/images/form/uxpin.png and b/frontend/resources/images/form/uxpin.png differ
diff --git a/frontend/resources/images/icons/help.svg b/frontend/resources/images/icons/help.svg
new file mode 100644
index 0000000000..b1cb218270
--- /dev/null
+++ b/frontend/resources/images/icons/help.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/resources/images/pot.png b/frontend/resources/images/pot.png
deleted file mode 100644
index ab8a96da49..0000000000
Binary files a/frontend/resources/images/pot.png and /dev/null differ
diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss
index 34ff9c2320..ebd5568d1e 100644
--- a/frontend/resources/styles/common/base.scss
+++ b/frontend/resources/styles/common/base.scss
@@ -11,6 +11,13 @@ body {
display: flex;
flex-direction: column;
font-family: "worksans", sans-serif;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+
+#app {
+ width: 100vw;
height: 100vh;
overflow: hidden;
}
diff --git a/frontend/resources/styles/common/dependencies/colors.scss b/frontend/resources/styles/common/dependencies/colors.scss
index 57b538061d..8ca5bbb8a7 100644
--- a/frontend/resources/styles/common/dependencies/colors.scss
+++ b/frontend/resources/styles/common/dependencies/colors.scss
@@ -15,7 +15,7 @@ $color-dashboard: #f6f6f6;
$color-primary: #31efb8;
// Secondary colors
-$color-success: #58c35c;
+$color-success: #49d793;
$color-complete: #a599c6;
$color-warning: #fc8802;
$color-danger: #e65244;
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index ca1c5c61b7..bb6dafbfb0 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -55,6 +55,7 @@
@import "main/partials/zoom-widget";
@import "main/partials/activity-bar";
@import "main/partials/color-palette";
+@import "main/partials/text-palette";
@import "main/partials/colorpicker";
@import "main/partials/dashboard";
@import "main/partials/dashboard-header";
diff --git a/frontend/resources/styles/main/layouts/handoff.scss b/frontend/resources/styles/main/layouts/handoff.scss
index 087c0c6f47..3f8b6f1511 100644
--- a/frontend/resources/styles/main/layouts/handoff.scss
+++ b/frontend/resources/styles/main/layouts/handoff.scss
@@ -1,4 +1,5 @@
-$width-settings-bar: 16rem;
+$width-left-toolbar: 48px;
+$width-settings-bar: 256px;
.handoff-layout {
height: 100vh;
@@ -60,18 +61,21 @@ $width-settings-bar: 16rem;
.settings-bar {
transition: width 0.2s;
+ width: $width-settings-bar;
&.expanded {
width: $width-settings-bar * 3;
}
&.settings-bar-right,
&.settings-bar-left {
+ height: 100%;
position: relative;
left: unset;
right: unset;
.settings-bar-inside {
padding-top: 0.5rem;
+ overflow-y: auto;
}
}
}
diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss
index c9c1acfa64..3ee47482f1 100644
--- a/frontend/resources/styles/main/partials/color-bullet.scss
+++ b/frontend/resources/styles/main/partials/color-bullet.scss
@@ -5,30 +5,26 @@
// Copyright (c) UXBOX Labs SL
.color-cell {
+ display: grid;
+ grid-template-columns: 100%;
+ grid-template-rows: 1fr auto;
+ height: 100%;
+ justify-items: center;
+ width: 65px;
+
.color-bullet {
- // Creates strange artifacts
border: 2px solid $color-gray-60;
- // box-shadow: 0 0 0 2px $color-gray-60;
- border-radius: 50%;
+ position: relative;
+ width: var(--bullet-size);
+ height: var(--bullet-size);
&:hover {
border-color: $color-primary;
}
}
- &.cell-big .color-bullet {
- width: 50px;
- height: 50px;
- }
-
- &.cell-small .color-bullet {
- width: 40px;
- height: 40px;
- }
-
- .color-bullet.color-big {
- width: 50px;
- height: 50px;
+ & > * {
+ overflow: hidden;
}
}
@@ -41,7 +37,6 @@
ul.palette-menu .color-bullet {
width: 20px;
height: 20px;
- border-radius: 12px;
border: 1px solid $color-gray-10;
margin-right: 5px;
background-size: 8px;
@@ -67,14 +62,12 @@ ul.palette-menu .color-bullet {
grid-area: color;
width: 20px;
height: 20px;
- border-radius: 12px;
border: 1px solid $color-gray-10;
background-size: 8px;
}
.asset-section .asset-list-item .color-bullet {
border: 1px solid $color-gray-20;
- border-radius: 10px;
height: 20px;
margin-right: $size-1;
width: 20px;
@@ -91,38 +84,31 @@ ul.palette-menu .color-bullet {
.color-bullet {
display: flex;
flex-direction: row;
- background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
- left center;
- background-color: $color-white;
+ border-radius: 50%;
- & > * {
+ & .color-bullet-wrapper {
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
+ left center;
+ background-color: $color-white;
+ clip-path: circle(50%);
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ width: 100%;
+ }
+
+ &.is-gradient {
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
+ left center;
+ background-color: $color-white;
+ }
+
+ & .color-bullet-wrapper > * {
width: 100%;
height: 100%;
}
}
-.color-bullet.is-library-color .color-bullet-left,
-.selected-colors .color-bullet .color-bullet-left {
- border-radius: 10px 0 0 10px;
-}
-
-.color-bullet.is-library-color .color-bullet-right,
-.selected-colors .color-bullet .color-bullet-right {
- border-radius: 0 10px 10px 0;
-}
-
-.color-palette .color-bullet .color-bullet-left {
- border-radius: 25px 0 0 25px;
-}
-
-.color-palette .color-bullet .color-bullet-right {
- border-radius: 0 25px 25px 0;
-}
-
-.color-data .color-bullet.is-library-color {
- border-radius: 50%;
-}
-
.color-data .color-bullet.multiple {
background: transparent;
@@ -133,7 +119,7 @@ ul.palette-menu .color-bullet {
.color-data .color-bullet {
border: 1px solid $color-gray-30;
- border-radius: $br-small;
+ border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
@@ -144,10 +130,6 @@ ul.palette-menu .color-bullet {
margin: 5px 4px 0 0;
width: 20px;
- &.color-name {
- border-radius: 10px;
- }
-
&.palette-th {
align-items: center;
border: 1px solid $color-gray-30;
@@ -198,3 +180,11 @@ ul.palette-menu .color-bullet {
fill: $color-black;
}
}
+
+.color-data .color-bullet.is-not-library-color {
+ border-radius: $br-small;
+
+ & .color-bullet-wrapper {
+ clip-path: none;
+ }
+}
diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss
index fb42b2418b..2edac74577 100644
--- a/frontend/resources/styles/main/partials/color-palette.scss
+++ b/frontend/resources/styles/main/partials/color-palette.scss
@@ -9,11 +9,10 @@
align-items: center;
background-color: $color-gray-50;
border-top: 1px solid $color-gray-60;
- display: flex;
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
+
+ display: grid;
+ grid-template-columns: auto auto 1fr auto auto;
+
z-index: 11;
& .right-arrow,
@@ -46,16 +45,21 @@
@include animation(0, 0.5s, fadeOutDown);
}
- &.left-sidebar-open {
- left: 303px;
- width: calc(100% - 303px);
- }
-
& .context-menu-items {
bottom: 1.5rem;
top: initial;
min-width: 10rem;
}
+
+ & .resize-area {
+ position: absolute;
+ height: 8px;
+ width: 100%;
+ z-index: 10;
+ cursor: ns-resize;
+ top: 0;
+ left: 0;
+ }
}
.color-palette-actions {
@@ -119,8 +123,8 @@
display: flex;
overflow: hidden;
width: 100%;
- height: 5rem;
padding: 0.25rem;
+ height: 100%;
&.size-small {
height: 3.5rem;
@@ -134,6 +138,7 @@
transition: all 0.6s ease;
width: 100%;
scroll-behavior: smooth;
+ height: 100%;
}
.color-cell {
@@ -144,24 +149,21 @@
flex-shrink: 0;
position: relative;
- &.cell-big {
- flex-basis: 66px;
- }
-
- &.cell-small {
- flex-basis: 52px;
- }
-
.color-text {
color: $color-gray-20;
font-size: $fs12;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- width: 66px;
+ width: 65px;
text-align: center;
margin-top: 0.25rem;
+
+ .no-text & {
+ display: none;
+ }
}
+
&.current {
.color-text {
color: $color-gray-50;
@@ -252,7 +254,7 @@
ul.palette-menu {
left: 8px;
top: auto;
- bottom: 4.5rem;
+ bottom: var(--height);
color: $color-black;
li {
diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss
index 0003951f45..b3d37d1a25 100644
--- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss
+++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss
@@ -415,7 +415,7 @@
width: 12px;
}
- &.feedback {
+ &.separator {
border-top: 1px solid $color-gray-10;
}
}
diff --git a/frontend/resources/styles/main/partials/debug-icons-preview.scss b/frontend/resources/styles/main/partials/debug-icons-preview.scss
index 8ddba1688c..afca851233 100644
--- a/frontend/resources/styles/main/partials/debug-icons-preview.scss
+++ b/frontend/resources/styles/main/partials/debug-icons-preview.scss
@@ -2,7 +2,9 @@
display: flex;
flex-direction: column;
overflow: scroll;
+ height: 100%;
}
+
.debug-icons-preview {
display: flex;
flex-wrap: wrap;
diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss
index c666f03cd5..3ebfd23741 100644
--- a/frontend/resources/styles/main/partials/handoff.scss
+++ b/frontend/resources/styles/main/partials/handoff.scss
@@ -233,10 +233,7 @@
.attributes-shadow {
display: flex;
-
- .attributes-label {
- margin-right: 2px;
- }
+ margin-left: 4px;
}
}
diff --git a/frontend/resources/styles/main/partials/left-toolbar.scss b/frontend/resources/styles/main/partials/left-toolbar.scss
index 9b5cf562e8..1c787bd96b 100644
--- a/frontend/resources/styles/main/partials/left-toolbar.scss
+++ b/frontend/resources/styles/main/partials/left-toolbar.scss
@@ -5,16 +5,8 @@
// Copyright (c) 2015-2020 Andrey Antukh
// Copyright (c) 2015-2020 Juan de la Cruz
-$width-left-toolbar: 48px;
-
.left-toolbar {
background-color: $color-gray-50;
- bottom: 0;
- height: 100%;
- position: fixed;
- left: 0;
- width: $width-left-toolbar;
- z-index: 11;
}
.left-toolbar-inside {
@@ -23,7 +15,6 @@ $width-left-toolbar: 48px;
display: flex;
flex-direction: column;
overflow: visible;
- padding-top: 48px;
height: 100%;
}
@@ -44,6 +35,7 @@ $width-left-toolbar: 48px;
justify-content: center;
position: relative;
width: 48px;
+ color: $color-gray-20;
svg {
fill: $color-gray-20;
@@ -53,6 +45,7 @@ $width-left-toolbar: 48px;
&:hover {
background-color: $color-primary;
+ color: $color-gray-50;
svg {
fill: $color-gray-50;
@@ -61,6 +54,7 @@ $width-left-toolbar: 48px;
&.selected {
background-color: $color-gray-60;
+ color: $color-primary;
svg {
fill: $color-primary;
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index d852fbd0ec..44ebe2550b 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -105,6 +105,26 @@
}
}
+ .modal-item-element {
+ display: flex;
+ padding-bottom: 3px;
+ margin-left: 10px;
+ font-size: $fs14;
+ color: $color-info;
+
+ .modal-component-icon {
+ margin-right: 16px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $color-info;
+ }
+ }
+ }
+
.modal-content {
display: flex;
flex-direction: column;
@@ -1206,3 +1226,83 @@
background-color: rgba(0, 0, 0, 0.9);
}
}
+
+// Nudge modal
+
+.nudge-modal-overlay {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: fixed;
+ left: calc(50vw - 107px);
+ top: calc(50vh - 57px);
+ height: 110px;
+ width: 215px;
+ padding: 8px 20px;
+ background-color: $color-white;
+ box-shadow: 0px 2px 8px 0px rgb(0 0 0 / 20%);
+ z-index: 1000;
+
+ &.transparent {
+ background-color: rgba($color-white, 0);
+ }
+
+ .nudge-modal-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ height: 100%;
+ width: 100%;
+
+ .nudge-modal-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 7px;
+
+ .modal-close-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+
+ svg {
+ height: 12px;
+ width: 12px;
+ transform: rotate(45deg);
+ }
+ }
+
+ .nudge-modal-title {
+ padding: 0;
+ margin: 0;
+ color: $color-black;
+ font-size: $fs12;
+ }
+ }
+
+ .nudge-modal-body {
+ display: flex;
+ justify-content: space-between;
+
+ .nudge-subtitle {
+ margin: 0;
+ }
+
+ input {
+ width: 72px;
+ background-color: transparent;
+ border: none;
+ border-bottom: 1px solid $color-black;
+ margin-bottom: 12px;
+
+ &:active,
+ &:focus,
+ &:hover {
+ border-bottom: 1px solid $color-primary;
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss
index eed7e64b3e..75907e30fc 100644
--- a/frontend/resources/styles/main/partials/sidebar-assets.scss
+++ b/frontend/resources/styles/main/partials/sidebar-assets.scss
@@ -231,15 +231,23 @@
.asset-grid {
display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+ grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 6vh;
column-gap: 0.5rem;
row-gap: 0.5rem;
&.big {
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 10vh;
+ .three-row & {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .four-row & {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
.grid-cell {
padding: $size-1;
diff --git a/frontend/resources/styles/main/partials/sidebar-document-history.scss b/frontend/resources/styles/main/partials/sidebar-document-history.scss
index 93ea710f1a..72553e3367 100644
--- a/frontend/resources/styles/main/partials/sidebar-document-history.scss
+++ b/frontend/resources/styles/main/partials/sidebar-document-history.scss
@@ -40,6 +40,9 @@
font-size: $fs12;
color: $color-gray-20;
fill: $color-gray-20;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
}
.history-entry {
diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss
index 95dd6ef0c0..e9bd57dabe 100644
--- a/frontend/resources/styles/main/partials/sidebar-element-options.scss
+++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss
@@ -1168,9 +1168,8 @@
}
header {
- padding: 15px 17px;
display: flex;
- align-items: center;
+ flex-direction: column;
position: relative;
.backend-filters {
@@ -1225,7 +1224,13 @@
border-radius: $br-small;
color: $color-gray-20;
border: 1px solid $color-gray-30;
- margin: 0px;
+ width: 88%;
+ margin: 15px 17px;
+ }
+
+ .title {
+ font-size: $fs14;
+ margin: 9px 17px;
}
.options {
diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss
index ce99e46335..e8747f1325 100644
--- a/frontend/resources/styles/main/partials/sidebar-layers.scss
+++ b/frontend/resources/styles/main/partials/sidebar-layers.scss
@@ -207,6 +207,11 @@ span.element-name {
margin-left: auto;
position: relative;
width: 32px;
+ right: 20px;
+
+ &.is-parent {
+ right: 0;
+ }
svg {
height: 13px;
@@ -242,7 +247,7 @@ span.element-name {
}
.toggle-content {
- margin-left: auto;
+ margin-left: 8px;
width: 12px;
svg {
diff --git a/frontend/resources/styles/main/partials/sidebar-sitemap.scss b/frontend/resources/styles/main/partials/sidebar-sitemap.scss
index 3a14879f97..ffc67c1371 100644
--- a/frontend/resources/styles/main/partials/sidebar-sitemap.scss
+++ b/frontend/resources/styles/main/partials/sidebar-sitemap.scss
@@ -5,8 +5,8 @@
// Copyright (c) 2015-2016 Andrey Antukh
// Copyright (c) 2015-2016 Juan de la Cruz
-.sitemap {
- flex: none !important;
+#sitemap {
+ height: var(--height, 200px);
.element-list {
li {
@@ -118,6 +118,15 @@
}
}
}
+
+ & .resize-area {
+ position: absolute;
+ width: 100%;
+ height: 16px;
+ bottom: -8px;
+ left: 0;
+ cursor: ns-resize;
+ }
}
.add-page,
diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss
index 1de3a52642..e086a1d06b 100644
--- a/frontend/resources/styles/main/partials/sidebar.scss
+++ b/frontend/resources/styles/main/partials/sidebar.scss
@@ -5,67 +5,25 @@
// Copyright (c) 2015-2020 Andrey Antukh
// Copyright (c) 2015-2020 Juan de la Cruz
-$width-settings-bar: 16rem;
-// This width is also used in update-viewport-size at frontend/src/app/main/data/workspace.cljs
-
.settings-bar {
background-color: $color-gray-50;
border-left: 1px solid $color-gray-60;
- bottom: 0;
- height: 100%;
- position: fixed;
- right: 0;
- width: $width-settings-bar;
-
- &.expanded {
- width: $width-settings-bar * 3;
- }
-
- z-index: 10;
- overflow-y: auto;
+ position: relative;
&.settings-bar-left {
border-left: none;
border-right: 1px solid $color-gray-60;
- left: 48px;
+
+ & .tab-container-tabs {
+ padding-left: 1.5rem;
+ }
}
.settings-bar-inside {
- align-items: flex-start;
display: grid;
grid-template-columns: 100%;
-
- &[data-layout*="sitemap-pages"] {
- grid-template-rows: auto;
- }
-
- &[data-layout*="layers"] {
- grid-template-rows: auto 1fr;
- }
-
- &[data-layout*="libraries"] {
- grid-template-rows: auto 1fr;
- }
-
- &[data-layout*="layers"][data-layout*="sitemap-pages"] {
- grid-template-rows: 11.5rem 1fr;
- }
-
- &[data-layout*="libraries"][data-layout*="sitemap-pages"] {
- grid-template-rows: 11.5rem 1fr;
- }
-
- &[data-layout*="layers"][data-layout*="libraries"] {
- grid-template-rows: auto 30% 1fr;
- }
-
- &[data-layout*="layers"][data-layout*="libraries"][data-layout*="sitemap-pages"] {
- grid-template-rows: 11.5rem 25% 1fr;
- }
-
- flex-direction: column;
- padding-top: 48px;
- height: 100%;
+ grid-template-rows: 100%;
+ height: calc(100% - 2px);
.tool-window {
position: relative;
@@ -75,7 +33,6 @@ $width-settings-bar: 16rem;
flex: 1;
width: 100%;
height: 100%;
- overflow: hidden;
.tool-window-bar {
align-items: center;
@@ -163,14 +120,30 @@ $width-settings-bar: 16rem;
height: auto;
}
}
+
+ & > .resize-area {
+ position: absolute;
+ width: 8px;
+ height: 100%;
+ z-index: 10;
+ cursor: ew-resize;
+ }
+
+ &.settings-bar-left > .resize-area {
+ right: -8px;
+ }
+
+ &.settings-bar-right > .resize-area {
+ left: -4px;
+ }
}
.tool-window-content {
display: flex;
flex-direction: column;
- overflow-y: auto;
height: 100%;
width: 100%;
+ overflow-y: auto;
}
.element-list {
@@ -222,3 +195,47 @@ $width-settings-bar: 16rem;
width: 100%;
}
}
+
+button.collapse-sidebar {
+ background: none;
+ border: none;
+ cursor: pointer;
+ height: 2.5rem;
+ padding-top: 0.75rem;
+ position: absolute;
+ width: 1rem;
+
+ & svg {
+ width: 12px;
+ height: 12px;
+ fill: $color-gray-20;
+ transform: rotate(180deg);
+ }
+
+ &.collapsed {
+ background: $color-gray-60;
+ left: 48px;
+ top: 48px;
+ width: 28px;
+ height: 48px;
+ padding: 0;
+ border-radius: 0px 4px 4px 0px;
+ border-left: 1px solid $color-gray-50;
+
+ & svg {
+ transform: rotate(0deg);
+ }
+ }
+}
+
+#layers.tool-window {
+ overflow: auto;
+}
+
+.layers-tab {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-template-columns: 100%;
+ height: 100%;
+ overflow: hidden;
+}
diff --git a/frontend/resources/styles/main/partials/tab-container.scss b/frontend/resources/styles/main/partials/tab-container.scss
index 76417795ae..be52565e56 100644
--- a/frontend/resources/styles/main/partials/tab-container.scss
+++ b/frontend/resources/styles/main/partials/tab-container.scss
@@ -1,8 +1,8 @@
.tab-container {
- display: flex;
- flex-direction: column;
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-template-columns: 100%;
height: 100%;
- width: 100%;
}
.tab-container-tabs {
@@ -31,11 +31,8 @@
}
.tab-container-content {
- flex: 1;
- height: 100%;
- max-height: 100%;
- overflow-x: hidden;
overflow-y: auto;
+ overflow-x: hidden;
}
.tab-element,
diff --git a/frontend/resources/styles/main/partials/text-palette.scss b/frontend/resources/styles/main/partials/text-palette.scss
new file mode 100644
index 0000000000..b1c223da1e
--- /dev/null
+++ b/frontend/resources/styles/main/partials/text-palette.scss
@@ -0,0 +1,26 @@
+.typography-item {
+ padding: 0 1rem;
+ margin-right: 1rem;
+ cursor: pointer;
+
+ & > * {
+ white-space: nowrap;
+ }
+
+ & .typography-name {
+ color: $color-white;
+ }
+
+ & .typography-font,
+ & .typography-data {
+ font-size: 16px;
+ color: $color-gray-30;
+ }
+
+ .no-text & {
+ & .typography-font,
+ & .typography-data {
+ display: none;
+ }
+ }
+}
diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss
index 29c7441c9a..8ccc4fcf14 100644
--- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss
+++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss
@@ -6,7 +6,7 @@
overflow: hidden;
display: flex;
flex-direction: column;
- z-index: 10;
+ z-index: 11;
&.invisible {
visibility: hidden;
@@ -179,3 +179,14 @@
}
}
}
+
+.thumbnail-close {
+ grid-row: 1 / span 2;
+ grid-column: 1 / span 1;
+ z-index: 11;
+}
+
+.thumbnail-close.invisible {
+ visibility: hidden;
+ pointer-events: none;
+}
diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss
index 1e0c48f417..3a5f1af69d 100644
--- a/frontend/resources/styles/main/partials/workspace-header.scss
+++ b/frontend/resources/styles/main/partials/workspace-header.scss
@@ -5,16 +5,36 @@
// Copyright (c) UXBOX Labs SL
.workspace-header {
+ position: relative;
align-items: center;
background-color: $color-gray-50;
border-bottom: 1px solid $color-gray-60;
display: flex;
- height: 48px;
padding: $size-1 $size-4 $size-1 55px;
- position: relative;
- z-index: 12;
justify-content: space-between;
+ display: grid;
+ grid-template-areas: "left center right";
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 100%;
+ padding: 0;
+
+ .left-area {
+ grid-area: left;
+ display: flex;
+ height: 100%;
+ }
+
+ .center-area {
+ grid-area: center;
+ }
+
+ .right-area {
+ grid-area: right;
+ display: flex;
+ height: 100%;
+ }
+
.main-icon {
align-items: center;
background-color: $color-gray-60;
@@ -23,9 +43,6 @@
display: flex;
height: 100%;
justify-content: center;
- left: 0;
- position: absolute;
- top: 0;
width: 48px;
a {
@@ -46,6 +63,7 @@
}
.menu-section {
+ margin-left: 1rem;
display: flex;
justify-content: flex-start;
align-items: center;
@@ -75,6 +93,8 @@
justify-content: flex-end;
align-items: center;
position: relative;
+ padding-right: 1rem;
+ border-right: 1px solid black;
> * {
margin-left: $size-5;
@@ -132,14 +152,24 @@
position: absolute;
top: 40px;
left: 40px;
- width: 270px;
+ width: 183px;
z-index: 12;
- @include animation(0, 0.2s, fadeInDown);
background-color: $color-white;
border-radius: $br-small;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
+ :first-child {
+ &:hover {
+ border-radius: $br-small $br-small 0px 0px;
+ }
+ }
+ :last-child {
+ &:hover {
+ border-radius: 0px 0px $br-small $br-small;
+ }
+ }
+
li {
cursor: pointer;
font-size: $fs14;
@@ -158,11 +188,6 @@
margin: 0 $size-1;
}
- .shortcut {
- color: $color-gray-20;
- font-size: $fs12;
- }
-
&:hover {
background-color: $color-primary-lighter;
}
@@ -173,8 +198,68 @@
}
}
+ .sub-menu {
+ position: absolute;
+ left: 230px;
+ width: 270px;
+ z-index: 12;
+ background-color: $color-white;
+ border-radius: $br-small;
+ box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
+
+ :first-child {
+ &:hover {
+ border-radius: $br-small $br-small 0px 0px;
+ }
+ }
+ :last-child {
+ &:hover {
+ border-radius: 0px 0px $br-small $br-small;
+ }
+ }
+
+ &.file {
+ top: 40px;
+ }
+
+ &.edit {
+ top: 77px;
+ }
+
+ &.view {
+ top: 114px;
+ }
+
+ &.preferences {
+ top: 150px;
+ }
+
+ li {
+ cursor: pointer;
+ font-size: $fs14;
+ padding: $size-2;
+ display: flex;
+ justify-content: space-between;
+
+ span {
+ color: $color-gray-60;
+ margin: 0 $size-1;
+ }
+
+ .shortcut {
+ color: $color-gray-20;
+ font-size: $fs12;
+ }
+
+ &:hover {
+ background-color: $color-primary-lighter;
+ }
+ }
+ }
+
.active-users {
- align-items: center;
+ flex: 1;
+ justify-content: center;
display: flex;
margin: 0;
@@ -191,6 +276,26 @@
}
}
}
+
+ & button.document-history {
+ background: $color-gray-60;
+ border-radius: 3px;
+ border: none;
+ cursor: pointer;
+ height: 24px;
+ padding: 3px;
+ width: 24px;
+
+ & svg {
+ width: 18px;
+ fill: $color-gray-20;
+ height: 18px;
+ }
+
+ &.selected svg {
+ fill: $color-primary;
+ }
+ }
}
.persistence-status-widget {
diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss
index 561be4c629..2bba2d53af 100644
--- a/frontend/resources/styles/main/partials/workspace.scss
+++ b/frontend/resources/styles/main/partials/workspace.scss
@@ -5,8 +5,68 @@
// Copyright (c) 2015-2016 Andrey Antukh
// Copyright (c) 2015-2016 Juan de la Cruz
+$width-left-toolbar: 48px;
+
+$width-settings-bar: 256px;
+$width-settings-bar-min: 255px;
+$width-settings-bar-max: 500px;
+
+$height-palette: 79px;
+$height-palette-min: 54px;
+$height-palette-max: 80px;
+
#workspace {
+ width: 100vw;
+ height: 100vh;
user-select: none;
+
+ display: grid;
+ grid-template-areas:
+ "header header header header"
+ "toolbar left-sidebar viewport right-sidebar"
+ "toolbar left-sidebar color-palette right-sidebar";
+
+ grid-template-rows: auto 1fr auto;
+ grid-template-columns: auto auto 1fr auto;
+
+ .workspace-header {
+ grid-area: header;
+ height: 48px;
+ }
+
+ .left-toolbar {
+ grid-area: toolbar;
+ width: $width-left-toolbar;
+ }
+
+ .settings-bar.settings-bar-left {
+ min-width: $width-settings-bar;
+ max-width: 500px;
+ width: var(--width, $width-settings-bar);
+ grid-area: left-sidebar;
+ }
+
+ .settings-bar.settings-bar-right {
+ min-width: $width-settings-bar;
+ max-width: 500px;
+ width: $width-settings-bar;
+ grid-area: right-sidebar;
+ }
+
+ .workspace-loader {
+ grid-area: viewport;
+ }
+
+ .workspace-content {
+ grid-area: viewport;
+ }
+
+ .color-palette {
+ grid-area: color-palette;
+ min-height: $height-palette-min;
+ max-height: $height-palette-max;
+ height: var(--height, $height-palette);
+ }
}
.workspace-context-menu {
@@ -34,11 +94,11 @@
margin: 2px;
}
- span:first-child {
+ span {
color: $color-gray-60;
}
- span:last-child {
+ span.shortcut {
color: $color-gray-20;
font-size: $fs12;
}
@@ -57,13 +117,46 @@
}
}
}
+
+ .icon-menu-item {
+ display: flex;
+ justify-content: flex-start;
+
+ &:hover {
+ background-color: $color-primary-lighter;
+ }
+
+ span.title {
+ margin-left: 5px;
+ }
+
+ .selected-icon {
+ svg {
+ width: 10px;
+ height: 10px;
+ }
+ }
+
+ .shape-icon {
+ margin-left: 3px;
+ svg {
+ width: 13px;
+ height: 13px;
+ }
+ }
+
+ .icon-wrapper {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ margin: 0;
+ }
+ }
}
.workspace-loader {
display: flex;
justify-content: center;
align-items: center;
- height: 100vh;
svg#loader-pencil {
fill: $color-gray-50;
@@ -73,12 +166,8 @@
.workspace-content {
background-color: $color-canvas;
display: flex;
- height: 100%;
- width: calc(100% - #{$width-left-toolbar} - 2 * #{$width-settings-bar});
padding: 0;
margin: 0;
- position: fixed;
- right: $width-settings-bar;
&.scrolling {
cursor: grab;
@@ -104,10 +193,10 @@
.coordinates {
background-color: $color-dark-bg;
border-radius: $br-small;
- bottom: -10px;
+ bottom: 0px;
padding-left: 5px;
position: fixed;
- right: calc(#{$width-settings-bar} + 10px);
+ right: calc(#{$width-settings-bar} + 14px);
text-align: center;
width: 125px;
white-space: nowrap;
@@ -137,14 +226,12 @@
}
.workspace-viewport {
- height: calc(100% - 40px);
overflow: hidden;
transition: none;
- width: 100%;
-
display: grid;
- grid-template-rows: 20px 100%;
- grid-template-columns: 20px 100%;
+ grid-template-rows: 20px 1fr;
+ grid-template-columns: 20px 1fr;
+ flex: 1;
}
.viewport {
@@ -175,10 +262,14 @@
.render-shapes {
position: absolute;
+ width: 100%;
+ height: 100%;
}
.viewport-controls {
position: absolute;
+ width: 100%;
+ height: 100%;
}
}
diff --git a/frontend/scripts/compress-png b/frontend/scripts/compress-png
new file mode 100755
index 0000000000..b18a64b96a
--- /dev/null
+++ b/frontend/scripts/compress-png
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+# This script automates compressing PNG images using the lossless Zopfli
+# Compression Algorithm. The process is slow but can produce significantly
+# better compression and, thus, smaller file sizes.
+#
+# This script is meant to be run manually, for example, before making a new
+# release.
+#
+# Requirements
+#
+# zopflipng - https://github.com/google/zopfli
+# Debian/Ubuntu: sudo apt install zopfli
+# Fedora: sudo dnf install zopfli
+# macOS: brew install zopfli
+#
+# Usage
+#
+# This script takes a single positional argument which is the path where to
+# search for PNG files. By default, the target path is the current working
+# directory. Run from the root of the repository to compress all PNG images. Run
+# from the `frontend` subdirectory to compress all PNG images within that
+# directory. Alternatively, run from any directory and pass an explicit path to
+# `compress-png` to limit the script to that path/directory.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+readonly TARGET="${1:-.}"
+readonly ABS_TARGET="$(command -v realpath &>/dev/null && realpath "$TARGET")"
+
+function png_total_size() {
+ find "$TARGET" -type f -iname '*.png' -exec du -ch {} + | tail -1
+}
+
+echo "Compressing PNGs in ${ABS_TARGET:-$TARGET}"
+
+echo "Before"
+png_total_size
+
+readonly opts=(
+ # More iterations means slower, potentially better compression.
+ #--iterations=500
+ -m
+ # Try all filter strategies (slow).
+ #--filters=01234mepb
+ # According to docs, remove colors behind alpha channel 0. No visual
+ # difference, removes hidden information.
+ --lossy_transparent
+ # Avoid information loss that could affect how images are rendered, see
+ # https://github.com/penpot/penpot/issues/1533#issuecomment-1030005203
+ # https://github.com/google/zopfli/issues/113
+ --keepchunks=cHRM,gAMA,pHYs,iCCP,sRGB,oFFs,sTER
+ # Since we have git behind our back, overwrite PNG files in-place (only
+ # when result is smaller).
+ -y
+)
+time find "$TARGET" -type f -iname '*.png' -exec zopflipng "${opts[@]}" {} {} \;
+
+echo "After"
+png_total_size
diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs
index e3f26495e3..36a9a9e719 100644
--- a/frontend/src/app/main/data/comments.cljs
+++ b/frontend/src/app/main/data/comments.cljs
@@ -7,6 +7,7 @@
(ns app.main.data.comments
(:require
[app.common.data :as d]
+ [app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.main.repo :as rp]
[beicon.core :as rx]
@@ -24,7 +25,7 @@
(s/def ::page-id ::us/uuid)
(s/def ::page-name ::us/string)
(s/def ::participants (s/every ::us/uuid :kind set?))
-(s/def ::position ::us/point)
+(s/def ::position ::gpt/point)
(s/def ::project-id ::us/uuid)
(s/def ::seqn ::us/integer)
(s/def ::thread-id ::us/uuid)
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index c986116f83..6e34b0f5d9 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -834,4 +834,15 @@
action (if in-project? file-created project-created)]
(->> (rp/mutation! action-name params)
- (rx/map action))))))
\ No newline at end of file
+ (rx/map action))))))
+
+(defn open-selected-file
+ []
+ (ptk/reify ::open-selected-file
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [files (get-in state [:dashboard-local :selected-files])]
+ (if (= 1 (count files))
+ (let [file (get-in state [:dashboard-files (first files)])]
+ (rx/of (go-to-workspace file)))
+ (rx/empty))))))
diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs
index 96fb4dbba8..b9593a0860 100644
--- a/frontend/src/app/main/data/fonts.cljs
+++ b/frontend/src/app/main/data/fonts.cljs
@@ -14,6 +14,7 @@
[app.common.uuid :as uuid]
[app.main.fonts :as fonts]
[app.main.repo :as rp]
+ [app.util.storage :refer [storage]]
[app.util.webapi :as wa]
[beicon.core :as rx]
[cuerdas.core :as str]
@@ -250,3 +251,28 @@
(let [team-id (:current-team-id state)]
(->> (rp/mutation! :delete-font-variant {:id id :team-id team-id})
(rx/ignore))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Workspace related events
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn add-recent-font
+ [font]
+ (ptk/reify ::add-recent-font
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [recent-fonts (get-in state [:workspace-data :recent-fonts])
+ most-recent-fonts (into [font] (comp (remove #(= font %)) (take 3)) recent-fonts)]
+ (assoc-in state [:workspace-data :recent-fonts] most-recent-fonts)))
+ ptk/EffectEvent
+ (effect [_ state _]
+ (let [most-recent-fonts (get-in state [:workspace-data :recent-fonts])]
+ (swap! storage assoc ::recent-fonts most-recent-fonts)))))
+
+(defn load-recent-fonts
+ []
+ (ptk/reify ::load-recent-fonts
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [saved-recent-fonts (::recent-fonts @storage)]
+ (assoc-in state [:workspace-data :recent-fonts] saved-recent-fonts)))))
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index fcf19fc542..f66c0af21a 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -170,13 +170,15 @@
(get-redirect-event))
(rx/observe-on :async)))))))
+(s/def ::invitation-token ::us/not-empty-string)
(s/def ::login-params
- (s/keys :req-un [::email ::password]))
+ (s/keys :req-un [::email ::password]
+ :opt-un [::invitation-token]))
(declare login-from-register)
(defn login
- [{:keys [email password] :as data}]
+ [{:keys [email password invitation-token] :as data}]
(us/verify ::login-params data)
(ptk/reify ::login
ptk/WatchEvent
@@ -184,9 +186,10 @@
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)
+
params {:email email
:password password
- :scope "webapp"}]
+ :invitation-token invitation-token}]
;; NOTE: We can't take the profile value from login because
;; there are cases when login is successfull but the cookie is
@@ -197,31 +200,32 @@
;; the returned profile is an NOT authenticated profile, we
;; proceed to logout and show an error message.
- (rx/merge
- (->> (rp/mutation :login params)
- (rx/map fetch-profile)
- (rx/catch on-error))
+ (->> (rp/mutation :login (d/without-nils params))
+ (rx/merge-map (fn [data]
+ (rx/merge
+ (rx/of (fetch-profile))
+ (->> stream
+ (rx/filter profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter (complement is-authenticated?))
+ (rx/tap on-error)
+ (rx/map #(ex/raise :type :authentication))
+ (rx/observe-on :async))
- (->> stream
- (rx/filter profile-fetched?)
- (rx/take 1)
- (rx/map deref)
- (rx/filter (complement is-authenticated?))
- (rx/tap on-error)
- (rx/map #(ex/raise :type :authentication))
- (rx/observe-on :async))
+ (->> stream
+ (rx/filter profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter is-authenticated?)
+ (rx/map (fn [profile]
+ (with-meta (merge data profile)
+ {::ev/source "login"})))
+ (rx/tap on-success)
+ (rx/map logged-in)
+ (rx/observe-on :async)))))
+ (rx/catch on-error))))))
- (->> stream
- (rx/filter profile-fetched?)
- (rx/take 1)
- (rx/map deref)
- (rx/filter is-authenticated?)
- (rx/map (fn [profile]
- (with-meta profile
- {::ev/source "login"})))
- (rx/tap on-success)
- (rx/map logged-in)
- (rx/observe-on :async)))))))
(defn login-from-token
[{:keys [profile] :as tdata}]
@@ -447,6 +451,20 @@
(->> (rp/query :team-users {:team-id team-id})
(rx/map #(partial fetched %)))))))
+;; --- Update Nudge
+
+(defn update-nudge
+ [value]
+ (ptk/reify ::update-nudge
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:profile :props] assoc :nudge value))
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [props {:nudge value}]
+ (->> (rp/mutation :update-profile-props {:props props})
+ (rx/map (constantly (fetch-profile))))))))
+
;; --- 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 6ff2212393..c79909eecc 100644
--- a/frontend/src/app/main/data/viewer.cljs
+++ b/frontend/src/app/main/data/viewer.cljs
@@ -7,9 +7,10 @@
(ns app.main.data.viewer
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
+ [app.common.geom.point :as gpt]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
- [app.common.types.interactions :as cti]
+ [app.common.spec.interactions :as cti]
[app.common.uuid :as uuid]
[app.main.data.comments :as dcm]
[app.main.data.fonts :as df]
@@ -469,7 +470,7 @@
(defn open-overlay
[frame-id position close-click-outside background-overlay animation]
(us/verify ::us/uuid frame-id)
- (us/verify ::us/point position)
+ (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)
@@ -494,7 +495,7 @@
(defn toggle-overlay
[frame-id position close-click-outside background-overlay animation]
(us/verify ::us/uuid frame-id)
- (us/verify ::us/point position)
+ (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)
@@ -570,7 +571,7 @@
(conj id))]
(-> state
(assoc-in [:viewer-local :selected]
- (cp/expand-region-selection objects selection)))))))
+ (cph/expand-region-selection objects selection)))))))
(defn select-all
[]
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index 2cb718f2ad..15c46bfdbc 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -17,8 +17,8 @@
[app.common.pages :as cp]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
- [app.common.pages.spec :as spec]
[app.common.spec :as us]
+ [app.common.spec.shape :as spec.shape]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cfg]
@@ -30,6 +30,7 @@
[app.main.data.workspace.drawing :as dwd]
[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.interactions :as dwi]
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.libraries :as dwl]
@@ -58,7 +59,7 @@
[cuerdas.core :as str]
[potok.core :as ptk]))
-(s/def ::shape-attrs ::cp/shape-attrs)
+(s/def ::shape-attrs ::spec.shape/shape-attrs)
(s/def ::set-of-string
(s/every string? :kind set?))
@@ -83,7 +84,8 @@
:snap-grid
:scale-text
:dynamic-alignment
- :display-artboard-names})
+ :display-artboard-names
+ :snap-guides})
(s/def ::layout-flags (s/coll-of ::layout-flag))
@@ -95,7 +97,8 @@
:display-grid
:snap-grid
:dynamic-alignment
- :display-artboard-names})
+ :display-artboard-names
+ :snap-guides})
(def layout-presets
{:assets
@@ -193,7 +196,7 @@
(->> stream
(rx/filter #(= ::dwc/index-initialized %))
- (rx/first)
+ (rx/take 1)
(rx/map #(file-initialized bundle)))))))))
ptk/EffectEvent
@@ -429,6 +432,15 @@
stored
(d/concat-set flags)))))))
+(defn remove-layout-flags
+ [& flags]
+ (ptk/reify ::remove-layout-flags
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :workspace-layout
+ (fn [stored]
+ (reduce disj stored (d/concat-set flags)))))))
+
;; --- Set element options mode
(defn set-options-mode
@@ -472,13 +484,13 @@
(initialize [state local]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- shapes (cp/select-toplevel-shapes objects {:include-frames? true})
+ shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
local (assoc local :vport size :zoom 1)]
(cond
(or (not (mth/finite? (:width srect)))
(not (mth/finite? (:height srect))))
- (assoc local :vbox (assoc size :x 0 :y 0 :left-offset 0))
+ (assoc local :vbox (assoc size :x 0 :y 0))
(or (> (:width srect) width)
(> (:height srect) height))
@@ -519,25 +531,46 @@
(update :y y)))))))
(defn update-viewport-size
- [{:keys [width height] :as size}]
+ [resize-type {:keys [width height] :as size}]
(ptk/reify ::update-viewport-size
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
- (fn [{:keys [vport left-sidebar? zoom] :as local}]
- (if (or (mth/almost-zero? width) (mth/almost-zero? height))
+ (fn [{:keys [vport] :as local}]
+ (if (or (nil? vport)
+ (mth/almost-zero? width)
+ (mth/almost-zero? height))
;; If we have a resize to zero just keep the old value
local
(let [wprop (/ (:width vport) width)
hprop (/ (:height vport) height)
- left-offset (if left-sidebar? 0 (/ (* -1 15 16) zoom))]
- (-> local ;; This matches $width-settings-bar
- (assoc :vport size) ;; in frontend/resources/styles/main/partials/sidebar.scss
- (update :vbox (fn [vbox]
- (-> vbox
- (update :width #(/ % wprop))
- (update :height #(/ % hprop))
- (assoc :left-offset left-offset))))))))))))
+
+ vbox (:vbox local)
+ vbox-x (:x vbox)
+ vbox-y (:y vbox)
+ vbox-width (:width vbox)
+ vbox-height (:height vbox)
+
+ vbox-width' (/ vbox-width wprop)
+ vbox-height' (/ vbox-height hprop)
+
+ vbox-x'
+ (case resize-type
+ :left (+ vbox-x (- vbox-width vbox-width'))
+ :right vbox-x
+ (+ vbox-x (/ (- vbox-width vbox-width') 2)))
+
+ vbox-y'
+ (case resize-type
+ :top (+ vbox-y (- vbox-height vbox-height'))
+ :bottom vbox-y
+ (+ vbox-y (/ (- vbox-height vbox-height') 2)))]
+ (-> local
+ (assoc :vport size)
+ (assoc-in [:vbox :x] vbox-x')
+ (assoc-in [:vbox :y] vbox-y')
+ (assoc-in [:vbox :width] vbox-width')
+ (assoc-in [:vbox :height] vbox-height')))))))))
(defn start-panning []
(ptk/reify ::start-panning
@@ -593,14 +626,12 @@
(defn- impl-update-zoom
[{:keys [vbox] :as local} center zoom]
- (let [vbox (update vbox :x + (:left-offset vbox))
- new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
+ (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
old-zoom (:zoom local)
center (if center center (gsh/center-rect vbox))
scale (/ old-zoom new-zoom)
mtx (gmt/scale-matrix (gpt/point scale) center)
- vbox' (gsh/transform-rect vbox mtx)
- vbox' (update vbox' :x - (:left-offset vbox))]
+ vbox' (gsh/transform-rect vbox mtx)]
(-> local
(assoc :zoom new-zoom)
(update :vbox merge (select-keys vbox' [:x :y :width :height])))))
@@ -644,7 +675,7 @@
(update [_ state]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- shapes (cp/select-toplevel-shapes objects {:include-frames? true})
+ shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)]
(if (empty? shapes)
state
@@ -716,14 +747,25 @@
;; --- Delete Selected
-(def delete-selected
+(defn delete-selected
"Deselect all and remove all selected shapes."
+ []
(ptk/reify ::delete-selected
ptk/WatchEvent
(watch [_ state _]
- (let [selected (wsh/lookup-selected state)]
- (rx/of (dwc/delete-shapes selected)
- (dws/deselect-all))))))
+ (let [selected (wsh/lookup-selected state)
+ hover-guides (get-in state [:workspace-guides :hover])
+ options-mode (get-in state [:workspace-local :options-mode])]
+ (cond
+ (and (= options-mode :prototype) (d/not-empty? selected))
+ (rx/of (dwi/remove-interactions selected))
+
+ (and (= options-mode :design) (d/not-empty? selected))
+ (rx/of (dwc/delete-shapes selected)
+ (dws/deselect-all))
+
+ (d/not-empty? hover-guides)
+ (rx/of (dwgu/remove-guides hover-guides)))))))
;; --- Shape Vertical Ordering
@@ -763,7 +805,7 @@
:frame-id (:frame-id obj)
:page-id page-id
:shapes [id]
- :index (cp/position-on-parent id objects)}))
+ :index (cph/get-position-on-parent objects id)}))
selected)]
;; TODO: maybe missing the :reg-objects event?
(rx/of (dch/commit-changes {:redo-changes rchanges
@@ -790,7 +832,7 @@
{:type :mov-objects
:parent-id (:parent-id obj)
:page-id page-id
- :index (cp/position-on-parent id objects)
+ :index (cph/get-position-on-parent objects id)
:shapes [id]}))
(reverse ids))
@@ -922,13 +964,13 @@
:id id
:operations [{:type :set
:attr :constraints-h
- :val (spec/default-constraints-h
- (assoc obj :parent-id parent-id :frame-id frame-id))
+ :val (gsh/default-constraints-h
+ (assoc obj :parent-id parent-id :frame-id frame-id))
:ignore-touched true}
{:type :set
:attr :constraints-v
- :val (spec/default-constraints-v
- (assoc obj :parent-id parent-id :frame-id frame-id))
+ :val (gsh/default-constraints-v
+ (assoc obj :parent-id parent-id :frame-id frame-id))
:ignore-touched true}]}))
shapes-to-unconstraint)
@@ -992,11 +1034,11 @@
objects (wsh/lookup-page-objects state page-id)
;; Ignore any shape whose parent is also intented to be moved
- ids (cp/clean-loops objects ids)
+ ids (cph/clean-loops objects ids)
;; If we try to move a parent into a child we remove it
- ids (filter #(not (cp/is-parent? objects parent-id %)) ids)
- parents (into #{parent-id} (map #(cp/get-parent % objects)) ids)
+ ids (filter #(not (cph/is-parent? objects parent-id %)) ids)
+ parents (into #{parent-id} (map #(cph/get-parent-id objects %)) ids)
groups-to-delete
(loop [current-id (first parents)
@@ -1014,7 +1056,7 @@
(empty? (remove removed-id? (:shapes group))))
;; Adds group to the remove and check its parent
- (let [to-check (concat to-check [(cp/get-parent current-id objects)])]
+ (let [to-check (concat to-check [(cph/get-parent-id objects current-id)])]
(recur (first to-check)
(rest to-check)
(conj removed-id? current-id)
@@ -1059,8 +1101,8 @@
(not (:component-root? shape)))
parent (get objects parent-id)
- component-shape (cph/get-component-shape shape objects)
- component-shape-parent (cph/get-component-shape parent objects)
+ component-shape (cph/get-component-shape objects shape)
+ component-shape-parent (cph/get-component-shape objects parent)
detach? (and instance-part? (not= (:id component-shape)
(:id component-shape-parent)))
@@ -1068,7 +1110,7 @@
reroot? (and sub-instance? (not component-shape-parent))
ids-to-detach (when detach?
- (cons id (cph/get-children id objects)))]
+ (cons id (cph/get-children-ids objects id)))]
[(cond-> shapes-to-detach detach? (into ids-to-detach))
(cond-> shapes-to-deroot deroot? (conj id))
@@ -1145,7 +1187,7 @@
;; --- Shape / Selection Alignment and Distribution
-(declare align-object-to-frame)
+(declare align-object-to-parent)
(declare align-objects-list)
(defn can-align? [selected objects]
@@ -1153,7 +1195,7 @@
(empty? selected) false
(> (count selected) 1) true
:else
- (not= uuid/zero (:frame-id (get objects (first selected))))))
+ (not= uuid/zero (:parent-id (get objects (first selected))))))
(defn align-objects
[axis]
@@ -1165,7 +1207,7 @@
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
moved (if (= 1 (count selected))
- (align-object-to-frame objects (first selected) axis)
+ (align-object-to-parent objects (first selected) axis)
(align-objects-list objects selected axis))
moved-objects (->> moved (group-by :id))
ids (keys moved-objects)
@@ -1173,11 +1215,12 @@
(when (can-align? selected objects)
(rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))
-(defn align-object-to-frame
+(defn align-object-to-parent
[objects object-id axis]
(let [object (get objects object-id)
- frame (get objects (:frame-id object))]
- (gal/align-to-rect object frame axis objects)))
+ parent (:parent-id (get objects object-id))
+ parent-obj (get objects parent)]
+ (gal/align-to-rect object parent-obj axis objects)))
(defn align-objects-list
[objects selected axis]
@@ -1197,15 +1240,16 @@
(ptk/reify ::distribute-objects
ptk/WatchEvent
(watch [_ state _]
- (let [page-id (:current-page-id state)
- objects (wsh/lookup-page-objects state page-id)
- selected (wsh/lookup-selected state)
- moved (-> (map #(get objects %) selected)
- (gal/distribute-space axis objects))
+ (let [page-id (:current-page-id state)
+ objects (wsh/lookup-page-objects state page-id)
+ selected (wsh/lookup-selected state)
+ moved (-> (map #(get objects %) selected)
+ (gal/distribute-space axis objects))
- moved-objects (->> moved (group-by :id))
- ids (keys moved-objects)
- update-fn (fn [shape] (first (get moved-objects (:id shape))))]
+ moved (d/index-by :id moved)
+ ids (keys moved)
+
+ update-fn #(get moved (:id %))]
(when (can-distribute? selected)
(rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))
@@ -1253,7 +1297,7 @@
(boolean? blocked) (assoc :blocked blocked)
(boolean? hidden) (assoc :hidden hidden)))
objects (wsh/lookup-page-objects state)
- ids (into ids (->> ids (mapcat #(cp/get-children % objects))))]
+ ids (into ids (->> ids (mapcat #(cph/get-children-ids objects %))))]
(rx/of (dch/update-shapes ids update-fn))))))
(defn toggle-visibility-selected
@@ -1450,7 +1494,7 @@
(watch [_ state _]
(let [selected (wsh/lookup-selected state)
objects (wsh/lookup-page-objects state)
- all-selected (into [] (mapcat #(cp/get-object-with-children % objects)) selected)
+ all-selected (into [] (mapcat #(cph/get-children-with-self objects %)) selected)
head (get objects (first selected))
not-group-like? (and (= (count selected) 1)
@@ -1555,7 +1599,7 @@
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
- (cp/clean-loops objects))
+ (cph/clean-loops objects))
pdata (reduce (partial collect-object-ids objects) {} selected)
initial {:type :copied-shapes
:file-id (:current-file-id state)
@@ -1603,7 +1647,7 @@
(->> (rx/concat paste-transit-str
paste-plain-text-str
paste-image-str)
- (rx/first)
+ (rx/take 1)
(rx/catch
(fn [err]
(js/console.error "Clipboard error:" err)
@@ -1738,18 +1782,18 @@
[frame-id frame-id delta])
(empty? page-selected)
- (let [frame-id (cp/frame-id-by-position page-objects mouse-pos)
+ (let [frame-id (cph/frame-id-by-position page-objects mouse-pos)
delta (gpt/subtract mouse-pos orig-pos)]
[frame-id frame-id delta])
:else
- (let [base (cp/get-base-shape page-objects page-selected)
- index (cp/position-on-parent (:id base) page-objects)
- frame-id (:frame-id base)
+ (let [base (cph/get-base-shape page-objects page-selected)
+ index (cph/get-position-on-parent page-objects (:id base))
+ frame-id (:frame-id base)
parent-id (:parent-id base)
- delta (if in-viewport?
- (gpt/subtract mouse-pos orig-pos)
- (gpt/subtract (gpt/point (:selrect base)) orig-pos))]
+ delta (if in-viewport?
+ (gpt/subtract mouse-pos orig-pos)
+ (gpt/subtract (gpt/point (:selrect base)) orig-pos))]
[frame-id parent-id delta index]))))
;; Change the indexes if the paste is done with an element selected
@@ -1771,7 +1815,7 @@
;; Check if the shape is an instance whose master is defined in a
;; library that is not linked to the current file
(foreign-instance? [shape paste-objects state]
- (let [root (cph/get-root-shape shape paste-objects)
+ (let [root (cph/get-root-shape paste-objects shape)
root-file-id (:component-file root)]
(and (some? root)
(not= root-file-id (:current-file-id state))
@@ -1863,7 +1907,7 @@
height 16
page-id (:current-page-id state)
frame-id (-> (wsh/lookup-page-objects state page-id)
- (cp/frame-id-by-position @ms/mouse-position))
+ (cph/frame-id-by-position @ms/mouse-position))
shape (gsh/setup-selrect
{:id id
:type :text
@@ -1954,7 +1998,7 @@
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- shapes (cp/select-toplevel-shapes objects {:include-frames? true})
+ shapes (cph/get-immediate-children objects)
selected (wsh/lookup-selected state)
selected-objs (map #(get objects %) selected)
has-frame? (some #(= (:type %) :frame) selected-objs)]
@@ -2031,3 +2075,9 @@
;; Shapes to path
(d/export dwps/convert-selected-to-path)
+
+;; Guides
+(d/export dwgu/update-guides)
+(d/export dwgu/remove-guide)
+(d/export dwgu/set-hover-guide)
+
diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs
index ab0ec4fdd4..6c705cb619 100644
--- a/frontend/src/app/main/data/workspace/bool.cljs
+++ b/frontend/src/app/main/data/workspace/bool.cljs
@@ -9,8 +9,8 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
[app.common.pages.changes-builder :as cb]
+ [app.common.pages.helpers :as cph]
[app.common.path.shapes-to-path :as stp]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
@@ -22,12 +22,12 @@
(defn selected-shapes
[state]
- (let [objects (wsh/lookup-page-objects state)]
+ (let [objects (wsh/lookup-page-objects state)]
(->> (wsh/lookup-selected state)
- (cp/clean-loops objects)
- (map #(get objects %))
- (filter #(not= :frame (:type %)))
- (map #(assoc % ::index (cp/position-on-parent (:id %) objects)))
+ (cph/clean-loops objects)
+ (map (d/getf objects))
+ (remove cph/frame-shape?)
+ (map #(assoc % ::index (cph/get-position-on-parent objects (:id %))))
(sort-by ::index))))
(defn create-bool-data
@@ -51,7 +51,7 @@
(merge head-data)
(gsh/update-bool-selrect shapes objects))]
- [bool-shape (cp/position-on-parent (:id head) objects)]))
+ [bool-shape (cph/get-position-on-parent objects (:id head))]))
(defn group->bool
[group bool-type objects]
diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs
index 9516d4db32..cd2aacd5a9 100644
--- a/frontend/src/app/main/data/workspace/changes.cljs
+++ b/frontend/src/app/main/data/workspace/changes.cljs
@@ -9,8 +9,8 @@
[app.common.data :as d]
[app.common.logging :as log]
[app.common.pages :as cp]
- [app.common.pages.spec :as spec]
[app.common.spec :as us]
+ [app.common.spec.change :as spec.change]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
@@ -137,8 +137,8 @@
[:workspace-data]
[:workspace-libraries file-id :data])]
(try
- (us/assert ::spec/changes redo-changes)
- (us/assert ::spec/changes undo-changes)
+ (us/assert ::spec.change/changes redo-changes)
+ (us/assert ::spec.change/changes undo-changes)
;; (prn "====== commit-changes ======" path)
;; (cljs.pprint/pprint redo-changes)
diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs
index 017cc967d6..0cb4eca12d 100644
--- a/frontend/src/app/main/data/workspace/colors.cljs
+++ b/frontend/src/app/main/data/workspace/colors.cljs
@@ -206,11 +206,9 @@
stop? (rx/filter (ptk/type? ::stop-picker) stream)
update-events
- (fn [[color shift?]]
- (rx/of (if shift?
- (change-stroke ids color)
- (change-fill ids color))
- (stop-picker)))]
+ (fn [color]
+ (rx/of (change-fill ids color)))]
+
(rx/merge
;; Stream that updates the stroke/width and stops if `esc` pressed
(->> sub
@@ -219,12 +217,12 @@
;; Hide the modal if the stop event is emitted
(->> stop?
- (rx/first)
+ (rx/take 1)
(rx/map #(md/hide))))))
ptk/UpdateEvent
(update [_ state]
- (let [handle-change-color (fn [color shift?] (rx/push! sub [color shift?]))]
+ (let [handle-change-color (fn [color] (rx/push! sub color))]
(-> state
(assoc-in [:workspace-local :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index ede10867f2..c3a7f572ef 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -99,4 +99,5 @@
(rx/filter (ptk/type? ::dw/initialize-viewport))
(rx/take 1)
(rx/mapcat #(rx/of (center-to-comment-thread thread)
+ (dw/select-for-drawing :comments)
(dcm/open-thread thread)))))))))
diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs
index 1fb8fc85b7..461a0f7f54 100644
--- a/frontend/src/app/main/data/workspace/common.cljs
+++ b/frontend/src/app/main/data/workspace/common.cljs
@@ -11,9 +11,11 @@
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
- [app.common.types.interactions :as cti]
- [app.common.types.page-options :as cto]
+ [app.common.spec.interactions :as csi]
+ [app.common.spec.page :as csp]
+ [app.common.spec.shape :as spec.shape]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
@@ -27,7 +29,7 @@
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :warn)
-(s/def ::shape-attrs ::cp/shape-attrs)
+(s/def ::shape-attrs ::spec.shape/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?))
@@ -57,12 +59,13 @@
;; --- Common Helpers & Events
+;; TODO: looks duplicate
+
(defn get-frame-at-point
[objects point]
- (let [frames (cp/select-frames objects)]
+ (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)]
@@ -194,9 +197,9 @@
(let [expand-fn (fn [expanded]
(merge expanded
(->> ids
- (map #(cp/get-parents % objects))
+ (map #(cph/get-parent-ids objects %))
flatten
- (filter #(not= % uuid/zero))
+ (remove #(= % uuid/zero))
(map (fn [id] {id true}))
(into {}))))]
(update-in state [:workspace-local :expanded] expand-fn)))))
@@ -263,9 +266,9 @@
;; Calculate the frame over which we're drawing
(let [position @ms/mouse-position
- frame-id (:frame-id attrs (cp/frame-id-by-position objects position))
+ frame-id (:frame-id attrs (cph/frame-id-by-position objects position))
shape (when-not (empty? selected)
- (cp/get-base-shape objects 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
@@ -273,7 +276,7 @@
[frame-id frame-id nil]
;; Otherwise, we add it to next to the selected shape
- (let [index (cp/position-on-parent (:id shape) objects)
+ (let [index (cph/get-position-on-parent objects (:id shape))
{:keys [frame-id parent-id]} shape]
[frame-id parent-id (inc index)])))))
@@ -313,37 +316,41 @@
[redo-changes undo-changes])))
(defn add-shape
- [attrs]
- (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)
+ ([attrs]
+ (add-shape attrs {}))
- id (or (:id attrs) (uuid/next))
- name (-> objects
- (retrieve-used-names)
- (generate-unique-name (:name 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)))
- [rchanges uchanges] (add-shape-changes
- page-id
- objects
- selected
- (-> attrs
- (assoc :id id )
- (assoc :name name)))]
+ selected (wsh/lookup-selected state)
- (rx/concat
- (rx/of (dch/commit-changes {:redo-changes rchanges
- :undo-changes uchanges
- :origin it})
- (select-shapes (d/ordered-set id)))
- (when (= :text (:type attrs))
- (->> (rx/of (start-edition-mode id))
- (rx/observe-on :async))))))))
+ [rchanges uchanges] (add-shape-changes
+ page-id
+ objects
+ selected
+ (-> attrs
+ (assoc :id id )
+ (assoc :name name)))]
+
+ (rx/concat
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
+ (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
@@ -351,8 +358,9 @@
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- to-move-shapes (->> (cp/select-toplevel-shapes objects {:include-frames? false})
- (filterv #(= (:frame-id %) uuid/zero))
+
+ to-move-shapes (->> (cph/get-immediate-children objects)
+ (remove cph/frame-shape?)
(mapv :id)
(d/enumerate)
(filterv (comp shapes second)))
@@ -389,7 +397,7 @@
objects (wsh/lookup-page-objects state page-id)
options (wsh/lookup-page-options state page-id)
- ids (cp/clean-loops objects ids)
+ ids (cph/clean-loops objects ids)
flows (:flows options)
groups-to-unmask
@@ -409,7 +417,7 @@
interacting-shapes
(filter (fn [shape]
(let [interactions (:interactions shape)]
- (some #(and (cti/has-destination %)
+ (some #(and (csi/has-destination %)
(contains? ids (:destination %)))
interactions)))
(vals objects))
@@ -429,14 +437,14 @@
all-parents
(reduce (fn [res id]
- (into res (cp/get-parents id objects)))
+ (into res (cph/get-parent-ids objects id)))
(d/ordered-set)
ids)
all-children
(->> ids
(reduce (fn [res id]
- (into res (cp/get-children id objects)))
+ (into res (cph/get-children-ids objects id)))
[])
(reverse)
(into (d/ordered-set)))
@@ -458,7 +466,7 @@
{:type :add-obj
:id (:id item)
:page-id page-id
- :index (cp/position-on-parent id objects)
+ :index (cph/get-position-on-parent objects id)
:frame-id (:frame-id item)
:parent-id (:parent-id item)
:obj item}))))
@@ -482,7 +490,7 @@
:operations [{:type :set
:attr :interactions
:val (vec (remove (fn [interaction]
- (and (cti/has-destination interaction)
+ (and (csi/has-destination interaction)
(contains? ids (:destination interaction))))
(:interactions obj)))}]})))
mk-mod-int-add-xf
@@ -501,7 +509,7 @@
{:type :set-option
:page-id page-id
:option :flows
- :value (cto/remove-flow flows (:id flow))})))
+ :value (csp/remove-flow flows (:id flow))})))
mk-mod-add-flow-xf
(comp (filter some?)
@@ -583,7 +591,7 @@
y (:y data (- vbc-y (/ height 2)))
page-id (:current-page-id state)
frame-id (-> (wsh/lookup-page-objects state page-id)
- (cp/frame-id-by-position {:x frame-x :y frame-y}))
+ (cph/frame-id-by-position {:x frame-x :y frame-y}))
shape (-> (cp/make-minimal-shape type)
(merge data)
(merge {:x x :y y})
diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs
index 14ec47a2ac..c333a7a293 100644
--- a/frontend/src/app/main/data/workspace/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/drawing.cljs
@@ -42,6 +42,15 @@
(when (= tool :path)
(rx/of (start-drawing :path)))
+ (when (= tool :curve)
+ (let [stopper (->> stream (rx/filter dwc/interrupt?))]
+ (->> stream
+ (rx/filter (ptk/type? ::common/handle-finish-drawing))
+ (rx/take 1)
+ (rx/observe-on :async)
+ (rx/map #(select-for-drawing tool data))
+ (rx/take-until stopper))))
+
;; NOTE: comments are a special case and they manage they
;; own interrupt cycle.q
(when (and (not= tool :comments)
@@ -74,7 +83,7 @@
(rx/of (handle-drawing type))
(->> stream
(rx/filter (ptk/type? ::common/handle-finish-drawing) )
- (rx/first)
+ (rx/take 1)
(rx/map #(fn [state] (update state :workspace-drawing dissoc :lock)))))))))))
(defn handle-drawing
diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs
index 01632a744c..561a372b87 100644
--- a/frontend/src/app/main/data/workspace/drawing/box.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/box.cljs
@@ -9,7 +9,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.workspace.drawing.common :as common]
[app.main.data.workspace.state-helpers :as wsh]
@@ -62,7 +62,7 @@
layout (get state :workspace-layout)
zoom (get-in state [:workspace-local :zoom] 1)
- frames (cp/select-frames objects)
+ frames (cph/get-frames objects)
fid (or (->> frames
(filter #(gsh/has-point? % initial))
first
diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs
index fb93c6f5cc..624bbf3084 100644
--- a/frontend/src/app/main/data/workspace/drawing/common.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/common.cljs
@@ -24,7 +24,8 @@
(ptk/reify ::handle-finish-drawing
ptk/WatchEvent
(watch [_ state _]
- (let [shape (get-in state [:workspace-drawing :object])]
+ (let [tool (get-in state [:workspace-drawing :tool])
+ shape (get-in state [:workspace-drawing :object])]
(rx/concat
(when (:initialized? shape)
(let [page-id (:current-page-id state)
@@ -55,7 +56,7 @@
(rx/of (dwu/start-undo-transaction))
(rx/empty))
- (rx/of (dwc/add-shape shape))
+ (rx/of (dwc/add-shape shape {:no-select? (= tool :curve)}))
(if (= :frame (:type shape))
(->> (uw/ask! {:cmd :selection/query
diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs
index 463c1e9ead..f221ff4f25 100644
--- a/frontend/src/app/main/data/workspace/drawing/curve.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs
@@ -8,7 +8,7 @@
(:require
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.data.workspace.drawing.common :as common]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms]
@@ -44,10 +44,10 @@
ptk/UpdateEvent
(update [_ state]
- (let [objects (wsh/lookup-page-objects state)
- content (get-in state [:workspace-drawing :object :content] [])
+ (let [objects (wsh/lookup-page-objects state)
+ content (get-in state [:workspace-drawing :object :content] [])
position (get-in content [0 :params] nil)
- frame-id (cp/frame-id-by-position objects position)]
+ frame-id (cph/frame-id-by-position objects position)]
(-> state
(assoc-in [:workspace-drawing :object :frame-id] frame-id))))))
diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs
index bf452c58fe..2cfdc1a4ef 100644
--- a/frontend/src/app/main/data/workspace/groups.cljs
+++ b/frontend/src/app/main/data/workspace/groups.cljs
@@ -10,6 +10,7 @@
[app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.changes-builder :as cb]
+ [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]
@@ -22,7 +23,7 @@
(->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
- (map #(assoc % ::index (cp/position-on-parent (:id %) objects)))
+ (map #(assoc % ::index (cph/get-position-on-parent objects (:id %))))
(sort-by ::index)))
(defn- get-empty-groups-after-group-creation
@@ -34,10 +35,8 @@
group, one (or many) groups can become empty because they have had a
single shape which is moved to the created group."
[objects parent-id shapes]
- (let [ids (cp/clean-loops objects (into #{} (map :id) shapes))
- parents (->> ids
- (reduce #(conj %1 (cp/get-parent %2 objects))
- #{}))]
+ (let [ids (cph/clean-loops objects (into #{} (map :id) shapes))
+ parents (into #{} (map #(cph/get-parent-id objects %)) ids)]
(loop [current-id (first parents)
to-check (rest parents)
removed-id? ids
@@ -53,7 +52,7 @@
(empty? (remove removed-id? (:shapes group))))
;; Adds group to the remove and check its parent
- (let [to-check (concat to-check [(cp/get-parent current-id objects)]) ]
+ (let [to-check (concat to-check [(cph/get-parent-id objects current-id)]) ]
(recur (first to-check)
(rest to-check)
(conj removed-id? current-id)
@@ -139,7 +138,7 @@
(defn prepare-remove-group
[it page-id group objects]
(let [children (mapv #(get objects %) (:shapes group))
- parent-id (cp/get-parent (:id group) objects)
+ parent-id (cph/get-parent-id objects (:id group))
parent (get objects parent-id)
index-in-parent
@@ -149,7 +148,7 @@
(ffirst))
ids-to-detach (when (:component-id group)
- (cp/get-children (:id group) objects))
+ (cph/get-children-ids objects (:id group)))
detach-fn (fn [attrs]
(dissoc attrs
@@ -202,7 +201,7 @@
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
- selected (cp/clean-loops objects selected)
+ selected (cph/clean-loops objects selected)
shapes (shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [[group rchanges uchanges]
@@ -241,7 +240,7 @@
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
- selected (cp/clean-loops objects selected)
+ selected (cph/clean-loops objects selected)
shapes (shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs
new file mode 100644
index 0000000000..3fce84bf23
--- /dev/null
+++ b/frontend/src/app/main/data/workspace/guides.cljs
@@ -0,0 +1,115 @@
+;; 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.guides
+ (:require
+ [app.common.geom.point :as gpt]
+ [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.main.data.workspace.changes :as dwc]
+ [app.main.data.workspace.state-helpers :as wsh]
+ [beicon.core :as rx]
+ [cljs.spec.alpha :as s]
+ [potok.core :as ptk]))
+
+(defn make-update-guide [guide]
+ (fn [other]
+ (cond-> other
+ (= (:id other) (:id guide))
+ (merge guide))))
+
+(defn update-guides [guide]
+ (us/verify ::csp/guide guide)
+ (ptk/reify ::update-guides
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [page (wsh/lookup-page state)
+ guides (get-in page [:options :guides] {})
+ new-guides (assoc guides (:id guide) guide)
+
+ changes
+ (-> (pcb/empty-changes it)
+ (pcb/with-page page)
+ (pcb/set-page-option :guides new-guides))]
+ (rx/of (dwc/commit-changes changes))))))
+
+(defn remove-guide [guide]
+ (us/verify ::csp/guide guide)
+ (ptk/reify ::remove-guide
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [sdisj (fnil disj #{})]
+ (-> state
+ (update-in [:workspace-guides :hover] sdisj (:id guide)))))
+
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [page (wsh/lookup-page state)
+ guides (get-in page [:options :guides] {})
+ new-guides (dissoc guides (:id guide))
+
+ changes
+ (-> (pcb/empty-changes it)
+ (pcb/with-page page)
+ (pcb/set-page-option :guides new-guides))]
+ (rx/of (dwc/commit-changes changes))))))
+
+(defn remove-guides
+ [ids]
+ (ptk/reify ::remove-guides
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [page (wsh/lookup-page state)
+ guides (get-in page [:options :guides] {})
+ guides (-> (select-keys guides ids) (vals))]
+ (rx/from (->> guides (mapv #(remove-guide %))))))))
+
+(defn move-frame-guides
+ "Move guides that are inside a frame when that frame is moved"
+ [ids]
+ (us/verify (s/coll-of uuid?) ids)
+
+ (ptk/reify ::move-frame-guides
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [objects (wsh/lookup-page-objects state)
+
+ is-frame? (fn [id] (= :frame (get-in objects [id :type])))
+ frame-ids? (into #{} (filter is-frame?) ids)
+
+ object-modifiers (get state :workspace-modifiers)
+
+ build-move-event
+ (fn [guide]
+ (let [frame (get objects (:frame-id guide))
+ frame' (-> (merge frame (get object-modifiers (:frame-id guide)))
+ (gsh/transform-shape))
+
+ moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
+ (gpt/point (:x frame') (:y frame')))
+
+ guide (update guide :position + (get moved (:axis guide)))]
+ (update-guides guide)))]
+
+ (->> (wsh/lookup-page-options state)
+ :guides
+ (vals)
+ (filter (comp frame-ids? :frame-id))
+ (map build-move-event)
+ (rx/from))))))
+
+(defn set-hover-guide
+ [id hover?]
+ (ptk/reify ::set-hover-guide
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [sconj (fnil conj #{})
+ sdisj (fnil disj #{})]
+ (if hover?
+ (update-in state [:workspace-guides :hover] sconj id)
+ (update-in state [:workspace-guides :hover] sdisj id))))))
diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs
index bf7ba200f7..93331bd300 100644
--- a/frontend/src/app/main/data/workspace/interactions.cljs
+++ b/frontend/src/app/main/data/workspace/interactions.cljs
@@ -10,8 +10,8 @@
[app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
- [app.common.types.interactions :as cti]
- [app.common.types.page-options :as cto]
+ [app.common.spec.interactions :as csi]
+ [app.common.spec.page :as csp]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
@@ -45,7 +45,7 @@
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
- :value (cto/add-flow flows new-flow)}]
+ :value (csp/add-flow flows new-flow)}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
@@ -76,7 +76,7 @@
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
- :value (cto/remove-flow flows flow-id)}]
+ :value (csp/remove-flow flows flow-id)}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
@@ -100,8 +100,8 @@
{:redo-changes [{:type :set-option
:page-id page-id
:option :flows
- :value (cto/update-flow flows flow-id
- #(cto/rename-flow % name))}]
+ :value (csp/update-flow flows flow-id
+ #(csp/rename-flow % name))}]
:undo-changes [{:type :set-option
:page-id page-id
:option :flows
@@ -126,6 +126,14 @@
;; --- Interactions
+(defn- connected-frame?
+ "Check if some frame is origin or destination of any navigate interaction
+ 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))))))
+
(defn add-new-interaction
([shape] (add-new-interaction shape nil))
([shape destination]
@@ -134,22 +142,22 @@
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- frame (cph/get-frame shape objects)
+ frame (cph/get-frame objects shape)
flows (get-in state [:workspace-data
:pages-index
page-id
:options
:flows] [])
- flow (cto/get-frame-flow flows (:id frame))]
+ flow (csp/get-frame-flow flows (:id frame))]
(rx/concat
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
- (let [new-interaction (cti/set-destination
- cti/default-interaction
- destination)]
+ (let [new-interaction (csi/set-destination
+ csi/default-interaction
+ destination)]
(update shape :interactions
- cti/add-interaction new-interaction)))))
- (when (and (not (cph/connected-frame? (:id frame) objects))
+ csi/add-interaction new-interaction)))))
+ (when (and (not (connected-frame? objects (:id frame)))
(nil? flow))
(rx/of (add-flow (:id frame))))))))))
@@ -161,7 +169,16 @@
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
- cti/remove-interaction index)))))))
+ csi/remove-interaction index)))))))
+
+(defn remove-interactions
+ [ids]
+ (ptk/reify ::remove-interactions
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (dch/update-shapes ids
+ (fn [shape]
+ (assoc shape :interactions [])))))))
(defn update-interaction
[shape index update-fn]
@@ -171,7 +188,7 @@
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
- cti/update-interaction index update-fn)))))))
+ csi/update-interaction index update-fn)))))))
(declare move-edit-interaction)
(declare finish-edit-interaction)
@@ -244,11 +261,11 @@
(rx/of (update-interaction shape index
(fn [interaction]
(cond-> interaction
- (not (cti/has-destination interaction))
- (cti/set-action-type :navigate)
+ (not (csi/has-destination interaction))
+ (csi/set-action-type :navigate)
:always
- (cti/set-destination (:id frame))))))))))))))
+ (csi/set-destination (:id frame))))))))))))))
;; --- Overlays
(declare move-overlay-pos)
@@ -278,7 +295,7 @@
overlay-pos (-> shape
(get-in [:interactions index])
:overlay-position)
- orig-frame (cph/get-frame shape objects)
+ orig-frame (cph/get-frame objects shape)
frame-pos (gpt/point (:x orig-frame) (:y orig-frame))
offset (-> initial-pos
(gpt/subtract overlay-pos)
@@ -326,7 +343,7 @@
new-interactions
(update interactions index
- #(cti/set-overlay-position % overlay-pos))]
+ #(csi/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/layers.cljs b/frontend/src/app/main/data/workspace/layers.cljs
index 4344e13e89..0f06eac314 100644
--- a/frontend/src/app/main/data/workspace/layers.cljs
+++ b/frontend/src/app/main/data/workspace/layers.cljs
@@ -70,7 +70,7 @@
(let [opacity-events (->> stream ;; Stop buffering after time without opacities
(rx/filter (ptk/type? ::pressed-opacity))
(rx/buffer-time 600)
- (rx/first)
+ (rx/take 1)
(rx/map #(set-opacity (calculate-opacity (map deref %)))))]
(rx/concat
(rx/of (set-opacity (calculate-opacity [opacity]))) ;; First opacity is always fired
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 027e6514af..05bfb6d572 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -11,7 +11,12 @@
[app.common.geom.shapes :as geom]
[app.common.logging :as log]
[app.common.pages :as cp]
+ [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.uuid :as uuid]
[app.main.data.messages :as dm]
[app.main.data.workspace.changes :as dch]
@@ -88,7 +93,7 @@
color (-> color
(assoc :id id)
(assoc :name (default-color-name color)))]
- (us/assert ::cp/color color)
+ (us/assert ::spec.color/color color)
(ptk/reify ::add-color
IDeref
(-deref [_] color)
@@ -105,7 +110,7 @@
:origin it})))))))
(defn add-recent-color
[color]
- (us/assert ::cp/recent-color color)
+ (us/assert ::spec.color/recent-color color)
(ptk/reify ::add-recent-color
ptk/WatchEvent
(watch [it _ _]
@@ -123,12 +128,12 @@
(defn update-color
[{:keys [id] :as color} file-id]
- (us/assert ::cp/color color)
+ (us/assert ::spec.color/color color)
(us/assert ::us/uuid file-id)
(ptk/reify ::update-color
ptk/WatchEvent
(watch [it state _]
- (let [[path name] (cp/parse-path-name (:name color))
+ (let [[path name] (cph/parse-path-name (:name color))
color (assoc color :path path :name name)
prev (get-in state [:workspace-data :colors id])
rchg {:type :mod-color
@@ -159,7 +164,7 @@
(defn add-media
[{:keys [id] :as media}]
- (us/assert ::cp/media-object media)
+ (us/assert ::spec.file/media-object media)
(ptk/reify ::add-media
ptk/WatchEvent
(watch [it _ _]
@@ -180,7 +185,7 @@
ptk/WatchEvent
(watch [it state _]
(let [object (get-in state [:workspace-data :media id])
- [path name] (cp/parse-path-name new-name)
+ [path name] (cph/parse-path-name new-name)
rchanges [{:type :mod-media
:object {:id id
@@ -215,7 +220,7 @@
([typography] (add-typography typography true))
([typography edit?]
(let [typography (update typography :id #(or % (uuid/next)))]
- (us/assert ::cp/typography typography)
+ (us/assert ::spec.typography/typography typography)
(ptk/reify ::add-typography
IDeref
(-deref [_] typography)
@@ -235,14 +240,12 @@
(defn update-typography
[typography file-id]
- (us/assert ::cp/typography typography)
+ (us/assert ::spec.typography/typography typography)
(us/assert ::us/uuid file-id)
(ptk/reify ::update-typography
ptk/WatchEvent
(watch [it state _]
- (let [[path name] (cp/parse-path-name (:name typography))
- typography (assoc typography :path path :name name)
- prev (get-in state [:workspace-data :typographies (:id typography)])
+ (let [prev (get-in state [:workspace-data :typographies (:id typography)])
rchg {:type :mod-typography
:typography typography}
uchg {:type :mod-typography
@@ -303,7 +306,7 @@
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
- (cp/clean-loops objects))]
+ (cph/clean-loops objects))]
(rx/of (add-component2 selected))))))
(defn rename-component
@@ -318,7 +321,7 @@
;; are small posibilities of race conditions with component
;; deletion.
(when-let [component (get-in state [:workspace-data :components id])]
- (let [[path name] (cp/parse-path-name new-name)
+ (let [[path name] (cph/parse-path-name new-name)
objects (get component :objects)
;; Give the same name to the root shape
new-objects (assoc-in objects
@@ -336,7 +339,6 @@
:name (:name component)
:path (:path component)
:objects objects}]]
-
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it})))))))
@@ -347,12 +349,10 @@
(ptk/reify ::duplicate-component
ptk/WatchEvent
(watch [it state _]
- (let [component (cp/get-component id
- (:current-file-id state)
- (dwlh/get-local-file state)
- nil)
- all-components (vals (get-in state [:workspace-data :components]))
- unames (set (map :name all-components))
+ (let [libraries (dwlh/get-libraries state)
+ 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-shape new-shapes _updated-shapes]
@@ -399,14 +399,13 @@
[file-id component-id position]
(us/assert ::us/uuid file-id)
(us/assert ::us/uuid component-id)
- (us/assert ::us/point position)
+ (us/assert ::gpt/point position)
(ptk/reify ::instantiate-component
ptk/WatchEvent
(watch [it state _]
- (let [local-library (dwlh/get-local-file state)
- libraries (get state :workspace-libraries)
- component (cp/get-component component-id file-id local-library libraries)
- component-shape (cp/get-shape component component-id)
+ (let [libraries (dwlh/get-libraries state)
+ component (cph/get-component libraries file-id component-id)
+ component-shape (cph/get-shape component component-id)
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract position orig-pos)
@@ -415,7 +414,7 @@
objects (wsh/lookup-page-objects state page-id)
unames (volatile! (dwc/retrieve-used-names objects))
- frame-id (cp/frame-id-by-position objects (gpt/add orig-pos delta))
+ frame-id (cph/frame-id-by-position objects (gpt/add orig-pos delta))
update-new-shape
(fn [new-shape original-shape]
@@ -427,7 +426,7 @@
(cond-> new-shape
true
(as-> $
- (geom/move $ delta)
+ (geom/move $ delta)
(assoc $ :frame-id frame-id)
(assoc $ :parent-id
(or (:parent-id $) (:frame-id $)))
@@ -446,7 +445,7 @@
(dissoc :component-root?))))
[new-shape new-shapes _]
- (cp/clone-object component-shape
+ (cph/clone-object component-shape
nil
(get component :objects)
update-new-shape)
@@ -481,13 +480,12 @@
(ptk/reify ::detach-component
ptk/WatchEvent
(watch [it state _]
- (let [local-library (dwlh/get-local-file state)
- container (cp/get-container (get state :current-page-id)
- :page
- local-library)
+ (let [file (dwlh/get-local-file state)
+ page-id (get state :current-page-id)
+ container (cph/get-container file :page page-id)
[rchanges uchanges]
- (dwlh/generate-detach-instance id container)]
+ (dwlh/generate-detach-instance container id)]
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
@@ -497,20 +495,19 @@
(ptk/reify ::detach-selected-components
ptk/WatchEvent
(watch [it state _]
- (let [page-id (:current-page-id state)
- objects (wsh/lookup-page-objects state page-id)
- local-library (dwlh/get-local-file state)
- container (cp/get-container page-id :page local-library)
-
- selected (->> state
- (wsh/lookup-selected)
- (cp/clean-loops objects))
+ (let [page-id (:current-page-id state)
+ objects (wsh/lookup-page-objects state page-id)
+ file (dwlh/get-local-file state)
+ container (cph/get-container file :page page-id)
+ selected (->> state
+ (wsh/lookup-selected)
+ (cph/clean-loops objects))
[rchanges uchanges]
(reduce (fn [changes id]
(dwlh/concat-changes
- changes
- (dwlh/generate-detach-instance id container)))
+ changes
+ (dwlh/generate-detach-instance container id)))
dwlh/empty-changes
selected)]
@@ -536,7 +533,7 @@
(defn ext-library-changed
[file-id modified-at revn changes]
(us/assert ::us/uuid file-id)
- (us/assert ::cp/changes changes)
+ (us/assert ::spec.change/changes changes)
(ptk/reify ::ext-library-changed
ptk/UpdateEvent
(update [_ state]
@@ -556,21 +553,18 @@
ptk/WatchEvent
(watch [it state _]
(log/info :msg "RESET-COMPONENT of shape" :id (str id))
- (let [local-library (dwlh/get-local-file state)
- libraries (dwlh/get-libraries state)
- container (cp/get-container (get state :current-page-id)
- :page
- local-library)
- [rchanges uchanges]
- (dwlh/generate-sync-shape-direct container
- id
- local-library
- libraries
- true)]
- (log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes
- rchanges
- local-library))
+ (let [file (dwlh/get-local-file state)
+ libraries (dwlh/get-libraries state)
+ page-id (:current-page-id state)
+ container (cph/get-container file :page page-id)
+
+ [rchanges uchanges]
+ (dwlh/generate-sync-shape-direct libraries container id true)]
+
+ (log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes
+ rchanges
+ file))
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it}))))))
@@ -591,52 +585,52 @@
(watch [it state _]
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id))
(let [page-id (get state :current-page-id)
- local-library (dwlh/get-local-file state)
+
+ local-file (dwlh/get-local-file state)
libraries (dwlh/get-libraries state)
- [rchanges uchanges]
- (dwlh/generate-sync-shape-inverse page-id
- id
- local-library
- libraries)
+ container (cph/get-container local-file :page page-id)
+ shape (cph/get-shape container id)
+
+ [rchanges uchanges]
+ (dwlh/generate-sync-shape-inverse libraries container id)
- container (cp/get-container page-id :page local-library)
- shape (cp/get-shape container id)
file-id (:component-file shape)
file (dwlh/get-file state file-id)
xf-filter (comp
- (filter :local-change?)
- (map #(dissoc % :local-change?)))
+ (filter :local-change?)
+ (map #(dissoc % :local-change?)))
local-rchanges (into [] xf-filter rchanges)
local-uchanges (into [] xf-filter uchanges)
xf-remove (comp
- (remove :local-change?)
- (map #(dissoc % :local-change?)))
+ (remove :local-change?)
+ (map #(dissoc % :local-change?)))
rchanges (into [] xf-remove rchanges)
uchanges (into [] xf-remove uchanges)]
(log/debug :msg "UPDATE-COMPONENT finished"
:js/local-rchanges (log-changes
- local-rchanges
- local-library)
+ local-rchanges
+ file)
:js/rchanges (log-changes
- rchanges
- file))
+ rchanges
+ file))
- (rx/of (when (seq local-rchanges)
- (dch/commit-changes {:redo-changes local-rchanges
- :undo-changes local-uchanges
- :origin it
- :file-id (:id local-library)}))
- (when (seq rchanges)
- (dch/commit-changes {:redo-changes rchanges
- :undo-changes uchanges
- :origin it
- :file-id file-id})))))))
+ (rx/of
+ (when (seq local-rchanges)
+ (dch/commit-changes {:redo-changes local-rchanges
+ :undo-changes local-uchanges
+ :origin it
+ :file-id (:id local-file)}))
+ (when (seq rchanges)
+ (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it
+ :file-id file-id})))))))
(defn update-component-sync
[shape-id file-id]
@@ -652,6 +646,16 @@
(sync-file file-id file-id))
(dwu/commit-undo-transaction))))))
+(defn update-component-in-bulk
+ [shapes file-id]
+ (ptk/reify ::update-component-in-bulk
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/concat
+ (rx/of (dwu/start-undo-transaction))
+ (rx/map #(update-component-sync (:id %) file-id) (rx/from shapes))
+ (rx/of (dwu/commit-undo-transaction))))))
+
(declare sync-file-2nd-stage)
(defn sync-file
@@ -693,28 +697,28 @@
(sequence xf-scat file-changes))]
(log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes
- rchanges
- file))
+ rchanges
+ file))
(rx/concat
- (rx/of (dm/hide-tag :sync-dialog))
- (when rchanges
- (rx/of (dch/commit-changes {:redo-changes rchanges
- :undo-changes uchanges
- :origin it
- :file-id file-id})))
- (when (not= file-id library-id)
+ (rx/of (dm/hide-tag :sync-dialog))
+ (when rchanges
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it
+ :file-id file-id})))
+ (when (not= file-id library-id)
;; When we have just updated the library file, give some time for the
;; update to finish, before marking this file as synced.
;; TODO: look for a more precise way of syncing this.
;; Maybe by using the stream (second argument passed to watch)
;; to wait for the corresponding changes-committed and then proceed
;; with the :update-sync mutation.
- (rx/concat (rx/timer 3000)
- (rp/mutation :update-sync
- {:file-id file-id
- :library-id library-id})))
- (when (some? library-changes)
- (rx/of (sync-file-2nd-stage file-id library-id))))))))
+ (rx/concat (rx/timer 3000)
+ (rp/mutation :update-sync
+ {:file-id file-id
+ :library-id library-id})))
+ (when (some? library-changes)
+ (rx/of (sync-file-2nd-stage file-id library-id))))))))
(defn sync-file-2nd-stage
"If some components have been modified, we need to launch another synchronization
@@ -741,8 +745,8 @@
uchanges (d/concat-vec uchanges1 uchanges2)]
(when rchanges
(log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes
- rchanges
- file))
+ rchanges
+ file))
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it
@@ -777,11 +781,11 @@
(st/emit! dm/hide))]
(rx/of (dm/info-dialog
- (tr "workspace.updates.there-are-updates")
- :inline-actions
- [{:label (tr "workspace.updates.update")
- :callback do-update}
- {:label (tr "workspace.updates.dismiss")
- :callback do-dismiss}]
- :sync-dialog))))))
+ (tr "workspace.updates.there-are-updates")
+ :inline-actions
+ [{:label (tr "workspace.updates.update")
+ :callback do-update}
+ {:label (tr "workspace.updates.dismiss")
+ :callback do-dismiss}]
+ :sync-dialog))))))
diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
index b917a971d6..d2e62573fd 100644
--- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
@@ -11,6 +11,7 @@
[app.common.geom.shapes :as geom]
[app.common.logging :as log]
[app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.text :as txt]
[app.main.data.workspace.groups :as dwg]
@@ -57,7 +58,7 @@
(letfn [(concat-changes' [[rchanges1 uchanges1] [rchanges2 uchanges2]]
[(d/concat-vec rchanges1 rchanges2)
(d/concat-vec uchanges1 uchanges2)])]
- (transduce (remove nil?) concat-changes' empty-changes rest)))
+ (transduce (remove nil?) (completing concat-changes') empty-changes rest)))
(defn get-local-file
[state]
@@ -70,8 +71,12 @@
(get-in state [:workspace-libraries file-id :data])))
(defn get-libraries
+ "Retrieve all libraries, including the local file."
[state]
- (get state :workspace-libraries))
+ (let [{:keys [id] :as local} (:workspace-data state)]
+ (-> (:workspace-libraries state)
+ (assoc id {:id id
+ :data local}))))
(defn pretty-file
[file-id state]
@@ -120,7 +125,7 @@
(some? (:parent-id new-shape))
(dissoc :component-root?)))]
- (cp/clone-object shape nil objects update-new-shape update-original-shape)))
+ (cph/clone-object shape nil objects update-new-shape update-original-shape)))
(defn generate-add-component
"If there is exactly one id, and it's a group, use it as root. Otherwise,
@@ -204,18 +209,18 @@
"Clone the root shape of the component and all children. Generate new
ids from all of them."
[component]
- (let [component-root (cp/get-component-root component)]
- (cp/clone-object component-root
- nil
- (get component :objects)
- identity)))
+ (let [component-root (cph/get-component-root component)]
+ (cph/clone-object component-root
+ nil
+ (get component :objects)
+ identity)))
(defn generate-detach-instance
"Generate changes to remove the links between a shape and all its children
with a component."
- [shape-id container]
+ [container shape-id]
(log/debug :msg "Detach instance" :shape-id shape-id :container (:id container))
- (let [shapes (cp/get-object-with-children shape-id (:objects container))
+ (let [shapes (cph/get-children-with-self (:objects container) shape-id)
rchanges (mapv (fn [obj]
(make-change
container
@@ -293,7 +298,7 @@
(generate-sync-container asset-type
library-id
state
- (cp/make-container page :page))]
+ (cph/make-container page :page))]
(recur (next pages)
(into rchanges page-rchanges)
(into uchanges page-uchanges)))
@@ -319,8 +324,7 @@
(generate-sync-container asset-type
library-id
state
- (cp/make-container local-component
- :component))]
+ (cph/make-container local-component :component))]
(recur (next local-components)
(into rchanges comp-rchanges)
(into uchanges comp-uchanges)))
@@ -331,12 +335,13 @@
or a component) that use assets of the given type in the given library."
[asset-type library-id state container]
- (if (cp/page? container)
+ (if (cph/page? container)
(log/debug :msg "Sync page in local file" :page-id (:id container))
(log/debug :msg "Sync component in local library" :component-id (:id container)))
- (let [has-asset-reference? (has-asset-reference-fn asset-type library-id (cp/page? container))
- linked-shapes (cp/select-objects has-asset-reference? container)]
+ (let [has-asset-reference? (has-asset-reference-fn asset-type library-id (cph/page? container))
+ linked-shapes (->> (vals (:objects container))
+ (filter has-asset-reference?))]
(loop [shapes (seq linked-shapes)
rchanges []
uchanges []]
@@ -398,11 +403,9 @@
(defmethod generate-sync-shape :components
[_ _ state container shape]
- (generate-sync-shape-direct container
- (:id shape)
- (get-local-file state)
- (get-libraries state)
- false))
+ (let [shape-id (:id shape)
+ libraries (get-libraries state)]
+ (generate-sync-shape-direct libraries container shape-id false)))
(defn- generate-sync-text-shape
[shape container update-node]
@@ -624,19 +627,18 @@
(defn generate-sync-shape-direct
"Generate changes to synchronize one shape that the root of a component
instance, and all its children, from the given component."
- [container shape-id local-library libraries reset?]
+ [libraries container shape-id reset?]
(log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?)
- (let [shape-inst (cp/get-shape container shape-id)
- component (cp/get-component (:component-id shape-inst)
- (:component-file shape-inst)
- local-library
- libraries)
- shape-main (cp/get-shape component (:shape-ref shape-inst))
+ (let [shape-inst (cph/get-shape container shape-id)
+ component (cph/get-component libraries
+ (:component-file shape-inst)
+ (:component-id shape-inst))
+ shape-main (cph/get-shape component (:shape-ref shape-inst))
initial-root? (:component-root? shape-inst)
root-inst shape-inst
- root-main (cp/get-component-root component)]
+ root-main (cph/get-component-root component)]
(if component
(generate-sync-shape-direct-recursive container
@@ -683,9 +685,9 @@
(when set-remote-synced?
(change-remote-synced shape-inst container true)))
- children-inst (mapv #(cp/get-shape container %)
+ children-inst (mapv #(cph/get-shape container %)
(:shapes shape-inst))
- children-main (mapv #(cp/get-shape component %)
+ children-main (mapv #(cph/get-shape component %)
(:shapes shape-main))
only-inst (fn [child-inst]
@@ -743,20 +745,18 @@
(defn generate-sync-shape-inverse
"Generate changes to update the component a shape is linked to, from
the values in the shape and all its children."
- [page-id shape-id local-library libraries]
+ [libraries container shape-id]
(log/debug :msg "Sync shape inverse" :shape (str shape-id))
- (let [container (cp/get-container page-id :page local-library)
- shape-inst (cp/get-shape container shape-id)
- component (cp/get-component (:component-id shape-inst)
- (:component-file shape-inst)
- local-library
- libraries)
- shape-main (cp/get-shape component (:shape-ref shape-inst))
+ (let [shape-inst (cph/get-shape container shape-id)
+ component (cph/get-component libraries
+ (:component-file shape-inst)
+ (:component-id shape-inst))
+ shape-main (cph/get-shape component (:shape-ref shape-inst))
initial-root? (:component-root? shape-inst)
root-inst shape-inst
- root-main (cp/get-component-root component)]
+ root-main (cph/get-component-root component)]
(if component
(generate-sync-shape-inverse-recursive container
@@ -777,7 +777,7 @@
(if (nil? shape-main)
;; This should not occur, but protect against it in any case
empty-changes
- (let [component-container (cp/make-container component :component)
+ (let [component-container (cph/make-container component :component)
omit-touched? false
set-remote-synced? (not initial-root?)
@@ -805,9 +805,9 @@
(when set-remote-synced?
(change-remote-synced shape-inst container true)))
- children-inst (mapv #(cp/get-shape container %)
+ children-inst (mapv #(cph/get-shape container %)
(:shapes shape-inst))
- children-main (mapv #(cp/get-shape component %)
+ children-main (mapv #(cph/get-shape component %)
(:shapes shape-main))
only-inst (fn [child-inst]
@@ -885,13 +885,13 @@
(transduce (map only-inst-cb) concat-changes changes children-inst)
:else
- (if (cp/is-main-of child-main child-inst)
+ (if (cph/is-main-of? child-main child-inst)
(recur (next children-inst)
(next children-main)
(concat-changes changes (both-cb child-inst child-main)))
- (let [child-inst' (d/seek #(cp/is-main-of child-main %) children-inst)
- child-main' (d/seek #(cp/is-main-of % child-inst) children-main)]
+ (let [child-inst' (d/seek #(cph/is-main-of? child-main %) children-inst)
+ child-main' (d/seek #(cph/is-main-of? % child-inst) children-main)]
(cond
(nil? child-inst')
(recur children-inst
@@ -919,13 +919,13 @@
(defn- add-shape-to-instance
[component-shape index component container root-instance root-main omit-touched? set-remote-synced?]
(log/info :msg (str "ADD [P] " (:name component-shape)))
- (let [component-parent-shape (cp/get-shape component (:parent-id component-shape))
- parent-shape (d/seek #(cp/is-main-of component-parent-shape %)
- (cp/get-object-with-children (:id root-instance)
- (:objects container)))
- all-parents (vec (cons (:id parent-shape)
- (cp/get-parents (:id parent-shape)
- (:objects container))))
+ (let [component-parent-shape (cph/get-shape component (:parent-id component-shape))
+ parent-shape (d/seek #(cph/is-main-of? component-parent-shape %)
+ (cph/get-children-with-self (:objects container)
+ (:id root-instance)))
+ all-parents (into [(:id parent-shape)]
+ (cph/get-parent-ids (:objects container)
+ (:id parent-shape)))
update-new-shape (fn [new-shape original-shape]
(let [new-shape (reposition-shape new-shape
@@ -945,11 +945,11 @@
original-shape)
[_ new-shapes _]
- (cp/clone-object component-shape
- (:id parent-shape)
- (get component :objects)
- update-new-shape
- update-original-shape)
+ (cph/clone-object component-shape
+ (:id parent-shape)
+ (get component :objects)
+ update-new-shape
+ update-original-shape)
rchanges (d/concat-vec
(map (fn [shape']
@@ -978,20 +978,20 @@
:ignore-touched true}))
new-shapes)]
- (if (and (cp/touched-group? parent-shape :shapes-group) omit-touched?)
+ (if (and (cph/touched-group? parent-shape :shapes-group) omit-touched?)
empty-changes
[rchanges uchanges])))
(defn- add-shape-to-main
[shape index component page root-instance root-main]
(log/info :msg (str "ADD [C] " (:name shape)))
- (let [parent-shape (cp/get-shape page (:parent-id shape))
- component-parent-shape (d/seek #(cp/is-main-of % parent-shape)
- (cp/get-object-with-children (:id root-main)
- (:objects component)))
- all-parents (vec (cons (:id component-parent-shape)
- (cp/get-parents (:id component-parent-shape)
- (:objects component))))
+ (let [parent-shape (cph/get-shape page (:parent-id shape))
+ component-parent-shape (d/seek #(cph/is-main-of? % parent-shape)
+ (cph/get-children-with-self (:objects component)
+ (:id root-main)))
+ all-parents (into [(:id component-parent-shape)]
+ (cph/get-parent-ids (:objects component)
+ (:id component-parent-shape)))
update-new-shape (fn [new-shape _original-shape]
(reposition-shape new-shape
@@ -1005,11 +1005,11 @@
original-shape))
[_new-shape new-shapes updated-shapes]
- (cp/clone-object shape
- (:id component-parent-shape)
- (get page :objects)
- update-new-shape
- update-original-shape)
+ (cph/clone-object shape
+ (:id component-parent-shape)
+ (get page :objects)
+ update-new-shape
+ update-original-shape)
rchanges (d/concat-vec
(map (fn [shape']
@@ -1057,12 +1057,12 @@
(defn- remove-shape
[shape container omit-touched?]
(log/info :msg (str "REMOVE-SHAPE "
- (if (cp/page? container) "[P] " "[C] ")
+ (if (cph/page? container) "[P] " "[C] ")
(:name shape)))
(let [objects (get container :objects)
- parents (cp/get-parents (:id shape) objects)
+ parents (cph/get-parent-ids objects (:id shape))
parent (first parents)
- children (cp/get-children (:id shape) objects)
+ children (cph/get-children-ids objects (:id shape))
rchanges [(make-change
container
@@ -1080,7 +1080,7 @@
container
(as-> {:type :add-obj
:id id
- :index (cp/position-on-parent id objects)
+ :index (cph/get-position-on-parent objects id)
:parent-id (:parent-id shape')
:ignore-touched true
:obj shape'} $
@@ -1096,20 +1096,20 @@
{:type :reg-objects
:shapes (vec parents)})])]
- (if (and (cp/touched-group? parent :shapes-group) omit-touched?)
+ (if (and (cph/touched-group? parent :shapes-group) omit-touched?)
empty-changes
[rchanges uchanges])))
(defn- move-shape
[shape index-before index-after container omit-touched?]
(log/info :msg (str "MOVE "
- (if (cp/page? container) "[P] " "[C] ")
+ (if (cph/page? container) "[P] " "[C] ")
(:name shape)
" "
index-before
" -> "
index-after))
- (let [parent (cp/get-shape container (:parent-id shape))
+ (let [parent (cph/get-shape container (:parent-id shape))
rchanges [(make-change
container
@@ -1126,7 +1126,7 @@
:index index-before
:ignore-touched true})]]
- (if (and (cp/touched-group? parent :shapes-group) omit-touched?)
+ (if (and (cph/touched-group? parent :shapes-group) omit-touched?)
empty-changes
[rchanges uchanges])))
@@ -1138,7 +1138,7 @@
empty-changes
(do
(log/info :msg (str "CHANGE-TOUCHED "
- (if (cp/page? container) "[P] " "[C] ")
+ (if (cph/page? container) "[P] " "[C] ")
(:name dest-shape))
:options options)
(let [new-touched (cond
@@ -1174,7 +1174,7 @@
empty-changes
(do
(log/info :msg (str "CHANGE-REMOTE-SYNCED? "
- (if (cp/page? container) "[P] " "[C] ")
+ (if (cph/page? container) "[P] " "[C] ")
(:name shape))
:remote-synced? remote-synced?)
(let [rchanges [(make-change
@@ -1205,7 +1205,7 @@
(log/info :msg (str "SYNC "
(:name origin-shape)
" -> "
- (if (cp/page? container) "[P] " "[C] ")
+ (if (cph/page? container) "[P] " "[C] ")
(:name dest-shape)))
(let [; To synchronize geometry attributes we need to make a prior
@@ -1224,8 +1224,8 @@
(let [attr (first attrs)]
(if (nil? attr)
- (let [all-parents (vec (or (cp/get-parents (:id dest-shape)
- (:objects container)) []))
+ (let [all-parents (cph/get-parent-ids (:objects container)
+ (:id dest-shape))
rchanges [(make-change
container
{:type :mod-obj
@@ -1285,7 +1285,7 @@
(defn- make-change
[container change]
- (if (cp/page? container)
+ (if (cph/page? container)
(assoc change :page-id (:id container))
(assoc change :component-id (:id container))))
diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs
index 73172018c1..f104e8874d 100644
--- a/frontend/src/app/main/data/workspace/notifications.cljs
+++ b/frontend/src/app/main/data/workspace/notifications.cljs
@@ -8,8 +8,8 @@
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
- [app.common.pages :as cp]
[app.common.spec :as us]
+ [app.common.spec.change :as spec.change]
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
@@ -201,7 +201,7 @@
(s/def ::file-id uuid?)
(s/def ::session-id uuid?)
(s/def ::revn integer?)
-(s/def ::changes ::cp/changes)
+(s/def ::changes ::spec.change/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/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs
index c31745babd..87fb18be16 100644
--- a/frontend/src/app/main/data/workspace/path/changes.cljs
+++ b/frontend/src/app/main/data/workspace/path/changes.cljs
@@ -6,7 +6,7 @@
(ns app.main.data.workspace.path.changes
(:require
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.path.helpers :as helpers]
@@ -24,7 +24,7 @@
(let [shape-id (:id shape)
frame-id (:frame-id shape)
parent-id (:parent-id shape)
- parent-index (cp/position-on-parent shape-id objects)
+ parent-index (cph/get-position-on-parent objects shape-id)
[old-points old-selrect] (helpers/content->points+selrect shape old-content)
[new-points new-selrect] (helpers/content->points+selrect shape new-content)
diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs
index 8e7de7caeb..a6f0a41f51 100644
--- a/frontend/src/app/main/data/workspace/path/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/path/drawing.cljs
@@ -8,7 +8,7 @@
(:require
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]
[app.common.path.shapes-to-path :as upsp]
[app.common.spec :as us]
@@ -255,10 +255,10 @@
(ptk/reify ::setup-frame-path
ptk/UpdateEvent
(update [_ state]
- (let [objects (wsh/lookup-page-objects state)
- content (get-in state [:workspace-drawing :object :content] [])
+ (let [objects (wsh/lookup-page-objects state)
+ content (get-in state [:workspace-drawing :object :content] [])
position (get-in content [0 :params] nil)
- frame-id (cp/frame-id-by-position objects position)]
+ frame-id (cph/frame-id-by-position objects position)]
(-> state
(assoc-in [:workspace-drawing :object :frame-id] frame-id))))))
diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs
index eae4dfb91e..54dfda1783 100644
--- a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs
+++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs
@@ -6,8 +6,8 @@
(ns app.main.data.workspace.path.shapes-to-path
(:require
- [app.common.pages :as cp]
[app.common.pages.changes-builder :as cb]
+ [app.common.pages.helpers :as cph]
[app.common.path.shapes-to-path :as upsp]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
@@ -24,7 +24,7 @@
children-ids
(into #{}
- (mapcat #(cp/get-children % objects))
+ (mapcat #(cph/get-children-ids objects %))
selected)
changes
diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs
index eb89d83b2e..82f856c0ae 100644
--- a/frontend/src/app/main/data/workspace/persistence.cljs
+++ b/frontend/src/app/main/data/workspace/persistence.cljs
@@ -9,7 +9,10 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
+ [app.common.spec.change :as spec.change]
+ [app.common.spec.file :as spec.file]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
@@ -200,7 +203,7 @@
:updated-at (dt/now)))))))
(s/def ::shapes-changes-persisted
- (s/keys :req-un [::revn ::cp/changes]))
+ (s/keys :req-un [::revn ::spec.change/changes]))
(defn shapes-persisted-event? [event]
(= (ptk/type event) ::changes-persisted))
@@ -238,7 +241,7 @@
(s/def ::version ::us/integer)
(s/def ::revn ::us/integer)
(s/def ::ordering ::us/integer)
-(s/def ::data ::cp/data)
+(s/def ::data ::spec.file/data)
(s/def ::file ::dd/file)
(s/def ::project ::dd/project)
@@ -656,7 +659,7 @@
(rx/map extract-frame-changes)
(rx/share))
- frames (-> state wsh/lookup-page-objects cp/select-frames)
+ frames (-> state wsh/lookup-page-objects cph/get-frames)
no-thumb-frames (->> frames
(filter (comp nil? :thumbnail))
(mapv :id))]
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index 5763b5e840..08ec41784a 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -10,9 +10,9 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
- [app.common.types.interactions :as cti]
+ [app.common.spec.interactions :as cti]
[app.common.uuid :as uuid]
[app.main.data.modal :as md]
[app.main.data.workspace.changes :as dch]
@@ -47,51 +47,64 @@
(assoc-in state [:workspace-local :selrect] selrect))))
(defn handle-area-selection
- [preserve?]
- (letfn [(data->selrect [data]
- (let [start (:start data)
- stop (:stop data)
- start-x (min (:x start) (:x stop))
- start-y (min (:y start) (:y stop))
- end-x (max (:x start) (:x stop))
- end-y (max (:y start) (:y stop))]
- {:type :rect
- :x start-x
- :y start-y
- :width (mth/abs (- end-x start-x))
- :height (mth/abs (- end-y start-y))}))]
- (ptk/reify ::handle-area-selection
- 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)))
- stoper (->> stream (rx/filter stop?))
+ [preserve? ignore-groups?]
+ (ptk/reify ::handle-area-selection
+ 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)))
+ stoper (->> stream (rx/filter stop?))
- calculate-selrect
- (fn [data pos]
- (if data
- (assoc data :stop pos)
- {:start pos :stop pos}))
+ init-selrect
+ {:type :rect
+ :x1 (:x @ms/mouse-position)
+ :y1 (:y @ms/mouse-position)
+ :x2 (:x @ms/mouse-position)
+ :y2 (:y @ms/mouse-position)}
- selrect-stream
- (->> ms/mouse-position
- (rx/scan calculate-selrect nil)
- (rx/map data->selrect)
- (rx/filter #(or (> (:width %) (/ 10 zoom))
- (> (:height %) (/ 10 zoom))))
- (rx/take-until stoper))]
- (rx/concat
- (if preserve?
- (rx/empty)
- (rx/of (deselect-all)))
+ calculate-selrect
+ (fn [selrect [delta space?]]
+ (let [result
+ (cond-> selrect
+ :always
+ (-> (update :x2 + (:x delta))
+ (update :y2 + (:y delta)))
- (rx/merge
- (->> selrect-stream (rx/map update-selrect))
- (->> selrect-stream
- (rx/debounce 50)
- (rx/map #(select-shapes-by-current-selrect preserve?))))
+ space?
+ (-> (update :x1 + (:x delta))
+ (update :y1 + (:y delta))))]
+ (assoc result
+ :x (min (:x1 result) (:x2 result))
+ :y (min (:y1 result) (:y2 result))
+ :width (mth/abs (- (:x2 result) (:x1 result)))
+ :height (mth/abs (- (:y2 result) (:y1 result))))))
- (rx/of (update-selrect nil))))))))
+ selrect-stream
+ (->> ms/mouse-position
+ (rx/buffer 2 1)
+ (rx/map (fn [[from to]] (when (and from to) (gpt/to-vec from to))))
+ (rx/filter some?)
+ (rx/with-latest-from ms/keyboard-space)
+ (rx/scan calculate-selrect init-selrect)
+ (rx/filter #(or (> (:width %) (/ 10 zoom))
+ (> (:height %) (/ 10 zoom))))
+ (rx/take-until stoper))]
+ (rx/concat
+ (if preserve?
+ (rx/empty)
+ (rx/of (deselect-all)))
+
+ (rx/merge
+ (->> selrect-stream
+ (rx/map update-selrect))
+
+ (->> selrect-stream
+ (rx/buffer-time 100)
+ (rx/map #(last %))
+ (rx/dedupe)
+ (rx/map #(select-shapes-by-current-selrect preserve? ignore-groups?))))
+
+ (rx/of (update-selrect nil)))))))
;; --- Toggle shape's selection status (selected or deselected)
@@ -138,7 +151,7 @@
(conj id))]
(-> state
(assoc-in [:workspace-local :selected]
- (cp/expand-region-selection objects selection))))))))
+ (cph/expand-region-selection objects selection))))))))
(defn select-shapes
[ids]
@@ -150,7 +163,7 @@
ptk/WatchEvent
(watch [_ state _]
- (let [objects (wsh/lookup-page-objects state)]
+ (let [objects (wsh/lookup-page-objects state)]
(rx/of (dwc/expand-all-parents ids objects))))))
(defn select-all
@@ -158,39 +171,23 @@
(ptk/reify ::select-all
ptk/WatchEvent
(watch [_ state _]
- (let [page-id (:current-page-id state)
- objects (wsh/lookup-page-objects state page-id)
- new-selected (let [selected-objs
- (->> (wsh/lookup-selected state)
- (map #(get objects %)))
+ (let [page-id (:current-page-id state)
+ objects (wsh/lookup-page-objects state page-id)
- frame-ids
- (reduce #(conj %1 (:frame-id %2))
- #{}
- selected-objs)
+ selected (let [frame-ids (into #{} (comp
+ (map (d/getf objects))
+ (map :frame-id))
+ (wsh/lookup-selected state))
+ frame-id (if (= 1 (count frame-ids))
+ (first frame-ids)
+ uuid/zero)]
+ (cph/get-immediate-children objects frame-id))
- common-frame-id
- (when (= (count frame-ids) 1) (first frame-ids))]
+ selected (into (d/ordered-set)
+ (comp (remove :blocked) (map :id))
+ selected)]
- (if (and common-frame-id
- (not= (:id common-frame-id) uuid/zero))
- (-> (get objects common-frame-id)
- :shapes)
- (->> (cp/select-toplevel-shapes objects
- {:include-frames? true
- :include-frame-children? false})
- (map :id))))
-
- is-not-blocked (fn [shape-id] (not (get-in state [:workspace-data
- :pages-index page-id
- :objects shape-id
- :blocked] false)))
-
- selected-ids (into lks/empty-linked-set
- (comp (filter some?)
- (filter is-not-blocked))
- new-selected)]
- (rx/of (select-shapes selected-ids))))))
+ (rx/of (select-shapes selected))))))
(defn deselect-all
"Clear all possible state of drawing, edition
@@ -216,7 +213,7 @@
;; --- Select Shapes (By selrect)
(defn select-shapes-by-current-selrect
- [preserve?]
+ [preserve? ignore-groups?]
(ptk/reify ::select-shapes-by-current-selrect
ptk/WatchEvent
(watch [_ state _]
@@ -235,8 +232,9 @@
:page-id page-id
:rect selrect
:include-frames? true
+ :ignore-groups? ignore-groups?
:full-frame? true})
- (rx/map #(cp/clean-loops objects %))
+ (rx/map #(cph/clean-loops objects %))
(rx/map #(into initial-set (filter (comp not blocked?)) %))
(rx/map select-shapes)))))))
@@ -256,7 +254,7 @@
;; in the later vector position
selected (->> children
reverse
- (d/seek #(geom/has-point? % position)))]
+ (d/seek #(geom/has-point? % position)))]
(when selected
(rx/of (select-shape (:id selected))))))))
@@ -300,11 +298,8 @@
[objects page-id unames ids delta]
(let [unames (volatile! unames)
update-unames! (fn [new-name] (vswap! unames conj new-name))
- all-ids (reduce (fn [ids-set id]
- (into ids-set (cons id (cp/get-children id objects))))
- #{}
- ids)
- ids-map (into {} (map #(vector % (uuid/next)) all-ids))]
+ all-ids (reduce #(into %1 (cons %2 (cph/get-children-ids objects %2))) #{} ids)
+ ids-map (into {} (map #(vector % (uuid/next))) all-ids)]
(loop [ids (seq ids)
chgs []]
(if ids
@@ -312,8 +307,8 @@
result (prepare-duplicate-change objects page-id unames update-unames! ids-map id delta)
result (if (vector? result) result [result])]
(recur
- (next ids)
- (into chgs result)))
+ (next ids)
+ (into chgs result)))
chgs))))
(defn duplicate-changes-update-indices
@@ -323,7 +318,7 @@
(let [process-id
(fn [index-map id]
(let [parent-id (get-in objects [id :parent-id])
- parent-index (cp/position-on-parent id objects)]
+ parent-index (cph/get-position-on-parent objects id)]
(update index-map parent-id (fnil conj []) [id parent-index])))
index-map (reduce process-id {} ids)]
(-> changes (update-indices index-map))))
@@ -331,7 +326,7 @@
(defn- prepare-duplicate-change
[objects page-id unames update-unames! ids-map id delta]
(let [obj (get objects id)]
- (if (= :frame (:type obj))
+ (if (cph/frame-shape? obj)
(prepare-duplicate-frame-change objects page-id unames update-unames! ids-map obj delta)
(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map obj delta (:frame-id obj) (:parent-id obj)))))
@@ -384,12 +379,12 @@
(mapcat #(prepare-duplicate-shape-change objects page-id unames update-unames! ids-map % delta new-id new-id)))
new-frame (-> obj
- (assoc :id new-id
- :name frame-name
- :frame-id uuid/zero
- :shapes [])
- (geom/move delta)
- (d/update-when :interactions #(cti/remap-interactions % ids-map objects)))
+ (assoc :id new-id
+ :name frame-name
+ :frame-id uuid/zero
+ :shapes [])
+ (geom/move delta)
+ (d/update-when :interactions #(cti/remap-interactions % ids-map objects)))
fch {:type :add-obj
:old-id (:id obj)
@@ -435,7 +430,7 @@
;; The default is leave normal shapes in place, but put
;; new frames to the right of the original.
- (if (= (:type obj) :frame)
+ (if (cph/frame-shape? obj)
(gpt/point (+ (:width obj) 50) 0)
(gpt/point 0 0))
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 939f9e26ed..64fcba6299 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -17,6 +17,7 @@
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
+ [app.main.ui.hooks.resize :as r]
[app.util.dom :as dom]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -38,9 +39,17 @@
:command (ds/a-mod "h")
:fn #(st/emit! (dw/go-to-layout :document-history))}
- :toggle-palette {:tooltip (ds/alt "P")
- :command (ds/a-mod "p")
- :fn #(st/emit! (dw/toggle-layout-flags :colorpalette))}
+ :toggle-colorpalette {:tooltip (ds/alt "P")
+ :command (ds/a-mod "p")
+ :fn #(do (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :textpalette)
+ (dw/toggle-layout-flags :colorpalette)))}
+
+ :toggle-textpalette {:tooltip (ds/alt "T")
+ :command (ds/a-mod "t")
+ :fn #(do (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :colorpalette)
+ (dw/toggle-layout-flags :textpalette)))}
:toggle-rules {:tooltip (ds/meta-shift "R")
:command (ds/c-mod "shift+r")
@@ -58,6 +67,10 @@
:command (ds/c-mod "shift+'")
:fn #(st/emit! (dw/toggle-layout-flags :snap-grid))}
+ :toggle-snap-guide {:tooltip (ds/meta-shift "G")
+ :command (ds/c-mod "shift+G")
+ :fn #(st/emit! (dw/toggle-layout-flags :snap-guides))}
+
:toggle-alignment {:tooltip (ds/meta "\\")
:command (ds/c-mod "\\")
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
@@ -67,7 +80,7 @@
:fn #(st/emit! (dw/toggle-layout-flags :scale-text))}
:increase-zoom {:tooltip "+"
- :command "+"
+ :command ["+" "="]
:fn #(st/emit! (dw/increase-zoom nil))}
:decrease-zoom {:tooltip "-"
@@ -177,7 +190,8 @@
:cut {:tooltip (ds/meta "X")
:command (ds/c-mod "x")
- :fn #(st/emit! (dw/copy-selected) dw/delete-selected)}
+ :fn #(st/emit! (dw/copy-selected)
+ (dw/delete-selected))}
:paste {:tooltip (ds/meta "V")
:disabled true
@@ -186,7 +200,7 @@
:delete {:tooltip (ds/supr)
:command ["del" "backspace"]
- :fn #(st/emit! dw/delete-selected)}
+ :fn #(st/emit! (dw/delete-selected))}
:bring-forward {:tooltip (ds/meta ds/up-arrow)
:command (ds/c-mod "up")
@@ -334,9 +348,15 @@
:command (ds/c-mod "alt+l")
:fn #(st/emit! (dw/toggle-proportion-lock))}
- :create-artboard-from-selection {:tooltip (ds/meta (ds/alt "G"))
- :command (ds/c-mod "alt+g")
- :fn #(st/emit! (dw/create-artboard-from-selection))}})
+ :create-artboard-from-selection {:tooltip (ds/meta (ds/alt "G"))
+ :command (ds/c-mod "alt+g")
+ :fn #(st/emit! (dw/create-artboard-from-selection))}
+
+ :hide-ui {:tooltip "\\"
+ :command "\\"
+ :fn #(st/emit! (dw/toggle-layout-flags :hide-ui))}
+
+ })
(def opacity-shortcuts
(into {} (->>
diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs
index f144670bac..bade10256e 100644
--- a/frontend/src/app/main/data/workspace/state_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/state_helpers.cljs
@@ -7,7 +7,13 @@
(ns app.main.data.workspace.state-helpers
(:require
[app.common.data :as d]
- [app.common.pages :as cp]))
+ [app.common.pages.helpers :as cph]))
+
+(defn lookup-page
+ ([state]
+ (lookup-page state (:current-page-id state)))
+ ([state page-id]
+ (get-in state [:workspace-data :pages-index page-id])))
(defn lookup-page-objects
([state]
@@ -37,7 +43,7 @@
:or {omit-blocked? false}}]
(let [objects (lookup-page-objects state)
selected (->> (get-in state [:workspace-local :selected])
- (cp/clean-loops objects))
+ (cph/clean-loops objects))
selectable? (fn [id]
(and (contains? objects id)
(or (not omit-blocked?)
diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs
index 118fbc0dad..b649250aaf 100644
--- a/frontend/src/app/main/data/workspace/svg_upload.cljs
+++ b/frontend/src/app/main/data/workspace/svg_upload.cljs
@@ -12,7 +12,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :refer [max-safe-int min-safe-int]]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
@@ -454,9 +454,9 @@
ptk/WatchEvent
(watch [it state _]
(try
- (let [page-id (:current-page-id state)
- objects (wsh/lookup-page-objects state page-id)
- frame-id (cp/frame-id-by-position objects position)
+ (let [page-id (:current-page-id state)
+ objects (wsh/lookup-page-objects state page-id)
+ frame-id (cph/frame-id-by-position objects position)
selected (wsh/lookup-selected state)
[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data)
diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index 04d6b0eb9d..061130b15b 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -10,7 +10,7 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.text :as txt]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
@@ -160,8 +160,9 @@
shape (get objects id)
update-fn #(update-shape % txt/is-root-node? attrs/merge attrs)
- shape-ids (cond (= (:type shape) :text) [id]
- (= (:type shape) :group) (cp/get-children id objects))]
+
+ shape-ids (cond (cph/text-shape? shape) [id]
+ (cph/group-shape? shape) (cph/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids update-fn))))))
@@ -186,8 +187,9 @@
attrs))
update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs)
- shape-ids (cond (= (:type shape) :text) [id]
- (= (:type shape) :group) (cp/get-children id objects))]
+ shape-ids (cond
+ (cph/text-shape? shape) [id]
+ (cph/group-shape? shape) (cph/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids update-fn))))))))
@@ -208,8 +210,9 @@
(txt/is-paragraph-node? node)))
update-fn #(update-shape % update-node? attrs/merge attrs)
- shape-ids (cond (= (:type shape) :text) [id]
- (= (:type shape) :group) (cp/get-children id objects))]
+ shape-ids (cond
+ (cph/text-shape? shape) [id]
+ (cph/group-shape? shape) (cph/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids update-fn)))))))
;; --- RESIZE UTILS
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 94c3c3af0f..ddc4f5686e 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -12,10 +12,11 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [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.guides :as dwg]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@@ -143,9 +144,7 @@
(let [objects (wsh/lookup-page-objects state)
shapes (->> shapes
(remove #(get % :blocked false))
- (mapcat (fn [shape]
- (->> (cp/get-children (:id shape) objects)
- (map #(get objects %)))))
+ (mapcat #(cph/get-children objects (:id %)))
(concat shapes))
update-shape
@@ -162,12 +161,14 @@
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
- children-ids (->> ids (mapcat #(cp/get-children % objects)))
- ids-with-children (d/concat-vec children-ids ids)
+ ids-with-children (into (vec ids) (mapcat #(cph/get-children-ids objects %)) ids)
object-modifiers (get state :workspace-modifiers)
- ignore-tree (get-ignore-tree object-modifiers objects ids)]
+ shapes (map (d/getf objects) ids)
+ ignore-tree (->> (map #(get-ignore-tree object-modifiers objects %) shapes)
+ (reduce merge {}))]
(rx/of (dwu/start-undo-transaction)
+ (dwg/move-frame-guides ids-with-children)
(dch/update-shapes
ids-with-children
(fn [shape]
@@ -199,7 +200,7 @@
shape
(nil? root)
- (cp/get-root-shape shape objects)
+ (cph/get-root-shape objects shape)
:else root)
@@ -209,7 +210,7 @@
transformed-shape
(nil? transformed-root)
- (cp/get-root-shape transformed-shape objects)
+ (cph/get-root-shape objects transformed-shape)
:else transformed-root)
@@ -246,7 +247,7 @@
(reduce set-child modif-tree children)))
(defn- get-ignore-tree
- "Retrieves a map with the flag `ignore-tree` given a tree of modifiers"
+ "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
([modif-tree objects shape]
(get-ignore-tree modif-tree objects shape nil nil {}))
@@ -262,7 +263,7 @@
ignore-tree (assoc ignore-tree shape-id ignore-geometry?)
set-child
- (fn [modif-tree child]
+ (fn [ignore-tree child]
(get-ignore-tree modif-tree objects child root transformed-root ignore-tree))]
(reduce set-child ignore-tree children))))
@@ -433,24 +434,25 @@
group (gsh/selection-rect shapes)
group-center (gsh/center-selrect group)
initial-angle (gpt/angle @ms/mouse-position group-center)
- calculate-angle (fn [pos ctrl?]
+ calculate-angle (fn [pos ctrl? shift?]
(let [angle (- (gpt/angle pos group-center) initial-angle)
angle (if (neg? angle) (+ 360 angle) angle)
- modval (mod angle 45)
- angle (if ctrl?
- (if (< 22.5 modval)
- (+ angle (- 45 modval))
- (- angle modval))
- angle)
angle (if (= angle 360)
0
+ angle)
+ angle (if ctrl?
+ (* (mth/floor (/ angle 45)) 45)
+ angle)
+ angle (if shift?
+ (* (mth/floor (/ angle 15)) 15)
angle)]
angle))]
(rx/concat
(->> ms/mouse-position
(rx/with-latest vector ms/mouse-position-ctrl)
- (rx/map (fn [[pos ctrl?]]
- (let [delta-angle (calculate-angle pos ctrl?)]
+ (rx/with-latest vector ms/mouse-position-shift)
+ (rx/map (fn [[[pos ctrl?] shift?]]
+ (let [delta-angle (calculate-angle pos ctrl? shift?)]
(set-rotation-modifiers delta-angle shapes group-center))))
(rx/take-until stoper))
(rx/of (apply-modifiers (map :id shapes))
@@ -519,7 +521,7 @@
(watch [_ _ stream]
(->> stream
(rx/filter (ptk/type? ::dws/duplicate-selected))
- (rx/first)
+ (rx/take 1)
(rx/map #(start-move from-position))))))
(defn- start-move
@@ -602,13 +604,14 @@
(watch [_ state stream]
(if (= same-event (get-in state [:workspace-local :current-move-selected]))
(let [selected (wsh/lookup-selected state {:omit-blocked? true})
+ nudge (get-in state [:profile :props :nudge] {:big 10 :small 1})
move-events (->> stream
(rx/filter (ptk/type? ::move-selected))
(rx/filter #(= direction (deref %))))
stopper (->> move-events
(rx/debounce 100)
- (rx/first))
- scale (if shift? (gpt/point 10) (gpt/point 1))
+ (rx/take 1))
+ scale (if shift? (gpt/point (:big nudge)) (gpt/point (:small nudge)))
mov-vec (gpt/multiply (get-displacement direction) scale)]
(rx/concat
@@ -658,10 +661,10 @@
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- frame-id (cp/frame-id-by-position objects position)
+ frame-id (cph/frame-id-by-position objects position)
moving-shapes (->> ids
- (cp/clean-loops objects)
+ (cph/clean-loops objects)
(map #(get objects %))
(remove #(or (nil? %)
(= (:frame-id %) frame-id))))
@@ -678,7 +681,7 @@
{:type :mov-objects
:page-id page-id
:parent-id (:parent-id shape)
- :index (cp/get-index-in-parent objects (:id shape))
+ :index (cph/get-index-in-parent objects (:id shape))
:shapes [(:id shape)]})))]
(when-not (empty? uch)
diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs
index d25c22ea5f..b4a93cd53a 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.spec :as spec]
[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/changes)
-(s/def ::redo-changes ::spec/changes)
+(s/def ::undo-changes ::spec.change/changes)
+(s/def ::redo-changes ::spec.change/changes)
(s/def ::undo-entry
(s/keys :req-un [::undo-changes ::redo-changes]))
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index f93a53ae99..b94e0af0e1 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -9,7 +9,7 @@
(: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.path.commands :as upc]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st]
@@ -92,7 +92,7 @@
(l/derived :workspace-drawing st/state))
(def selected-shapes
- (l/derived wsh/lookup-selected st/state))
+ (l/derived wsh/lookup-selected st/state =))
(defn make-selected-ref
[id]
@@ -124,6 +124,11 @@
:move-overlay-index])
workspace-local =))
+(def typography-data
+ (l/derived #(select-keys % [:rename-typography
+ :edit-typography])
+ workspace-local =))
+
(def local-displacement
(l/derived #(select-keys % [:modifiers :selected])
workspace-local =))
@@ -152,8 +157,11 @@
(def current-hover
(l/derived :hover workspace-local))
-(def editors
- (l/derived :editors workspace-local))
+(def context-menu
+ (l/derived :context-menu workspace-local))
+
+(def current-hover-ids
+ (l/derived :hover-ids context-menu))
(def selected-assets
(l/derived :selected-assets workspace-local))
@@ -161,6 +169,9 @@
(def workspace-layout
(l/derived :workspace-layout st/state))
+(def current-file-id
+ (l/derived :current-file-id st/state))
+
(def workspace-file
(l/derived (fn [state]
(let [file (:workspace-file state)
@@ -182,6 +193,11 @@
(get-in state [:workspace-data :recent-colors] []))
st/state))
+(def workspace-recent-fonts
+ (l/derived (fn [state]
+ (get-in state [:workspace-data :recent-fonts] []))
+ st/state))
+
(def workspace-file-typography
(l/derived (fn [state]
(when-let [file (:workspace-data state)]
@@ -202,7 +218,7 @@
:media
:typographies
:components]))
- st/state))
+ st/state =))
(def workspace-libraries
(l/derived :workspace-libraries st/state))
@@ -230,7 +246,7 @@
(l/derived :options workspace-page))
(def workspace-frames
- (l/derived cp/select-frames workspace-page-objects =))
+ (l/derived cph/get-frames workspace-page-objects =))
(def workspace-editor
(l/derived :workspace-editor st/state))
@@ -260,9 +276,11 @@
(defn select-bool-children [id]
(let [selector
(fn [state]
- (let [objects (wsh/lookup-page-objects state)
- modifiers (:workspace-modifiers state)]
- (as-> (cp/select-children id objects) $
+ (let [objects (wsh/lookup-page-objects state)
+ modifiers (:workspace-modifiers state)
+ children (->> (cph/get-children-ids objects id)
+ (select-keys objects))]
+ (as-> children $
(gsh/merge-modifiers $ modifiers)
(d/mapm (set-content-modifiers state) $))))]
(l/derived selector st/state =)))
@@ -277,7 +295,7 @@
(defn is-child-selected?
[id]
(letfn [(selector [{:keys [selected objects]}]
- (let [children (cp/get-children id objects)]
+ (let [children (cph/get-children-ids objects id)]
(some #(contains? selected %) children)))]
(l/derived selector selected-data =)))
@@ -291,7 +309,7 @@
(def selected-shapes-with-children
(letfn [(selector [{:keys [selected objects]}]
(let [xform (comp (remove nil?)
- (mapcat #(cp/get-children % objects)))
+ (mapcat #(cph/get-children-ids objects %)))
shapes (into selected xform selected)]
(mapv (d/getf objects) shapes)))]
(l/derived selector selected-data =)))
diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs
index b76f478593..fa08642824 100644
--- a/frontend/src/app/main/render.cljs
+++ b/frontend/src/app/main/render.cljs
@@ -19,8 +19,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
- [app.common.uuid :as uuid]
+ [app.common.pages.helpers :as cph]
[app.config :as cfg]
[app.main.fonts :as fonts]
[app.main.ui.shapes.bool :as bool]
@@ -59,12 +58,11 @@
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
- (let [shapes (cp/select-toplevel-shapes objects {:include-frames? true
- :include-frame-children? false})
+ (let [shapes (cph/get-immediate-children objects)
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
- rect (cond->> (gsh/selection-rect shapes)
- (some? vport)
- (gal/adjust-to-viewport vport))]
+ rect (cond->> (gsh/selection-rect shapes)
+ (some? vport)
+ (gal/adjust-to-viewport vport))]
(-> rect
(update :x to-finite 0)
(update :y to-finite 0)
@@ -101,11 +99,9 @@
bool-shape (bool/bool-shape shape-wrapper)]
(mf/fnc bool-wrapper
[{:keys [shape] :as props}]
- (let [childs (mf/use-memo
- (mf/deps (:id shape) objects)
- (fn []
- (->> (cp/get-children (:id shape) objects)
- (select-keys objects))))]
+ (let [childs (mf/with-memo [(:id shape) objects]
+ (->> (cph/get-children-ids objects (:id shape))
+ (select-keys objects)))]
[:& bool-shape {:shape shape :childs childs}]))))
(defn svg-raw-wrapper-factory
@@ -166,15 +162,12 @@
[{:keys [data width height thumbnails? embed? include-metadata?] :as props
:or {embed? false include-metadata? false}}]
(let [objects (:objects data)
- root (get objects uuid/zero)
- shapes
- (->> (:shapes root)
- (map #(get objects %)))
+ shapes (cph/get-immediate-children objects)
root-children
(->> shapes
- (filter #(not= :frame (:type %)))
- (mapcat #(cp/get-object-with-children (:id %) objects)))
+ (remove cph/frame-shape?)
+ (mapcat #(cph/get-children-with-self objects (:id %))))
vport (when (and (some? width) (some? height))
{:width width :height height})
@@ -204,7 +197,9 @@
:height "100%"
:background background-color}}
- [:& export/export-page {:options (:options data)}]
+ (when include-metadata?
+ [:& export/export-page {:options (:options data)}])
+
[:& ff/fontfaces-style {:shapes root-children}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]
@@ -227,31 +222,25 @@
include-metadata? (mf/use-ctx export/include-metadata-ctx)
modifier
- (mf/use-memo
- (mf/deps (:x frame) (:y frame))
- (fn []
- (-> (gpt/point (:x frame) (:y frame))
- (gpt/negate)
- (gmt/translate-matrix))))
+ (mf/with-memo [(:x frame) (:y frame)]
+ (-> (gpt/point (:x frame) (:y frame))
+ (gpt/negate)
+ (gmt/translate-matrix)))
objects
- (mf/use-memo
- (mf/deps frame-id objects modifier)
- (fn []
- (let [update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)]
- (->> (cp/get-children frame-id objects)
- (into [frame-id])
- (reduce update-fn 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)
+ (into [frame-id])
+ (reduce update-fn objects))))
frame
- (mf/use-memo
- (mf/deps modifier)
- (fn [] (assoc-in frame [:modifiers :displacement] modifier)))
+ (mf/with-memo [modifier]
+ (assoc-in frame [:modifiers :displacement] modifier))
wrapper
- (mf/use-memo
- (mf/deps objects)
- (fn [] (frame-wrapper-factory objects)))
+ (mf/with-memo [objects]
+ (frame-wrapper-factory objects))
width (* (:width frame) zoom)
height (* (:height frame) zoom)
@@ -284,7 +273,7 @@
(mf/use-memo
(mf/deps modifier objects group-id)
(fn []
- (let [modifier-ids (concat [group-id] (cp/get-children group-id objects))
+ (let [modifier-ids (cons group-id (cph/get-children-ids objects group-id))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
modifiers (reduce update-fn {} modifier-ids)]
(gsh/merge-modifiers objects modifiers))))
@@ -338,7 +327,7 @@
(mf/use-memo
(mf/deps modifier id objects)
(fn []
- (let [modifier-ids (concat [id] (cp/get-children id objects))
+ (let [modifier-ids (cons id (cph/get-children-ids objects id))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)]
(reduce update-fn objects modifier-ids))))
diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs
index 41bd7ae79e..862d7bbd70 100644
--- a/frontend/src/app/main/snap.cljs
+++ b/frontend/src/app/main/snap.cljs
@@ -10,7 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.uuid :refer [zero]]
[app.main.refs :as refs]
[app.main.worker :as uw]
@@ -19,20 +19,38 @@
[beicon.core :as rx]
[clojure.set :as set]))
-(def ^:const snap-accuracy 5)
+(def ^:const snap-accuracy 10)
(def ^:const snap-path-accuracy 10)
(def ^:const snap-distance-accuracy 10)
(defn- remove-from-snap-points
- [remove-id?]
+ [remove-snap?]
(fn [query-result]
(->> query-result
- (map (fn [[value data]] [value (remove (comp remove-id? second) data)]))
+ (map (fn [[value data]] [value (remove remove-snap? data)]))
(filter (fn [[_ data]] (seq data))))))
+(defn make-remove-snap
+ "Creates a filter for the snap data. Used to disable certain layouts"
+ [layout filter-shapes]
+
+ (fn [{:keys [type id]}]
+ (cond
+ (= type :layout)
+ (or (not (contains? layout :display-grid))
+ (not (contains? layout :snap-grid)))
+
+ (= type :guide)
+ (or (not (contains? layout :rules))
+ (not (contains? layout :snap-guides)))
+
+ :else
+ (or (contains? filter-shapes id)
+ (not (contains? layout :dynamic-alignment))))))
+
(defn- flatten-to-points
[query-result]
- (mapcat (fn [[_ data]] (map (fn [[point _]] point) data)) query-result))
+ (mapcat (fn [[_ data]] (map :pt data)) query-result))
(defn- calculate-distance [query-result point coord]
(->> query-result
@@ -57,19 +75,19 @@
;; Otherwise the root frame is the common
:else zero)))
-(defn get-snap-points [page-id frame-id filter-shapes point coord]
+(defn get-snap-points [page-id frame-id remove-snap? point coord]
(let [value (get point coord)]
(->> (uw/ask! {:cmd :snaps/range-query
:page-id page-id
:frame-id frame-id
- :coord coord
+ :axis coord
:ranges [[(- value 0.5) (+ value 0.5)]]})
- (rx/first)
- (rx/map (remove-from-snap-points filter-shapes))
+ (rx/take 1)
+ (rx/map (remove-from-snap-points remove-snap?))
(rx/map flatten-to-points))))
(defn- search-snap
- [page-id frame-id points coord filter-shapes zoom]
+ [page-id frame-id points coord remove-snap? zoom]
(let [snap-accuracy (/ snap-accuracy zoom)
ranges (->> points
(map coord)
@@ -78,10 +96,10 @@
(->> (uw/ask! {:cmd :snaps/range-query
:page-id page-id
:frame-id frame-id
- :coord coord
+ :axis coord
:ranges ranges})
- (rx/first)
- (rx/map (remove-from-snap-points filter-shapes))
+ (rx/take 1)
+ (rx/map (remove-from-snap-points remove-snap?))
(rx/map (get-min-distance-snap points coord)))))
(defn snap->vector [[[from-x to-x] [from-y to-y]]]
@@ -91,13 +109,12 @@
(gpt/to-vec from to))))
(defn- closest-snap
- [page-id frame-id points filter-shapes zoom]
- (let [snap-x (search-snap page-id frame-id points :x filter-shapes zoom)
- snap-y (search-snap page-id frame-id points :y filter-shapes zoom)]
+ [page-id frame-id points remove-snap? zoom]
+ (let [snap-x (search-snap page-id frame-id points :x remove-snap? zoom)
+ snap-y (search-snap page-id frame-id points :y remove-snap? zoom)]
(->> (rx/combine-latest snap-x snap-y)
(rx/map snap->vector))))
-
(defn sr-distance [coord sr1 sr2]
(let [c1 (if (= coord :x) :x1 :y1)
c2 (if (= coord :x) :x2 :y2)
@@ -185,9 +202,9 @@
:frame-id (->> shapes first :frame-id)
:include-frames? true
:rect area-selrect})
- (rx/map #(cp/clean-loops objects %))
+ (rx/map #(cph/clean-loops objects %))
(rx/map #(set/difference % (into #{} (map :id shapes))))
- (rx/map (fn [ids] (map #(get objects %) ids)))))
+ (rx/map #(map (d/getf objects) %))))
(defn closest-distance-snap
[page-id shapes objects zoom movev]
@@ -209,12 +226,8 @@
[page-id shapes layout zoom point]
(let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :id shapes))
- filter-shapes (fn [id] (if (= id :layout)
- (or (not (contains? layout :display-grid))
- (not (contains? layout :snap-grid)))
- (or (filter-shapes id)
- (not (contains? layout :dynamic-alignment)))))]
- (->> (closest-snap page-id frame-id [point] filter-shapes zoom)
+ remove-snap? (make-remove-snap layout filter-shapes)]
+ (->> (closest-snap page-id frame-id [point] remove-snap? zoom)
(rx/map #(or % (gpt/point 0 0)))
(rx/map #(gpt/add point %)))))
@@ -222,11 +235,8 @@
[page-id shapes objects layout zoom movev]
(let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :id shapes))
- filter-shapes (fn [id] (if (= id :layout)
- (or (not (contains? layout :display-grid))
- (not (contains? layout :snap-grid)))
- (or (filter-shapes id)
- (not (contains? layout :dynamic-alignment)))))
+ remove-snap? (make-remove-snap layout filter-shapes)
+
shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
@@ -236,7 +246,7 @@
;; Move the points in the translation vector
(map #(gpt/add % movev)))]
- (->> (rx/merge (closest-snap page-id frame-id shapes-points filter-shapes zoom)
+ (->> (rx/merge (closest-snap page-id frame-id shapes-points remove-snap? zoom)
(when (contains? layout :dynamic-alignment)
(closest-distance-snap page-id shapes objects zoom movev)))
(rx/reduce gpt/min)
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 4cb2b5bbfb..05c58144ea 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -29,7 +29,8 @@
(mf/defc on-main-error
[{:keys [error] :as props}]
- (mf/use-effect (st/emitf (rt/assign-exception error)))
+ (mf/with-effect
+ (st/emit! (rt/assign-exception error)))
[:span "Internal application error"])
(mf/defc main-page
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 399e4ddb46..936d5ebb0d 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -30,9 +30,11 @@
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
+(s/def ::invitation-token ::us/not-empty-string)
(s/def ::login-form
- (s/keys :req-un [::email ::password]))
+ (s/keys :req-un [::email ::password]
+ :opt-un [::invitation-token]))
(defn- login-with-oauth
[event provider params]
@@ -62,36 +64,47 @@
(mf/defc login-form
[{:keys [params] :as props}]
- (let [error (mf/use-state false)
- form (fm/use-form :spec ::login-form
- :inital {})
+ (let [initial (mf/use-memo (mf/deps params) (constantly params))
+
+ error (mf/use-state false)
+ form (fm/use-form :spec ::login-form :initial initial)
on-error
(fn [_]
(reset! error (tr "errors.wrong-credentials")))
+ on-succes
+ (fn [data]
+ (prn "SUCCESS" data)
+ (when-let [token (:invitation-token data)]
+ (st/emit! (rt/nav :auth-verify-token {} {:token token}))))
+
on-submit
(mf/use-callback
- (mf/deps form)
- (fn [_]
+ (fn [form _event]
(reset! error nil)
(let [params (with-meta (:clean-data @form)
- {:on-error on-error})]
+ {:on-error on-error
+ :on-success on-succes})]
(st/emit! (du/login params)))))
on-submit-ldap
(mf/use-callback
(mf/deps form)
(fn [event]
- (let [params (merge (:clean-data @form) params)]
- (login-with-ldap event (with-meta params {:on-error on-error})))))]
+ (reset! error nil)
+ (let [params (:clean-data @form)]
+ (login-with-ldap event (with-meta params
+ {:on-error on-error
+ :on-success on-succes})))))]
[:*
(when-let [message @error]
[:& msgs/inline-banner
{:type :warning
:content message
- :on-close #(reset! error nil)}])
+ :on-close #(reset! error nil)
+ :data-test "login-banner"}])
[:& fm/form {:on-submit on-submit :form form}
[:div.fields-row
@@ -111,7 +124,8 @@
[:div.buttons-stack
[:& fm/submit-button
- {:label (tr "auth.login-submit")}]
+ {:label (tr "auth.login-submit")
+ :data-test "login-submit"}]
(when (contains? @cf/flags :login-with-ldap)
[:& fm/submit-button
@@ -149,7 +163,7 @@
[{:keys [params] :as props}]
[:div.generic-form.login-form
[:div.form-container
- [:h1 (tr "auth.login-title")]
+ [:h1 {:data-test "login-title"} (tr "auth.login-title")]
[:div.subtitle (tr "auth.login-subtitle")]
[:& login-form {:params params}]
@@ -163,18 +177,21 @@
[:div.links
[:div.link-entry
- [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
+ [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
+ :data-test "forgot-password"}
(tr "auth.forgot-password")]]
(when (contains? @cf/flags :registration)
[:div.link-entry
[:span (tr "auth.register") " "]
- [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
+ [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))
+ :data-test "register-submit"}
(tr "auth.register-submit")]])]
(when (contains? @cf/flags :demo-users)
[:div.links.demo
[:div.link-entry
[:span (tr "auth.create-demo-profile") " "]
- [:a {:on-click (st/emitf (du/create-demo-profile))}
+ [:a {:on-click (st/emitf (du/create-demo-profile))
+ :data-test "demo-account-link"}
(tr "auth.create-demo-account")]]])]])
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index 31c1eb2302..6477cfb32d 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -67,7 +67,8 @@
:type "text"}]]
[:& fm/submit-button
- {:label (tr "auth.recovery-request-submit")}]]))
+ {:label (tr "auth.recovery-request-submit")
+ :data-test "recovery-resquest-submit"}]]))
;; --- Recovery Request Page
@@ -82,5 +83,6 @@
[:div.links
[:div.link-entry
- [:a {:on-click #(st/emit! (rt/nav :auth-login))}
+ [:a {:on-click #(st/emit! (rt/nav :auth-login))
+ :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 84c362fb23..84ed3de243 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -32,14 +32,10 @@
(defn- validate
[data]
- (let [password (:password data)
- terms-privacy (:terms-privacy data)]
+ (let [password (:password data)]
(cond-> {}
(> 8 (count password))
- (assoc :password {:message "errors.password-too-short"})
-
- (and (not terms-privacy) false)
- (assoc :terms-privacy {:message "errors.terms-privacy-agreement-invalid"}))))
+ (assoc :password {:message "errors.password-too-short"}))))
(s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
@@ -65,6 +61,10 @@
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
+ :email-as-password
+ (swap! form assoc-in [:errors :password]
+ {:message "errors.email-as-password"})
+
(st/emit! (dm/error (tr "errors.generic")))))
(defn- handle-prepare-register-success
@@ -98,7 +98,8 @@
:name :email
:tab-index "2"
:help-icon i/at
- :label (tr "auth.email")}]]
+ :label (tr "auth.email")
+ :data-test "email-input"}]]
[:div.fields-row
[:& fm/input {:name :password
:tab-index "3"
@@ -108,12 +109,13 @@
[:& fm/submit-button
{:label (tr "auth.register-submit")
- :disabled @submitted?}]]))
+ :disabled @submitted?
+ :data-test "register-form-submit"}]]))
(mf/defc register-page
[{:keys [params] :as props}]
[:div.form-container
- [:h1 (tr "auth.register-title")]
+ [:h1 {:data-test "registration-title"} (tr "auth.register-title")]
[:div.subtitle (tr "auth.register-subtitle")]
(when (contains? @cf/flags :demo-warning)
@@ -132,7 +134,8 @@
[:div.link-entry
[:span (tr "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login {} params))
- :tab-index "4"}
+ :tab-index "4"
+ :data-test "login-here-link"}
(tr "auth.login-here")]]
(when (contains? @cf/flags :demo-users)
@@ -234,7 +237,7 @@
(mf/defc register-validate-page
[{:keys [params] :as props}]
[:div.form-container
- [:h1 (tr "auth.register-title")]
+ [:h1 {:data-test "register-title"} (tr "auth.register-title")]
[:div.subtitle (tr "auth.register-subtitle")]
[:& register-validate-form {:params params}]
diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs
index 119a4cd404..1f6a7c9aa2 100644
--- a/frontend/src/app/main/ui/auth/verify_token.cljs
+++ b/frontend/src/app/main/ui/auth/verify_token.cljs
@@ -41,13 +41,15 @@
[tdata]
(case (:state tdata)
:created
- (st/emit! (dm/success (tr "auth.notifications.team-invitation-accepted"))
- (du/fetch-profile)
- (rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
+ (st/emit!
+ (dm/success (tr "auth.notifications.team-invitation-accepted"))
+ (du/fetch-profile)
+ (rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
:pending
- (let [token (:invitation-token tdata)]
- (st/emit! (rt/nav :auth-register {} {:invitation-token token})))))
+ (let [token (:invitation-token tdata)
+ route-id (:redirect-to tdata :auth-register)]
+ (st/emit! (rt/nav route-id {} {:invitation-token token})))))
(defmethod handle-token :default
[_tdata]
diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs
index f65abba074..ad5619c95b 100644
--- a/frontend/src/app/main/ui/comments.cljs
+++ b/frontend/src/app/main/ui/comments.cljs
@@ -18,6 +18,7 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.time :as dt]
+ [cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
@@ -104,7 +105,7 @@
(when (or @show-buttons?
(seq @content))
[:div.buttons
- [:input.btn-primary {:type "button" :value "Post" :on-click on-submit}]
+ [:input.btn-primary {:type "button" :value "Post" :on-click on-submit :disabled (str/empty-or-nil? @content)}]
[:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])]))
(mf/defc draft-thread
@@ -154,7 +155,8 @@
[:input.btn-primary
{:on-click on-submit
:type "button"
- :value "Post"}]
+ :value "Post"
+ :disabled (str/empty-or-nil? content)}]
[:input.btn-secondary
{:on-click on-esc
:type "button"
@@ -300,7 +302,7 @@
(mf/deps thread comments-map)
(fn []
(when-let [node (mf/ref-val ref)]
- (.scrollIntoViewIfNeeded ^js node))))
+ (dom/scroll-into-view-if-needed! node))))
(when (some? comment)
[:div.thread-content
diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs
index 62faf19f00..aa177e0947 100644
--- a/frontend/src/app/main/ui/components/color_bullet.cljs
+++ b/frontend/src/app/main/ui/components/color_bullet.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.components.color-bullet
(:require
[app.util.color :as uc]
+ [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
@@ -21,17 +22,18 @@
[:div.color-bullet.multiple {:on-click #(when on-click (on-click %))}]
;; No multiple selection
- (let [color (if (string? color) {:color color :opacity 1} color)
- background (if (:gradient color) (uc/color->background color) "auto")]
- [:div.color-bullet.tooltip.tooltip-right {:class (if (:id color) "is-library-color" "is-not-library-color")
- :on-click #(when on-click (on-click %))
- :alt (or (:name color) (:color color) (gradient-type->string (:type (:gradient color))))
- :style {:background background}}
- (when (not(:gradient color))
- [:*
+ (let [color (if (string? color) {:color color :opacity 1} color)]
+ [:div.color-bullet.tooltip.tooltip-right
+ {:class (dom/classnames :is-library-color (some? (:id color))
+ :is-not-library-color (nil? (:id color))
+ :is-gradient (some? (:gradient color)))
+ :on-click #(when on-click (on-click %))
+ :alt (or (:name color) (:color color) (gradient-type->string (:type (:gradient color))))}
+ (if (:gradient color)
+ [:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}]
+ [:div.color-bullet-wrapper
[:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}]
- [:div.color-bullet-right {:style {:background (uc/color->background color)}}]]
- )])))
+ [:div.color-bullet-right {:style {:background (uc/color->background color)}}]])])))
(mf/defc color-name [{:keys [color size on-click on-double-click]}]
(let [color (if (string? color) {:color color :opacity 1} color)
diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs
index b83efcc14a..908e992e48 100644
--- a/frontend/src/app/main/ui/components/context_menu.cljs
+++ b/frontend/src/app/main/ui/components/context_menu.cljs
@@ -101,7 +101,7 @@
[:span i/arrow-slide]
parent-option]]
[:li.separator]])
- (for [[index [option-name option-handler sub-options]] (d/enumerate (:options level))]
+ (for [[index [option-name option-handler sub-options data-test]] (d/enumerate (:options level))]
(when option-name
(if (= option-name :separator)
[:li.separator]
@@ -111,12 +111,14 @@
(if-not sub-options
[:a.context-menu-action {:on-click #(do (dom/stop-propagation %)
(on-close)
- (option-handler %))}
+ (option-handler %))
+ :data-test data-test}
(if (and in-dashboard? (= option-name "Default"))
(tr "dashboard.default-team-name")
option-name)]
[:a.context-menu-action.submenu
{:data-no-close true
- :on-click (enter-submenu option-name sub-options)}
+ :on-click (enter-submenu option-name sub-options)
+ :data-test data-test}
option-name
[:span i/arrow-slide]])])))])]])))
diff --git a/frontend/src/app/main/ui/components/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs
index 8aa5c55b53..3ee25d09c3 100644
--- a/frontend/src/app/main/ui/components/file_uploader.cljs
+++ b/frontend/src/app/main/ui/components/file_uploader.cljs
@@ -12,7 +12,7 @@
(mf/defc file-uploader
{::mf/forward-ref true}
- [{:keys [accept multi label-text label-class input-id on-selected] :as props} input-ref]
+ [{:keys [accept multi label-text label-class input-id on-selected data-test] :as props} input-ref]
(let [opt-pick-one #(if multi % (first %))
on-files-selected
@@ -37,5 +37,6 @@
:accept accept
:type "file"
:ref input-ref
- :on-change on-files-selected}]]))
+ :on-change on-files-selected
+ :data-test data-test}]]))
diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs
index fd15aa757f..54022b3faa 100644
--- a/frontend/src/app/main/ui/components/forms.cljs
+++ b/frontend/src/app/main/ui/components/forms.cljs
@@ -12,6 +12,7 @@
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
+ [clojure.string]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@@ -19,7 +20,7 @@
(def use-form fm/use-form)
(mf/defc input
- [{:keys [label help-icon disabled form hint trim children] :as props}]
+ [{:keys [label help-icon disabled form hint trim children data-test] :as props}]
(let [input-type (get props :type "text")
input-name (get props :name)
more-classes (get props :class)
@@ -112,7 +113,7 @@
help-icon'])
(cond
(and touched? (:message error))
- [:span.error (tr (:message error))]
+ [:span.error {:data-test (clojure.string/join [data-test "-error"]) }(tr (:message error))]
(string? hint)
[:span.hint hint])]]))
@@ -170,10 +171,9 @@
[:span.hint hint])]]))
(mf/defc select
- [{:keys [options label form default] :as props
+ [{:keys [options label form default data-test] :as props
:or {default ""}}]
(let [input-name (get props :name)
-
form (or form (mf/use-ctx form-ctx))
value (or (get-in @form [:data input-name]) default)
cvalue (d/seek #(= value (:value %)) options)
@@ -181,7 +181,8 @@
[:div.custom-select
[:select {:value value
- :on-change on-change}
+ :on-change on-change
+ :data-test data-test}
(for [item options]
[:option {:key (:value item) :value (:value item)} (:label item)])]
@@ -194,7 +195,7 @@
i/arrow-slide]]]))
(mf/defc submit-button
- [{:keys [label form on-click disabled] :as props}]
+ [{:keys [label form on-click disabled data-test] :as props}]
(let [form (or form (mf/use-ctx form-ctx))]
[:input.btn-primary.btn-large
{:name "submit"
@@ -202,6 +203,7 @@
:disabled (or (not (:valid @form)) (true? disabled))
:on-click on-click
:value label
+ :data-test data-test
:type "submit"}]))
(mf/defc form
diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs
new file mode 100644
index 0000000000..0a54feeae5
--- /dev/null
+++ b/frontend/src/app/main/ui/components/shape_icon.cljs
@@ -0,0 +1,34 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.components.shape-icon
+ (:require
+ [app.main.ui.icons :as i]
+ [rumext.alpha :as mf]))
+
+
+(mf/defc element-icon
+ [{:keys [shape] :as props}]
+ (case (:type shape)
+ :frame i/artboard
+ :image i/image
+ :line i/line
+ :circle i/circle
+ :path i/curve
+ :rect i/box
+ :text i/text
+ :group (if (some? (:component-id shape))
+ i/component
+ (if (:masked-group? shape)
+ i/mask
+ i/folder))
+ :bool (case (:bool-type shape)
+ :difference i/bool-difference
+ :exclude i/bool-exclude
+ :intersection i/bool-intersection
+ #_:default i/bool-union)
+ :svg-raw i/file-svg
+ nil))
diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs
index a39f47b1dd..7e154ef950 100644
--- a/frontend/src/app/main/ui/confirm.cljs
+++ b/frontend/src/app/main/ui/confirm.cljs
@@ -24,6 +24,7 @@
on-accept
on-cancel
hint
+ items
cancel-label
accept-label
accept-style] :as props}]
@@ -51,17 +52,15 @@
(st/emit! (modal/hide))
(on-cancel props)))]
- (mf/use-effect
- (fn []
- (let [on-keydown
- (fn [event]
- (when (k/enter? event)
- (dom/prevent-default event)
- (dom/stop-propagation event)
- (st/emit! (modal/hide))
- (on-accept props)))
- key (events/listen js/document EventType.KEYDOWN on-keydown)]
- #(events/unlistenByKey key))))
+ (mf/with-effect
+ (letfn [(on-keydown [event]
+ (when (k/enter? event)
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (st/emit! (modal/hide))
+ (on-accept props)))]
+ (->> (events/listen js/document EventType.KEYDOWN on-keydown)
+ (partial events/unlistenByKey))))
[:div.modal-overlay
[:div.modal-container.confirm-dialog
@@ -72,9 +71,18 @@
{:on-click cancel-fn} i/close]]
[:div.modal-content
- [:h3 message]
+ (when (and (string? message) (not= message ""))
+ [:h3 message])
(when (string? hint)
- [:p hint])]
+ [:p hint])
+ (when (> (count items) 0)
+ [:*
+ [:p (tr "ds.component-subtitle")]
+ [:ul
+ (for [item items]
+ [:li.modal-item-element
+ [:span.modal-component-icon i/component]
+ [:span (:name item)]])]])]
[:div.modal-footer
[:div.action-buttons
diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs
index c52be68919..2d571ed06e 100644
--- a/frontend/src/app/main/ui/cursors.cljs
+++ b/frontend/src/app/main/ui/cursors.cljs
@@ -30,6 +30,9 @@
(def pointer-node (cursor-ref :pointer-node 0 0 10 32))
(def resize-alt (cursor-ref :resize-alt))
(def text (cursor-ref :text))
+(def zoom (cursor-ref :zoom))
+(def zoom-in (cursor-ref :zoom-in))
+(def zoom-out (cursor-ref :zoom-out))
;; Dynamic cursors
(def resize-ew (cursor-fn :resize-h 0))
@@ -38,6 +41,9 @@
(def resize-nwse (cursor-fn :resize-h 135))
(def rotate (cursor-fn :rotate 90))
+;;
+(def resize-ew-2 (cursor-fn :resize-h-2 0))
+(def resize-ns-2 (cursor-fn :resize-h-2 90))
(mf/defc debug-preview
{::mf/wrap-props false}
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 33addf9f1e..a377a70df8 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -22,7 +22,10 @@
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
[app.main.ui.hooks :as hooks]
- [rumext.alpha :as mf]))
+ [app.util.keyboard :as kbd]
+ [goog.events :as events]
+ [rumext.alpha :as mf])
+ (:import goog.events.EventType))
(defn ^boolean uuid-str?
[s]
@@ -92,10 +95,18 @@
(hooks/use-shortcuts ::dashboard sc/shortcuts)
+ (mf/with-effect [team-id]
+ (st/emit! (dd/initialize {:id team-id})))
+
(mf/use-effect
- (mf/deps team-id)
(fn []
- (st/emit! (dd/initialize {:id team-id}))))
+ (let [events [(events/listen goog/global EventType.KEYDOWN
+ (fn [event]
+ (when (kbd/enter? event)
+ (st/emit! (dd/open-selected-file)))))]]
+ (fn []
+ (doseq [key events]
+ (events/unlistenByKey key))))))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs
index 4fd92a4745..582ea47ac4 100644
--- a/frontend/src/app/main/ui/dashboard/comments.cljs
+++ b/frontend/src/app/main/ui/dashboard/comments.cljs
@@ -55,6 +55,7 @@
[:div.dashboard-comments-section
[:div.button
{:on-click show-dropdown
+ :data-test "open-comments"
:class (dom/classnames :open @show-dropdown?
:unread (boolean (seq tgroups)))}
i/chat]
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index dba16d15b1..085f26b9bc 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -203,27 +203,28 @@
(for [sub-project (:projects team)]
[(get-project-name sub-project)
(on-move (:id team)
- (:id sub-project))])])]))
+ (:id sub-project))])])
+ "move-to-other-team"]))
options (if multi?
- [[(tr "dashboard.duplicate-multi" file-count) on-duplicate]
+ [[(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])
+ [(tr "dashboard.move-to-multi" file-count) nil sub-options "move-to-multi"])
[(tr "dashboard.export-multi" file-count) on-export-files]
[:separator]
- [(tr "labels.delete-multi-files" file-count) on-delete]]
+ [(tr "labels.delete-multi-files" file-count) on-delete nil "delete-multi-files"]]
[[(tr "dashboard.open-in-new-tab") on-new-tab]
- [(tr "labels.rename") on-edit]
- [(tr "dashboard.duplicate") on-duplicate]
+ [(tr "labels.rename") on-edit nil "file-rename"]
+ [(tr "dashboard.duplicate") on-duplicate nil "file-duplicate"]
(when (or (seq current-projects) (seq other-teams))
- [(tr "dashboard.move-to") nil sub-options])
+ [(tr "dashboard.move-to") nil sub-options "file-move-to"])
(if (:is-shared file)
- [(tr "dashboard.remove-shared") on-del-shared]
- [(tr "dashboard.add-shared") on-add-shared])
- [(tr "dashboard.export-single") on-export-files]
+ [(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 "labels.delete") on-delete]])]
+ [(tr "labels.delete") on-delete nil "file-delete"]])]
[:& context-menu {:on-close on-menu-close
:show show?
diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs
index 9d050206ab..770b67f54f 100644
--- a/frontend/src/app/main/ui/dashboard/files.cljs
+++ b/frontend/src/app/main/ui/dashboard/files.cljs
@@ -70,7 +70,7 @@
(with-meta {::ev/origin "project"}))))
(swap! local assoc :edition false)))}]
[:div.dashboard-title
- [:h1 {:on-double-click on-edit}
+ [:h1 {:on-double-click on-edit :data-test "project-title"}
(:name project)]]))
[:& project-menu {:project project
@@ -82,7 +82,7 @@
:on-import on-import}]
[:div.dashboard-header-actions
- [:a.btn-secondary.btn-small {:on-click on-create-clicked}
+ [:a.btn-secondary.btn-small {:on-click on-create-clicked :data-test "new-file"}
(tr "dashboard.new-file")]
(when-not (:is-default project)
diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs
index 0199f42130..706bc1fb68 100644
--- a/frontend/src/app/main/ui/dashboard/fonts.cljs
+++ b/frontend/src/app/main/ui/dashboard/fonts.cljs
@@ -149,10 +149,10 @@
[:span (tr "dashboard.fonts.fonts-added" (i18n/c (count (vals @fonts))))]
[:div.table-field.options
[:div.btn-primary
- {:on-click #(on-upload-all (vals @fonts))}
+ {:on-click #(on-upload-all (vals @fonts)) :data-test "upload-all"}
[:span (tr "dashboard.fonts.upload-all")]]
[:div.btn-secondary
- {:on-click #(on-dismiss-all (vals @fonts))}
+ {:on-click #(on-dismiss-all (vals @fonts)) :data-test "dismiss-all"}
[:span (tr "dashboard.fonts.dismiss-all")]]]])
(for [item (sort-by :font-family (vals @fonts))]
@@ -277,8 +277,8 @@
:fixed? false
:top -15
:left -115
- :options [[(tr "labels.edit") #(reset! edit? true)]
- [(tr "labels.delete") on-delete]]}]])]))
+ :options [[(tr "labels.edit") #(reset! edit? true) nil "font-edit"]
+ [(tr "labels.delete") on-delete nil "font-delete"]]}]])]))
(mf/defc installed-fonts
diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs
index 6edfbb955b..1f80a452dd 100644
--- a/frontend/src/app/main/ui/dashboard/import.cljs
+++ b/frontend/src/app/main/ui/dashboard/import.cljs
@@ -21,7 +21,7 @@
[potok.core :as ptk]
[rumext.alpha :as mf]))
-(log/set-level! :warn)
+(log/set-level! :debug)
(def ^:const emit-delay 1000)
diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs
index 1df3839bdf..291071d7d3 100644
--- a/frontend/src/app/main/ui/dashboard/placeholder.cljs
+++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs
@@ -18,7 +18,7 @@
[:div.grid-item]]
(true? default?)
- [:div.grid-empty-placeholder.drafts
+ [:div.grid-empty-placeholder.drafts {:data-test "empty-placeholder"}
[:div.text
[:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]]
diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs
index 1df2385378..c374ef16de 100644
--- a/frontend/src/app/main/ui/dashboard/project_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs
@@ -108,19 +108,20 @@
:top top
:left left
:options [(when-not (:is-default project)
- [(tr "labels.rename") on-edit])
+ [(tr "labels.rename") on-edit nil "project-rename"])
(when-not (:is-default project)
- [(tr "dashboard.duplicate") on-duplicate])
+ [(tr "dashboard.duplicate") on-duplicate nil "project-duplicate"])
(when-not (:is-default project)
[(tr "dashboard.pin-unpin") toggle-pin])
(when (and (seq teams) (not (:is-default project)))
[(tr "dashboard.move-to") nil
(for [team teams]
- [(:name team) (on-move (:id team))])])
+ [(:name team) (on-move (:id team))])
+ "project-move-to"])
(when (some? on-import)
- [(tr "dashboard.import") on-import-files])
+ [(tr "dashboard.import") on-import-files nil "file-import"])
(when-not (:is-default project)
[:separator])
(when-not (:is-default project)
- [(tr "labels.delete") on-delete])]}]]))
+ [(tr "labels.delete") on-delete nil "project-delete"])]}]]))
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index 9a3ddc966b..b8352a11f4 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -30,7 +30,7 @@
[:div.dashboard-title
[:h1 (tr "dashboard.projects-title")]]
- [:a.btn-secondary.btn-small {:on-click create}
+ [:a.btn-secondary.btn-small {:on-click create :data-test "new-project-button"}
(tr "dashboard.new-project")]]))
(mf/defc project-item
@@ -140,11 +140,11 @@
i/pin)])
[:a.btn-secondary.btn-small.tooltip.tooltip-bottom
- {:on-click create-file :alt (tr "dashboard.new-file")}
+ {:on-click create-file :alt (tr "dashboard.new-file") :data-test "project-new-file"}
i/close]
[:a.btn-secondary.btn-small.tooltip.tooltip-bottom
- {:on-click on-menu-click :alt (tr "dashboard.options")}
+ {:on-click on-menu-click :alt (tr "dashboard.options") :data-test "project-options"}
i/actions]]
[:& line-grid
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index 061b41f45b..7b5cec3f2a 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -221,7 +221,7 @@
[:span.team-text {:title (:name team)} (:name team)]]])
[:hr]
- [:li.action {:on-click on-create-clicked}
+ [:li.action {:on-click on-create-clicked :data-test "create-new-team"}
(tr "dashboard.create-new-team")]]))
(s/def ::member-id ::us/uuid)
@@ -349,21 +349,21 @@
:on-accept delete-fn}))]
[:ul.dropdown.options-dropdown
- [:li {:on-click go-members} (tr "labels.members")]
- [:li {:on-click go-settings} (tr "labels.settings")]
+ [:li {:on-click go-members :data-test "team-members"} (tr "labels.members")]
+ [:li {:on-click go-settings :data-test "team-settings"} (tr "labels.settings")]
[:hr]
- [:li {:on-click on-rename-clicked} (tr "labels.rename")]
+ [:li {:on-click on-rename-clicked :data-test "rename-team"} (tr "labels.rename")]
(cond
(get-in team [:permissions :is-owner])
- [:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
+ [:li {:on-click on-leave-as-owner-clicked :data-test "leave-team"} (tr "dashboard.leave-team")]
(> (count members) 1)
[:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])
(when (get-in team [:permissions :is-owner])
- [:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))
+ [:li {:on-click on-delete-clicked :data-test "delete-team"} (tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch
@@ -466,13 +466,14 @@
[:div.sidebar-content-section
[:ul.sidebar-nav.no-overflow
- [:li.recent-projects
+ [:li
{:on-click go-fonts
+ :data-test "fonts"
:class-name (when fonts? "current")}
[:span.element-title (tr "labels.fonts")]]]]
[:hr]
- [:div.sidebar-content-section
+ [:div.sidebar-content-section {:data-test "pinned-projects"}
(if (seq pinned-projects)
[:ul.sidebar-nav
(for [item pinned-projects]
@@ -501,28 +502,42 @@
(st/emit! section))))]
[:div.profile-section
- [:div.profile {:on-click #(reset! show true)}
+ [:div.profile {:on-click #(reset! show true)
+ :data-test "profile-btn"}
[:img {:src photo}]
[:span (:fullname profile)]
[:& dropdown {:on-close #(reset! show false)
:show @show}
[:ul.dropdown
- [:li {:on-click (partial on-click :settings-profile)}
+ [:li {:on-click (partial on-click :settings-profile)
+ :data-test "profile-profile-opt"}
[:span.icon i/user]
- [:span.text (tr "labels.profile")]]
- [:li {:on-click (partial on-click :settings-password)}
- [:span.icon i/lock]
- [:span.text (tr "labels.password")]]
- [:li {:on-click #(on-click (du/logout) %)}
- [:span.icon i/exit]
- [:span.text (tr "labels.logout")]]
+ [:span.text (tr "labels.your-account")]]
+ [:li.separator {:on-click #(dom/open-new-window "https://help.penpot.app")
+ :data-test "help-center-profile-opt"}
+ [:span.icon i/help]
+ [:span.text (tr "labels.help-center")]]
+ [:li {:on-click #(dom/open-new-window "https://penpot.app/libraries-templates.html")
+ :data-test "libraries-templates-profile-opt"}
+ [:span.icon i/download]
+ [:span.text (tr "labels.libraries-and-templates")]]
+ ;;[:li {:on-click #(dom/open-new-window "https://penpot.app?no-redirect=1")
+ [:li {:on-click #(dom/open-new-window "https://landing-next.penpot.app?no-redirect=1")
+ :data-test "about-penpot-profile-opt"} ;; Parameter ?no-redirect is to force stay in landing page
+ [:span.icon i/logo-icon] ;; instead of redirecting to app
+ [:span.text (tr "labels.about-penpot")]]
(when (contains? @cf/flags :user-feedback)
- [:li.feedback {:on-click (partial on-click :settings-feedback)}
+ [:li.separator {:on-click (partial on-click :settings-feedback)
+ :data-test "feedback-profile-opt"}
[:span.icon i/msg-info]
- [:span.text (tr "labels.give-feedback")]
- ])]]]
+ [:span.text (tr "labels.give-feedback")]])
+
+ [:li.separator {:on-click #(on-click (du/logout) %)
+ :data-test "logout-profile-opt"}
+ [:span.icon i/exit]
+ [:span.text (tr "labels.logout")]]]]]
(when (and team profile)
[:& comments-section {:profile profile
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index 35758737d0..6d98ddae77 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -48,7 +48,7 @@
[:a {:on-click go-settings} (tr "labels.settings")]]]]
(if (and members-section? (:is-admin permissions))
- [:a.btn-secondary.btn-small {:on-click invite-member}
+ [:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"}
(tr "dashboard.invite-profile")]
[:div])]))
diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs
new file mode 100644
index 0000000000..742066c822
--- /dev/null
+++ b/frontend/src/app/main/ui/hooks/resize.cljs
@@ -0,0 +1,109 @@
+;; 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.hooks.resize
+ (:require
+ [app.common.geom.point :as gpt]
+ [app.common.logging :as log]
+ [app.main.refs :as refs]
+ [app.util.dom :as dom]
+ [app.util.storage :refer [storage]]
+ [rumext.alpha :as mf]))
+
+(log/set-level! :warn)
+
+(def last-resize-type nil)
+
+(defn set-resize-type! [type]
+ (set! last-resize-type type))
+
+(defn use-resize-hook
+ [key initial min-val max-val axis negate? resize-type]
+
+ (let [current-file-id (mf/deref refs/current-file-id)
+ size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial))
+ parent-ref (mf/use-ref nil)
+
+ dragging-ref (mf/use-ref false)
+ start-size-ref (mf/use-ref nil)
+ start-ref (mf/use-ref nil)
+
+ on-pointer-down
+ (mf/use-callback
+ (mf/deps @size-state)
+ (fn [event]
+ (dom/capture-pointer event)
+ (mf/set-ref-val! start-size-ref @size-state)
+ (mf/set-ref-val! dragging-ref true)
+ (mf/set-ref-val! start-ref (dom/get-client-position event))
+ (set! last-resize-type resize-type)))
+
+ on-lost-pointer-capture
+ (mf/use-callback
+ (fn [event]
+ (dom/release-pointer event)
+ (mf/set-ref-val! start-size-ref nil)
+ (mf/set-ref-val! dragging-ref false)
+ (mf/set-ref-val! start-ref nil)
+ (set! last-resize-type nil)))
+
+ on-mouse-move
+ (mf/use-callback
+ (mf/deps min-val max-val)
+ (fn [event]
+ (when (mf/ref-val dragging-ref)
+ (let [start (mf/ref-val start-ref)
+ pos (dom/get-client-position event)
+ delta (-> (gpt/to-vec start pos)
+ (cond-> negate? gpt/negate)
+ (get axis))
+ start-size (mf/ref-val start-size-ref)
+ new-size (-> (+ start-size delta) (max min-val) (min max-val))]
+ (reset! size-state new-size)
+ (swap! storage assoc-in [::saved-resize current-file-id key] new-size)))))]
+ {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move
+ :parent-ref parent-ref
+ :size @size-state}))
+
+(defn use-resize-observer
+ [callback]
+ (assert (some? callback))
+
+ (let [prev-val-ref (mf/use-ref nil)
+ current-observer-ref (mf/use-ref nil)
+
+ ;; 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 (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)))]
+
+ (mf/use-effect
+ (fn []
+ ;; On dismount we need to disconnect the current observer
+ (fn []
+ (let [current-observer (mf/ref-val current-observer-ref)]
+ (when (some? current-observer)
+ (log/debug :action "disconnect")
+ (.disconnect current-observer))))))
+ node-ref))
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 0230fa055a..e2d61a77b4 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -64,6 +64,7 @@
(def full-screen-off (icon-xref :full-screen-off))
(def grid (icon-xref :grid))
(def grid-snap (icon-xref :grid-snap))
+(def help (icon-xref :help))
(def icon-empty (icon-xref :icon-empty))
(def icon-list (icon-xref :icon-list))
(def icon-lock (icon-xref :icon-lock))
diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs
index aff6c6f81e..7325580dad 100644
--- a/frontend/src/app/main/ui/messages.cljs
+++ b/frontend/src/app/main/ui/messages.cljs
@@ -15,7 +15,7 @@
[rumext.alpha :as mf]))
(mf/defc banner
- [{:keys [type position status controls content actions on-close] :as props}]
+ [{:keys [type position status controls content actions on-close data-test] :as props}]
[:div.banner {:class (dom/classnames
:warning (= type :warning)
:error (= type :error)
@@ -34,7 +34,8 @@
i/msg-error)]
[:div.content {:class (dom/classnames
:inline-actions (= controls :inline-actions)
- :bottom-actions (= controls :bottom-actions))}
+ :bottom-actions (= controls :bottom-actions))
+ :data-test data-test}
content
(when (or (= controls :bottom-actions) (= controls :inline-actions))
[:div.actions
@@ -59,7 +60,7 @@
(mf/defc inline-banner
{::mf/wrap [mf/memo]}
- [{:keys [type content on-close actions] :as props}]
+ [{:keys [type content on-close actions data-test] :as props}]
[:& banner {:type type
:position :inline
:status :visible
@@ -70,5 +71,6 @@
:none))
:content content
:on-close on-close
- :actions actions}])
+ :actions actions
+ :data-test data-test}])
diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs
index 0f31f117d3..76fb137557 100644
--- a/frontend/src/app/main/ui/onboarding.cljs
+++ b/frontend/src/app/main/ui/onboarding.cljs
@@ -27,14 +27,14 @@
[:img {:src "images/login-on.jpg" :border "0" :alt (tr "onboarding.welcome.alt")}]]
[:div.modal-right
[:div.modal-title
- [:h2 (tr "onboarding.welcome.title")]]
+ [:h2 {:data-test "onboarding-welcome"} (tr "onboarding.welcome.title")]]
[:span.release "Beta version " (:main @cf/version)]
[:div.modal-content
[:p (tr "onboarding.welcome.desc1")]
[:p (tr "onboarding.welcome.desc2")]
[:p (tr "onboarding.welcome.desc3")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click next} (tr "labels.continue")]]]
+ [:button.btn-secondary {:on-click next :data-test "onboarding-next-btn"} (tr "labels.continue")]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
[:img.deco.right {:src "images/deco-right.png" :border "0"}]])
@@ -55,7 +55,7 @@
"\u00A0"
(tr "onboarding.contrib.desc2.2")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click next} (tr "labels.continue")]]]])
+ [:button.btn-secondary {:on-click next :data-test "opsource-next-btn"} (tr "labels.continue")]]]])
(defmulti render-slide :slide)
@@ -67,13 +67,14 @@
[:img {:src "images/on-design.gif" :border "0" :alt (tr "onboarding.slide.0.alt")}]]
[:div.modal-right
[:div.modal-title
- [:h2 (tr "onboarding.slide.0.title")]]
+ [:h2 {:data-test "slide-0-title"} (tr "onboarding.slide.0.title")]]
[:div.modal-content
[:p (tr "onboarding.slide.0.desc1")]
[:p (tr "onboarding.slide.0.desc2")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click #(navigate 1)} (tr "labels.continue")]
- [:span.skip {:on-click skip} (tr "labels.skip")]
+ [:button.btn-secondary {:on-click #(navigate 1)
+ :data-test "slide-0-btn"} (tr "labels.continue")]
+ [:span.skip {:on-click skip :data-test "skip-btn"} (tr "labels.skip")]
[:& rc/navigation-bullets
{:slide slide
:navigate navigate
@@ -87,13 +88,14 @@
[:img {:src "images/on-proto.gif" :border "0" :alt (tr "onboarding.slide.1.alt")}]]
[:div.modal-right
[:div.modal-title
- [:h2 (tr "onboarding.slide.1.title")]]
+ [:h2 {:data-test "slide-1-title"} (tr "onboarding.slide.1.title")]]
[:div.modal-content
[:p (tr "onboarding.slide.1.desc1")]
[:p (tr "onboarding.slide.1.desc2")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click #(navigate 2)} (tr "labels.continue")]
- [:span.skip {:on-click skip} (tr "labels.skip")]
+ [:button.btn-secondary {:on-click #(navigate 2)
+ :data-test "slide-1-btn"} (tr "labels.continue")]
+ [:span.skip {:on-click skip :data-test "skip-btn"} (tr "labels.skip")]
[:& rc/navigation-bullets
{:slide slide
:navigate navigate
@@ -107,12 +109,13 @@
[:img {:src "images/on-feed.gif" :border "0" :alt (tr "onboarding.slide.2.alt")}]]
[:div.modal-right
[:div.modal-title
- [:h2 (tr "onboarding.slide.2.title")]]
+ [:h2 {:data-test "slide-2-title"} (tr "onboarding.slide.2.title")]]
[:div.modal-content
[:p (tr "onboarding.slide.2.desc1")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click #(navigate 3)} (tr "labels.continue")]
- [:span.skip {:on-click skip} (tr "labels.skip")]
+ [:button.btn-secondary {:on-click #(navigate 3)
+ :data-test "slide-2-btn"} (tr "labels.continue")]
+ [:span.skip {:on-click skip :data-test "skip-btn"} (tr "labels.skip")]
[:& rc/navigation-bullets
{:slide slide
:navigate navigate
@@ -126,12 +129,13 @@
[:img {:src "images/on-handoff.gif" :border "0" :alt (tr "onboarding.slide.3.alt")}]]
[:div.modal-right
[:div.modal-title
- [:h2 (tr "onboarding.slide.3.title")]]
+ [:h2 {:data-test "slide-3-title"} (tr "onboarding.slide.3.title")]]
[:div.modal-content
[:p (tr "onboarding.slide.3.desc1")]
[:p (tr "onboarding.slide.3.desc2")]]
[:div.modal-navigation
- [:button.btn-secondary {:on-click skip} (tr "labels.start")]
+ [:button.btn-secondary {:on-click skip
+ :data-test "slide-3-btn"} (tr "labels.start")]
[:& rc/navigation-bullets
{:slide slide
:navigate navigate
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs
index 6e5961a8fa..23773ba768 100644
--- a/frontend/src/app/main/ui/onboarding/team_choice.cljs
+++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs
@@ -30,7 +30,7 @@
;; the onboarding templates modal.
on-fly-solo
(fn []
- (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
+ (tm/schedule 400 #(st/emit! (modal/hide))))
;; When user choices the option of `team up`, we proceed to show
;; the team creation modal.
@@ -42,15 +42,16 @@
[:div.modal-overlay
[:div.modal-container.onboarding.final.animated.fadeInUp
[:div.modal-top
- [:h1 (tr "onboarding.welcome.title")]
+ [:h1 {:data-test "onboarding-welcome-title"} (tr "onboarding.welcome.title")]
[:p (tr "onboarding.welcome.desc3")]]
[:div.modal-columns
[:div.modal-left
- [:div.content-button {:on-click on-fly-solo}
+ [:div.content-button {:on-click on-fly-solo
+ :data-test "fly-solo-op"}
[:h2 (tr "onboarding.choice.fly-solo")]
[:p (tr "onboarding.choice.fly-solo-desc")]]]
[:div.modal-right
- [:div.content-button {:on-click on-team-up}
+ [:div.content-button {:on-click on-team-up :data-test "team-up-button"}
[:h2 (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]]]
[:img.deco {:src "images/deco-left.png" :border "0"}]
@@ -71,7 +72,7 @@
[:div.modal-overlay
[:div.modal-container.onboarding-team
[:div.title
- [:h2 (tr "onboarding.choice.team-up")]
+ [:h2 {:data-test "onboarding-choice-team-up"} (tr "onboarding.choice.team-up")]
[:p (tr "onboarding.choice.team-up-desc")]]
[:& fm/form {:form form
@@ -120,14 +121,12 @@
on-success
(mf/use-callback
(fn [_form response]
- (let [project-id (:default-project-id response)
- team-id (:id response)]
+ (let [team-id (:id response)]
(st/emit!
(modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
- (modal/show {:type :onboarding-templates
- :project-id project-id}))))))
+ (modal/hide))))))
on-error
(mf/use-callback
diff --git a/frontend/src/app/main/ui/onboarding/templates.cljs b/frontend/src/app/main/ui/onboarding/templates.cljs
index de4a5b381a..84bfc15582 100644
--- a/frontend/src/app/main/ui/onboarding/templates.cljs
+++ b/frontend/src/app/main/ui/onboarding/templates.cljs
@@ -70,7 +70,8 @@
[:div.modal-container.onboarding-templates
[:div.modal-header
[:div.modal-close-button
- {:on-click close-fn} i/close]]
+ {:on-click close-fn
+ :data-test "close-templates-btn"} i/close]]
[:div.modal-content
[:h3 (tr "onboarding.templates.title")]
diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs
index 85b6b499b1..c478f05dbb 100644
--- a/frontend/src/app/main/ui/releases.cljs
+++ b/frontend/src/app/main/ui/releases.cljs
@@ -10,8 +10,9 @@
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.releases.common :as rc]
- [app.main.ui.releases.v1-10]
+ [app.main.ui.releases.v1-12]
[app.main.ui.releases.v1-11]
+ [app.main.ui.releases.v1-10]
[app.main.ui.releases.v1-4]
[app.main.ui.releases.v1-5]
[app.main.ui.releases.v1-6]
@@ -82,4 +83,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
- (rc/render-release-notes (assoc params :version "1.11")))
+ (rc/render-release-notes (assoc params :version "1.12")))
diff --git a/frontend/src/app/main/ui/releases/v1_12.cljs b/frontend/src/app/main/ui/releases/v1_12.cljs
new file mode 100644
index 0000000000..b494cb568a
--- /dev/null
+++ b/frontend/src/app/main/ui/releases/v1_12.cljs
@@ -0,0 +1,107 @@
+;; 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.releases.v1-12
+ (:require
+ [app.main.ui.releases.common :as c]
+ [rumext.alpha :as mf]))
+
+(defmethod c/render-release-notes "1.12"
+ [{:keys [slide klass next finish navigate version]}]
+ (mf/html
+ (case @slide
+ :start
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Beta release 1.12"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "What's new?"]]
+ [: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."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]]]
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]]
+
+ 0
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/1.12-ui.gif" :border "0" :alt "Adjustable UI"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Adjustable UI"]]
+ [:div.modal-content
+ [:p "Adjust the workspace interface to your unique experience. Resize the sidebar, the layers panel or hide the whole UI to have maximum space."]
+ [:p "Along with a better organization of panels (say hello to typography toolbar!) and new shortcuts that will speed your workflow."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& c/navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 1
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/1.12-guides.gif" :border "0" :alt "Guides"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Guides"]]
+ [:div.modal-content
+ [:p "One of our most requested features! Itโs hard to believe how far Penpot has come without guides, but they are here at last."]
+ [:p "And they donโt come alone, but with some nice improvements to the rulers."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& c/navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 2
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/1.12-scrollbars.gif" :border "0" :alt "Scrollbars"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Scrollbars"]]
+ [:div.modal-content
+ [:p "Scrollbars at the design workspace will make it more obvious how to navigate it and easier for some users, for instance those who love using graphic tablets, from now on, will feel just as comfortable as those who use a mouseAnd they donโt come alone, but with some nice improvements to the rulers."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& c/navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 3
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/1.12-nudge.gif" :border "0" :alt "Nudge amount"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Nudge amount"]]
+ [:div.modal-content
+ [:p "Set your desired distance to move objects using the keyboard."]
+ [:p "This is a must if youโre working with grids (if youโre not, you should ;)), being able to adjust the movement to your baseline grid (8px? 5px?) is a huge timesaver that will improve your quality of life while designing."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click finish} "Start!"]
+ [:& c/navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs
index d5909a9ed5..6d021f5343 100644
--- a/frontend/src/app/main/ui/render.cljs
+++ b/frontend/src/app/main/ui/render.cljs
@@ -10,7 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.fonts :as df]
[app.main.render :as render]
@@ -60,7 +60,7 @@
(gpt/negate)
(gmt/translate-matrix))
- mod-ids (cons frame-id (cp/get-children frame-id objects))
+ mod-ids (cons frame-id (cph/get-children-ids objects frame-id))
updt-fn #(-> %1
(assoc-in [%2 :modifiers :displacement] modifier)
(update %2 gsh/transform-shape))
@@ -78,28 +78,24 @@
vbox (str/join " " coords)
frame-wrapper
- (mf/use-memo
- (mf/deps objects)
- #(render/frame-wrapper-factory objects))
+ (mf/with-memo [objects]
+ (render/frame-wrapper-factory objects))
group-wrapper
- (mf/use-memo
- (mf/deps objects)
- #(render/group-wrapper-factory objects))
+ (mf/with-memo [objects]
+ (render/group-wrapper-factory objects))
shape-wrapper
- (mf/use-memo
- (mf/deps objects)
- #(render/shape-wrapper-factory objects))
+ (mf/with-memo [objects]
+ (render/shape-wrapper-factory objects))
text-shapes
(->> objects
(filter (fn [[_ shape]] (= :text (:type shape))))
(mapv second))]
- (mf/use-effect
- (mf/deps width height)
- #(dom/set-page-style {:size (str (mth/ceil width) "px "
+ (mf/with-effect [width height]
+ (dom/set-page-style {:size (str (mth/ceil width) "px "
(mth/ceil height) "px")}))
[:& (mf/provider embed/context) {:value false}
@@ -137,7 +133,7 @@
[objects object-id]
(if (uuid/zero? object-id)
(let [object (get objects object-id)
- shapes (cp/select-toplevel-shapes objects {:include-frames? true})
+ shapes (cph/get-immediate-children objects)
srect (gsh/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (gsh/transform-shape object)
@@ -148,20 +144,19 @@
(mf/defc render-object
[{:keys [file-id page-id object-id render-texts?] :as props}]
(let [objects (mf/use-state nil)]
- (mf/use-effect
- (mf/deps file-id page-id object-id)
- (fn []
- (->> (rx/zip
- (repo/query! :font-variants {:file-id file-id})
- (repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id}))
- (rx/subs
- (fn [[fonts {:keys [data]}]]
- (when (seq fonts)
- (st/emit! (df/fonts-fetched fonts)))
- (let [objs (get-in data [:pages-index page-id :objects])
- objs (adapt-root-frame objs object-id)]
- (reset! objects objs)))))
- (constantly nil)))
+
+ (mf/with-effect [file-id page-id object-id]
+ (->> (rx/zip
+ (repo/query! :font-variants {:file-id file-id})
+ (repo/query! :trimmed-file {:id file-id :page-id page-id :object-id object-id}))
+ (rx/subs
+ (fn [[fonts {:keys [data]}]]
+ (when (seq fonts)
+ (st/emit! (df/fonts-fetched fonts)))
+ (let [objs (get-in data [:pages-index page-id :objects])
+ objs (adapt-root-frame objs object-id)]
+ (reset! objects objs)))))
+ (constantly nil))
(when @objects
[:& object-svg {:objects @objects
@@ -172,14 +167,13 @@
(mf/defc render-sprite
[{:keys [file-id component-id] :as props}]
(let [file (mf/use-state nil)]
- (mf/use-effect
- (mf/deps file-id)
- (fn []
- (->> (repo/query! :file {:id file-id})
- (rx/subs
- (fn [result]
- (reset! file result))))
- (constantly nil)))
+
+ (mf/with-effect [file-id]
+ (->> (repo/query! :file {:id file-id})
+ (rx/subs
+ (fn [result]
+ (reset! file result))))
+ (constantly nil))
(when @file
[:*
diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs
index 6adc821c29..46edd62c13 100644
--- a/frontend/src/app/main/ui/settings.cljs
+++ b/frontend/src/app/main/ui/settings.cljs
@@ -22,7 +22,7 @@
[]
[:header.dashboard-header
[:div.dashboard-title
- [:h1 (tr "dashboard.your-account-title")]]])
+ [:h1 {:data-test "account-title"} (tr "dashboard.your-account-title")]]])
(mf/defc settings
[{:keys [route] :as props}]
diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs
index 7c538fbffa..0574499096 100644
--- a/frontend/src/app/main/ui/settings/change_email.cljs
+++ b/frontend/src/app/main/ui/settings/change_email.cljs
@@ -86,7 +86,8 @@
[:div.modal-header
[:div.modal-header-title
- [:h2 (tr "modals.change-email.title")]]
+ [:h2 {:data-test "change-email-title"}
+ (tr "modals.change-email.title")]]
[:div.modal-close-button
{:on-click on-close} i/close]]
@@ -108,7 +109,7 @@
:trim true}]]]]
[:div.modal-footer
- [:div.action-buttons
+ [:div.action-buttons {:data-test "change-email-submit"}
[:& fm/submit-button
{:label (tr "modals.change-email.submit")}]]]]]]))
diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs
index f4e7262192..886d966800 100644
--- a/frontend/src/app/main/ui/settings/delete_account.cljs
+++ b/frontend/src/app/main/ui/settings/delete_account.cljs
@@ -51,7 +51,8 @@
[:div.modal-footer
[:div.action-buttons
- [:button.btn-warning.btn-large {:on-click on-accept}
+ [:button.btn-warning.btn-large {:on-click on-accept
+ :data-test "delete-account-btn"}
(tr "modals.delete-account.confirm")]
[:button.btn-secondary.btn-large {:on-click on-close}
(tr "modals.delete-account.cancel")]]]]]))
diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs
index c2317e32d3..1793d5bfb2 100644
--- a/frontend/src/app/main/ui/settings/options.cljs
+++ b/frontend/src/app/main/ui/settings/options.cljs
@@ -48,20 +48,24 @@
[:h2 (t locale "labels.language")]
[:div.fields-row
- [:& fm/select {:options (into [{:label "Auto (browser)" :value ""}]
+ [:& fm/select {:options (into [{:label "Auto (browser)" :value "default"}]
i18n/supported-locales)
:label (t locale "dashboard.select-ui-language")
:default ""
- :name :lang}]]
-
- [:h2 (t locale "dashboard.theme-change")]
- [:div.fields-row
+ :name :lang
+ :data-test "setting-lang"}]]
+
+ ;; TODO: Do not show as long as we only have one theme
+ #_[:h2 (t locale "dashboard.theme-change")]
+ #_[:div.fields-row
[:& fm/select {:label (t locale "dashboard.select-ui-theme")
:name :theme
:default "default"
- :options [{:label "Default" :value "default"}]}]]
+ :options [{:label "Default" :value "default"}]
+ :data-test "theme-lang"}]]
[:& fm/submit-button
- {:label (t locale "dashboard.update-settings")}]]))
+ {:label (t locale "dashboard.update-settings")
+ :data-test "submit-lang-change"}]]))
;; --- Password Page
@@ -72,4 +76,5 @@
[:div.dashboard-settings
[:div.form-container
+ {:data-test "settings-form"}
[:& options-form {:locale locale}]]])
diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs
index 12d43561e6..225af7fc17 100644
--- a/frontend/src/app/main/ui/settings/password.cljs
+++ b/frontend/src/app/main/ui/settings/password.cljs
@@ -22,6 +22,9 @@
:old-password-not-match
(swap! form assoc-in [:errors :password-old]
{:message (tr "errors.wrong-old-password")})
+ :email-as-password
+ (swap! form assoc-in [:errors :password-1]
+ {:message (tr "errors.email-as-password")})
(let [msg (tr "generic.error")]
(st/emit! (dm/error msg)))))
@@ -89,7 +92,8 @@
:label (t locale "labels.confirm-password")}]]
[:& fm/submit-button
- {:label (t locale "dashboard.update-settings")}]]))
+ {:label (t locale "dashboard.update-settings")
+ :data-test "submit-password"}]]))
;; --- Password Page
diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs
index eded1e265e..e92a71eaa6 100644
--- a/frontend/src/app/main/ui/settings/profile.cljs
+++ b/frontend/src/app/main/ui/settings/profile.cljs
@@ -71,7 +71,8 @@
[:div.links
[:div.link-item
- [:a {:on-click #(modal/show! :delete-account {})}
+ [:a {:on-click #(modal/show! :delete-account {})
+ :data-test "remove-acount-btn"}
(t locale "dashboard.remove-account")]]]]))
;; --- Profile Photo Form
@@ -94,7 +95,8 @@
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:ref file-input
- :on-selected on-file-selected}]]]))
+ :on-selected on-file-selected
+ :data-test "profile-image-input"}]]]))
;; --- Profile Page
diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs
index aa3ec05ec9..0b9cce30de 100644
--- a/frontend/src/app/main/ui/settings/sidebar.cljs
+++ b/frontend/src/app/main/ui/settings/sidebar.cljs
@@ -80,13 +80,14 @@
[:span.element-title (tr "labels.password")]]
[:li {:class (when options? "current")
- :on-click go-settings-options}
+ :on-click go-settings-options
+ :data-test "settings-profile"}
i/tree
[:span.element-title (tr "labels.settings")]]
[:hr]
- [:li {:on-click show-release-notes}
+ [:li {:on-click show-release-notes :data-test "release-notes"}
i/pencil
[:span.element-title (tr "labels.release-notes")]]
diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs
index ae5967d1ba..dd6a041abb 100644
--- a/frontend/src/app/main/ui/shapes/attrs.cljs
+++ b/frontend/src/app/main/ui/shapes/attrs.cljs
@@ -6,8 +6,8 @@
(ns app.main.ui.shapes.attrs
(:require
- [app.common.pages.spec :as spec]
- [app.common.types.radius :as ctr]
+ [app.common.spec.radius :as ctr]
+ [app.common.spec.shape :refer [stroke-caps-line stroke-caps-marker]]
[app.main.ui.context :as muc]
[app.util.object :as obj]
[app.util.svg :as usvg]
@@ -81,6 +81,10 @@
(defn add-fill [attrs shape render-id]
(let [fill-attrs (cond
+ (contains? shape :fill-image)
+ (let [fill-image-id (str "fill-image-" render-id)]
+ {:fill (str/format "url(#%s)" fill-image-id)})
+
(contains? shape :fill-color-gradient)
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
{:fill (str/format "url(#%s)" fill-color-gradient-id)})
@@ -88,14 +92,10 @@
(contains? shape :fill-color)
{:fill (:fill-color shape)}
- (contains? shape :fill-image)
- (let [fill-image-id (str "fill-image-" render-id)]
- {:fill (str/format "url(#%s)" fill-image-id) })
-
;; If contains svg-attrs the origin is svg. If it's not svg origin
;; we setup the default fill as transparent (instead of black)
(and (not (contains? shape :svg-attrs))
- (not (#{ :svg-raw :group } (:type shape))))
+ (not (#{:svg-raw :group} (:type shape))))
{:fill "none"}
:else
@@ -131,7 +131,7 @@
;; For simple line caps we use svg stroke-line-cap attribute. This
;; only works if all caps are the same and we are not using the tricks
;; for inner or outer strokes.
- (and (spec/stroke-caps-line (:stroke-cap-start shape))
+ (and (stroke-caps-line (:stroke-cap-start shape))
(= (:stroke-cap-start shape) (:stroke-cap-end shape))
(not (#{:inner :outer} (:stroke-alignment shape)))
(not= :dotted stroke-style))
@@ -141,15 +141,15 @@
(assoc :strokeLinecap "round")
;; For other cap types we use markers.
- (and (or (spec/stroke-caps-marker (:stroke-cap-start shape))
- (and (spec/stroke-caps-line (:stroke-cap-start shape))
+ (and (or (stroke-caps-marker (:stroke-cap-start shape))
+ (and (stroke-caps-line (:stroke-cap-start shape))
(not= (:stroke-cap-start shape) (:stroke-cap-end shape))))
(not (#{:inner :outer} (:stroke-alignment shape))))
(assoc :markerStart
(str/format "url(#marker-%s-%s)" render-id (name (:stroke-cap-start shape))))
- (and (or (spec/stroke-caps-marker (:stroke-cap-end shape))
- (and (spec/stroke-caps-line (:stroke-cap-end shape))
+ (and (or (stroke-caps-marker (:stroke-cap-end shape))
+ (and (stroke-caps-line (:stroke-cap-end shape))
(not= (:stroke-cap-start shape) (:stroke-cap-end shape))))
(not (#{:inner :outer} (:stroke-alignment shape))))
(assoc :markerEnd
@@ -206,3 +206,16 @@
[shape]
(-> (obj/new)
(add-style-attrs shape)))
+
+(defn extract-fill-attrs
+ [shape]
+ (let [render-id (mf/use-ctx muc/render-ctx)
+ fill-styles (-> (obj/get shape "style" (obj/new))
+ (add-fill shape render-id))]
+ (-> (obj/new)
+ (obj/set! "style" fill-styles))))
+
+(defn extract-border-radius-attrs
+ [shape]
+ (-> (obj/new)
+ (add-border-radius shape)))
diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs
index 1ea58e5ed5..d61fe8a49d 100644
--- a/frontend/src/app/main/ui/shapes/export.cljs
+++ b/frontend/src/app/main/ui/shapes/export.cljs
@@ -64,6 +64,7 @@
(let [add! (add-factory shape)
group? (= :group (:type shape))
rect? (= :rect (:type shape))
+ image? (= :image (:type shape))
text? (= :text (:type shape))
path? (= :path (:type shape))
mask? (and group? (:masked-group? shape))
@@ -92,12 +93,21 @@
(add! :constraints-v)
(add! :fixed-scroll)
- (cond-> (and rect? (some? (:r1 shape)))
+ (cond-> (and (or rect? image?) (some? (:r1 shape)))
(-> (add! :r1)
(add! :r2)
(add! :r3)
(add! :r4)))
+ (cond-> (and image? (some? (:rx shape)))
+ (-> (add! :rx)
+ (add! :ry)))
+
+ (cond-> image?
+ (-> (add! :fill-color)
+ (add! :fill-opacity)
+ (add! :fill-color-gradient)))
+
(cond-> path?
(-> (add! :stroke-cap-start)
(add! :stroke-cap-end)))
@@ -155,20 +165,30 @@
:name name
:starting-frame starting-frame}])])
+(mf/defc export-guides
+ [{:keys [guides]}]
+ [:> "penpot:guides" #js {}
+ (for [{:keys [position frame-id axis]} (vals guides)]
+ [:> "penpot:guide" #js {:position position
+ :frame-id frame-id
+ :axis (d/name axis)}])])
+
(mf/defc export-page
[{:keys [options]}]
(let [saved-grids (get options :saved-grids)
- flows (get options :flows)]
- (when (or (seq saved-grids) (seq flows))
- (let [parse-grid
- (fn [[type params]]
- {:type type :params params})
+ flows (get options :flows)
+ guides (get options :guides)]
+ [:> "penpot:page" #js {}
+ (when (d/not-empty? saved-grids)
+ (let [parse-grid (fn [[type params]] {:type type :params params})
grids (->> saved-grids (mapv parse-grid))]
- [:> "penpot:page" #js {}
- (when (seq saved-grids)
- [:& export-grid-data {:grids grids}])
- (when (seq flows)
- [:& export-flows {:flows flows}])]))))
+ [:& export-grid-data {:grids grids}]))
+
+ (when (d/not-empty? flows)
+ [:& export-flows {:flows flows}])
+
+ (when (d/not-empty? guides)
+ [:& export-guides {:guides guides}])]))
(defn- export-shadow-data [{:keys [shadow]}]
(mf/html
diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs
index 986dbc37e3..e3c8f2dc71 100644
--- a/frontend/src/app/main/ui/shapes/fill_image.cljs
+++ b/frontend/src/app/main/ui/shapes/fill_image.cljs
@@ -8,6 +8,7 @@
(:require
[app.common.geom.shapes :as gsh]
[app.config :as cfg]
+ [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.embed :as embed]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@@ -23,7 +24,11 @@
fill-image-id (str "fill-image-" render-id)
uri (cfg/resolve-file-media (:fill-image shape))
embed (embed/use-data-uris [uri])
- transform (gsh/transform-matrix shape)]
+ transform (gsh/transform-matrix shape)
+ shape-without-image (dissoc shape :fill-image)
+ fill-attrs (-> (attrs/extract-fill-attrs shape-without-image)
+ (obj/set! "width" width)
+ (obj/set! "height" height))]
[:pattern {:id fill-image-id
:patternUnits "userSpaceOnUse"
@@ -33,6 +38,9 @@
:width width
:patternTransform transform
:data-loading (str (not (contains? embed uri)))}
- [:image {:xlinkHref (get embed uri uri)
- :width width
- :height height}]]))))
+ [:g
+ [:> :rect fill-attrs]
+ [:image {:xlinkHref (get embed uri uri)
+ :width width
+ :height height}]
+ ]]))))
diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs
index 6ca3abbead..bd9bca876e 100644
--- a/frontend/src/app/main/ui/shapes/gradients.cljs
+++ b/frontend/src/app/main/ui/shapes/gradients.cljs
@@ -46,41 +46,40 @@
:stop-color color
:stop-opacity opacity}])]))
-
-
(mf/defc radial-gradient [{:keys [id gradient shape]}]
- (let [{:keys [x y width height]} (:selrect shape)
- transform (if (= :path (:type shape))
- (gsh/transform-matrix shape)
- (gmt/matrix))
- [x y] (if (= (:type shape) :frame) [0 0] [x y])
- translate-vec (gpt/point (+ x (* width (:start-x gradient)))
- (+ y (* height (:start-y gradient))))
+ (let [path? (= :path (:type shape))
+ shape-transform (or (when path? (:transform shape)) (gmt/matrix))
+ shape-transform-inv (or (when path? (:transform-inverse shape)) (gmt/matrix))
- gradient-vec (gpt/to-vec (gpt/point (* width (:start-x gradient))
- (* height (:start-y gradient)))
- (gpt/point (* width (:end-x gradient))
- (* height (:end-y gradient))))
+ {:keys [start-x start-y end-x end-y] gwidth :width} gradient
- angle (gpt/angle gradient-vec
- (gpt/point 1 0))
+ gradient-vec (gpt/to-vec (gpt/point start-x start-y)
+ (gpt/point end-x end-y))
- scale-factor-y (/ (gpt/length gradient-vec) (/ height 2))
- scale-factor-x (* scale-factor-y (:width gradient))
+ angle (+ (gpt/angle gradient-vec) 90)
- scale-vec (gpt/point (* scale-factor-y (/ height 2))
- (* scale-factor-x (/ width 2)))
+ bb-shape (gsh/selection-rect [shape])
- transform (gmt/multiply transform
- (gmt/translate-matrix translate-vec)
- (gmt/rotate-matrix angle)
- (gmt/scale-matrix scale-vec))
+ ;; Paths don't have a transform in SVG because we transform the points
+ ;; we need to compensate the difference between the original rectangle
+ ;; and the transformed one. This factor is that calculation.
+ factor (if path?
+ (/ (:height (:selrect shape)) (:height bb-shape))
+ 1.0)
+ transform (-> (gmt/matrix)
+ (gmt/translate (gpt/point start-x start-y))
+ (gmt/multiply shape-transform)
+ (gmt/rotate angle)
+ (gmt/scale (gpt/point gwidth factor))
+ (gmt/multiply shape-transform-inv)
+ (gmt/translate (gpt/negate (gpt/point start-x start-y))))
+
+ gradient-radius (gpt/length gradient-vec)
base-props #js {:id id
- :cx 0
- :cy 0
- :r 1
- :gradientUnits "userSpaceOnUse"
+ :cx start-x
+ :cy start-y
+ :r gradient-radius
:gradientTransform 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 5fa4eec47a..988c522472 100644
--- a/frontend/src/app/main/ui/shapes/image.cljs
+++ b/frontend/src/app/main/ui/shapes/image.cljs
@@ -6,11 +6,12 @@
(ns app.main.ui.shapes.image
(:require
- [app.common.geom.shapes :as geom]
+ [app.common.geom.shapes :as gsh]
[app.config :as cfg]
+ [app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
+ [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.embed :as embed]
- [app.util.dom :as dom]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@@ -23,22 +24,40 @@
uri (cfg/resolve-file-media metadata)
embed (embed/use-data-uris [uri])
- transform (geom/transform-matrix shape)
+ transform (gsh/transform-matrix shape)
+
+ fill-attrs (-> (attrs/extract-fill-attrs shape)
+ (obj/set! "width" width)
+ (obj/set! "height" height))
+
+ render-id (mf/use-ctx muc/render-ctx)
+ fill-image-id (str "fill-image-" render-id)
+ shape (assoc shape :fill-image fill-image-id)
props (-> (attrs/extract-style-attrs shape)
+ (obj/merge! (attrs/extract-border-radius-attrs shape))
(obj/merge!
#js {:x x
:y y
:transform transform
:width width
- :height height
- :preserveAspectRatio "none"
- :data-loading (str (not (contains? embed uri)))}))
+ :height height}))
+ path? (some? (.-d props))]
- on-drag-start (fn [event]
- ;; Prevent browser dragging of the image
- (dom/prevent-default event))]
-
- [:> "image" (obj/merge!
- props
- #js {:xlinkHref (get embed uri uri)
- :onDragStart on-drag-start})]))
+ [:g
+ [:defs
+ [:pattern {:id fill-image-id
+ :patternUnits "userSpaceOnUse"
+ :x x
+ :y y
+ :height height
+ :width width
+ :data-loading (str (not (contains? embed uri)))}
+ [:g
+ [:> :rect fill-attrs]
+ [:image {:xlinkHref (get embed uri uri)
+ :width width
+ :height height}]]]]
+ [:& shape-custom-stroke {:shape shape}
+ (if path?
+ [:> :path props]
+ [:> :rect props])]]))
diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs
index 432f4ec050..144c472d9b 100644
--- a/frontend/src/app/main/ui/viewer.cljs
+++ b/frontend/src/app/main/ui/viewer.cljs
@@ -230,6 +230,8 @@
:section section}]
[:div.viewer-content
+ [:div.thumbnail-close {:on-click #(st/emit! dv/close-thumbnails-panel)
+ :class (dom/classnames :invisible (not (:show-thumbnails local false)))}]
[:& thumbnails-panel {:frames frames
:show? (:show-thumbnails local false)
:page page
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
index 2792ae1fef..7a09206895 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs
@@ -26,7 +26,7 @@
:rect [:layout :fill :stroke :shadow :blur :svg]
:circle [:layout :fill :stroke :shadow :blur :svg]
:path [:layout :fill :stroke :shadow :blur :svg]
- :image [:image :layout :shadow :blur :svg]
+ :image [:image :layout :fill :stroke :shadow :blur :svg]
:text [:layout :text :shadow :blur]})
(mf/defc attributes
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs
index c1ee2269f0..8c600a2d74 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs
@@ -24,7 +24,7 @@
(defn has-color? [shape]
(and
- (not (contains? #{:image :text :group} (:type shape)))
+ (not (contains? #{:text :group} (:type shape)))
(or (:fill-color shape)
(:fill-color-gradient shape))))
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 bc8c485898..abf9fc37bb 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs
@@ -7,7 +7,7 @@
(ns app.main.ui.viewer.handoff.attributes.layout
(:require
[app.common.math :as mth]
- [app.common.types.radius :as ctr]
+ [app.common.spec.radius :as ctr]
[app.main.ui.components.copy-button :refer [copy-button]]
[app.util.code-gen :as cg]
[app.util.i18n :refer [t]]
diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs
index 67a934f5c7..94146d5195 100644
--- a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs
@@ -35,22 +35,18 @@
(let [color-format (mf/use-state :hex)]
[:div.attributes-shadow-block
[:div.attributes-shadow-row
- [:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (tr))]
- [:div.attributes-shadow
- [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-x")]
- [:div.attributes-value (str (:offset-x shadow))]]
+ [:div.attributes-label (->> shadow :style d/name (str "workspace.options.shadow-options.") (tr))]
+ [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.offsetx")}
+ [:div.attributes-value (str (:offset-x shadow) "px")]]
- [:div.attributes-shadow
- [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.offset-y")]
- [:div.attributes-value (str (:offset-y shadow))]]
+ [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.offsety")}
+ [:div.attributes-value (str (:offset-y shadow) "px")]]
- [:div.attributes-shadow
- [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.blur")]
- [:div.attributes-value (str (:blur shadow))]]
+ [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.blur")}
+ [:div.attributes-value (str (:blur shadow) "px")]]
- [:div.attributes-shadow
- [:div.attributes-label (tr "handoff.attributes.shadow.shorthand.spread")]
- [:div.attributes-value (str (:spread shadow))]]
+ [:div.attributes-shadow {:title (tr "workspace.options.shadow-options.spread")}
+ [:div.attributes-value (str (:spread shadow) "px")]]
[:& copy-button {:data (shadow-copy-data shadow)}]]
@@ -60,12 +56,10 @@
(mf/defc shadow-panel [{:keys [shapes]}]
(let [shapes (->> shapes (filter has-shadow?))]
- (when (seq shapes)
+ (when (and (seq shapes) (> (count shapes) 0))
[:div.attributes-block
[:div.attributes-block-title
- [:div.attributes-block-title-text (tr "handoff.attributes.shadow")]
- (when (= (count shapes) 1)
- [:& copy-button {:data (shape-copy-data (first shapes))}])]
+ [:div.attributes-block-title-text (tr "handoff.attributes.shadow")]]
[:div.attributes-shadow-blocks
(for [shape shapes]
diff --git a/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs
index 6dd87c2a76..4319c262d1 100644
--- a/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs
@@ -9,8 +9,9 @@
[app.common.data :as d]
[app.main.data.viewer :as dv]
[app.main.store :as st]
+ [app.main.ui.components.shape-icon :as si]
[app.main.ui.icons :as i]
- [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name]]
+ [app.main.ui.workspace.sidebar.layers :refer [layer-name]]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[okulary.core :as l]
@@ -70,7 +71,7 @@
[:div.element-list-body {:class (dom/classnames :selected selected?
:icon-layer (= (:type item) :icon))
:on-click select-shape}
- [:& element-icon {:shape item}]
+ [:& si/element-icon {:shape item}]
[:& layer-name {:shape item}]
(when (and (not disable-collapse?) (:shapes item))
diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs
index 0f1854fc15..7aa9c7568f 100644
--- a/frontend/src/app/main/ui/viewer/handoff/render.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs
@@ -8,7 +8,7 @@
"The main container for a frame in handoff mode"
(:require
[app.common.geom.shapes :as geom]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.data.viewer :as dv]
[app.main.store :as st]
[app.main.ui.shapes.bool :as bool]
@@ -116,12 +116,12 @@
(mf/fnc bool-container
{::mf/wrap-props false}
[props]
- (let [shape (unchecked-get props "shape")
- children-ids (cp/get-children (:id shape) objects)
- childs (select-keys objects children-ids)
- props (-> (obj/new)
- (obj/merge! props)
- (obj/merge! #js {:childs childs}))]
+ (let [shape (unchecked-get props "shape")
+ children (->> (cph/get-children-ids objects (:id shape))
+ (select-keys objects))
+ props (-> (obj/new)
+ (obj/merge! props)
+ (obj/merge! #js {:childs children}))]
[:> bool-wrapper props]))))
(defn svg-raw-container-factory
diff --git a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
index 0cbf82a799..ac9d30d3f6 100644
--- a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
+++ b/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs
@@ -7,12 +7,12 @@
(ns app.main.ui.viewer.handoff.right-sidebar
(:require
[app.common.data :as d]
+ [app.main.ui.components.shape-icon :as si]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.handoff.attributes :refer [attributes]]
[app.main.ui.viewer.handoff.code :refer [code]]
[app.main.ui.viewer.handoff.selection-feedback :refer [resolve-shapes]]
- [app.main.ui.workspace.sidebar.layers :refer [element-icon]]
[app.util.i18n :refer [tr]]
[rumext.alpha :as mf]))
@@ -44,9 +44,8 @@
[:span.tool-window-bar-title (tr "handoff.tabs.code.selected.multiple" (count shapes))]]
[:*
[:span.tool-window-bar-icon
- [:& element-icon {:shape first-shape}]]
- [:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (tr))]])
- ]
+ [:& si/element-icon {:shape first-shape}]]
+ [:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (tr))]])]
[:div.tool-window-content
[:& tab-container {:on-change-tab #(do
(reset! expanded false)
diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs
index 8a3e078a06..d5a833f796 100644
--- a/frontend/src/app/main/ui/viewer/header.cljs
+++ b/frontend/src/app/main/ui/viewer/header.cljs
@@ -172,6 +172,10 @@
(let [go-to-dashboard
(st/emitf (dv/go-to-dashboard))
+ go-to-handoff
+ (fn []
+ (st/emit! dv/close-thumbnails-panel (dv/go-to-section :handoff)))
+
navigate
(fn [section]
(st/emit! (dv/go-to-section section)))]
@@ -203,7 +207,7 @@
(and (= (:type permissions) :share-link)
(contains? (:flags permissions) :section-handoff)))
[:button.mode-zone-button.tooltip.tooltip-bottom
- {:on-click #(navigate :handoff)
+ {:on-click go-to-handoff
:class (dom/classnames :active (= section :handoff))
:alt (tr "viewer.header.handoff-section" (sc/get-tooltip :open-handoff))}
i/code])]
diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs
index 1258e49f24..e3e0b55b40 100644
--- a/frontend/src/app/main/ui/viewer/interactions.cljs
+++ b/frontend/src/app/main/ui/viewer/interactions.cljs
@@ -9,8 +9,8 @@
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
- [app.common.pages :as cp]
- [app.common.types.page-options :as cto]
+ [app.common.pages.helpers :as cph]
+ [app.common.spec.page :as csp]
[app.main.data.comments :as dcm]
[app.main.data.viewer :as dv]
[app.main.refs :as refs]
@@ -35,11 +35,10 @@
update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)]
- (->> (cp/get-children frame-id objects)
+ (->> (cph/get-children-ids objects frame-id)
(into [frame-id])
(reduce update-fn objects)))))
-
(mf/defc viewport
{::mf/wrap [mf/memo]}
[{:keys [page interactions-mode frame base-frame frame-offset size]}]
@@ -107,7 +106,7 @@
frames (:frames page)
frame (get frames index)
current-flow (mf/use-state
- (cto/get-frame-flow flows (:id frame)))
+ (csp/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 38c628c4f4..ab39c835ec 100644
--- a/frontend/src/app/main/ui/viewer/shapes.cljs
+++ b/frontend/src/app/main/ui/viewer/shapes.cljs
@@ -10,8 +10,8 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
- [app.common.pages :as cp]
- [app.common.types.interactions :as cti]
+ [app.common.pages.helpers :as cph]
+ [app.common.spec.interactions :as cti]
[app.main.data.viewer :as dv]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -330,7 +330,8 @@
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
- childs (select-keys objects (cp/get-children (:id shape) objects))
+ childs (->> (cph/get-children-ids objects (:id shape))
+ (select-keys objects))
props (obj/merge! #js {} props
#js {:childs childs
:objects objects})]
@@ -399,7 +400,7 @@
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
frame-id (:id frame)
- modifier-ids (into [frame-id] (cp/get-children frame-id objects))
+ modifier-ids (into [frame-id] (cph/get-children-ids objects frame-id))
objects (reduce update-fn objects modifier-ids)
frame (assoc-in frame [:modifiers :displacement] modifier)
diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs
index c383149a03..a38e8b16c3 100644
--- a/frontend/src/app/main/ui/viewer/thumbnails.cljs
+++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs
@@ -104,14 +104,13 @@
{:class (dom/classnames :expanded @expanded?
:invisible (not show?))
- :ref container
- }
+ :ref container}
[:& thumbnails-summary {:on-toggle-expand #(swap! expanded? not)
:on-close on-close
:total (count frames)}]
[:& thumbnails-content {:expanded? @expanded?
- :total (count frames)}
+ :total (count frames)}
(for [[i frame] (d/enumerate frames)]
[:& thumbnail-item {:index i
:frame frame
diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs
index 7181f147fa..bf9f519c7a 100644
--- a/frontend/src/app/main/ui/workspace.cljs
+++ b/frontend/src/app/main/ui/workspace.cljs
@@ -12,6 +12,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
+ [app.main.ui.hooks.resize :refer [use-resize-observer]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.colorpalette :refer [colorpalette]]
[app.main.ui.workspace.colorpicker]
@@ -20,68 +21,68 @@
[app.main.ui.workspace.header :refer [header]]
[app.main.ui.workspace.left-toolbar :refer [left-toolbar]]
[app.main.ui.workspace.libraries]
- [app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
+ [app.main.ui.workspace.nudge]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
+ [app.main.ui.workspace.textpalette :refer [textpalette]]
[app.main.ui.workspace.viewport :refer [viewport]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
+ [debug :refer [debug?]]
[okulary.core :as l]
[rumext.alpha :as mf]))
;; --- Workspace
-(mf/defc workspace-rules
- {::mf/wrap-props false
- ::mf/wrap [mf/memo]}
- [props]
- (let [zoom (or (obj/get props "zoom") 1)
- vbox (obj/get props "vbox")
- vport (obj/get props "vport")
- colorpalette? (obj/get props "colorpalette?")]
-
- [:*
- [:div.empty-rule-square]
- [:& horizontal-rule {:zoom zoom
- :vbox vbox
- :vport vport}]
- [:& vertical-rule {:zoom zoom
- :vbox vbox
- :vport vport}]
- [:& coordinates/coordinates {:colorpalette? colorpalette?}]]))
-
(mf/defc workspace-content
{::mf/wrap-props false}
[props]
(let [selected (mf/deref refs/selected-shapes)
local (mf/deref refs/viewport-data)
- {:keys [zoom vbox vport options-mode]} local
+ {:keys [options-mode]} local
file (obj/get props "file")
- layout (obj/get props "layout")]
+ layout (obj/get props "layout")
+
+ colorpalette? (:colorpalette layout)
+ textpalette? (:textpalette layout)
+ hide-ui? (:hide-ui layout)
+
+ on-resize
+ (mf/use-callback
+ (mf/deps (:vport local))
+ (fn [resize-type size]
+ (when (and (:vport local) (not= size (:vport local)))
+ (st/emit! (dw/update-viewport-size resize-type size)))))
+
+ node-ref (use-resize-observer on-resize)]
[:*
- (when (:colorpalette layout)
+ (when (and colorpalette? (not hide-ui?))
[:& colorpalette])
- [:section.workspace-content
+ (when (and textpalette? (not hide-ui?))
+ [:& textpalette])
+
+ [:section.workspace-content {:ref node-ref}
[:section.workspace-viewport
- (when (contains? layout :rules)
- [:& workspace-rules {:zoom zoom
- :vbox vbox
- :vport vport
- :colorpalette? (contains? layout :colorpalette)}])
+ (when (debug? :coordinates)
+ [:& coordinates/coordinates {:colorpalette? colorpalette?}])
[:& viewport {:file file
:local local
:selected selected
:layout layout}]]]
- [:& left-toolbar {:layout layout}]
-
- ;; Aside
- [:& left-sidebar {:layout layout}]
- [:& right-sidebar {:section options-mode
- :selected selected}]]))
+ (when-not hide-ui?
+ [:*
+ [:& left-toolbar {:layout layout}]
+ (if (:collapse-left-sidebar layout)
+ [:button.collapse-sidebar.collapsed {:on-click #(st/emit! (dw/toggle-layout-flags :collapse-left-sidebar))}
+ i/arrow-slide]
+ [:& left-sidebar {:layout layout}])
+ [:& right-sidebar {:section options-mode
+ :selected selected
+ :layout layout}]])]))
(def trimmed-page-ref (l/derived :trimmed-page st/state =))
@@ -117,39 +118,34 @@
layout (mf/deref refs/workspace-layout)]
;; Setting the layout preset by its name
- (mf/use-effect
- (mf/deps layout-name)
- (fn []
- (st/emit! (dw/setup-layout layout-name))))
+ (mf/with-effect [layout-name]
+ (st/emit! (dw/setup-layout layout-name)))
- (mf/use-effect
- (mf/deps project-id file-id)
- (fn []
- (st/emit! (dw/initialize-file project-id file-id))
- (fn []
- (st/emit! ::dwp/force-persist
- (dw/finalize-file project-id file-id)))))
+ (mf/with-effect [project-id file-id]
+ (st/emit! (dw/initialize-file project-id file-id))
+ (fn []
+ (st/emit! ::dwp/force-persist
+ (dw/finalize-file project-id file-id))))
;; Close any non-modal dialog that may be still open
- (mf/use-effect
- (fn [] (st/emit! dm/hide)))
+ (mf/with-effect
+ (st/emit! dm/hide))
;; Set properly the page title
- (mf/use-effect
- (mf/deps (:name file))
- (fn []
- (when (:name file)
- (dom/set-html-title (tr "title.workspace" (:name file))))))
+ (mf/with-effect [(:name file)]
+ (when (:name file)
+ (dom/set-html-title (tr "title.workspace" (:name file)))))
[:& (mf/provider ctx/current-file-id) {:value (:id file)}
[:& (mf/provider ctx/current-team-id) {:value (:team-id project)}
[:& (mf/provider ctx/current-project-id) {:value (:id project)}
[:& (mf/provider ctx/current-page-id) {:value page-id}
[:section#workspace
- [:& header {:file file
- :page-id page-id
- :project project
- :layout layout}]
+ (when (not (:hide-ui layout))
+ [:& header {:file file
+ :page-id page-id
+ :project project
+ :layout layout}])
[:& context-menu]
@@ -161,3 +157,5 @@
:layout layout}]
[:& workspace-loader])]]]]]))
+
+
diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs
index 006fdaaa5c..86b79e0f44 100644
--- a/frontend/src/app/main/ui/workspace/colorpalette.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs
@@ -12,8 +12,10 @@
[app.main.store :as st]
[app.main.ui.components.color-bullet :as cb]
[app.main.ui.components.dropdown :refer [dropdown]]
+ [app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as i]
[app.util.color :as uc]
+ [app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
@@ -38,7 +40,7 @@
;; --- Components
(mf/defc palette-item
- [{:keys [color size]}]
+ [{:keys [color]}]
(let [ids-with-children (map :id (mf/deref refs/selected-shapes-with-children))
select-color
(fn [event]
@@ -46,14 +48,13 @@
(st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color)))
(st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color)))))]
- [:div.color-cell {:class (str "cell-"(name size))
- :on-click select-color}
+ [:div.color-cell {:on-click select-color}
[:& cb/color-bullet {:color color}]
- [:& cb/color-name {:color color :size size}]]))
+ [:& cb/color-name {:color color}]]))
(mf/defc palette
- [{:keys [current-colors recent-colors file-colors shared-libs selected size]}]
- (let [state (mf/use-state {:show-menu false })
+ [{:keys [current-colors recent-colors file-colors shared-libs selected]}]
+ (let [state (mf/use-state {:show-menu false})
width (:width @state 0)
visible (mth/round (/ width 66))
@@ -64,6 +65,9 @@
container (mf/use-ref nil)
+ {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]}
+ (use-resize-hook :palette 72 54 80 :y true :bottom)
+
on-left-arrow-click
(mf/use-callback
(mf/deps max-offset visible)
@@ -111,7 +115,13 @@
(fn []
(events/unlistenByKey key1))))
- [:div.color-palette.left-sidebar-open
+ [:div.color-palette {:ref parent-ref
+ :class (dom/classnames :no-text (< size 72))
+ :style #js {"--height" (str size "px")
+ "--bullet-size" (str (if (< size 72) (- size 15) (- size 30)) "px")}}
+ [:div.resize-area {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move}]
[:& dropdown {:show (:show-menu @state)
:on-close #(swap! state assoc :show-menu false)}
[:ul.workspace-context-menu.palette-menu
@@ -146,33 +156,18 @@
[:div.color-sample
(for [[idx color] (map-indexed vector (take 7 (reverse recent-colors))) ]
[:& cb/color-bullet {:key (str "color-" idx)
- :color color}])]]
-
- [:hr.dropdown-separator]
-
- [:li
- {:on-click #(st/emit! (mdc/change-palette-size :big))}
- (when (= size :big) i/tick)
- (tr "workspace.libraries.colors.big-thumbnails")]
-
- [:li
- {:on-click #(st/emit! (mdc/change-palette-size :small))}
- (when (= size :small) i/tick)
- (tr "workspace.libraries.colors.small-thumbnails")]]]
+ :color color}])]]]]
[:div.color-palette-actions
{:on-click #(swap! state assoc :show-menu true)}
[:div.color-palette-actions-button i/actions]]
[:span.left-arrow {:on-click on-left-arrow-click} i/arrow-slide]
- [:div.color-palette-content {:class (if (= size :big) "size-big" "size-small")
- :ref container :on-wheel on-scroll}
+ [:div.color-palette-content {:ref container :on-wheel on-scroll}
[:div.color-palette-inside {:style {:position "relative"
:right (str (* 66 offset) "px")}}
(for [[idx item] (map-indexed vector current-colors)]
- [:& palette-item {:size size
- :color item
- :key idx}])]]
+ [:& palette-item {:color item :key idx}])]]
[:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]]))
@@ -183,13 +178,12 @@
(vals))))
(mf/defc colorpalette
+ {::mf/wrap [mf/memo]}
[]
(let [recent-colors (mf/deref refs/workspace-recent-colors)
file-colors (mf/deref refs/workspace-file-colors)
shared-libs (mf/deref refs/workspace-libraries)
selected (or (mf/deref selected-palette-ref) :recent)
- size (or (mf/deref selected-palette-size-ref) :big)
-
current-library-colors (mf/use-state [])]
(mf/use-effect
@@ -219,5 +213,4 @@
:recent-colors recent-colors
:file-colors file-colors
:shared-libs shared-libs
- :selected selected
- :size size}]))
+ :selected selected}]))
diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs
index 2b34513072..991e96e107 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs
@@ -167,6 +167,11 @@
(fn [color]
(let [editing-stop (:editing-stop @state)
is-gradient? (some? (:gradient color))]
+
+ (if is-gradient?
+ (st/emit! (dc/start-gradient (:gradient color)))
+ (st/emit! (dc/stop-gradient)))
+
(if (and (some? editing-stop) (not is-gradient?))
(handle-change-color (color->components (:color color) (:opacity color)))
(do (reset! dirty? false)
@@ -226,9 +231,9 @@
;; Updates color when used el pixel picker
(mf/use-effect
- (mf/deps picking-color? picked-color-select)
+ (mf/deps picking-color? picked-color picked-color-select)
(fn []
- (when (and picking-color? picked-color-select)
+ (when (and picking-color? picked-color picked-color-select)
(let [[r g b alpha] picked-color
hex (uc/rgb->hex [r g b])
[h s v] (uc/hex->hsv hex)]
@@ -358,7 +363,7 @@
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
(let [;; picker height in pixels
- h 360
+ h 430
;; Checks for overflow outside the viewport height
overflow-fix (max 0 (+ y (- 50) h (- vh)))]
(cond
diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs
index 58556d94ff..f020dc1e4a 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/context_menu.cljs
@@ -8,15 +8,17 @@
"A workspace specific context menu (mouse right click)."
(:require
[app.common.data :as d]
- [app.common.types.page-options :as cto]
+ [app.common.spec.page :as csp]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.libraries :as dwl]
+ [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shortcuts :as sc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
+ [app.main.ui.components.shape-icon :as si]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
@@ -33,8 +35,11 @@
(dom/prevent-default event)
(dom/stop-propagation event))
+
+
+
(mf/defc menu-entry
- [{:keys [title shortcut on-click children] :as props}]
+ [{:keys [title shortcut on-click children selected? icon] :as props}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
@@ -64,22 +69,32 @@
(when (and (some? dom) (some? submenu-node))
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
- [:li {:ref set-dom-node
- :on-click on-click
- :on-pointer-enter on-pointer-enter
- :on-pointer-leave on-pointer-leave}
- [:span.title title]
- [:span.shortcut (or shortcut "")]
+ (if icon
+ [:li.icon-menu-item {:ref set-dom-node
+ :on-click on-click
+ :on-pointer-enter on-pointer-enter
+ :on-pointer-leave on-pointer-leave}
+ [:span.icon-wrapper
+ (if selected? [:span.selected-icon i/tick]
+ [:span.selected-icon])
+ [:span.shape-icon icon]]
+ [:span.title title]]
+ [:li {:ref set-dom-node
+ :on-click on-click
+ :on-pointer-enter on-pointer-enter
+ :on-pointer-leave on-pointer-leave}
+ [:span.title title]
+ [:span.shortcut (or shortcut "")]
- (when (> (count children) 1)
- [:span.submenu-icon i/arrow-slide])
+ (when (> (count children) 1)
+ [:span.submenu-icon i/arrow-slide])
- (when (> (count children) 1)
- [:ul.workspace-context-menu
- {:ref submenu-ref
- :style {:display "none" :left 250}
- :on-context-menu prevent-default}
- children])]))
+ (when (> (count children) 1)
+ [:ul.workspace-context-menu
+ {:ref submenu-ref
+ :style {:display "none" :left 250}
+ :on-context-menu prevent-default}
+ children])])))
(mf/defc menu-separator
[]
@@ -88,7 +103,8 @@
(mf/defc context-menu-edit
[]
(let [do-copy (st/emitf (dw/copy-selected))
- do-cut (st/emitf (dw/copy-selected) dw/delete-selected)
+ do-cut (st/emitf (dw/copy-selected)
+ (dw/delete-selected))
do-paste (st/emitf dw/paste)
do-duplicate (st/emitf (dw/duplicate-selected false))]
[:*
@@ -108,12 +124,20 @@
[:& menu-separator]]))
(mf/defc context-menu-layer-position
- []
+ [{:keys [hover-objs shapes]}]
(let [do-bring-forward (st/emitf (dw/vertical-order-selected :up))
do-bring-to-front (st/emitf (dw/vertical-order-selected :top))
do-send-backward (st/emitf (dw/vertical-order-selected :down))
- do-send-to-back (st/emitf (dw/vertical-order-selected :bottom))]
+ do-send-to-back (st/emitf (dw/vertical-order-selected :bottom))
+ select-shapes (fn [id] (st/emitf (dws/select-shape id)))]
[:*
+ (when (> (count hover-objs) 1)
+ [:& menu-entry {:title (tr "workspace.shape.menu.select-layer")}
+ (for [object hover-objs]
+ [:& menu-entry {:title (:name object)
+ :selected? (some #(= object %) shapes)
+ :on-click (select-shapes (:id object))
+ :icon (si/element-icon {:shape object})}])])
[:& menu-entry {:title (tr "workspace.shape.menu.forward")
:shortcut (sc/get-tooltip :bring-forward)
:on-click do-bring-forward}]
@@ -186,10 +210,10 @@
(when (not has-frame?)
[:*
- [:& menu-entry {:title (tr "workspace.shape.menu.create-artboard-from-selection")
- :shortcut (sc/get-tooltip :create-artboard-from-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 :create-artboard-from-selection)
+ :on-click do-create-artboard-from-selection}]
+ [:& menu-separator]])]))
(mf/defc context-menu-path
[{:keys [shapes disable-flatten? disable-booleans?]}]
@@ -284,7 +308,7 @@
is-frame? (and single? has-frame?)]
(when (and prototype? is-frame?)
- (let [flow (cto/get-frame-flow flows (-> shapes first :id))]
+ (let [flow (csp/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)}]
@@ -303,6 +327,7 @@
shape-id (->> shapes first :id)
component-id (->> shapes first :component-id)
component-file (-> shapes first :component-file)
+ component-shapes (filter #(contains? % :component-id) shapes)
current-file-id (mf/use-ctx ctx/current-file-id)
local-component? (= component-file current-file-id)
@@ -314,6 +339,7 @@
do-show-component (st/emitf (dw/go-to-component component-id))
do-navigate-component-file (st/emitf (dwl/nav-to-component-file component-file))
do-update-component (st/emitf (dwl/update-component-sync shape-id component-file))
+ do-update-component-in-bulk (st/emitf (dwl/update-component-in-bulk component-shapes component-file))
do-update-remote-component
(st/emitf (modal/show
@@ -324,7 +350,18 @@
:cancel-label (tr "modals.update-remote-component.cancel")
:accept-label (tr "modals.update-remote-component.accept")
:accept-style :primary
- :on-accept do-update-component}))]
+ :on-accept do-update-component}))
+
+ do-update-in-bulk (st/emitf (modal/show
+ {:type :confirm
+ :message ""
+ :title (tr "modals.update-remote-component-in-bulk.message")
+ :hint (tr "modals.update-remote-component-in-bulk.hint")
+ :items component-shapes
+ :cancel-label (tr "modals.update-remote-component.cancel")
+ :accept-label (tr "modals.update-remote-component.accept")
+ :accept-style :primary
+ :on-accept do-update-component-in-bulk}))]
[:*
(when (and (not has-frame?) (not is-component?))
[:*
@@ -335,7 +372,10 @@
(when has-component?
[:& menu-entry {:title (tr "workspace.shape.menu.detach-instances-in-bulk")
:shortcut (sc/get-tooltip :detach-component)
- :on-click do-detach-component-in-bulk}])])
+ :on-click do-detach-component-in-bulk}]
+ (when (not single?)
+ [:& menu-entry {:title (tr "workspace.shape.menu.update-components-in-bulk")
+ :on-click do-update-in-bulk}]))])
(when is-component?
;; WARNING: this menu is the same as the context menu at the sidebar.
@@ -367,7 +407,7 @@
(mf/defc context-menu-delete
[]
- (let [do-delete (st/emitf dw/delete-selected)]
+ (let [do-delete (st/emitf (dw/delete-selected))]
[:& menu-entry {:title (tr "workspace.shape.menu.delete")
:shortcut (sc/get-tooltip :delete)
:on-click do-delete}]))
@@ -376,8 +416,11 @@
[{:keys [mdata] :as props}]
(let [{:keys [disable-booleans? disable-flatten?]} mdata
shapes (mf/deref refs/selected-objects)
+ hover-ids (mf/deref refs/current-hover-ids)
+ hover-objs (mf/deref (refs/objects-by-id hover-ids))
props #js {:shapes shapes
+ :hover-objs hover-objs
:disable-booleans? disable-booleans?
:disable-flatten? disable-flatten?}]
(when-not (empty? shapes)
@@ -394,10 +437,15 @@
(mf/defc viewport-context-menu
[]
- (let [do-paste (st/emitf dw/paste)]
- [:& menu-entry {:title (tr "workspace.shape.menu.paste")
- :shortcut (sc/get-tooltip :paste)
- :on-click do-paste}]))
+ (let [do-paste (st/emitf dw/paste)
+ do-hide-ui (st/emitf (dw/toggle-layout-flags :hide-ui))]
+ [:*
+ [:& menu-entry {:title (tr "workspace.shape.menu.paste")
+ :shortcut (sc/get-tooltip :paste)
+ :on-click do-paste}]
+ [:& menu-entry {:title (tr "workspace.shape.menu.hide-ui")
+ :shortcut (sc/get-tooltip :hide-ui)
+ :on-click do-hide-ui}]]))
(mf/defc context-menu
[]
@@ -407,17 +455,17 @@
dropdown-ref (mf/use-ref)]
(mf/use-effect
- (mf/deps mdata)
- #(let [dropdown (mf/ref-val dropdown-ref)]
- (when dropdown
- (let [bounding-rect (dom/get-bounding-rect dropdown)
- window-size (dom/get-window-size)
- delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
- delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
- new-style (str "top: " (- top delta-y) "px; "
- "left: " (- left delta-x) "px;")]
- (when (or (> delta-x 0) (> delta-y 0))
- (.setAttribute ^js dropdown "style" new-style))))))
+ (mf/deps mdata)
+ #(let [dropdown (mf/ref-val dropdown-ref)]
+ (when dropdown
+ (let [bounding-rect (dom/get-bounding-rect dropdown)
+ window-size (dom/get-window-size)
+ delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
+ delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
+ new-style (str "top: " (- top delta-y) "px; "
+ "left: " (- left delta-x) "px;")]
+ (when (or (> delta-x 0) (> delta-y 0))
+ (.setAttribute ^js dropdown "style" new-style))))))
[:& dropdown {:show (boolean mdata)
:on-close (st/emitf dw/hide-context-menu)}
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index ca418b63fa..91dbaae85a 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -18,6 +18,7 @@
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
+ [app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i]
[app.main.ui.workspace.presence :refer [active-sessions]]
[app.util.dom :as dom]
@@ -99,12 +100,11 @@
(mf/defc menu
[{:keys [layout project file team-id page-id] :as props}]
- (let [show-menu? (mf/use-state false)
- editing? (mf/use-state false)
-
- frames (mf/deref refs/workspace-frames)
-
+ (let [show-menu? (mf/use-state false)
+ show-sub-menu? (mf/use-state false)
+ editing? (mf/use-state false)
edit-input-ref (mf/use-ref nil)
+ frames (mf/deref refs/workspace-frames)
add-shared-fn
(st/emitf (dw/set-file-shared (:id file) true))
@@ -192,7 +192,21 @@
(dom/trigger-download filename body))
(fn [_error]
(st/emit! (dm/error (tr "errors.unexpected-error"))))
- (st/emitf dm/hide)))))))]
+ (st/emitf dm/hide)))))))
+
+ on-item-hover
+ (mf/use-callback
+ (fn [item]
+ (fn [event]
+ (dom/stop-propagation event)
+ (reset! show-sub-menu? item))))
+
+ on-item-click
+ (mf/use-callback
+ (fn [item]
+ (fn [event]
+ (dom/stop-propagation event)
+ (reset! show-sub-menu? item))))]
(mf/use-effect
(mf/deps @editing?)
@@ -223,6 +237,54 @@
[:& dropdown {:show @show-menu?
:on-close #(reset! show-menu? false)}
[:ul.menu
+ [:li {:on-click (on-item-click :file)
+ :on-pointer-enter (on-item-hover :file)}
+ [:span (tr "workspace.header.menu.option.file")]
+ [:span i/arrow-slide]]
+ [:li {:on-click (on-item-click :edit)
+ :on-pointer-enter (on-item-hover :edit)}
+ [:span (tr "workspace.header.menu.option.edit")] [:span i/arrow-slide]]
+ [:li {:on-click (on-item-click :view)
+ :on-pointer-enter (on-item-hover :view)}
+ [:span (tr "workspace.header.menu.option.view")] [:span i/arrow-slide]]
+ [:li {:on-click (on-item-click :preferences)
+ :on-pointer-enter (on-item-hover :preferences)}
+ [:span (tr "workspace.header.menu.option.preferences")] [:span i/arrow-slide]]
+ (when (contains? @cf/flags :user-feedback)
+ [:*
+ [:li.feedback {:on-click (st/emitf (rt/nav-new-window* {:rname :settings-feedback}))}
+ [:span (tr "labels.give-feedback")]]])]]
+
+ [:& dropdown {:show (= @show-sub-menu? :file)
+ :on-close #(reset! show-sub-menu? false)}
+ [:ul.sub-menu.file
+ (if (:is-shared file)
+ [:li {:on-click on-remove-shared}
+ [:span (tr "dashboard.remove-shared")]]
+ [:li {:on-click on-add-shared}
+ [:span (tr "dashboard.add-shared")]])
+ [:li.export-file {:on-click on-export-file}
+ [:span (tr "dashboard.export-single")]]
+ (when (seq frames)
+ [:li.export-file {:on-click on-export-frames}
+ [:span (tr "dashboard.export-frames")]])]]
+
+ [:& dropdown {:show (= @show-sub-menu? :edit)
+ :on-close #(reset! show-sub-menu? false)}
+ [:ul.sub-menu.edit
+ [:li {:on-click #(st/emit! (dw/select-all))}
+ [:span (tr "workspace.header.menu.select-all")]
+ [:span.shortcut (sc/get-tooltip :select-all)]]
+ [:li {:on-click #(st/emit! (dw/toggle-layout-flags :scale-text))}
+ [:span
+ (if (contains? layout :scale-text)
+ (tr "workspace.header.menu.disable-scale-text")
+ (tr "workspace.header.menu.enable-scale-text"))]
+ [:span.shortcut (sc/get-tooltip :toggle-scale-text)]]]]
+
+ [:& dropdown {:show (= @show-sub-menu? :view)
+ :on-close #(reset! show-sub-menu? false)}
+ [:ul.sub-menu.view
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :rules))}
[:span
(if (contains? layout :rules)
@@ -237,13 +299,6 @@
(tr "workspace.header.menu.show-grid"))]
[:span.shortcut (sc/get-tooltip :toggle-grid)]]
- [:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-grid))}
- [:span
- (if (contains? layout :snap-grid)
- (tr "workspace.header.menu.disable-snap-grid")
- (tr "workspace.header.menu.enable-snap-grid"))]
- [:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
-
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :sitemap :layers))}
[:span
(if (or (contains? layout :sitemap) (contains? layout :layers))
@@ -251,18 +306,25 @@
(tr "workspace.header.menu.show-layers"))]
[:span.shortcut (sc/get-tooltip :toggle-layers)]]
- [:li {:on-click #(st/emit! (dw/toggle-layout-flags :colorpalette))}
+ [:li {:on-click (fn []
+ (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :textpalette)
+ (dw/toggle-layout-flags :colorpalette)))}
[:span
(if (contains? layout :colorpalette)
(tr "workspace.header.menu.hide-palette")
(tr "workspace.header.menu.show-palette"))]
- [:span.shortcut (sc/get-tooltip :toggle-palette)]]
+ [:span.shortcut (sc/get-tooltip :toggle-colorpalette)]]
- [:li {:on-click #(st/emit! (dw/toggle-layout-flags :display-artboard-names))}
+ [:li {:on-click (fn []
+ (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :colorpalette)
+ (dw/toggle-layout-flags :textpalette)))}
[:span
- (if (contains? layout :display-artboard-names)
- (tr "workspace.header.menu.hide-artboard-names")
- (tr "workspace.header.menu.show-artboard-names"))]]
+ (if (contains? layout :textpalette)
+ (tr "workspace.header.menu.hide-textpalette")
+ (tr "workspace.header.menu.show-textpalette"))]
+ [:span.shortcut (sc/get-tooltip :toggle-textpalette)]]
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :assets))}
[:span
@@ -271,9 +333,33 @@
(tr "workspace.header.menu.show-assets"))]
[:span.shortcut (sc/get-tooltip :toggle-assets)]]
- [:li {:on-click #(st/emit! (dw/select-all))}
- [:span (tr "workspace.header.menu.select-all")]
- [:span.shortcut (sc/get-tooltip :select-all)]]
+ [:li {:on-click #(st/emit! (dw/toggle-layout-flags :display-artboard-names))}
+ [:span
+ (if (contains? layout :display-artboard-names)
+ (tr "workspace.header.menu.hide-artboard-names")
+ (tr "workspace.header.menu.show-artboard-names"))]]
+
+ [:li {:on-click #(st/emit! (dw/toggle-layout-flags :hide-ui))}
+ [:span
+ (tr "workspace.shape.menu.hide-ui")]
+ [:span.shortcut (sc/get-tooltip :hide-ui)]]]]
+
+ [:& dropdown {:show (= @show-sub-menu? :preferences)
+ :on-close #(reset! show-sub-menu? false)}
+ [:ul.sub-menu.preferences
+ [:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-guides))}
+ [:span
+ (if (contains? layout :snap-guides)
+ (tr "workspace.header.menu.disable-snap-guides")
+ (tr "workspace.header.menu.enable-snap-guides"))]
+ [:span.shortcut (sc/get-tooltip :toggle-snap-guide)]]
+
+ [:li {:on-click #(st/emit! (dw/toggle-layout-flags :snap-grid))}
+ [:span
+ (if (contains? layout :snap-grid)
+ (tr "workspace.header.menu.disable-snap-grid")
+ (tr "workspace.header.menu.enable-snap-grid"))]
+ [:span.shortcut (sc/get-tooltip :toggle-snap-grid)]]
[:li {:on-click #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
[:span
@@ -282,29 +368,8 @@
(tr "workspace.header.menu.enable-dynamic-alignment"))]
[:span.shortcut (sc/get-tooltip :toggle-alignment)]]
- [:li {:on-click #(st/emit! (dw/toggle-layout-flags :scale-text))}
- [:span
- (if (contains? layout :scale-text)
- (tr "workspace.header.menu.disable-scale-text")
- (tr "workspace.header.menu.enable-scale-text"))]
- [:span.shortcut (sc/get-tooltip :toggle-scale-text)]]
-
- (if (:is-shared file)
- [:li {:on-click on-remove-shared}
- [:span (tr "dashboard.remove-shared")]]
- [:li {:on-click on-add-shared}
- [:span (tr "dashboard.add-shared")]])
-
- [:li.export-file {:on-click on-export-file}
- [:span (tr "dashboard.export-single")]]
-
- (when (seq frames)
- [:li.export-file {:on-click on-export-frames}
- [:span (tr "dashboard.export-frames")]])
-
- (when (contains? @cf/flags :user-feedback)
- [:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))}
- [:span (tr "labels.give-feedback")]])]]]))
+ [:li {:on-click #(st/emit! (modal/show {:type :nudge-option}))}
+ [:span (tr "modals.nudge-title")]]]]]))
;; --- Header Component
@@ -325,31 +390,40 @@
(st/emitf (dw/go-to-viewer params)))]
[:header.workspace-header
- [:div.main-icon
- [:a {:on-click go-back} i/logo-icon]]
+ [:div.left-area
+ [:div.main-icon
+ [:a {:on-click go-back} i/logo-icon]]
- [:& menu {:layout layout
- :project project
- :file file
- :team-id team-id
- :page-id page-id}]
+ [:& menu {:layout layout
+ :project project
+ :file file
+ :team-id team-id
+ :page-id page-id}]]
- [:div.users-section
- [:& active-sessions]]
+ [:div.center-area
+ [:div.users-section
+ [:& active-sessions]]]
- [:div.options-section
- [:& persistence-state-widget]
+ [:div.right-area
+ [:div.options-section
+ [:& persistence-state-widget]
+ [:button.document-history
+ {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
+ :class (when (contains? layout :document-history) "selected")
+ :on-click (st/emitf (dw/toggle-layout-flags :document-history))}
+ i/recent]]
- [:& zoom-widget-workspace
- {:zoom zoom
- :on-increase #(st/emit! (dw/increase-zoom nil))
- :on-decrease #(st/emit! (dw/decrease-zoom nil))
- :on-zoom-reset #(st/emit! dw/reset-zoom)
- :on-zoom-fit #(st/emit! dw/zoom-to-fit-all)
- :on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}]
+ [:div.options-section
+ [:& zoom-widget-workspace
+ {:zoom zoom
+ :on-increase #(st/emit! (dw/increase-zoom nil))
+ :on-decrease #(st/emit! (dw/decrease-zoom nil))
+ :on-zoom-reset #(st/emit! dw/reset-zoom)
+ :on-zoom-fit #(st/emit! dw/zoom-to-fit-all)
+ :on-zoom-selected #(st/emit! dw/zoom-to-selected-shape)}]
- [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
- {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
- :on-click go-viewer}
- i/play]]]))
+ [:a.btn-icon-dark.btn-small.tooltip.tooltip-bottom-left
+ {:alt (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
+ :on-click go-viewer}
+ i/play]]]]))
diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
index 94c48e702c..dbddf07778 100644
--- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs
+++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
@@ -14,6 +14,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
+ [app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -75,17 +76,20 @@
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
:class (when (= selected-drawtool :frame) "selected")
- :on-click (partial select-drawtool :frame)}
+ :on-click (partial select-drawtool :frame)
+ :data-test "artboard-btn"}
i/artboard]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
:class (when (= selected-drawtool :rect) "selected")
- :on-click (partial select-drawtool :rect)}
+ :on-click (partial select-drawtool :rect)
+ :data-test "rect-btn"}
i/box]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
:class (when (= selected-drawtool :circle) "selected")
- :on-click (partial select-drawtool :circle)}
+ :on-click (partial select-drawtool :circle)
+ :data-test "ellipse-btn"}
i/circle]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
@@ -98,12 +102,14 @@
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
:class (when (= selected-drawtool :curve) "selected")
- :on-click (partial select-drawtool :curve)}
+ :on-click (partial select-drawtool :curve)
+ :data-test "curve-btn"}
i/pencil]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
:class (when (= selected-drawtool :path) "selected")
- :on-click (partial select-drawtool :path)}
+ :on-click (partial select-drawtool :path)
+ :data-test "path-btn"}
i/pen]
[:li.tooltip.tooltip-right
@@ -114,22 +120,19 @@
[:ul.left-toolbar-options.panels
[:li.tooltip.tooltip-right
- {:alt (tr "workspace.sidebar.layers" (sc/get-tooltip :toggle-layers))
- :class (when (contains? layout :layers) "selected")
- :on-click (st/emitf (dw/go-to-layout :layers))}
- i/layers]
+ {:alt (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
+ :class (when (contains? layout :textpalette) "selected")
+ :on-click (fn []
+ (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :colorpalette)
+ (dw/toggle-layout-flags :textpalette)))}
+ "Ag"]
+
[:li.tooltip.tooltip-right
- {:alt (tr "workspace.toolbar.assets" (sc/get-tooltip :toggle-assets))
- :class (when (contains? layout :assets) "selected")
- :on-click (st/emitf (dw/go-to-layout :assets))}
- i/library]
- [:li.tooltip.tooltip-right
- {:alt (tr "workspace.sidebar.history" (sc/get-tooltip :toggle-history))
- :class (when (contains? layout :document-history) "selected")
- :on-click (st/emitf (dw/go-to-layout :document-history))}
- i/recent]
- [:li.tooltip.tooltip-right
- {:alt (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-palette))
+ {:alt (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
:class (when (contains? layout :colorpalette) "selected")
- :on-click (st/emitf (dw/toggle-layout-flags :colorpalette))}
+ :on-click (fn []
+ (r/set-resize-type! :bottom)
+ (st/emit! (dw/remove-layout-flags :textpalette)
+ (dw/toggle-layout-flags :colorpalette)))}
i/palette]]]]))
diff --git a/frontend/src/app/main/ui/workspace/nudge.cljs b/frontend/src/app/main/ui/workspace/nudge.cljs
new file mode 100644
index 0000000000..c479a00bac
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/nudge.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.workspace.nudge
+ (:require
+ [app.main.data.modal :as modal]
+ [app.main.data.users :as du]
+ [app.main.refs :as refs]
+ [app.main.store :as st]
+ [app.main.ui.components.numeric-input :refer [numeric-input]]
+ [app.main.ui.icons :as i]
+ [app.util.dom :as dom]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.keyboard :as k]
+ [goog.events :as events]
+ [rumext.alpha :as mf])
+ (:import goog.events.EventType))
+
+
+(mf/defc nudge-modal
+ {::mf/register modal/components
+ ::mf/register-as :nudge-option}
+ []
+ (let [profile (mf/deref refs/profile)
+ nudge (get-in profile [:props :nudge] {:big 10 :small 1})
+ update-nudge (fn [value size] (let [update-nudge (if (= :big size)
+ {:big value :small (:small nudge)}
+ {:small value :big (:big nudge)})]
+ (st/emit! (du/update-nudge update-nudge))))
+ update-big (fn [value] (update-nudge value :big))
+ update-small (fn [value] (update-nudge value :small))
+ close #(modal/hide!)]
+ (mf/with-effect
+ (letfn [(on-keydown [event]
+ (when (k/enter? event)
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (close)))]
+ (->> (events/listen js/document EventType.KEYDOWN on-keydown)
+ (partial events/unlistenByKey))))
+
+ [:div.nudge-modal-overlay
+ [:div.nudge-modal-container
+ [:div.nudge-modal-header
+ [:p.nudge-modal-title (tr "modals.nudge-title")]
+ [:button.modal-close-button {:on-click close} i/close]]
+ [:div.nudge-modal-body
+ [:div.input-wrapper
+ [:span
+ [:p.nudge-subtitle (tr "modals.small-nudge")]
+ [:> numeric-input {:min 1
+ :value (:small nudge)
+ :on-change update-small}]]]
+ [:div.input-wrapper
+ [:span
+ [:p.nudge-subtitle (tr "modals.big-nudge")]
+ [:> numeric-input {:min 1
+ :value (:big nudge)
+ :on-change update-big}]]]]]]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/workspace/rules.cljs b/frontend/src/app/main/ui/workspace/rules.cljs
deleted file mode 100644
index a9d5b34b2e..0000000000
--- a/frontend/src/app/main/ui/workspace/rules.cljs
+++ /dev/null
@@ -1,122 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.main.ui.workspace.rules
- (:require
- [app.common.colors :as colors]
- [app.common.math :as mth]
- [app.util.object :as obj]
- [rumext.alpha :as mf]))
-
-(defn- calculate-step-size
- [zoom]
- (cond
- (< 0 zoom 0.008) 10000
- (< 0.008 zoom 0.015) 5000
- (< 0.015 zoom 0.04) 2500
- (< 0.04 zoom 0.07) 1000
- (< 0.07 zoom 0.2) 500
- (< 0.2 zoom 0.5) 250
- (< 0.5 zoom 1) 100
- (<= 1 zoom 2) 50
- (< 2 zoom 4) 25
- (< 4 zoom 6) 10
- (< 6 zoom 15) 5
- (< 15 zoom 25) 2
- (< 25 zoom) 1
- :else 1))
-
-(defn draw-rule!
- [dctx {:keys [zoom size start type]}]
- (when start
- (let [txfm (- (* (- 0 start) zoom) 20)
- step (calculate-step-size zoom)
-
- minv (max (mth/round start) -100000)
- minv (* (mth/ceil (/ minv step)) step)
-
- maxv (min (mth/round (+ start (/ size zoom))) 100000)
- maxv (* (mth/floor (/ maxv step)) step)
-
- path (js/Path2D.)]
-
- (if (= type :horizontal)
- (.translate dctx txfm 0)
- (.translate dctx 0 txfm))
-
- (obj/set! dctx "font" "12px worksans")
- (obj/set! dctx "fillStyle" colors/gray-30)
- (obj/set! dctx "strokeStyle" colors/gray-30)
- (obj/set! dctx "textAlign" "center")
-
- (loop [i minv]
- (if (<= i maxv)
- (let [pos (+ (* i zoom) 0)]
- (.save dctx)
- (if (= type :horizontal)
- (do
- ;; Write the rule numbers
- (.fillText dctx (str i) pos 13)
-
- ;; Build the rules lines
- (.moveTo path pos 17)
- (.lineTo path pos 20))
- (do
- ;; Write the rule numbers
- (.translate dctx 12 pos)
- (.rotate dctx (/ (* 270 js/Math.PI) 180))
- (.fillText dctx (str i) 0 0)
-
- ;; Build the rules lines
- (.moveTo path 17 pos)
- (.lineTo path 20 pos)))
- (.restore dctx)
- (recur (+ i step)))
-
- ;; Put the path in the canvas
- (.stroke dctx path))))))
-
-
-(mf/defc horizontal-rule
- [{:keys [zoom vbox vport] :as props}]
- (let [canvas (mf/use-ref)
- width (- (:width vport) 20)]
- (mf/use-layout-effect
- (mf/deps zoom width (:x vbox))
- (fn []
- (let [node (mf/ref-val canvas)
- dctx (.getContext ^js node "2d")]
- (obj/set! node "width" width)
- (draw-rule! dctx {:zoom zoom
- :type :horizontal
- :size width
- :start (+ (:x vbox) (:left-offset vbox))}))))
-
- [:canvas.horizontal-rule
- {:ref canvas
- :width width
- :height 20}]))
-
-(mf/defc vertical-rule
- [{:keys [zoom vbox vport] :as props}]
- (let [canvas (mf/use-ref)
- height (- (:height vport) 20)]
- (mf/use-layout-effect
- (mf/deps zoom height (:y vbox))
- (fn []
- (let [node (mf/ref-val canvas)
- dctx (.getContext ^js node "2d")]
- (obj/set! node "height" height)
- (draw-rule! dctx {:zoom zoom
- :type :vertical
- :size height
- :count 100
- :start (:y vbox)}))))
-
- [:canvas.vertical-rule
- {:ref canvas
- :width 20
- :height height}]))
diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs
index 315d697a9b..359e2c56f9 100644
--- a/frontend/src/app/main/ui/workspace/shapes.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes.cljs
@@ -12,9 +12,7 @@
others are defined using a generic wrapper implemented in
common."
(:require
- [app.common.pages :as cp]
- [app.common.uuid :as uuid]
- [app.main.refs :as refs]
+ [app.common.pages.helpers :as cph]
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.image :as image]
[app.main.ui.shapes.rect :as rect]
@@ -29,7 +27,6 @@
[app.main.ui.workspace.shapes.text :as text]
[app.util.object :as obj]
[debug :refer [debug?]]
- [okulary.core :as l]
[rumext.alpha :as mf]))
(declare shape-wrapper)
@@ -42,31 +39,23 @@
(def image-wrapper (common/generic-wrapper-factory image/image-shape))
(def rect-wrapper (common/generic-wrapper-factory rect/rect-shape))
-(defn make-is-moving-ref
- [id]
- (fn []
- (let [check-moving (fn [local]
- (and (= :move (:transform local))
- (contains? (:selected local) id)))]
- (l/derived check-moving refs/workspace-local))))
-
(mf/defc root-shape
"Draws the root shape of the viewport and recursively all the shapes"
{::mf/wrap-props false}
[props]
(let [objects (obj/get props "objects")
active-frames (obj/get props "active-frames")
- root-shapes (get-in objects [uuid/zero :shapes])
- shapes (->> root-shapes (mapv #(get objects %)))
-
- root-children (->> shapes
- (filter #(not= :frame (:type %)))
- (mapcat #(cp/get-object-with-children (:id %) objects)))]
-
+ shapes (cph/get-immediate-children objects)]
[:*
- [:& ff/fontfaces-style {:shapes root-children}]
+ ;; Render font faces only for shapes that are part of the root
+ ;; frame but don't belongs to any other frame.
+ (let [xform (comp
+ (remove cph/frame-shape?)
+ (mapcat #(cph/get-children-with-self objects (:id %))))]
+ [:& ff/fontfaces-style {:shapes (into [] xform shapes)}])
+
(for [item shapes]
- (if (= (:type item) :frame)
+ (if (cph/frame-shape? item)
[:& frame-wrapper {:shape item
:key (:id item)
:objects objects
diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
index fe79dd29e0..def9684643 100644
--- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
@@ -7,7 +7,7 @@
(ns app.main.ui.workspace.shapes.frame
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.ui.hooks :as hooks]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.shape :refer [shape-container]]
@@ -105,7 +105,7 @@
(hooks/use-equal-memo))
all-children
- (-> (cp/get-children-objects (:id shape) objects)
+ (-> (cph/get-children objects (:id shape))
(hooks/use-equal-memo))
show-thumbnail?
diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs
index da154e26a7..6173862014 100644
--- a/frontend/src/app/main/ui/workspace/sidebar.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar.cljs
@@ -6,14 +6,21 @@
(ns app.main.ui.workspace.sidebar
(:require
+ [app.main.data.workspace :as dw]
[app.main.refs :as refs]
+ [app.main.store :as st]
+ [app.main.ui.components.tab-container :refer [tab-container tab-element]]
+ [app.main.ui.hooks.resize :refer [use-resize-hook]]
+ [app.main.ui.icons :as i]
[app.main.ui.workspace.comments :refer [comments-sidebar]]
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
[app.main.ui.workspace.sidebar.layers :refer [layers-toolbox]]
[app.main.ui.workspace.sidebar.options :refer [options-toolbox]]
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
- [cuerdas.core :as str]
+ [app.util.dom :as dom]
+ [app.util.i18n :refer [tr]]
+ [app.util.object :as obj]
[rumext.alpha :as mf]))
;; --- Left Sidebar (Component)
@@ -21,19 +28,40 @@
(mf/defc left-sidebar
{:wrap [mf/memo]}
[{:keys [layout ] :as props}]
- [:aside.settings-bar.settings-bar-left
- [:div.settings-bar-inside
- {:data-layout (str/join "," layout)}
- (when (contains? layout :layers)
- [:*
- [:& sitemap {:layout layout}]
- [:& layers-toolbox]])
+ (let [section (cond (contains? layout :layers) :layers
+ (contains? layout :assets) :assets)
- (when (contains? layout :document-history)
- [:& history-toolbox])
+ {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]}
+ (use-resize-hook :left-sidebar 255 255 500 :x false :left)
- (when (contains? layout :assets)
- [:& assets-toolbox])]])
+ handle-collapse
+ (fn []
+ (st/emit! (dw/toggle-layout-flags :collapse-left-sidebar)))]
+
+ [:aside.settings-bar.settings-bar-left {:ref parent-ref
+ :class (dom/classnames
+ :two-row (<= size 300)
+ :three-row (and (> size 300) (<= size 400))
+ :four-row (> size 400))
+ :style #js {"--width" (str size "px")}}
+ [:div.resize-area {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move}]
+
+ [:div.settings-bar-inside
+ [:button.collapse-sidebar
+ {:on-click handle-collapse}
+ i/arrow-slide]
+ [:& tab-container {:on-change-tab #(st/emit! (dw/go-to-layout %))
+ :selected section}
+
+ [:& tab-element {:id :layers :title (tr "workspace.sidebar.layers")}
+ [:div.layers-tab
+ [:& sitemap {:layout layout}]
+ [:& layers-toolbox]]]
+
+ [:& tab-element {:id :assets :title (tr "workspace.toolbar.assets")}
+ [:& assets-toolbox]]]]]))
;; --- Right Sidebar (Component)
@@ -41,10 +69,18 @@
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
- (let [drawing-tool (:tool (mf/deref refs/workspace-drawing))]
- [:aside.settings-bar
+ (let [layout (obj/get props "layout")
+ drawing-tool (:tool (mf/deref refs/workspace-drawing))]
+
+ [:aside.settings-bar.settings-bar-right
[:div.settings-bar-inside
- (if (= drawing-tool :comments)
+ (cond
+ (= drawing-tool :comments)
[:& comments-sidebar]
+
+ (contains? layout :document-history)
+ [:& history-toolbox]
+
+ :else
[:> options-toolbox props])]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
index 13b6506961..8ed877406d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
@@ -8,7 +8,7 @@
(:require
[app.common.data :as d]
[app.common.media :as cm]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.text :as txt]
[app.config :as cfg]
@@ -43,18 +43,18 @@
[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.
-;
-; TODO: change update operations to admit multiple ids, thus avoiding the need of
-; emitting many events and opening an undo transaction. Also move the logic
-; of grouping, deleting, etc. to events in the data module, since now the
-; selection info is in the global state.
+;; TODO: refactor to remove duplicate code and less parameter passing.
+;; - Move all state to [:workspace-local :assets-bar file-id :open-boxes {}
+;; :open-groups {}
+;; :reverse-sort?
+;; :listing-thumbs?
+;; :selected-assets {}]
+;; - Move selection code to independent functions that receive the state as a parameter.
+;;
+;; TODO: change update operations to admit multiple ids, thus avoiding the need of
+;; emitting many events and opening an undo transaction. Also move the logic
+;; of grouping, deleting, etc. to events in the data module, since now the
+;; selection info is in the global state.
;; ---- Group assets management ----
@@ -74,7 +74,7 @@
(compare (d/name key1) (d/name key2))))]
(when-not (empty? assets)
(reduce (fn [groups asset]
- (let [path-vector (cp/split-path (or (:path asset) ""))]
+ (let [path-vector (cph/split-path (or (:path asset) ""))]
(update-in groups (conj path-vector "")
(fn [group]
(if-not group
@@ -86,30 +86,30 @@
(defn add-group
[asset group-name]
(-> (:path asset)
- (cp/merge-path-item group-name)
- (cp/merge-path-item (:name asset))))
+ (cph/merge-path-item group-name)
+ (cph/merge-path-item (:name asset))))
(defn rename-group
[asset path last-path]
(-> (:path asset)
(str/slice 0 (count path))
- (cp/split-path)
+ (cph/split-path)
butlast
(vec)
(conj last-path)
- (cp/join-path)
+ (cph/join-path)
(str (str/slice (:path asset) (count path)))
- (cp/merge-path-item (:name asset))))
+ (cph/merge-path-item (:name asset))))
(defn ungroup
[asset path]
(-> (:path asset)
(str/slice 0 (count path))
- (cp/split-path)
+ (cph/split-path)
butlast
- (cp/join-path)
+ (cph/join-path)
(str (str/slice (:path asset) (count path)))
- (cp/merge-path-item (:name asset))))
+ (cph/merge-path-item (:name asset))))
(s/def ::asset-name ::us/not-empty-string)
(s/def ::name-group-form
@@ -121,8 +121,8 @@
[{:keys [path last-path accept] :as ctx
:or {path "" last-path ""}}]
(let [initial (mf/use-memo
- (mf/deps last-path)
- (constantly {:asset-name last-path}))
+ (mf/deps last-path)
+ (constantly {:asset-name last-path}))
form (fm/use-form :spec ::name-group-form
:initial initial)
@@ -225,27 +225,27 @@
(mf/defc asset-group-title
[{:keys [file-id box path group-open? on-rename on-ungroup]}]
(when-not (empty? path)
- (let [[other-path last-path truncated] (cp/compact-path path 35)
+ (let [[other-path last-path truncated] (cph/compact-path path 35)
menu-state (mf/use-state auto-pos-menu-state)
on-fold-group
(mf/use-callback
- (mf/deps file-id box path group-open?)
- (fn [event]
- (dom/stop-propagation event)
- (st/emit! (dwl/set-assets-group-open file-id
- box
- path
- (not group-open?)))))
+ (mf/deps file-id box path group-open?)
+ (fn [event]
+ (dom/stop-propagation event)
+ (st/emit! (dwl/set-assets-group-open file-id
+ box
+ path
+ (not group-open?)))))
on-context-menu
(mf/use-callback
- (fn [event]
- (swap! menu-state #(open-auto-pos-menu % event))))
+ (fn [event]
+ (swap! menu-state #(open-auto-pos-menu % event))))
on-close-menu
(mf/use-callback
- (fn []
- (swap! menu-state close-auto-pos-menu)))]
+ (fn []
+ (swap! menu-state close-auto-pos-menu)))]
[:div.group-title {:class (when-not group-open? "closed")
:on-click on-fold-group
@@ -268,38 +268,38 @@
(mf/defc components-item
[{:keys [component renaming listing-thumbs? selected-components
on-asset-click on-context-menu on-drag-start do-rename cancel-rename]}]
- [:div {:key (:id component)
- :class-name (dom/classnames
- :selected (contains? selected-components (:id component))
- :grid-cell @listing-thumbs?
- :enum-item (not @listing-thumbs?))
- :id (str "component-shape-id-" (:id component))
- :draggable true
- :on-click #(on-asset-click % (:id component) nil)
- :on-context-menu (on-context-menu (:id component))
- :on-drag-start (partial on-drag-start component)}
- [:& component-svg {:group (get-in component [:objects (:id component)])
- :objects (:objects component)}]
- (let [renaming? (= renaming (:id component))]
- [:& editable-label
- {:class-name (dom/classnames
- :cell-name @listing-thumbs?
- :item-name (not @listing-thumbs?)
- :editing renaming?)
- :value (cp/merge-path-item (:path component) (:name component))
- :tooltip (cp/merge-path-item (:path component) (:name component))
- :display-value (if @listing-thumbs?
- (:name component)
- (cp/compact-name (:path component)
- (:name component)))
- :editing? renaming?
- :disable-dbl-click? true
- :on-change do-rename
- :on-cancel cancel-rename}])])
+ [:div {:key (:id component)
+ :class-name (dom/classnames
+ :selected (contains? selected-components (:id component))
+ :grid-cell @listing-thumbs?
+ :enum-item (not @listing-thumbs?))
+ :id (str "component-shape-id-" (:id component))
+ :draggable true
+ :on-click #(on-asset-click % (:id component) nil)
+ :on-context-menu (on-context-menu (:id component))
+ :on-drag-start (partial on-drag-start component)}
+ [:& component-svg {:group (get-in component [:objects (:id component)])
+ :objects (:objects component)}]
+ (let [renaming? (= renaming (:id component))]
+ [:& editable-label
+ {:class-name (dom/classnames
+ :cell-name @listing-thumbs?
+ :item-name (not @listing-thumbs?)
+ :editing renaming?)
+ :value (cph/merge-path-item (:path component) (:name component))
+ :tooltip (cph/merge-path-item (:path component) (:name component))
+ :display-value (if @listing-thumbs?
+ (:name component)
+ (cph/compact-name (:path component)
+ (:name component)))
+ :editing? renaming?
+ :disable-dbl-click? true
+ :on-change do-rename
+ :on-cancel cancel-rename}])])
(mf/defc components-group
[{:keys [file-id prefix groups open-groups renaming listing-thumbs? selected-components on-asset-click
- on-drag-start do-rename cancel-rename on-rename-group on-ungroup on-context-menu]}]
+ on-drag-start do-rename cancel-rename on-rename-group on-ungroup on-context-menu]}]
(let [group-open? (get open-groups prefix true)]
[:*
@@ -313,9 +313,9 @@
[:*
(let [components (get groups "" [])]
[:div {:class-name (dom/classnames
- :asset-grid @listing-thumbs?
- :big @listing-thumbs?
- :asset-enum (not @listing-thumbs?))}
+ :asset-grid @listing-thumbs?
+ :big @listing-thumbs?
+ :asset-enum (not @listing-thumbs?))}
(for [component components]
[:& components-item {:component component
:renaming renaming
@@ -329,7 +329,7 @@
(for [[path-item content] groups]
(when-not (empty? path-item)
[:& components-group {:file-id file-id
- :prefix (cp/merge-path-item prefix path-item)
+ :prefix (cph/merge-path-item prefix path-item)
:groups content
:open-groups open-groups
:renaming renaming
@@ -361,119 +361,119 @@
on-duplicate
(mf/use-callback
- (mf/deps @state)
- (fn []
- (if (empty? selected-components)
- (st/emit! (dwl/duplicate-component {:id (:component-id @state)}))
- (do
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected-components))
- (st/emit! (dwu/commit-undo-transaction))))))
+ (mf/deps @state)
+ (fn []
+ (if (empty? selected-components)
+ (st/emit! (dwl/duplicate-component {:id (:component-id @state)}))
+ (do
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected-components))
+ (st/emit! (dwu/commit-undo-transaction))))))
on-delete
(mf/use-callback
- (mf/deps @state file-id multi-components? multi-assets?)
- (fn []
- (if (or multi-components? multi-assets?)
- (on-assets-delete)
- (st/emit! (dwu/start-undo-transaction)
- (dwl/delete-component {:id (:component-id @state)})
- (dwl/sync-file file-id file-id)
- (dwu/commit-undo-transaction)))))
+ (mf/deps @state file-id multi-components? multi-assets?)
+ (fn []
+ (if (or multi-components? multi-assets?)
+ (on-assets-delete)
+ (st/emit! (dwu/start-undo-transaction)
+ (dwl/delete-component {:id (:component-id @state)})
+ (dwl/sync-file file-id file-id)
+ (dwu/commit-undo-transaction)))))
on-rename
(mf/use-callback
- (mf/deps @state)
- (fn []
- (swap! state assoc :renaming (:component-id @state))))
+ (mf/deps @state)
+ (fn []
+ (swap! state assoc :renaming (:component-id @state))))
do-rename
(mf/use-callback
- (mf/deps @state)
- (fn [new-name]
- (st/emit! (dwl/rename-component (:renaming @state) new-name))
- (swap! state assoc :renaming nil)))
+ (mf/deps @state)
+ (fn [new-name]
+ (st/emit! (dwl/rename-component (:renaming @state) new-name))
+ (swap! state assoc :renaming nil)))
cancel-rename
(mf/use-callback
- (fn []
- (swap! state assoc :renaming nil)))
+ (fn []
+ (swap! state assoc :renaming nil)))
on-context-menu
(mf/use-callback
- (mf/deps selected-components on-clear-selection)
- (fn [component-id]
- (fn [event]
- (when local?
- (when-not (contains? selected-components component-id)
- (on-clear-selection))
- (swap! state assoc :component-id component-id)
- (swap! menu-state #(open-auto-pos-menu % event))))))
+ (mf/deps selected-components on-clear-selection)
+ (fn [component-id]
+ (fn [event]
+ (when local?
+ (when-not (contains? selected-components component-id)
+ (on-clear-selection))
+ (swap! state assoc :component-id component-id)
+ (swap! menu-state #(open-auto-pos-menu % event))))))
on-close-menu
(mf/use-callback
- (fn []
- (swap! menu-state close-auto-pos-menu)))
+ (fn []
+ (swap! menu-state close-auto-pos-menu)))
create-group
(mf/use-callback
- (mf/deps components selected-components on-clear-selection)
- (fn [group-name]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> components
- (filter #(if multi-components?
- (contains? selected-components (:id %))
- (= (:component-id @state) (:id %))))
- (map #(dwl/rename-component
- (:id %)
- (add-group % group-name)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps components selected-components on-clear-selection)
+ (fn [group-name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> components
+ (filter #(if multi-components?
+ (contains? selected-components (:id %))
+ (= (:component-id @state) (:id %))))
+ (map #(dwl/rename-component
+ (:id %)
+ (add-group % group-name)))))
+ (st/emit! (dwu/commit-undo-transaction))))
rename-group
(mf/use-callback
- (mf/deps components)
- (fn [path last-path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> components
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/rename-component
- (:id %)
- (rename-group % path last-path)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps components)
+ (fn [path last-path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> components
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/rename-component
+ (:id %)
+ (rename-group % path last-path)))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-group
(mf/use-callback
- (mf/deps components selected-components)
- (fn [event]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:accept create-group})))
+ (mf/deps components selected-components)
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:accept create-group})))
on-rename-group
(mf/use-callback
- (mf/deps components)
- (fn [event path last-path]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:path path
- :last-path last-path
- :accept rename-group})))
+ (mf/deps components)
+ (fn [event path last-path]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:path path
+ :last-path last-path
+ :accept rename-group})))
on-ungroup
(mf/use-callback
- (mf/deps components)
- (fn [path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> components
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/rename-component
- (:id %)
- (ungroup % path)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps components)
+ (fn [path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> components
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/rename-component
+ (:id %)
+ (ungroup % path)))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-drag-start
(mf/use-callback
@@ -522,9 +522,9 @@
on-asset-click on-context-menu on-drag-start do-rename cancel-rename]}]
[:div {:key (:id object)
:class-name (dom/classnames
- :selected (contains? selected-objects (:id object))
- :grid-cell @listing-thumbs?
- :enum-item (not @listing-thumbs?))
+ :selected (contains? selected-objects (:id object))
+ :grid-cell @listing-thumbs?
+ :enum-item (not @listing-thumbs?))
:draggable true
:on-click #(on-asset-click % (:id object) nil)
:on-context-menu (on-context-menu (:id object))
@@ -535,15 +535,15 @@
(let [renaming? (= renaming (:id object))]
[:& editable-label
{:class-name (dom/classnames
- :cell-name @listing-thumbs?
- :item-name (not @listing-thumbs?)
- :editing renaming?)
- :value (cp/merge-path-item (:path object) (:name object))
- :tooltip (cp/merge-path-item (:path object) (:name object))
+ :cell-name @listing-thumbs?
+ :item-name (not @listing-thumbs?)
+ :editing renaming?)
+ :value (cph/merge-path-item (:path object) (:name object))
+ :tooltip (cph/merge-path-item (:path object) (:name object))
:display-value (if @listing-thumbs?
(:name object)
- (cp/compact-name (:path object)
- (:name object)))
+ (cph/compact-name (:path object)
+ (:name object)))
:editing? renaming?
:disable-dbl-click? true
:on-change do-rename
@@ -566,8 +566,8 @@
[:*
(let [objects (get groups "" [])]
[:div {:class-name (dom/classnames
- :asset-grid @listing-thumbs?
- :asset-enum (not @listing-thumbs?))}
+ :asset-grid @listing-thumbs?
+ :asset-enum (not @listing-thumbs?))}
(for [object objects]
[:& graphics-item {:object object
:renaming renaming
@@ -581,7 +581,7 @@
(for [[path-item content] groups]
(when-not (empty? path-item)
[:& graphics-group {:file-id file-id
- :prefix (cp/merge-path-item prefix path-item)
+ :prefix (cph/merge-path-item prefix path-item)
:groups content
:open-groups open-groups
:renaming renaming
@@ -638,96 +638,96 @@
on-rename
(mf/use-callback
- (mf/deps @state)
- (fn []
- (swap! state assoc :renaming (:object-id @state))))
+ (mf/deps @state)
+ (fn []
+ (swap! state assoc :renaming (:object-id @state))))
cancel-rename
(mf/use-callback
- (fn []
- (swap! state assoc :renaming nil)))
+ (fn []
+ (swap! state assoc :renaming nil)))
do-rename
(mf/use-callback
- (mf/deps @state)
- (fn [new-name]
- (st/emit! (dwl/rename-media (:renaming @state) new-name))
- (swap! state assoc :renaming nil)))
+ (mf/deps @state)
+ (fn [new-name]
+ (st/emit! (dwl/rename-media (:renaming @state) new-name))
+ (swap! state assoc :renaming nil)))
on-context-menu
(mf/use-callback
- (mf/deps selected-objects on-clear-selection)
- (fn [object-id]
- (fn [event]
- (when local?
- (when-not (contains? selected-objects object-id)
- (on-clear-selection))
- (swap! state assoc :object-id object-id)
- (swap! menu-state #(open-auto-pos-menu % event))))))
+ (mf/deps selected-objects on-clear-selection)
+ (fn [object-id]
+ (fn [event]
+ (when local?
+ (when-not (contains? selected-objects object-id)
+ (on-clear-selection))
+ (swap! state assoc :object-id object-id)
+ (swap! menu-state #(open-auto-pos-menu % event))))))
on-close-menu
(mf/use-callback
- (fn []
- (swap! menu-state close-auto-pos-menu)))
+ (fn []
+ (swap! menu-state close-auto-pos-menu)))
create-group
(mf/use-callback
- (mf/deps objects selected-objects on-clear-selection)
- (fn [group-name]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> objects
- (filter #(if multi-objects?
- (contains? selected-objects (:id %))
- (= (:object-id @state) (:id %))))
- (map #(dwl/rename-media
- (:id %)
- (add-group % group-name)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps objects selected-objects on-clear-selection)
+ (fn [group-name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> objects
+ (filter #(if multi-objects?
+ (contains? selected-objects (:id %))
+ (= (:object-id @state) (:id %))))
+ (map #(dwl/rename-media
+ (:id %)
+ (add-group % group-name)))))
+ (st/emit! (dwu/commit-undo-transaction))))
rename-group
(mf/use-callback
- (mf/deps objects)
- (fn [path last-path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> objects
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/rename-media
- (:id %)
- (rename-group % path last-path)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps objects)
+ (fn [path last-path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> objects
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/rename-media
+ (:id %)
+ (rename-group % path last-path)))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-group
(mf/use-callback
- (mf/deps objects selected-objects)
- (fn [event]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:accept create-group})))
+ (mf/deps objects selected-objects)
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:accept create-group})))
on-rename-group
(mf/use-callback
- (mf/deps objects)
- (fn [event path last-path]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:path path
- :last-path last-path
- :accept rename-group})))
+ (mf/deps objects)
+ (fn [event path last-path]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:path path
+ :last-path last-path
+ :accept rename-group})))
on-ungroup
(mf/use-callback
- (mf/deps objects)
- (fn [path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> objects
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/rename-media
- (:id %)
- (ungroup % path)))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps objects)
+ (fn [path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> objects
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/rename-media
+ (:id %)
+ (ungroup % path)))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-drag-start
(mf/use-callback
@@ -742,39 +742,39 @@
:box :graphics
:assets-count (count objects)
:open? open?}
- (when local?
- [:& asset-section-block {:role :title-button}
- [:div.assets-button {:on-click add-graphic}
- i/plus
- [:& file-uploader {:accept cm/str-image-types
- :multi true
- :ref input-ref
- :on-selected on-file-selected}]]])
+ (when local?
+ [:& asset-section-block {:role :title-button}
+ [:div.assets-button {:on-click add-graphic}
+ i/plus
+ [:& file-uploader {:accept cm/str-image-types
+ :multi true
+ :ref input-ref
+ :on-selected on-file-selected}]]])
- [:& asset-section-block {:role :content}
- [:& graphics-group {:file-id file-id
- :prefix ""
- :groups groups
- :open-groups open-groups
- :renaming (:renaming @state)
- :listing-thumbs? listing-thumbs?
- :selected-objects selected-objects
- :on-asset-click (partial on-asset-click groups)
- :on-drag-start on-drag-start
- :do-rename do-rename
- :cancel-rename cancel-rename
- :on-rename-group on-rename-group
- :on-ungroup on-ungroup
- :on-context-menu on-context-menu}]
- (when local?
- [:& auto-pos-menu
- {:on-close on-close-menu
- :state @menu-state
- :options [(when-not (or multi-objects? multi-assets?)
- [(tr "workspace.assets.rename") on-rename])
- [(tr "workspace.assets.delete") on-delete]
- (when-not multi-assets?
- [(tr "workspace.assets.group") on-group])]}])]]))
+ [:& asset-section-block {:role :content}
+ [:& graphics-group {:file-id file-id
+ :prefix ""
+ :groups groups
+ :open-groups open-groups
+ :renaming (:renaming @state)
+ :listing-thumbs? listing-thumbs?
+ :selected-objects selected-objects
+ :on-asset-click (partial on-asset-click groups)
+ :on-drag-start on-drag-start
+ :do-rename do-rename
+ :cancel-rename cancel-rename
+ :on-rename-group on-rename-group
+ :on-ungroup on-ungroup
+ :on-context-menu on-context-menu}]
+ (when local?
+ [:& auto-pos-menu
+ {:on-close on-close-menu
+ :state @menu-state
+ :options [(when-not (or multi-objects? multi-assets?)
+ [(tr "workspace.assets.rename") on-rename])
+ [(tr "workspace.assets.delete") on-delete]
+ (when-not multi-assets?
+ [(tr "workspace.assets.group") on-group])]}])]]))
;; ---- Colors box ----
@@ -808,7 +808,7 @@
edit-color
(fn [new-color]
(let [old-data (-> (select-keys color [:id :file-id])
- (assoc :name (cp/merge-path-item (:path color) (:name color))))
+ (assoc :name (cph/merge-path-item (:path color) (:name color))))
updated-color (merge new-color old-data)]
(st/emit! (dwl/update-color updated-color file-id))))
@@ -856,31 +856,31 @@
on-context-menu
(mf/use-callback
- (mf/deps color selected-colors on-clear-selection)
- (fn [event]
- (when local?
- (when-not (contains? selected-colors (:id color))
- (on-clear-selection))
- (swap! menu-state #(open-auto-pos-menu % event)))))
+ (mf/deps color selected-colors on-clear-selection)
+ (fn [event]
+ (when local?
+ (when-not (contains? selected-colors (:id color))
+ (on-clear-selection))
+ (swap! menu-state #(open-auto-pos-menu % event)))))
on-close-menu
(mf/use-callback
- (fn []
- (swap! menu-state close-auto-pos-menu)))]
+ (fn []
+ (swap! menu-state close-auto-pos-menu)))]
(mf/use-effect
- (mf/deps (:editing @state))
- #(when (:editing @state)
- (let [input (mf/ref-val input-ref)]
- (dom/select-text! input))
- nil))
+ (mf/deps (:editing @state))
+ #(when (:editing @state)
+ (let [input (mf/ref-val input-ref)]
+ (dom/select-text! input))
+ nil))
[:div.asset-list-item {:class-name (dom/classnames
- :selected (contains? selected-colors (:id color)))
+ :selected (contains? selected-colors (:id color)))
:on-context-menu on-context-menu
:on-click (when-not (:editing @state)
#(on-asset-click % (:id color)
- (partial apply-color (:id color))))}
+ (partial apply-color (:id color))))}
[:& bc/color-bullet {:color color}]
(if (:editing @state)
@@ -890,7 +890,7 @@
:on-blur input-blur
:on-key-down input-key-down
:auto-focus true
- :default-value (cp/merge-path-item (:path color) (:name color))}]
+ :default-value (cph/merge-path-item (:path color) (:name color))}]
[:div.name-block {:on-double-click rename-color-clicked}
(:name color)
@@ -898,15 +898,15 @@
[:span default-name])])
(when local?
[:& auto-pos-menu
- {:on-close on-close-menu
- :state @menu-state
- :options [(when-not (or multi-colors? multi-assets?)
- [(tr "workspace.assets.rename") rename-color-clicked])
- (when-not (or multi-colors? multi-assets?)
- [(tr "workspace.assets.edit") edit-color-clicked])
- [(tr "workspace.assets.delete") delete-color]
- (when-not multi-assets?
- [(tr "workspace.assets.group") (on-group (:id color))])]}])]))
+ {:on-close on-close-menu
+ :state @menu-state
+ :options [(when-not (or multi-colors? multi-assets?)
+ [(tr "workspace.assets.rename") rename-color-clicked])
+ (when-not (or multi-colors? multi-assets?)
+ [(tr "workspace.assets.edit") edit-color-clicked])
+ [(tr "workspace.assets.delete") delete-color]
+ (when-not multi-assets?
+ [(tr "workspace.assets.group") (on-group (:id color))])]}])]))
(mf/defc colors-group
[{:keys [file-id prefix groups open-groups local? selected-colors
@@ -945,7 +945,7 @@
(for [[path-item content] groups]
(when-not (empty? path-item)
[:& colors-group {:file-id file-id
- :prefix (cp/merge-path-item prefix path-item)
+ :prefix (cph/merge-path-item prefix path-item)
:key (str "group-" path-item)
:groups content
:open-groups open-groups
@@ -995,99 +995,99 @@
create-group
(mf/use-callback
- (mf/deps colors selected-colors on-clear-selection file-id)
- (fn [color-id]
- (fn [group-name]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> colors
- (filter #(if multi-colors?
- (contains? selected-colors (:id %))
- (= color-id (:id %))))
- (map #(dwl/update-color
- (assoc % :name
- (add-group % group-name))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction)))))
+ (mf/deps colors selected-colors on-clear-selection file-id)
+ (fn [color-id]
+ (fn [group-name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> colors
+ (filter #(if multi-colors?
+ (contains? selected-colors (:id %))
+ (= color-id (:id %))))
+ (map #(dwl/update-color
+ (assoc % :name
+ (add-group % group-name))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction)))))
rename-group
(mf/use-callback
- (mf/deps colors)
- (fn [path last-path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> colors
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/update-color
- (assoc % :name
- (rename-group % path last-path))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps colors)
+ (fn [path last-path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> colors
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/update-color
+ (assoc % :name
+ (rename-group % path last-path))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-group
(mf/use-callback
- (mf/deps colors selected-colors)
- (fn [color-id]
- (fn [event]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:accept (create-group color-id)}))))
+ (mf/deps colors selected-colors)
+ (fn [color-id]
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:accept (create-group color-id)}))))
on-rename-group
(mf/use-callback
- (mf/deps colors)
- (fn [event path last-path]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:path path
- :last-path last-path
- :accept rename-group})))
+ (mf/deps colors)
+ (fn [event path last-path]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:path path
+ :last-path last-path
+ :accept rename-group})))
on-ungroup
(mf/use-callback
- (mf/deps colors)
- (fn [path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> colors
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/update-color
- (assoc % :name
- (ungroup % path))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction))))]
+ (mf/deps colors)
+ (fn [path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> colors
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/update-color
+ (assoc % :name
+ (ungroup % path))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))]
[:& asset-section {:file-id file-id
:title (tr "workspace.assets.colors")
:box :colors
:assets-count (count colors)
:open? open?}
- (when local?
- [:& asset-section-block {:role :title-button}
- [:div.assets-button {:on-click add-color-clicked}
- i/plus]])
+ (when local?
+ [:& asset-section-block {:role :title-button}
+ [:div.assets-button {:on-click add-color-clicked}
+ i/plus]])
- [:& asset-section-block {:role :content}
- [:& colors-group {:file-id file-id
- :prefix ""
- :groups groups
- :open-groups open-groups
- :local? local?
- :selected-colors selected-colors
- :multi-colors? multi-colors?
- :multi-assets? multi-assets?
- :on-asset-click (partial on-asset-click groups)
- :on-assets-delete on-assets-delete
- :on-clear-selection on-clear-selection
- :on-group on-group
- :on-rename-group on-rename-group
- :on-ungroup on-ungroup
- :colors colors}]]]))
+ [:& asset-section-block {:role :content}
+ [:& colors-group {:file-id file-id
+ :prefix ""
+ :groups groups
+ :open-groups open-groups
+ :local? local?
+ :selected-colors selected-colors
+ :multi-colors? multi-colors?
+ :multi-assets? multi-assets?
+ :on-asset-click (partial on-asset-click groups)
+ :on-assets-delete on-assets-delete
+ :on-clear-selection on-clear-selection
+ :on-group on-group
+ :on-rename-group on-rename-group
+ :on-ungroup on-ungroup
+ :colors colors}]]]))
;; ---- Typography box ----
(mf/defc typographies-group
- [{:keys [file-id prefix groups open-groups file local? selected-typographies local
+ [{:keys [file-id prefix groups open-groups file local? selected-typographies local-data
editing-id on-asset-click handle-change apply-typography
on-rename-group on-ungroup on-context-menu]}]
(let [group-open? (get open-groups prefix true)]
@@ -1115,19 +1115,19 @@
:on-click #(on-asset-click % (:id typography)
(partial apply-typography typography))
:editing? (= editing-id (:id typography))
- :focus-name? (= (:rename-typography local) (:id typography))}])])
+ :focus-name? (= (:rename-typography local-data) (:id typography))}])])
(for [[path-item content] groups]
(when-not (empty? path-item)
[:& typographies-group {:file-id file-id
- :prefix (cp/merge-path-item prefix path-item)
+ :prefix (cph/merge-path-item prefix path-item)
:groups content
:open-groups open-groups
:file file
:local? local?
:selected-typographies selected-typographies
:editing-id editing-id
- :local local
+ :local-data local-data
:on-asset-click on-asset-click
:handle-change handle-change
:apply-typography apply-typography
@@ -1141,11 +1141,9 @@
(let [state (mf/use-state {:detail-open? false
:id nil})
+ local-data (mf/deref refs/typography-data)
menu-state (mf/use-state auto-pos-menu-state)
-
- local (deref refs/workspace-local)
-
- groups (group-assets typographies reverse-sort?)
+ groups (group-assets typographies reverse-sort?)
selected-typographies (:typographies selected-assets)
multi-typographies? (> (count selected-typographies) 1)
@@ -1171,88 +1169,92 @@
(fn [typography _event]
(let [ids (wsh/lookup-selected @st/state)
attrs (merge
- {:typography-ref-file file-id
- :typography-ref-id (:id typography)}
- (dissoc typography :id :name))]
- (run! #(st/emit! (dwt/update-text-attrs {:id % :editor (get-in local [:editors %]) :attrs attrs}))
+ {:typography-ref-file file-id
+ :typography-ref-id (:id typography)}
+ (dissoc typography :id :name))]
+ (run! #(st/emit!
+ (dwt/update-text-attrs
+ {:id %
+ :editor (get @refs/workspace-editor-state %)
+ :attrs attrs}))
ids)))
create-group
(mf/use-callback
- (mf/deps typographies selected-typographies on-clear-selection file-id)
- (fn [group-name]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> typographies
- (filter #(if multi-typographies?
- (contains? selected-typographies (:id %))
- (= (:id @state) (:id %))))
- (map #(dwl/update-typography
- (assoc % :name
- (add-group % group-name))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps typographies selected-typographies on-clear-selection file-id)
+ (fn [group-name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> typographies
+ (filter #(if multi-typographies?
+ (contains? selected-typographies (:id %))
+ (= (:id @state) (:id %))))
+ (map #(dwl/update-typography
+ (assoc % :name
+ (add-group % group-name))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
rename-group
(mf/use-callback
- (mf/deps typographies)
- (fn [path last-path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> typographies
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/update-typography
- (assoc % :name
- (rename-group % path last-path))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps typographies)
+ (fn [path last-path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> typographies
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/update-typography
+ (assoc % :name
+ (rename-group % path last-path))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-group
(mf/use-callback
- (mf/deps typographies selected-typographies)
- (fn [event]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:accept create-group})))
+ (mf/deps typographies selected-typographies)
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:accept create-group})))
on-rename-group
(mf/use-callback
- (mf/deps typographies)
- (fn [event path last-path]
- (dom/stop-propagation event)
- (modal/show! :name-group-dialog {:path path
- :last-path last-path
- :accept rename-group})))
+ (mf/deps typographies)
+ (fn [event path last-path]
+ (dom/stop-propagation event)
+ (modal/show! :name-group-dialog {:path path
+ :last-path last-path
+ :accept rename-group})))
on-ungroup
(mf/use-callback
- (mf/deps typographies)
- (fn [path]
- (on-clear-selection)
- (st/emit! (dwu/start-undo-transaction))
- (apply st/emit!
- (->> typographies
- (filter #(str/starts-with? (:path %) path))
- (map #(dwl/update-typography
- (assoc % :name
- (ungroup % path))
- file-id))))
- (st/emit! (dwu/commit-undo-transaction))))
+ (mf/deps typographies)
+ (fn [path]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> typographies
+ (filter #(str/starts-with? (:path %) path))
+ (map #(dwl/update-typography
+ (assoc % :name
+ (ungroup % path))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
on-context-menu
(mf/use-callback
- (mf/deps selected-typographies on-clear-selection)
- (fn [id event]
- (when local?
- (when-not (contains? selected-typographies id)
- (on-clear-selection))
- (swap! state assoc :id id)
- (swap! menu-state #(open-auto-pos-menu % event)))))
+ (mf/deps selected-typographies on-clear-selection)
+ (fn [id event]
+ (when local?
+ (when-not (contains? selected-typographies id)
+ (on-clear-selection))
+ (swap! state assoc :id id)
+ (swap! menu-state #(open-auto-pos-menu % event)))))
on-close-menu
(mf/use-callback
- (fn []
- (swap! menu-state close-auto-pos-menu)))
+ (fn []
+ (swap! menu-state close-auto-pos-menu)))
handle-rename-typography-clicked
(fn []
@@ -1273,14 +1275,15 @@
(dwl/sync-file file-id file-id)
(dwu/commit-undo-transaction)))))
- editing-id (or (:rename-typography local) (:edit-typography local))]
+ editing-id (or (:rename-typography local-data)
+ (:edit-typography local-data))]
(mf/use-effect
- (mf/deps local)
+ (mf/deps local-data)
(fn []
- (when (:rename-typography local)
+ (when (:rename-typography local-data)
(st/emit! #(update % :workspace-local dissoc :rename-typography)))
- (when (:edit-typography local)
+ (when (:edit-typography local-data)
(st/emit! #(update % :workspace-local dissoc :edit-typography)))))
[:& asset-section {:file-id file-id
@@ -1288,40 +1291,40 @@
:box :typographies
:assets-count (count typographies)
:open? open?}
+ (when local?
+ [:& asset-section-block {:role :title-button}
+ [:div.assets-button {:on-click add-typography}
+ i/plus]])
+
+ [:& asset-section-block {:role :content}
+ [:& typographies-group {:file-id file-id
+ :prefix ""
+ :groups groups
+ :open-groups open-groups
+ :state state
+ :file file
+ :local? local?
+ :selected-typographies selected-typographies
+ :editing-id editing-id
+ :local-data local-data
+ :on-asset-click (partial on-asset-click groups)
+ :handle-change handle-change
+ :apply-typography apply-typography
+ :on-rename-group on-rename-group
+ :on-ungroup on-ungroup
+ :on-context-menu on-context-menu}]
+
(when local?
- [:& asset-section-block {:role :title-button}
- [:div.assets-button {:on-click add-typography}
- i/plus]])
-
- [:& asset-section-block {:role :content}
- [:& typographies-group {:file-id file-id
- :prefix ""
- :groups groups
- :open-groups open-groups
- :state state
- :file file
- :local? local?
- :selected-typographies selected-typographies
- :editing-id editing-id
- :local local
- :on-asset-click (partial on-asset-click groups)
- :handle-change handle-change
- :apply-typography apply-typography
- :on-rename-group on-rename-group
- :on-ungroup on-ungroup
- :on-context-menu on-context-menu}]
-
- (when local?
- [:& auto-pos-menu
- {:on-close on-close-menu
- :state @menu-state
- :options [(when-not (or multi-typographies? multi-assets?)
- [(tr "workspace.assets.rename") handle-rename-typography-clicked])
- (when-not (or multi-typographies? multi-assets?)
- [(tr "workspace.assets.edit") handle-edit-typography-clicked])
- [(tr "workspace.assets.delete") handle-delete-typography]
- (when-not multi-assets?
- [(tr "workspace.assets.group") on-group])]}])]]))
+ [:& auto-pos-menu
+ {:on-close on-close-menu
+ :state @menu-state
+ :options [(when-not (or multi-typographies? multi-assets?)
+ [(tr "workspace.assets.rename") handle-rename-typography-clicked])
+ (when-not (or multi-typographies? multi-assets?)
+ [(tr "workspace.assets.edit") handle-edit-typography-clicked])
+ [(tr "workspace.assets.delete") handle-delete-typography]
+ (when-not multi-assets?
+ [(tr "workspace.assets.group") on-group])]}])]]))
;; --- Assets toolbox ----
@@ -1374,12 +1377,12 @@
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters)))))
- ; Sort by folder order, but putting all "root" items always first,
- ; independently of sort order.
- (sort-by #(str/lower (cp/merge-path-item (if (empty? (:path %))
- (if reverse-sort? "z" "a")
- (:path %))
- (:name %)))
+ ; Sort by folder order, but putting all "root" items always first,
+ ; independently of sort order.
+ (sort-by #(str/lower (cph/merge-path-item (if (empty? (:path %))
+ (if reverse-sort? "z" "a")
+ (:path %))
+ (:name %)))
comp-fn))))
(mf/defc file-library
@@ -1525,16 +1528,16 @@
(if local?
[:*
- [:span (tr "workspace.assets.file-library")]
- (when shared?
- [:span.tool-badge (tr "workspace.assets.shared")])]
+ [:span (tr "workspace.assets.file-library")]
+ (when shared?
+ [:span.tool-badge (tr "workspace.assets.shared")])]
[:*
- [:span (:name file)]
- [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"}
- [:a {:href (str "#" url)
- :target "_blank"
- :on-click dom/stop-propagation}
- i/chain]]])]
+ [:span (:name file)]
+ [:span.tool-link.tooltip.tooltip-left {:alt "Open library file"}
+ [:a {:href (str "#" url)
+ :target "_blank"
+ :on-click dom/stop-propagation}
+ i/chain]]])]
(when open?
(let [show-components? (and (or (= (:box filters) :all)
@@ -1655,33 +1658,33 @@
[:div.assets-bar
[:div.tool-window
- [:div.tool-window-content
- [:div.assets-bar-title
- (tr "workspace.assets.assets")
- [:div.libraries-button {:on-click #(modal/show! :libraries-dialog {})}
- i/text-align-justify
- (tr "workspace.assets.libraries")]]
+ [:div.tool-window-content
+ [:div.assets-bar-title
+ (tr "workspace.assets.assets")
+ [:div.libraries-button {:on-click #(modal/show! :libraries-dialog {})}
+ i/text-align-justify
+ (tr "workspace.assets.libraries")]]
- [:div.search-block
- [:input.search-input
- {:placeholder (tr "workspace.assets.search")
- :type "text"
- :value (:term @filters)
- :on-change on-search-term-change}]
- (if (str/empty? (:term @filters))
- [:div.search-icon
- i/search]
- [:div.search-icon.close
- {:on-click on-search-clear-click}
- i/close])]
+ [:div.search-block
+ [:input.search-input
+ {:placeholder (tr "workspace.assets.search")
+ :type "text"
+ :value (:term @filters)
+ :on-change on-search-term-change}]
+ (if (str/empty? (:term @filters))
+ [:div.search-icon
+ i/search]
+ [:div.search-icon.close
+ {:on-click on-search-clear-click}
+ i/close])]
- [:select.input-select {:value (:box @filters)
- :on-change on-box-filter-change}
- [:option {:value ":all"} (tr "workspace.assets.box-filter-all")]
- [:option {:value ":components"} (tr "workspace.assets.components")]
- [:option {:value ":graphics"} (tr "workspace.assets.graphics")]
- [:option {:value ":colors"} (tr "workspace.assets.colors")]
- [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]]
+ [:select.input-select {:value (:box @filters)
+ :on-change on-box-filter-change}
+ [:option {:value ":all"} (tr "workspace.assets.box-filter-all")]
+ [:option {:value ":components"} (tr "workspace.assets.components")]
+ [:option {:value ":graphics"} (tr "workspace.assets.graphics")]
+ [:option {:value ":colors"} (tr "workspace.assets.colors")]
+ [:option {:value ":typographies"} (tr "workspace.assets.typography")]]]]
[:div.libraries-wrapper
[:& file-library
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
index 768ea5f35f..9e65bbf54d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
@@ -7,12 +7,13 @@
(ns app.main.ui.workspace.sidebar.layers
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
+ [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.refs :as refs]
[app.main.store :as st]
+ [app.main.ui.components.shape-icon :as si]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
@@ -24,31 +25,6 @@
[okulary.core :as l]
[rumext.alpha :as mf]))
-;; --- Helpers
-
-(mf/defc element-icon
- [{:keys [shape] :as props}]
- (case (:type shape)
- :frame i/artboard
- :image i/image
- :line i/line
- :circle i/circle
- :path i/curve
- :rect i/box
- :text i/text
- :group (if (some? (:component-id shape))
- i/component
- (if (:masked-group? shape)
- i/mask
- i/folder))
- :bool (case (:bool-type shape)
- :difference i/bool-difference
- :exclude i/bool-exclude
- :intersection i/bool-intersection
- #_:default i/bool-union)
- :svg-raw i/file-svg
- nil))
-
;; --- Layer Name
(def shape-for-rename-ref
@@ -82,18 +58,16 @@
(when (kbd/enter? event) (accept-edit))
(when (kbd/esc? event) (cancel-edit)))]
- (mf/use-effect
- (mf/deps shape-for-rename)
- #(when (and (= shape-for-rename (:id shape))
- (not (:edition @local)))
- (start-edit)))
+ (mf/with-effect [shape-for-rename]
+ (when (and (= shape-for-rename (:id shape))
+ (not (:edition @local)))
+ (start-edit)))
- (mf/use-effect
- (mf/deps (:edition @local))
- #(when (:edition @local)
- (let [name-input (mf/ref-val name-ref)]
- (dom/select-text! name-input))
- nil))
+ (mf/with-effect [(:edition @local)]
+ (when (:edition @local)
+ (let [name-input (mf/ref-val name-ref)]
+ (dom/select-text! name-input)
+ nil)))
(if (:edition @local)
[:input.element-name
@@ -116,9 +90,10 @@
(mf/defc layer-item
[{:keys [index item selected objects] :as props}]
- (let [id (:id item)
- selected? (contains? selected id)
- container? (or (= (:type item) :frame) (= (:type item) :group))
+ (let [id (:id item)
+ selected? (contains? selected id)
+ container? (or (cph/frame-shape? item)
+ (cph/group-shape? item))
disable-drag (mf/use-state false)
@@ -184,7 +159,7 @@
(if (= side :center)
(st/emit! (dw/relocate-selected-shapes (:id item) 0))
(let [to-index (if (= side :top) (inc index) index)
- parent-id (cp/get-parent (:id item) objects)]
+ parent-id (cph/get-parent-id objects (:id item))]
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))
on-hold
@@ -204,12 +179,17 @@
:name (:name item)})]
(mf/use-effect
- (mf/deps selected)
+ (mf/deps selected? selected)
(fn []
- (let [subid
- (when (and (= (count selected) 1) selected?)
- (ts/schedule-on-idle
- #(.scrollIntoView (mf/ref-val dref) #js {:block "nearest", :behavior "smooth"})))]
+ (let [single? (= (count selected) 1)
+ node (mf/ref-val dref)
+
+ subid
+ (when (and single? selected?)
+ (ts/schedule
+ 100
+ #(dom/scroll-into-view! node #js {:block "nearest", :behavior "smooth"})))]
+
#(when (some? subid)
(rx/dispose! subid)))))
@@ -227,12 +207,12 @@
:icon-layer (= (:type item) :icon))
:on-click select-shape
:on-double-click #(dom/stop-propagation %)}
- [:& element-icon {:shape item}]
+ [:& si/element-icon {:shape item}]
[:& layer-name {:shape item
:on-start-edit #(reset! disable-drag true)
:on-stop-edit #(reset! disable-drag false)}]
- [:div.element-actions
+ [:div.element-actions {:class (when (:shapes item) "is-parent")}
[:div.toggle-element {:class (when (:hidden item) "selected")
:on-click toggle-visibility}
(if (:hidden item) i/eye-closed i/eye)]
@@ -271,6 +251,7 @@
{::mf/wrap [#(mf/memo % =)]}
[{:keys [objects] :as props}]
(let [selected (mf/deref refs/selected-shapes)
+ selected (hooks/use-equal-memo selected)
root (get objects uuid/zero)]
[:ul.element-list
[:& hooks/sortable-container {}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
index 733fd420b8..7e42847e26 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui.workspace.sidebar.options.common
(:require
+ [app.util.dom :as dom]
[rumext.alpha :as mf]))
(mf/defc advanced-options [{:keys [visible? children]}]
@@ -15,7 +16,7 @@
(fn []
(when-let [node (mf/ref-val ref)]
(when visible?
- (.scrollIntoViewIfNeeded ^js node)))))
+ (dom/scroll-into-view-if-needed! node)))))
(when visible?
[:div.advanced-options-wrapper {:ref ref}
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 8710ff7d52..4552b3c400 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,7 +6,7 @@
(ns app.main.ui.workspace.sidebar.options.menus.component
(:require
- [app.common.pages :as cp]
+ [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]
@@ -16,7 +16,7 @@
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
- [app.util.i18n :as i18n :refer [t]]
+ [app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(def component-attrs [:component-id :component-file :shape-ref])
@@ -25,47 +25,59 @@
[{:keys [ids values] :as props}]
(let [current-file-id (mf/use-ctx ctx/current-file-id)
- id (first ids)
- locale (mf/deref i18n/locale)
- local (mf/use-state {:menu-open false})
+ id (first ids)
+ local (mf/use-state {:menu-open false})
- show? (some? (:component-id values))
- local-library (mf/deref refs/workspace-local-library)
- libraries (mf/deref refs/workspace-libraries)
- {:keys [component-id component-file]} values
+ component-id (:component-id values)
+ library-id (:component-file values)
- component (cp/get-component component-id component-file local-library libraries)
+ local-file (deref refs/workspace-local-library)
+ libraries (deref refs/workspace-libraries)
- on-menu-click (mf/use-callback
- (fn [event]
- (dom/prevent-default event)
- (dom/stop-propagation event)
- (swap! local assoc :menu-open true)))
+ ;; 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) local-file)
- on-menu-close (mf/use-callback
- #(swap! local assoc :menu-open false))
+ component (cph/get-component libraries library-id component-id)
+ show? (some? component-id)
- do-detach-component (st/emitf (dwl/detach-component id))
- do-reset-component (st/emitf (dwl/reset-component id))
- do-update-component (st/emitf (dwl/update-component-sync id component-file))
+ on-menu-click
+ (mf/use-callback
+ (fn [event]
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (swap! local assoc :menu-open true)))
+
+ on-menu-close
+ (mf/use-callback
+ #(swap! local assoc :menu-open false))
+
+ do-detach-component
+ (st/emitf (dwl/detach-component id))
+
+ do-reset-component
+ (st/emitf (dwl/reset-component id))
+
+ do-update-component
+ (st/emitf (dwl/update-component-sync id library-id))
do-update-remote-component
(st/emitf (modal/show
{:type :confirm
:message ""
- :title (t locale "modals.update-remote-component.message")
- :hint (t locale "modals.update-remote-component.hint")
- :cancel-label (t locale "modals.update-remote-component.cancel")
- :accept-label (t locale "modals.update-remote-component.accept")
+ :title (tr "modals.update-remote-component.message")
+ :hint (tr "modals.update-remote-component.hint")
+ :cancel-label (tr "modals.update-remote-component.cancel")
+ :accept-label (tr "modals.update-remote-component.accept")
:accept-style :primary
:on-accept do-update-component}))
do-show-component (st/emitf (dw/go-to-component component-id))
- do-navigate-component-file (st/emitf (dwl/nav-to-component-file component-file))]
+ do-navigate-component-file (st/emitf (dwl/nav-to-component-file library-id))]
(when show?
[:div.element-set
[:div.element-set-title
- [:span (t locale "workspace.options.component")]]
+ [:span (tr "workspace.options.component")]]
[:div.element-set-content
[:div.row-flex.component-row
i/component
@@ -78,14 +90,14 @@
;; app/main/ui/workspace/context_menu.cljs
[:& context-menu {:on-close on-menu-close
:show (:menu-open @local)
- :options (if (= (:component-file values) current-file-id)
- [[(t locale "workspace.shape.menu.detach-instance") do-detach-component]
- [(t locale "workspace.shape.menu.reset-overrides") do-reset-component]
- [(t locale "workspace.shape.menu.update-main") do-update-component]
- [(t locale "workspace.shape.menu.show-main") do-show-component]]
+ :options (if (= library-id current-file-id)
+ [[(tr "workspace.shape.menu.detach-instance") do-detach-component]
+ [(tr "workspace.shape.menu.reset-overrides") do-reset-component]
+ [(tr "workspace.shape.menu.update-main") do-update-component]
+ [(tr "workspace.shape.menu.show-main") do-show-component]]
- [[(t locale "workspace.shape.menu.detach-instance") do-detach-component]
- [(t locale "workspace.shape.menu.reset-overrides") do-reset-component]
- [(t locale "workspace.shape.menu.go-main") do-navigate-component-file]
- [(t locale "workspace.shape.menu.update-main") do-update-remote-component]])}]]]]])))
+ [[(tr "workspace.shape.menu.detach-instance") do-detach-component]
+ [(tr "workspace.shape.menu.reset-overrides") do-reset-component]
+ [(tr "workspace.shape.menu.go-main") do-navigate-component-file]
+ [(tr "workspace.shape.menu.update-main") do-update-remote-component]])}]]]]])))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs
index a0ec0508fc..6a83b194b1 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs
@@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
- [app.common.pages.spec :as spec]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.refs :as refs]
@@ -50,8 +49,8 @@
;; first-level? (and in-frame?
;; (= (:parent-id values) (:frame-id values)))
- constraints-h (get values :constraints-h (spec/default-constraints-h values))
- constraints-v (get values :constraints-v (spec/default-constraints-v values))
+ constraints-h (get values :constraints-h (gsh/default-constraints-h values))
+ constraints-v (get values :constraints-v (gsh/default-constraints-v values))
on-constraint-button-clicked
(mf/use-callback
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 f704ce7152..a37c968fbb 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,9 +7,9 @@
(ns app.main.ui.workspace.sidebar.options.menus.interactions
(:require
[app.common.data :as d]
- [app.common.pages :as cp]
- [app.common.types.interactions :as cti]
- [app.common.types.page-options :as cto]
+ [app.common.pages.helpers :as cph]
+ [app.common.spec.interactions :as csi]
+ [app.common.spec.page :as csp]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
@@ -79,7 +79,7 @@
{:dissolve (tr "workspace.options.interaction-animation-dissolve")
:slide (tr "workspace.options.interaction-animation-slide")}
- (cti/allow-push? (:action-type interaction))
+ (csi/allow-push? (:action-type interaction))
(assoc :push (tr "workspace.options.interaction-animation-push"))))
(defn- easing-names
@@ -165,7 +165,7 @@
(mf/defc shape-flows
[{:keys [flows shape]}]
(when (= (:type shape) :frame)
- (let [flow (cto/get-frame-flow flows (:id shape))]
+ (let [flow (csp/get-frame-flow flows (:id shape))]
[:div.element-set.interactions-options
[:div.element-set-title
[:span (tr "workspace.options.flows.flow-start")]]
@@ -178,10 +178,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/use-memo (mf/deps objects)
- #(cp/select-frames objects))
+ (let [objects (deref refs/workspace-page-objects)
+ destination (get objects (:destination interaction))
+ frames (mf/with-memo [objects]
+ (cph/get-frames objects))
overlay-pos-type (:overlay-pos-type interaction)
close-click-outside? (:close-click-outside interaction false)
@@ -190,10 +190,10 @@
way (-> interaction :animation :way)
direction (-> interaction :animation :direction)
- extended-open? (mf/use-state false)
+ extended-open? (mf/use-state false)
- ext-delay-ref (mf/use-ref nil)
- ext-duration-ref (mf/use-ref nil)
+ ext-delay-ref (mf/use-ref nil)
+ ext-duration-ref (mf/use-ref nil)
select-text
(fn [ref] (fn [_] (dom/select-text! (mf/ref-val ref))))
@@ -201,27 +201,27 @@
change-event-type
(fn [event]
(let [value (-> event dom/get-target dom/get-value d/read-string)]
- (update-interaction index #(cti/set-event-type % value shape))))
+ (update-interaction index #(csi/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 #(cti/set-action-type % value))))
+ (update-interaction index #(csi/set-action-type % value))))
change-delay
(fn [value]
- (update-interaction index #(cti/set-delay % value)))
+ (update-interaction index #(csi/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 #(cti/set-destination % value))))
+ (update-interaction index #(csi/set-destination % value))))
change-preserve-scroll
(fn [event]
(let [value (-> event dom/get-target dom/checked?)]
- (update-interaction index #(cti/set-preserve-scroll % value))))
+ (update-interaction index #(csi/set-preserve-scroll % value))))
change-url
(fn [event]
@@ -237,55 +237,55 @@
(if (dom/valid? target)
(do
(dom/remove-class! target "error")
- (update-interaction index #(cti/set-url % value)))
+ (update-interaction index #(csi/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 #(cti/set-overlay-pos-type % value shape objects))))
+ (update-interaction index #(csi/set-overlay-pos-type % value shape objects))))
toggle-overlay-pos-type
(fn [pos-type]
- (update-interaction index #(cti/toggle-overlay-pos-type % pos-type shape objects)))
+ (update-interaction index #(csi/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 #(cti/set-close-click-outside % value))))
+ (update-interaction index #(csi/set-close-click-outside % value))))
change-background-overlay
(fn [event]
(let [value (-> event dom/get-target dom/checked?)]
- (update-interaction index #(cti/set-background-overlay % value))))
+ (update-interaction index #(csi/set-background-overlay % value))))
change-animation-type
(fn [event]
(let [value (-> event dom/get-target dom/get-value d/read-string)]
- (update-interaction index #(cti/set-animation-type % value))))
+ (update-interaction index #(csi/set-animation-type % value))))
change-duration
(fn [value]
- (update-interaction index #(cti/set-duration % value)))
+ (update-interaction index #(csi/set-duration % value)))
change-easing
(fn [event]
(let [value (-> event dom/get-target dom/get-value d/read-string)]
- (update-interaction index #(cti/set-easing % value))))
+ (update-interaction index #(csi/set-easing % value))))
change-way
(fn [event]
(let [value (-> event dom/get-target dom/get-value d/read-string)]
- (update-interaction index #(cti/set-way % value))))
+ (update-interaction index #(csi/set-way % value))))
change-direction
(fn [value]
- (update-interaction index #(cti/set-direction % value)))
+ (update-interaction index #(csi/set-direction % value)))
change-offset-effect
(fn [event]
(let [value (-> event dom/get-target dom/checked?)]
- (update-interaction index #(cti/set-offset-effect % value))))
+ (update-interaction index #(csi/set-offset-effect % value))))
]
[:*
@@ -316,7 +316,7 @@
[:option {:value (str value)} name]))]]
; Delay
- (when (cti/has-delay interaction)
+ (when (csi/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")}
@@ -337,7 +337,7 @@
[:option {:value (str value)} name])]]
; Destination
- (when (cti/has-destination interaction)
+ (when (csi/has-destination interaction)
[:div.interactions-element
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")]
[:select.input-select
@@ -352,7 +352,7 @@
[:option {:value (str (:id frame))} (:name frame)]))]])
; Preserve scroll
- (when (cti/has-preserve-scroll interaction)
+ (when (csi/has-preserve-scroll interaction)
[:div.interactions-element
[:div.input-checkbox
[:input {:type "checkbox"
@@ -363,7 +363,7 @@
(tr "workspace.options.interaction-preserve-scroll")]]])
; URL
- (when (cti/has-url interaction)
+ (when (csi/has-url interaction)
[:div.interactions-element
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-url")]
[:input.input-text {:type "url"
@@ -371,7 +371,7 @@
:default-value (str (:url interaction))
:on-blur change-url}]])
- (when (cti/has-overlay-opts interaction)
+ (when (csi/has-overlay-opts interaction)
[:*
; Overlay position (select)
[:div.interactions-element
@@ -433,7 +433,7 @@
[:label {:for (str "background-" index)}
(tr "workspace.options.interaction-background")]]]])
- (when (cti/has-animation? interaction)
+ (when (csi/has-animation? interaction)
[:*
; Animation select
[:div.interactions-element.separator
@@ -446,7 +446,7 @@
[:option {:value (str value)} name])]]
; Direction
- (when (cti/has-way? interaction)
+ (when (csi/has-way? interaction)
[:div.interactions-element.interactions-way-buttons
[:div.input-radio
[:input {:type "radio"
@@ -466,7 +466,7 @@
[:label {:for "way-out"} (tr "workspace.options.interaction-out")]]])
; Direction
- (when (cti/has-direction? interaction)
+ (when (csi/has-direction? interaction)
[:div.interactions-element.interactions-direction-buttons
[:div.element-set-actions-button
{:class (dom/classnames :active (= direction :right))
@@ -486,7 +486,7 @@
i/animate-up]])
; Duration
- (when (cti/has-duration? interaction)
+ (when (csi/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")}
@@ -498,7 +498,7 @@
[:span.after (tr "workspace.options.interaction-ms")]]])
; Easing
- (when (cti/has-easing? interaction)
+ (when (csi/has-easing? interaction)
[:div.interactions-element
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")]
[:select.input-select
@@ -515,7 +515,7 @@
:ease-in-out i/easing-ease-in-out)]])
; Offset effect
- (when (cti/has-offset-effect? interaction)
+ (when (csi/has-offset-effect? interaction)
[:div.interactions-element
[:div.input-checkbox
[:input {:type "checkbox"
@@ -550,7 +550,7 @@
[:& page-flows {:flows flows}])
[:div.element-set.interactions-options
- (when (and shape (not (cp/unframed-shape? shape)))
+ (when (and shape (not (cph/unframed-shape? shape)))
[:div.element-set-title
[:span (tr "workspace.options.interactions")]
[:div.add-page {:on-click add-interaction}
@@ -558,7 +558,7 @@
[:div.element-set-content
(when (= (count interactions) 0)
[:*
- (when (and shape (not (cp/unframed-shape? shape)))
+ (when (and shape (not (cph/unframed-shape? shape)))
[:*
[:div.interactions-help-icon i/plus]
[:div.interactions-help.separator (tr "workspace.options.add-interaction")]])
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 19978d1649..7a6493b2da 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
@@ -9,7 +9,7 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as math]
- [app.common.types.radius :as ctr]
+ [app.common.spec.radius :as ctr]
[app.main.data.workspace :as udw]
[app.main.data.workspace.changes :as dch]
[app.main.refs :as refs]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
index f7e126c705..6ad85d7fe0 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
@@ -77,13 +77,11 @@
update-color
(fn [index]
- (fn [color opacity]
+ (fn [color]
(let [color (dissoc color :id :file-id :gradient)]
(st/emit! (dch/update-shapes
ids
- #(-> %
- (assoc-in [:shadow index :color] color)
- (assoc-in [:shadow index :opacity] opacity)))))))
+ #(assoc-in % [:shadow index :color] color))))))
detach-color
(fn [index]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
index bfd6948091..4dd8c5c154 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
@@ -8,7 +8,6 @@
(:require
[app.common.colors :as clr]
[app.common.data :as d]
- [app.common.pages.spec :as spec]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
@@ -127,7 +126,7 @@
update-cap-attr
(fn [& kvs]
- #(if (spec/has-caps? %)
+ #(if (= :path (:type %))
(apply (partial assoc %) kvs)
%))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
index 24b9718c15..24c98f9718 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
@@ -301,6 +301,7 @@
opts #js {:ids ids
:values values
:on-change on-change
+ :show-recent true
:on-blur
(fn []
(tm/schedule
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
index c39f715994..78f57f88ef 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
@@ -9,10 +9,12 @@
["react-virtualized" :as rvt]
[app.common.data :as d]
[app.common.exceptions :as ex]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.text :as txt]
+ [app.main.data.fonts :as fts]
[app.main.data.shortcuts :as dsc]
[app.main.fonts :as fonts]
+ [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.editable-select :refer [editable-select]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
@@ -92,14 +94,15 @@
;; (conj backends id)))
(mf/defc font-selector
- [{:keys [on-select on-close current-font] :as props}]
- (let [selected (mf/use-state current-font)
- state (mf/use-state {:term "" :backends #{}})
+ [{:keys [on-select on-close current-font show-recent] :as props}]
+ (let [selected (mf/use-state current-font)
+ state (mf/use-state {:term "" :backends #{}})
- flist (mf/use-ref)
- input (mf/use-ref)
+ flist (mf/use-ref)
+ input (mf/use-ref)
- fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts))
+ fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts))
+ recent-fonts (mf/deref refs/workspace-recent-fonts)
select-next
(mf/use-callback
@@ -140,8 +143,11 @@
(mf/deps on-select on-close)
(fn [font]
(on-select font)
- (on-close)))
- ]
+ (on-close)))]
+
+ (mf/use-effect
+ (fn []
+ (st/emit! (fts/load-recent-fonts))))
(mf/use-effect
(mf/deps fonts)
@@ -183,6 +189,16 @@
:ref input
:spell-check false
:on-change on-filter-change}]
+ (when (and recent-fonts show-recent)
+ [:hr]
+ [*
+ [:p.title (tr "workspace.options.recent-fonts")]
+ (for [font recent-fonts]
+ [:& font-item {:key (:id font)
+ :font font
+ :style {}
+ :on-click on-select-and-close
+ :current? (= (:id font) (:id @selected))}])])
#_[:div.options
{:on-click #(swap! state assoc :show-options true)
@@ -233,7 +249,7 @@
:current? (= (:id font) (:id selected))}])))
(mf/defc font-options
- [{:keys [values on-change on-blur] :as props}]
+ [{:keys [values on-change on-blur show-recent] :as props}]
(let [{:keys [font-id font-size font-variant-id]} values
font-id (or font-id (:font-id txt/default-text-attrs))
@@ -242,12 +258,14 @@
fonts (mf/deref fonts/fontsdb)
font (get fonts font-id)
+ recent-fonts (mf/deref refs/workspace-recent-fonts)
+ last-font (mf/use-ref nil)
open-selector? (mf/use-state false)
change-font
(mf/use-callback
- (mf/deps on-change fonts)
+ (mf/deps on-change fonts recent-fonts)
(fn [new-font-id]
(let [{:keys [family] :as font} (get fonts new-font-id)
{:keys [id name weight style]} (fonts/get-default-variant font)]
@@ -255,7 +273,8 @@
:font-family family
:font-variant-id (or id name)
:font-weight weight
- :font-style style}))))
+ :font-style style})
+ (mf/set-ref-val! last-font font))))
on-font-size-change
(mf/use-callback
@@ -293,6 +312,8 @@
(reset! open-selector? false)
(when (some? on-blur)
(on-blur))
+ (when (mf/ref-val last-font)
+ (st/emit! (fts/add-recent-font (mf/ref-val last-font))))
))]
[:*
@@ -300,7 +321,8 @@
[:& font-selector
{:current-font font
:on-close on-font-selector-close
- :on-select on-font-select}])
+ :on-select on-font-select
+ :show-recent show-recent}])
[:div.row-flex
[:div.input-select.font-option
@@ -415,12 +437,13 @@
i/titlecase]]))
(mf/defc typography-options
- [{:keys [ids editor values on-change on-blur]}]
+ [{:keys [ids editor values on-change on-blur show-recent]}]
(let [opts #js {:editor editor
:ids ids
:values values
:on-change on-change
- :on-blur on-blur}]
+ :on-blur on-blur
+ :show-recent show-recent}]
[:div.element-set-content
[:> font-options opts]
[:div.row-flex
@@ -438,14 +461,18 @@
(let [open? (mf/use-state editing?)
hover-detach (mf/use-state false)
name-input-ref (mf/use-ref)
+ on-change-ref (mf/use-ref nil)
name-ref (mf/use-ref (:name typography))
on-name-blur
- (fn [event]
- (let [content (dom/get-target-val event)]
- (when-not (str/blank? content)
- (on-change {:name content}))))
+ (mf/use-callback
+ (mf/deps on-change)
+ (fn [event]
+ (let [content (dom/get-target-val event)]
+ (when-not (str/blank? content)
+ (let [[path name] (cph/parse-path-name content)]
+ (on-change {:name name :path path}))))))
handle-go-to-edit
(fn []
@@ -473,13 +500,20 @@
(dom/focus! node)
(dom/select-text! node))))))
+ (mf/use-effect
+ (mf/deps on-change)
+ (fn []
+ (mf/set-ref-val! on-change-ref {:on-change on-change})))
+
(mf/use-effect
(fn []
(fn []
(let [content (mf/ref-val name-ref)]
;; On destroy we check if it changed
(when (and (some? content) (not= content (:name typography)))
- (on-change {:name content}))))))
+ (let [{:keys [on-change]} (mf/ref-val on-change-ref)
+ [path name] (cph/parse-path-name content)]
+ (on-change {:name name :path path})))))))
[:*
[:div.element-set-options-group.typography-entry
@@ -552,7 +586,7 @@
[:input.element-name.adv-typography-name
{:type "text"
:ref name-input-ref
- :default-value (cp/merge-path-item (:path typography) (:name typography))
+ :default-value (cph/merge-path-item (:path typography) (:name typography))
:on-blur on-name-blur
:on-change on-name-change}]
@@ -561,4 +595,5 @@
i/actions]]]
[:& typography-options {:values typography
- :on-change on-change}]])]]))
+ :on-change on-change
+ :show-recent false}]])]]))
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 b71574c29c..eb91a0fa6a 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
@@ -8,9 +8,11 @@
(:require
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]]
[app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]]
+ [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs fill-menu]]
[app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]]
[app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]]
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
+ [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
[rumext.alpha :as mf]))
(mf/defc options
@@ -19,7 +21,9 @@
type (:type shape)
measure-values (select-keys shape measure-attrs)
layer-values (select-keys shape layer-attrs)
- constraint-values (select-keys shape constraint-attrs)]
+ constraint-values (select-keys shape constraint-attrs)
+ fill-values (select-keys shape fill-attrs)
+ stroke-values (select-keys shape stroke-attrs)]
[:*
[:& measures-menu {:ids ids
:type type
@@ -32,6 +36,14 @@
:type type
:values layer-values}]
+ [:& fill-menu {:ids ids
+ :type type
+ :values fill-values}]
+
+ [:& stroke-menu {:ids ids
+ :type type
+ :values stroke-values}]
+
[:& shadow-menu {:ids ids
:values (select-keys shape [:shadow])}]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
index d2a65a52b5..942c75ea51 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
@@ -14,6 +14,7 @@
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.context :as ctx]
[app.main.ui.hooks :as hooks]
+ [app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -106,7 +107,7 @@
(fn []
(when selected?
(let [node (mf/ref-val dref)]
- (.scrollIntoViewIfNeeded ^js node)))))
+ (dom/scroll-into-view-if-needed! node)))))
(mf/use-layout-effect
(mf/deps (:edition @local))
@@ -205,15 +206,25 @@
:project-id (:project-id file)}))))
show-pages? (mf/use-state true)
+ {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]}
+ (use-resize-hook :sitemap 200 38 400 :y false nil)
+
+ size (if @show-pages? size 38)
toggle-pages
(mf/use-callback #(reset! show-pages? not))]
- [:div.sitemap.tool-window
+ [:div#sitemap.tool-window {:ref parent-ref
+ :style #js {"--height" (str size "px")}}
[:div.tool-window-bar
[:span (tr "workspace.sidebar.sitemap")]
[:div.add-page {:on-click create} i/close]
- [:div.collapse-pages {:on-click toggle-pages} i/arrow-slide]]
+ [:div.collapse-pages {:on-click toggle-pages
+ :style {:transform (when (not @show-pages?) "rotate(-90deg)")}} i/arrow-slide]]
+
+ [:div.tool-window-content
+ [:& pages-list {:file file :key (:id file)}]]
(when @show-pages?
- [:div.tool-window-content
- [:& pages-list {:file file :key (:id file)}]])]))
+ [:div.resize-area {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move}])]))
diff --git a/frontend/src/app/main/ui/workspace/textpalette.cljs b/frontend/src/app/main/ui/workspace/textpalette.cljs
new file mode 100644
index 0000000000..4d9ca61586
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/textpalette.cljs
@@ -0,0 +1,152 @@
+;; 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.textpalette
+ (:require
+ [app.common.data :as d]
+ [app.main.data.workspace.texts :as dwt]
+ [app.main.fonts :as f]
+ [app.main.refs :as refs]
+ [app.main.store :as st]
+ [app.main.ui.components.dropdown :refer [dropdown]]
+ [app.main.ui.context :as ctx]
+ [app.main.ui.hooks.resize :refer [use-resize-hook]]
+ [app.main.ui.icons :as i]
+ [app.util.dom :as dom]
+ [app.util.i18n :refer [tr]]
+ [cuerdas.core :as str]
+ [rumext.alpha :as mf]))
+
+(mf/defc typography-item
+ [{:keys [file-id selected-ids typography name-only?]}]
+ (let [font-data (f/get-font-data (:font-id typography))
+ font-variant-id (:font-variant-id typography)
+ variant-data (->> font-data :variants (d/seek #(= (:id %) font-variant-id)))
+
+ handle-click
+ (mf/use-callback
+ (mf/deps typography selected-ids)
+ (fn []
+ (let [attrs (merge
+ {:typography-ref-file file-id
+ :typography-ref-id (:id typography)}
+ (dissoc typography :id :name))]
+
+ (run! #(st/emit!
+ (dwt/update-text-attrs
+ {:id %
+ :editor (get @refs/workspace-editor-state %)
+ :attrs attrs}))
+ selected-ids))))]
+
+ [:div.typography-item {:on-click handle-click}
+ [:div.typography-name
+ {:style {:font-family (:font-family typography)
+ :font-weight (:font-weight typography)
+ :font-style (:font-style typography)}}
+ (:name typography)]
+ (when-not name-only?
+ [:*
+ [:div.typography-font (:name font-data)]
+ [:div.typography-data (str (:font-size typography) "pt | " (:name variant-data))]])]))
+
+(mf/defc palette
+ [{:keys [selected-ids current-file-id file-typographies shared-libs]}]
+
+ (let [state (mf/use-state {:show-menu false})
+ selected (mf/use-state :file)
+
+ file-id
+ (case @selected
+ :recent nil
+ :file current-file-id
+ @selected)
+
+ current-typographies
+ (case @selected
+ :recent []
+ :file (vals file-typographies)
+ (vals (get-in shared-libs [@selected :data :typographies])))
+
+ container (mf/use-ref nil)
+
+ on-left-arrow-click
+ (mf/use-callback
+ (fn []
+ (when-let [node (mf/ref-val container)]
+ (.scrollBy node #js {:left -200 :behavior "smooth"}))))
+
+ on-right-arrow-click
+ (mf/use-callback
+ (fn []
+ (when-let [node (mf/ref-val container)]
+ (.scrollBy node #js {:left 200 :behavior "smooth"}))))
+
+ on-wheel
+ (mf/use-callback
+ (fn [event]
+ (let [delta (+ (.. event -nativeEvent -deltaY) (.. event -nativeEvent -deltaX))]
+ (if (pos? delta)
+ (on-right-arrow-click)
+ (on-left-arrow-click)))))
+
+ {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]}
+ (use-resize-hook :palette 72 54 80 :y true :bottom)]
+
+ [:div.color-palette {:ref parent-ref
+ :class (dom/classnames :no-text (< size 72))
+ :style #js {"--height" (str size "px")}}
+ [:div.resize-area {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move}]
+ [:& dropdown {:show (:show-menu @state)
+ :on-close #(swap! state assoc :show-menu false)}
+
+ [:ul.workspace-context-menu.palette-menu
+ (for [[idx cur-library] (map-indexed vector (vals shared-libs))]
+ (let [typographies (-> cur-library (get-in [:data :typographies]) vals)]
+ [:li.palette-library
+ {:key (str "library-" idx)
+ :on-click #(reset! selected (:id cur-library))}
+
+ (when (= @selected (:id cur-library)) i/tick)
+
+ [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count typographies)))]]))
+
+ [:li.palette-library
+ {:on-click #(reset! selected :file)}
+ (when (= selected :file) i/tick)
+ [:div.library-name (str (tr "workspace.libraries.colors.file-library")
+ (str/format " (%s)" (count file-typographies)))]]]]
+
+ [:div.color-palette-actions
+ {:on-click #(swap! state assoc :show-menu true)}
+ [:div.color-palette-actions-button i/actions]]
+
+ [:span.left-arrow {:on-click on-left-arrow-click} i/arrow-slide]
+
+ [:div.color-palette-content {:ref container :on-wheel on-wheel}
+ [:div.color-palette-inside
+ (for [[idx item] (map-indexed vector current-typographies)]
+ [:& typography-item
+ {:key idx
+ :file-id file-id
+ :selected-ids selected-ids
+ :typography item}])]]
+
+ [:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]]))
+
+(mf/defc textpalette
+ {::mf/wrap [mf/memo]}
+ []
+ (let [selected-ids (mf/deref refs/selected-shapes)
+ file-typographies (mf/deref refs/workspace-file-typography)
+ shared-libs (mf/deref refs/workspace-libraries)
+ current-file-id (mf/use-ctx ctx/current-file-id)]
+ [:& palette {:current-file-id current-file-id
+ :selected-ids selected-ids
+ :file-typographies file-typographies
+ :shared-libs shared-libs}]))
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index bb1eb36056..ffbdc7d39d 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -21,11 +21,14 @@
[app.main.ui.workspace.viewport.drawarea :as drawarea]
[app.main.ui.workspace.viewport.frame-grid :as frame-grid]
[app.main.ui.workspace.viewport.gradients :as gradients]
+ [app.main.ui.workspace.viewport.guides :as guides]
[app.main.ui.workspace.viewport.hooks :as hooks]
[app.main.ui.workspace.viewport.interactions :as interactions]
[app.main.ui.workspace.viewport.outline :as outline]
[app.main.ui.workspace.viewport.pixel-overlay :as pixel-overlay]
[app.main.ui.workspace.viewport.presence :as presence]
+ [app.main.ui.workspace.viewport.rules :as rules]
+ [app.main.ui.workspace.viewport.scroll-bars :as scroll-bars]
[app.main.ui.workspace.viewport.selection :as selection]
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
[app.main.ui.workspace.viewport.snap-points :as snap-points]
@@ -81,6 +84,7 @@
;; REFS
viewport-ref (mf/use-ref nil)
+ raw-position-ref (mf/use-ref nil) ;; Stores the raw position of the cursor
;; VARS
disable-paste (mf/use-var false)
@@ -89,6 +93,13 @@
;; STREAMS
move-stream (mf/use-memo #(rx/subject))
+ frame-parent (mf/use-memo
+ (mf/deps @hover-ids base-objects)
+ (fn []
+ (let [parent (get base-objects (last @hover-ids))]
+ (when (= :frame (:type parent))
+ parent))))
+
zoom (d/check-num zoom 1)
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
@@ -105,21 +116,21 @@
node-editing? (and edition (not= :text (get-in base-objects [edition :type])))
text-editing? (and edition (= :text (get-in base-objects [edition :type])))
- on-click (actions/on-click hover selected edition drawing-path? drawing-tool)
- on-context-menu (actions/on-context-menu hover)
+ on-click (actions/on-click hover selected edition drawing-path? drawing-tool space?)
+ on-context-menu (actions/on-context-menu hover hover-ids)
on-double-click (actions/on-double-click hover hover-ids drawing-path? base-objects edition)
on-drag-enter (actions/on-drag-enter)
on-drag-over (actions/on-drag-over)
on-drop (actions/on-drop file viewport-ref zoom)
on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? node-editing?
- drawing-path? create-comment? space? viewport-ref zoom)
+ drawing-path? create-comment? space? viewport-ref zoom panning)
on-mouse-up (actions/on-mouse-up disable-paste)
on-pointer-down (actions/on-pointer-down)
on-pointer-enter (actions/on-pointer-enter in-viewport?)
on-pointer-leave (actions/on-pointer-leave in-viewport?)
- on-pointer-move (actions/on-pointer-move viewport-ref zoom move-stream)
+ on-pointer-move (actions/on-pointer-move viewport-ref raw-position-ref zoom move-stream)
on-pointer-up (actions/on-pointer-up)
- on-move-selected (actions/on-move-selected hover hover-ids selected)
+ on-move-selected (actions/on-move-selected hover hover-ids selected space?)
on-menu-selected (actions/on-menu-selected hover hover-ids selected)
on-frame-enter (actions/on-frame-enter frame-hover)
@@ -132,7 +143,7 @@
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} drawing-tool)))
+ show-outlines? (and (nil? transform) (not edition) (not drawing-obj) (not (#{:comments :path :curve} drawing-tool)))
show-pixel-grid? (>= zoom 8)
show-presence? page-id
show-prototypes? (= options-mode :prototype)
@@ -145,14 +156,16 @@
(or drawing-obj transform))
show-selrect? (and selrect (empty? drawing))
show-measures? (and (not transform) (not node-editing?) show-distances?)
- show-artboard-names? (contains? layout :display-artboard-names)]
+ show-artboard-names? (contains? layout :display-artboard-names)
+ show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
+
+ disabled-guides? (or drawing-tool transform)]
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
(hooks/setup-viewport-size viewport-ref)
- (hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? node-editing?)
- (hooks/setup-resize layout viewport-ref)
+ (hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?)
(hooks/setup-keyboard alt? ctrl? space?)
- (hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom)
+ (hooks/setup-hover-shapes page-id move-stream raw-position-ref base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom)
(hooks/setup-viewport-modifiers modifiers base-objects)
(hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames base-objects vbox hover active-frames)
@@ -207,8 +220,6 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:preserveAspectRatio "xMidYMid meet"
:key (str "viewport" page-id)
- :width (:width vport 0)
- :height (:height vport 0)
:view-box (utils/format-viewbox vbox)
:ref viewport-ref
:class (when drawing-tool "drawing")
@@ -233,7 +244,7 @@
[:& outline/shape-outlines
{:objects base-objects
:selected selected
- :hover (when (not= :frame (:type @hover))
+ :hover (when (or @ctrl? (not= :frame (:type @hover)))
#{(or @frame-hover (:id @hover))})
:edition edition
:zoom zoom}])
@@ -244,7 +255,7 @@
:shapes selected-shapes
:zoom zoom
:edition edition
- :disable-handlers (or drawing-tool edition)
+ :disable-handlers (or drawing-tool edition @space?)
:on-move-selected on-move-selected
:on-context-menu on-menu-selected}])
@@ -294,7 +305,9 @@
(when show-grids?
[:& frame-grid/frame-grid
- {:zoom zoom :selected selected :transform transform}])
+ {:zoom zoom
+ :selected selected
+ :transform transform}])
(when show-pixel-grid?
[:& widgets/pixel-grid
@@ -325,12 +338,6 @@
{:zoom zoom
:tooltip tooltip}])
- (when show-presence?
- [:& presence/active-cursors
- {:page-id page-id}])
-
- [:& widgets/viewport-actions]
-
(when show-prototypes?
[:& interactions/interactions
{:selected selected
@@ -341,5 +348,31 @@
(when show-selrect?
[:& widgets/selection-rect {:data selrect
- :zoom zoom}])]]]))
+ :zoom zoom}])
+
+ (when show-presence?
+ [:& presence/active-cursors
+ {:page-id page-id}])
+
+ [:& widgets/viewport-actions]
+
+ [:& scroll-bars/viewport-scrollbars
+ {:objects base-objects
+ :zoom zoom
+ :vbox vbox
+ :viewport-ref viewport-ref}]
+
+ (when show-rules?
+ [:*
+ [:& rules/rules
+ {:zoom zoom
+ :vbox vbox
+ :selected-shapes selected-shapes}]
+
+ [:& guides/viewport-guides
+ {:zoom zoom
+ :vbox vbox
+ :hover-frame frame-parent
+ :modifiers modifiers
+ :disabled-guides? disabled-guides?}]])]]]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
index 9f1fc6b52f..0d6ca0ba8e 100644
--- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
@@ -28,10 +28,11 @@
(defn on-mouse-down
[{:keys [id blocked hidden type]} selected edition drawing-tool text-editing?
- node-editing? drawing-path? create-comment? space? viewport-ref zoom]
+ node-editing? drawing-path? create-comment? space? viewport-ref zoom panning]
(mf/use-callback
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
- node-editing? drawing-path? create-comment? space? viewport-ref zoom)
+ node-editing? drawing-path? create-comment? @space? viewport-ref zoom
+ panning)
(fn [bevent]
(when (or (dom/class? (dom/get-target bevent) "viewport-controls")
(dom/class? (dom/get-target bevent) "viewport-selrect"))
@@ -42,62 +43,75 @@
shift? (kbd/shift? event)
alt? (kbd/alt? event)
- left-click? (= 1 (.-which event))
- middle-click? (= 2 (.-which event))
+ left-click? (and (not panning) (= 1 (.-which event)))
+ middle-click? (and (not panning) (= 2 (.-which event)))
frame? (= :frame type)
selected? (contains? selected id)]
- (when middle-click?
- (dom/prevent-default bevent)
- (if ctrl?
- (let [raw-pt (dom/get-client-position event)
- viewport (mf/ref-val viewport-ref)
- pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
- (st/emit! (dw/start-zooming pt)))
- (st/emit! (dw/start-panning))))
+ (cond
+ middle-click?
+ (do
+ (dom/prevent-default bevent)
+ (if ctrl?
+ (let [raw-pt (dom/get-client-position event)
+ viewport (mf/ref-val viewport-ref)
+ pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
+ (st/emit! (dw/start-zooming pt)))
+ (st/emit! (dw/start-panning))))
- (when left-click?
- (st/emit! (ms/->MouseEvent :down ctrl? shift? alt?))
- (when (and (not= edition id) text-editing?)
- (st/emit! dw/clear-edition-mode))
+ left-click?
+ (do
+ (st/emit! (ms/->MouseEvent :down ctrl? shift? alt?))
- (when (and (not text-editing?)
- (not blocked)
- (not hidden)
- (not create-comment?)
- (not drawing-path?))
- (cond
- drawing-tool
- (st/emit! (dd/start-drawing drawing-tool))
+ (when (and (not= edition id) text-editing?)
+ (st/emit! dw/clear-edition-mode))
- node-editing?
- ;; Handle path node area selection
- (st/emit! (dwdp/handle-area-selection shift?))
+ (when (and (not text-editing?)
+ (not blocked)
+ (not hidden)
+ (not create-comment?)
+ (not drawing-path?))
+ (cond
+ node-editing?
+ ;; Handle path node area selection
+ (st/emit! (dwdp/handle-area-selection shift?))
- @space?
- (st/emit! (dw/start-panning))
+ (and @space? ctrl?)
+ (let [raw-pt (dom/get-client-position event)
+ viewport (mf/ref-val viewport-ref)
+ pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
+ (st/emit! (dw/start-zooming pt)))
- (or (not id) (and frame? (not selected?)))
- (st/emit! (dw/handle-area-selection shift?))
+ @space?
+ (st/emit! (dw/start-panning))
- (not drawing-tool)
- (st/emit! (when (or shift? (not selected?))
- (dw/select-shape id shift?))
- (dw/start-move-selected))))))))))
+ drawing-tool
+ (st/emit! (dd/start-drawing drawing-tool))
+
+ (or (not id) (and frame? (not selected?)) ctrl?)
+ (st/emit! (dw/handle-area-selection shift? ctrl?))
+
+ (not drawing-tool)
+ (st/emit! (when (or shift? (not selected?))
+ (dw/select-shape id shift?))
+ (dw/start-move-selected)))))))))))
(defn on-move-selected
- [hover hover-ids selected]
+ [hover hover-ids selected space?]
(mf/use-callback
- (mf/deps @hover @hover-ids selected)
+ (mf/deps @hover @hover-ids selected @space?)
(fn [bevent]
(let [event (.-nativeEvent bevent)
shift? (kbd/shift? event)
+ ctrl? (kbd/ctrl? event)
left-click? (= 1 (.-which event))]
(when (and left-click?
+ (not ctrl?)
(not shift?)
+ (not @space?)
(or (not @hover)
(= :frame (:type @hover))
(some #(contains? selected %) @hover-ids)))
@@ -130,9 +144,9 @@
(reset! frame-hover nil))))
(defn on-click
- [hover selected edition drawing-path? drawing-tool]
+ [hover selected edition drawing-path? drawing-tool space?]
(mf/use-callback
- (mf/deps @hover selected edition drawing-path? drawing-tool)
+ (mf/deps @hover selected edition drawing-path? drawing-tool @space?)
(fn [event]
(when (or (dom/class? (dom/get-target event) "viewport-controls")
(dom/class? (dom/get-target event) "viewport-selrect"))
@@ -147,7 +161,8 @@
(when (and hovering?
(not shift?)
- (not frame?)
+ (or ctrl? (not frame?))
+ (not @space?)
(not selected?)
(not edition)
(not drawing-path?)
@@ -171,24 +186,28 @@
(st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?))
- (when (and (not drawing-path?) shape)
- (cond frame?
- (st/emit! (dw/select-shape id shift?))
+ ;; 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?))
- (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))))
+ (and group? (> (count @hover-ids) 1))
+ (let [selected (get objects (second @hover-ids))]
+ (reset! hover selected)
+ (reset! hover-ids (into [] (rest @hover-ids)))
- (not= id edition)
- (st/emit! (dw/select-shape id)
- (dw/start-editing-selected))))))))
+ (st/emit! (dw/select-shape (:id selected))))
+
+ (not= id edition)
+ (st/emit! (dw/select-shape id)
+ (dw/start-editing-selected)))))))))
(defn on-context-menu
- [hover]
+ [hover hover-ids]
(mf/use-callback
- (mf/deps @hover)
+ (mf/deps @hover @hover-ids)
(fn [event]
(when (or (dom/class? (dom/get-target event) "viewport-controls")
(dom/class? (dom/get-target event) "viewport-selrect"))
@@ -200,18 +219,19 @@
#(st/emit!
(if (some? @hover)
(dw/show-shape-context-menu {:position position
- :shape @hover})
+ :shape @hover
+ :hover-ids @hover-ids})
(dw/show-context-menu {:position position})))))))))
(defn on-menu-selected
[hover hover-ids selected]
(mf/use-callback
- (mf/deps @hover hover-ids selected)
+ (mf/deps @hover @hover-ids selected)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [position (dom/get-client-position event)]
- (st/emit! (dw/show-shape-context-menu {:position position}))))))
+ (st/emit! (dw/show-shape-context-menu {:position position :hover-ids @hover-ids}))))))
(defn on-mouse-up
[disable-paste]
@@ -228,17 +248,17 @@
middle-click? (= 2 (.-which event))]
(when left-click?
- (st/emit! (dw/finish-panning)
- (ms/->MouseEvent :up ctrl? shift? alt?)))
+ (st/emit! (ms/->MouseEvent :up ctrl? shift? alt?)))
(when middle-click?
(dom/prevent-default event)
;; We store this so in Firefox the middle button won't do a paste of the content
(reset! disable-paste true)
- (timers/schedule #(reset! disable-paste false))
- (st/emit! (dw/finish-panning)
- (dw/finish-zooming)))))))
+ (timers/schedule #(reset! disable-paste false)))
+
+ (st/emit! (dw/finish-panning)
+ (dw/finish-zooming))))))
(defn on-pointer-enter [in-viewport?]
(mf/use-callback
@@ -251,58 +271,58 @@
(reset! in-viewport? false))))
(defn on-pointer-down []
- (mf/use-callback
- (fn [event]
+ (mf/use-callback
+ (fn [event]
;; We need to handle editor related stuff here because
;; handling on editor dom node does not works properly.
- (let [target (dom/get-target event)
- editor (.closest ^js target ".public-DraftEditor-content")]
+ (let [target (dom/get-target event)
+ editor (.closest ^js target ".public-DraftEditor-content")]
;; Capture mouse pointer to detect the movements even if cursor
;; leaves the viewport or the browser itself
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
- (if editor
- (.setPointerCapture editor (.-pointerId event))
- (.setPointerCapture target (.-pointerId event)))))))
+ (if editor
+ (.setPointerCapture editor (.-pointerId event))
+ (.setPointerCapture target (.-pointerId event)))))))
(defn on-pointer-up []
- (mf/use-callback
- (fn [event]
- (let [target (dom/get-target event)]
+ (mf/use-callback
+ (fn [event]
+ (let [target (dom/get-target event)]
; Release pointer on mouse up
- (.releasePointerCapture target (.-pointerId event))))))
+ (.releasePointerCapture target (.-pointerId event))))))
(defn on-key-down []
- (mf/use-callback
- (fn [event]
- (let [bevent (.getBrowserEvent ^js event)
- key (.-key ^js event)
- ctrl? (kbd/ctrl? event)
- shift? (kbd/shift? event)
- alt? (kbd/alt? event)
- meta? (kbd/meta? event)
- target (dom/get-target event)
- editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
- (= "rich-text" (obj/get target "className"))
- (= "INPUT" (obj/get target "tagName"))
- (= "TEXTAREA" (obj/get target "tagName")))]
+ (mf/use-callback
+ (fn [event]
+ (let [bevent (.getBrowserEvent ^js event)
+ key (.-key ^js event)
+ ctrl? (kbd/ctrl? event)
+ shift? (kbd/shift? event)
+ alt? (kbd/alt? event)
+ meta? (kbd/meta? event)
+ target (dom/get-target event)
+ editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
+ (= "rich-text" (obj/get target "className"))
+ (= "INPUT" (obj/get target "tagName"))
+ (= "TEXTAREA" (obj/get target "tagName")))]
- (when-not (.-repeat bevent)
- (st/emit! (ms/->KeyboardEvent :down key shift? ctrl? alt? meta? editing?)))))))
+ (when-not (.-repeat bevent)
+ (st/emit! (ms/->KeyboardEvent :down key shift? ctrl? alt? meta? editing?)))))))
(defn on-key-up []
- (mf/use-callback
- (fn [event]
- (let [key (.-key event)
- ctrl? (kbd/ctrl? event)
- shift? (kbd/shift? event)
- alt? (kbd/alt? event)
- meta? (kbd/meta? event)
- target (dom/get-target event)
- editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
- (= "rich-text" (obj/get target "className"))
- (= "INPUT" (obj/get target "tagName"))
- (= "TEXTAREA" (obj/get target "tagName")))]
- (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?))))))
+ (mf/use-callback
+ (fn [event]
+ (let [key (.-key event)
+ ctrl? (kbd/ctrl? event)
+ shift? (kbd/shift? event)
+ alt? (kbd/alt? event)
+ meta? (kbd/meta? event)
+ target (dom/get-target event)
+ editing? (or (some? (.closest ^js target ".public-DraftEditor-content"))
+ (= "rich-text" (obj/get target "className"))
+ (= "INPUT" (obj/get target "tagName"))
+ (= "TEXTAREA" (obj/get target "tagName")))]
+ (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?))))))
(defn on-mouse-move [viewport-ref zoom]
(let [last-position (mf/use-var nil)]
@@ -330,13 +350,14 @@
(kbd/shift? event)
(kbd/alt? event))))))))
-(defn on-pointer-move [viewport-ref zoom move-stream]
+(defn on-pointer-move [viewport-ref raw-position-ref zoom move-stream]
(mf/use-callback
(mf/deps zoom move-stream)
(fn [event]
(let [raw-pt (dom/get-client-position event)
viewport (mf/ref-val viewport-ref)
pt (utils/translate-point-to-viewport viewport zoom raw-pt)]
+ (mf/set-ref-val! raw-position-ref raw-pt)
(rx/push! move-stream pt)))))
(defn on-mouse-wheel [viewport-ref zoom]
@@ -386,29 +407,29 @@
:y #(+ % delta-y)})))))))))
(defn on-drag-enter []
- (mf/use-callback
- (fn [e]
- (when (or (dnd/has-type? e "penpot/shape")
- (dnd/has-type? e "penpot/component")
- (dnd/has-type? e "Files")
- (dnd/has-type? e "text/uri-list")
- (dnd/has-type? e "text/asset-id"))
- (dom/prevent-default e)))))
+ (mf/use-callback
+ (fn [e]
+ (when (or (dnd/has-type? e "penpot/shape")
+ (dnd/has-type? e "penpot/component")
+ (dnd/has-type? e "Files")
+ (dnd/has-type? e "text/uri-list")
+ (dnd/has-type? e "text/asset-id"))
+ (dom/prevent-default e)))))
(defn on-drag-over []
- (mf/use-callback
- (fn [e]
- (when (or (dnd/has-type? e "penpot/shape")
- (dnd/has-type? e "penpot/component")
- (dnd/has-type? e "Files")
- (dnd/has-type? e "text/uri-list")
- (dnd/has-type? e "text/asset-id"))
- (dom/prevent-default e)))))
+ (mf/use-callback
+ (fn [e]
+ (when (or (dnd/has-type? e "penpot/shape")
+ (dnd/has-type? e "penpot/component")
+ (dnd/has-type? e "Files")
+ (dnd/has-type? e "text/uri-list")
+ (dnd/has-type? e "text/asset-id"))
+ (dom/prevent-default e)))))
(defn on-image-uploaded []
- (mf/use-callback
- (fn [image position]
- (st/emit! (dw/image-uploaded image position)))))
+ (mf/use-callback
+ (fn [image position]
+ (st/emit! (dw/image-uploaded image position)))))
(defn on-drop [file viewport-ref zoom]
(let [on-image-uploaded (on-image-uploaded)]
@@ -483,19 +504,11 @@
(st/emit! (dw/upload-media-workspace params)))))))))
(defn on-paste [disable-paste in-viewport?]
- (mf/use-callback
- (fn [event]
+ (mf/use-callback
+ (fn [event]
;; We disable the paste just after mouse-up of a middle button so when panning won't
;; paste the content into the workspace
- (let [tag-name (-> event dom/get-target dom/get-tag-name)]
- (when (and (not (#{"INPUT" "TEXTAREA"} tag-name)) (not @disable-paste))
- (st/emit! (dw/paste-from-event event @in-viewport?)))))))
+ (let [tag-name (-> event dom/get-target dom/get-tag-name)]
+ (when (and (not (#{"INPUT" "TEXTAREA"} tag-name)) (not @disable-paste))
+ (st/emit! (dw/paste-from-event event @in-viewport?)))))))
-(defn on-resize [viewport-ref]
- (mf/use-callback
- (fn [_]
- (let [node (mf/ref-val viewport-ref)
- prnt (dom/get-parent node)
- size (dom/get-client-size prnt)]
- ;; We schedule the event so it fires after `initialize-page` event
- (timers/schedule #(st/emit! (dw/update-viewport-size size)))))))
diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
index 0164211a41..d8fa9096c6 100644
--- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
@@ -35,7 +35,7 @@
(l/derived (l/in [:workspace-local :editing-stop]) st/state))
(def current-gradient-ref
- (l/derived (l/in [:workspace-local :current-gradient]) st/state))
+ (l/derived (l/in [:workspace-local :current-gradient]) st/state =))
(mf/defc shadow [{:keys [id x y width height offset]}]
[:filter {:id id
@@ -158,17 +158,19 @@
(rx/filter ms/pointer-event?)
(rx/filter #(= :viewport (:source %)))
(rx/map :pt)
- (rx/subs #(case @moving-point
- :from-p (when on-change-start (on-change-start %))
- :to-p (when on-change-finish (on-change-finish %))
- :width-p (when on-change-width
- (let [width-v (gpt/unit (gpt/to-vec from-p width-p))
- distance (gpt/point-line-distance % from-p to-p)
- new-width-p (gpt/add
- from-p
- (gpt/multiply width-v (gpt/point distance)))]
- (on-change-width new-width-p)))
- nil)))]
+ (rx/subs
+ (fn [pt]
+ (case @moving-point
+ :from-p (when on-change-start (on-change-start pt))
+ :to-p (when on-change-finish (on-change-finish pt))
+ :width-p (when on-change-width
+ (let [width-v (gpt/unit (gpt/to-vec from-p width-p))
+ distance (gpt/point-line-distance pt from-p to-p)
+ new-width-p (gpt/add
+ from-p
+ (gpt/multiply width-v (gpt/point distance)))]
+ (on-change-width new-width-p)))
+ nil))))]
(fn [] (rx/dispose! subs)))))
[:g.gradient-handlers
[:defs
@@ -229,9 +231,15 @@
(mf/defc gradient-handlers
+ {::mf/wrap [mf/memo]}
[{:keys [id zoom]}]
- (let [shape (mf/deref (refs/object-by-id id))
+ (let [current-change (mf/use-state {})
+ shape-ref (mf/use-memo (mf/deps id) #(refs/object-by-id id))
+ shape (mf/deref shape-ref)
+
gradient (mf/deref current-gradient-ref)
+ gradient (merge gradient @current-change)
+
editing-spot (mf/deref editing-spot-ref)
transform (gsh/transform-matrix shape)
@@ -261,31 +269,43 @@
width-p (gpt/add from-p width-v)
- change! (fn [changes]
- (st/emit! (dc/update-gradient changes)))
+ change!
+ (mf/use-callback
+ (fn [changes]
+ (swap! current-change merge changes)
+ (st/emit! (dc/update-gradient changes))))
- on-change-start (fn [point]
- (let [point (gpt/transform point transform-inverse)
- start-x (/ (- (:x point) x) width)
- start-y (/ (- (:y point) y) height)
- start-x (mth/precision start-x 2)
- start-y (mth/precision start-y 2)]
- (change! {:start-x start-x :start-y start-y})))
+ on-change-start
+ (mf/use-callback
+ (mf/deps transform-inverse width height)
+ (fn [point]
+ (let [point (gpt/transform point transform-inverse)
+ start-x (/ (- (:x point) x) width)
+ start-y (/ (- (:y point) y) height)
+ start-x (mth/precision start-x 2)
+ start-y (mth/precision start-y 2)]
+ (change! {:start-x start-x :start-y start-y}))))
- on-change-finish (fn [point]
- (let [point (gpt/transform point transform-inverse)
- end-x (/ (- (:x point) x) width)
- end-y (/ (- (:y point) y) height)
- end-x (mth/precision end-x 2)
- end-y (mth/precision end-y 2)]
- (change! {:end-x end-x :end-y end-y})))
+ on-change-finish
+ (mf/use-callback
+ (mf/deps transform-inverse width height)
+ (fn [point]
+ (let [point (gpt/transform point transform-inverse)
+ end-x (/ (- (:x point) x) width)
+ end-y (/ (- (:y point) y) height)
+ end-x (mth/precision end-x 2)
+ end-y (mth/precision end-y 2)]
+ (change! {:end-x end-x :end-y end-y}))))
- on-change-width (fn [point]
- (let [scale-factor-y (/ gradient-length (/ height 2))
- norm-dist (/ (gpt/distance point from-p)
- (* (/ width 2) scale-factor-y))]
- (when (and norm-dist (mth/finite? norm-dist))
- (change! {:width norm-dist}))))]
+ on-change-width
+ (mf/use-callback
+ (mf/deps gradient-length width height)
+ (fn [point]
+ (let [scale-factor-y (/ gradient-length (/ height 2))
+ norm-dist (/ (gpt/distance point from-p)
+ (* (/ width 2) scale-factor-y))]
+ (when (and norm-dist (mth/finite? norm-dist))
+ (change! {:width norm-dist})))))]
(when (and gradient
(= id (:shape-id gradient))
diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs
new file mode 100644
index 0000000000..7bfde052ab
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs
@@ -0,0 +1,475 @@
+;; 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.viewport.guides
+ (:require
+ [app.common.colors :as colors]
+ [app.common.geom.point :as gpt]
+ [app.common.geom.shapes :as gsh]
+ [app.common.math :as mth]
+ [app.common.uuid :as uuid]
+ [app.main.data.workspace :as dw]
+ [app.main.refs :as refs]
+ [app.main.store :as st]
+ [app.main.streams :as ms]
+ [app.main.ui.cursors :as cur]
+ [app.main.ui.workspace.viewport.rules :as rules]
+ [app.util.dom :as dom]
+ [rumext.alpha :as mf]))
+
+(def guide-width 1)
+(def guide-opacity 0.7)
+(def guide-opacity-hover 1)
+(def guide-color colors/primary)
+(def guide-pill-width 34)
+(def guide-pill-height 20)
+(def guide-pill-corner-radius 4)
+(def guide-active-area 16)
+
+(def guide-creation-margin-left 8)
+(def guide-creation-margin-top 28)
+(def guide-creation-width 16)
+(def guide-creation-height 24)
+
+(defn use-guide
+ "Hooks to support drag/drop for existing guides and new guides"
+ [on-guide-change get-hover-frame zoom {:keys [id position axis frame-id]}]
+ (let [dragging-ref (mf/use-ref false)
+ start-ref (mf/use-ref nil)
+ start-pos-ref (mf/use-ref nil)
+ state (mf/use-state {:hover false
+ :new-position nil
+ :new-frame-id frame-id})
+
+ frame-id (:new-frame-id @state)
+
+ frame-ref (mf/use-memo (mf/deps frame-id) #(refs/object-by-id frame-id))
+ frame (mf/deref frame-ref)
+
+ on-pointer-enter
+ (mf/use-callback
+ (fn []
+ (st/emit! (dw/set-hover-guide id true))
+ (swap! state assoc :hover true)))
+
+ on-pointer-leave
+ (mf/use-callback
+ (fn []
+ (st/emit! (dw/set-hover-guide id false))
+ (swap! state assoc :hover false)))
+
+ on-pointer-down
+ (mf/use-callback
+ (fn [event]
+ (dom/capture-pointer event)
+ (mf/set-ref-val! dragging-ref true)
+ (mf/set-ref-val! start-ref (dom/get-client-position event))
+ (mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis))))
+
+ on-pointer-up
+ (mf/use-callback
+ (mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change)
+ (fn []
+ (when (some? on-guide-change)
+ (when (some? (:new-position @state))
+ (on-guide-change {:position (:new-position @state)
+ :frame-id (:new-frame-id @state)})))))
+
+ on-lost-pointer-capture
+ (mf/use-callback
+ (fn [event]
+ (dom/release-pointer event)
+ (mf/set-ref-val! dragging-ref false)
+ (mf/set-ref-val! start-ref nil)
+ (mf/set-ref-val! start-pos-ref nil)
+ (swap! state assoc :new-position nil)))
+
+ on-mouse-move
+ (mf/use-callback
+ (mf/deps position zoom)
+ (fn [event]
+
+ (when-let [_ (mf/ref-val dragging-ref)]
+ (let [start-pt (mf/ref-val start-ref)
+ start-pos (mf/ref-val start-pos-ref)
+ current-pt (dom/get-client-position event)
+ delta (/ (- (get current-pt axis) (get start-pt axis)) zoom)
+ new-position (if (some? position)
+ (+ position delta)
+ (+ start-pos delta))
+
+ ;; TODO: Change when pixel-grid flag exists
+ new-position (mth/round new-position)
+ new-frame-id (:id (get-hover-frame))]
+ (swap! state assoc
+ :new-position new-position
+ :new-frame-id new-frame-id)))))]
+ {:on-pointer-enter on-pointer-enter
+ :on-pointer-leave on-pointer-leave
+ :on-pointer-down on-pointer-down
+ :on-pointer-up on-pointer-up
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move
+ :state state
+ :frame frame}))
+
+;; This functions are auxiliary to get the coords of components depending on the axis
+;; we're handling
+
+(defn guide-area-axis
+ [pos vbox zoom frame axis]
+ (let [rules-pos (/ rules/rules-pos zoom)
+ guide-active-area (/ guide-active-area zoom)]
+ (cond
+ (and (some? frame) (= axis :x))
+ {:x (- pos (/ guide-active-area 2))
+ :y (:y frame)
+ :width guide-active-area
+ :height (:height frame)}
+
+ (some? frame)
+ {:x (:x frame)
+ :y (- pos (/ guide-active-area 2))
+ :width (:width frame)
+ :height guide-active-area}
+
+ (= axis :x)
+ {:x (- pos (/ guide-active-area 2))
+ :y (+ (:y vbox) rules-pos)
+ :width guide-active-area
+ :height (:height vbox)}
+
+ :else
+ {:x (+ (:x vbox) rules-pos)
+ :y (- pos (/ guide-active-area 2))
+ :width (:width vbox)
+ :height guide-active-area})))
+
+(defn guide-line-axis
+ ([pos vbox axis]
+ (if (= axis :x)
+ {:x1 pos
+ :y1 (:y vbox)
+ :x2 pos
+ :y2 (+ (:y vbox) (:height vbox))}
+
+ {:x1 (:x vbox)
+ :y1 pos
+ :x2 (+ (:x vbox) (:width vbox))
+ :y2 pos}))
+
+ ([pos vbox frame axis]
+ (if (= axis :x)
+ {:l1-x1 pos
+ :l1-y1 (:y vbox)
+ :l1-x2 pos
+ :l1-y2 (:y frame)
+ :l2-x1 pos
+ :l2-y1 (:y frame)
+ :l2-x2 pos
+ :l2-y2 (+ (:y frame) (:height frame))
+ :l3-x1 pos
+ :l3-y1 (+ (:y frame) (:height frame))
+ :l3-x2 pos
+ :l3-y2 (+ (:y vbox) (:height vbox))}
+ {:l1-x1 (:x vbox)
+ :l1-y1 pos
+ :l1-x2 (:x frame)
+ :l1-y2 pos
+ :l2-x1 (:x frame)
+ :l2-y1 pos
+ :l2-x2 (+ (:x frame) (:width frame))
+ :l2-y2 pos
+ :l3-x1 (+ (:x frame) (:width frame))
+ :l3-y1 pos
+ :l3-x2 (+ (:x vbox) (:width vbox))
+ :l3-y2 pos})))
+
+(defn guide-pill-axis
+ [pos vbox zoom axis]
+ (let [rules-pos (/ rules/rules-pos zoom)
+ guide-pill-width (/ guide-pill-width zoom)
+ guide-pill-height (/ guide-pill-height zoom)]
+
+ (if (= axis :x)
+ {:rect-x (- pos (/ guide-pill-width 2))
+ :rect-y (+ (:y vbox) rules-pos (- (/ guide-pill-width 2)) (/ 3 zoom))
+ :rect-width guide-pill-width
+ :rect-height guide-pill-height
+ :text-x pos
+ :text-y (+ (:y vbox) rules-pos (- (/ 3 zoom)))}
+
+ {:rect-x (+ (:x vbox) rules-pos (- (/ guide-pill-height 2)) (- (/ 4 zoom)))
+ :rect-y (- pos (/ guide-pill-width 2))
+ :rect-width guide-pill-height
+ :rect-height guide-pill-width
+ :text-x (+ (:x vbox) rules-pos (- (/ 3 zoom)))
+ :text-y pos})))
+
+(defn guide-inside-vbox?
+ ([vbox]
+ (partial guide-inside-vbox? vbox))
+
+ ([{:keys [x y width height]} {:keys [axis position]}]
+ (let [x1 x
+ x2 (+ x width)
+ y1 y
+ y2 (+ y height)]
+ (if (= axis :x)
+ (and (>= position x1)
+ (<= position x2))
+ (and (>= position y1)
+ (<= position y2))))))
+
+(defn guide-creation-area
+ [vbox zoom axis]
+ (if (= axis :x)
+ {:x (+ (:x vbox) (/ guide-creation-margin-left zoom))
+ :y (:y vbox)
+ :width (/ guide-creation-width zoom)
+ :height (:height vbox)}
+
+ {:x (+ (:x vbox) (+ guide-creation-margin-top zoom))
+ :y (:y vbox)
+ :width (:width vbox)
+ :height (/ guide-creation-height zoom)}))
+
+(defn is-guide-inside-frame?
+ [guide frame]
+
+ (if (= :x (:axis guide))
+ (and (>= (:position guide) (:x frame) )
+ (<= (:position guide) (+ (:x frame) (:width frame)) ))
+
+ (and (>= (:position guide) (:y frame) )
+ (<= (:position guide) (+ (:y frame) (:height frame)) ))))
+
+(mf/defc guide
+ {::mf/wrap [mf/memo]}
+ [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame disabled-guides? frame-modifier]}]
+
+ (let [axis (:axis guide)
+
+ handle-change-position
+ (mf/use-callback
+ (mf/deps on-guide-change)
+ (fn [changes]
+ (when on-guide-change
+ (on-guide-change (merge guide changes)))))
+
+ {:keys [on-pointer-enter
+ on-pointer-leave
+ on-pointer-down
+ on-pointer-up
+ on-lost-pointer-capture
+ on-mouse-move
+ state
+ frame]} (use-guide handle-change-position get-hover-frame zoom guide)
+
+ base-frame (or frame hover-frame)
+ frame (gsh/transform-shape (merge base-frame frame-modifier))
+
+ move-vec (gpt/to-vec (gpt/point (:x base-frame) (:y base-frame))
+ (gpt/point (:x frame) (:y frame)))
+
+ pos (+ (or (:new-position @state) (:position guide)) (get move-vec axis))
+ guide-width (/ guide-width zoom)
+ 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))
+ [:g.guide-area
+ (when-not disabled-guides?
+ (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)]
+ [:rect {:x x
+ :y y
+ :width width
+ :height height
+ :style {:fill "none"
+ :pointer-events "fill"
+ :cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}
+ :on-pointer-enter on-pointer-enter
+ :on-pointer-leave on-pointer-leave
+ :on-pointer-down on-pointer-down
+ :on-pointer-up on-pointer-up
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move}]))
+
+ (if (some? frame)
+ (let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2
+ l2-x1 l2-y1 l2-x2 l2-y2
+ l3-x1 l3-y1 l3-x2 l3-y2]}
+ (guide-line-axis pos vbox frame axis)]
+ [:g
+ (when (or hover? (:hover @state))
+ [:line {:x1 l1-x1
+ :y1 l1-y1
+ :x2 l1-x2
+ :y2 l1-y2
+ :style {:stroke guide-color
+ :stroke-opacity guide-opacity-hover
+ :stroke-dasharray (str "0, " (/ 6 zoom))
+ :stroke-linecap "round"
+ :stroke-width guide-width}}])
+ [:line {:x1 l2-x1
+ :y1 l2-y1
+ :x2 l2-x2
+ :y2 l2-y2
+ :style {:stroke guide-color
+ :stroke-width guide-width
+ :stroke-opacity (if (or hover? (:hover @state))
+ guide-opacity-hover
+ guide-opacity)}}]
+ (when (or hover? (:hover @state))
+ [:line {:x1 l3-x1
+ :y1 l3-y1
+ :x2 l3-x2
+ :y2 l3-y2
+ :style {:stroke guide-color
+ :stroke-opacity guide-opacity-hover
+ :stroke-width guide-width
+ :stroke-dasharray (str "0, " (/ 6 zoom))
+ :stroke-linecap "round"}}])])
+
+ (let [{:keys [x1 y1 x2 y2]} (guide-line-axis pos vbox axis)]
+ [:line {:x1 x1
+ :y1 y1
+ :x2 x2
+ :y2 y2
+ :style {:stroke guide-color
+ :stroke-width guide-width
+ :stroke-opacity (if (or hover? (:hover @state))
+ guide-opacity-hover
+ guide-opacity)}}]))
+
+ (when (or hover? (:hover @state))
+ (let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]}
+ (guide-pill-axis pos vbox zoom axis)]
+ [:g.guide-pill
+ [:rect {:x rect-x
+ :y rect-y
+ :width rect-width
+ :height rect-height
+ :rx guide-pill-corner-radius
+ :ry guide-pill-corner-radius
+ :style {:fill guide-color}}]
+
+ [:text {:x text-x
+ :y text-y
+ :text-anchor "middle"
+ :dominant-baseline "middle"
+ :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
+ :style {:font-size (/ rules/font-size zoom)
+ :font-family rules/font-family
+ :fill colors/black}}
+ (str (mth/round pos))]]))])))
+
+(mf/defc new-guide-area
+ [{:keys [vbox zoom axis get-hover-frame disabled-guides?]}]
+
+ (let [on-guide-change
+ (mf/use-callback
+ (mf/deps vbox)
+ (fn [guide]
+ (let [guide (-> guide
+ (assoc :id (uuid/next)
+ :axis axis))]
+ (when (guide-inside-vbox? vbox guide)
+ (st/emit! (dw/update-guides guide))))))
+
+ {:keys [on-pointer-enter
+ on-pointer-leave
+ on-pointer-down
+ on-pointer-up
+ on-lost-pointer-capture
+ on-mouse-move
+ state
+ frame]} (use-guide on-guide-change get-hover-frame zoom {:axis axis})]
+
+ [:g.new-guides
+ (when-not disabled-guides?
+ (let [{:keys [x y width height]} (guide-creation-area vbox zoom axis)]
+ [:rect {:x x
+ :y y
+ :width width
+ :height height
+ :on-pointer-enter on-pointer-enter
+ :on-pointer-leave on-pointer-leave
+ :on-pointer-down on-pointer-down
+ :on-pointer-up on-pointer-up
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-mouse-move on-mouse-move
+ :style {:fill "none"
+ :pointer-events "fill"
+ :cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}}]))
+
+ (when (:new-position @state)
+ [:& guide {:guide {:axis axis
+ :position (:new-position @state)}
+ :get-hover-frame get-hover-frame
+ :vbox vbox
+ :zoom zoom
+ :hover? true
+ :hover-frame frame}])]))
+
+(mf/defc viewport-guides
+ {::mf/wrap [mf/memo]}
+ [{:keys [zoom vbox hover-frame disabled-guides? modifiers]}]
+
+ (let [page (mf/deref refs/workspace-page)
+
+ guides (mf/use-memo
+ (mf/deps page vbox)
+ #(->> (get-in page [:options :guides] {})
+ (vals)
+ (filter (guide-inside-vbox? vbox))))
+
+ hover-frame-ref (mf/use-ref nil)
+
+ ;; We use the ref to not redraw every guide everytime the hovering frame change
+ ;; we're only interested to get the frame in the guide we're moving
+ get-hover-frame
+ (mf/use-callback
+ (fn []
+ (mf/ref-val hover-frame-ref)))
+
+ on-guide-change
+ (mf/use-callback
+ (mf/deps vbox)
+ (fn [guide]
+ (if (guide-inside-vbox? vbox guide)
+ (st/emit! (dw/update-guides guide))
+ (st/emit! (dw/remove-guide guide)))))]
+
+ (mf/use-effect
+ (mf/deps hover-frame)
+ (fn []
+ (mf/set-ref-val! hover-frame-ref hover-frame)))
+
+ [:g.guides {:pointer-events "none"}
+ [:& new-guide-area {:vbox vbox
+ :zoom zoom
+ :axis :x
+ :get-hover-frame get-hover-frame
+ :disabled-guides? disabled-guides?}]
+
+ [:& new-guide-area {:vbox vbox
+ :zoom zoom
+ :axis :y
+ :get-hover-frame get-hover-frame
+ :disabled-guides? disabled-guides?}]
+
+ (for [current guides]
+ [:& guide {:key (str "guide-" (:id current))
+ :guide current
+ :vbox vbox
+ :zoom zoom
+ :frame-modifier (get modifiers (:frame-id current))
+ :get-hover-frame get-hover-frame
+ :on-guide-change on-guide-change
+ :disabled-guides? disabled-guides?}])]))
+
diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
index 2ff6e17445..81a9352ba9 100644
--- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
@@ -8,7 +8,8 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
+ [app.common.geom.shapes.rect :as gshr]
+ [app.common.pages.helpers :as cph]
[app.main.data.shortcuts :as dsc]
[app.main.data.workspace :as dw]
[app.main.data.workspace.path.shortcuts :as psc]
@@ -31,10 +32,9 @@
on-key-up (actions/on-key-up)
on-mouse-move (actions/on-mouse-move viewport-ref zoom)
on-mouse-wheel (actions/on-mouse-wheel viewport-ref zoom)
- on-resize (actions/on-resize viewport-ref)
on-paste (actions/on-paste disable-paste in-viewport?)]
(mf/use-layout-effect
- (mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-resize on-paste)
+ (mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-paste)
(fn []
(let [node (mf/ref-val viewport-ref)
keys [(events/listen js/document EventType.KEYDOWN on-key-down)
@@ -43,7 +43,6 @@
;; bind with passive=false to allow the event to be cancelled
;; https://stackoverflow.com/a/57582286/3219895
(events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false})
- (events/listen js/window EventType.RESIZE on-resize)
(events/listen js/window EventType.PASTE on-paste)]]
(fn []
@@ -52,20 +51,21 @@
(defn setup-viewport-size [viewport-ref]
(mf/use-layout-effect
- (fn []
- (let [node (mf/ref-val viewport-ref)
- prnt (dom/get-parent node)
- size (dom/get-client-size prnt)]
- ;; We schedule the event so it fires after `initialize-page` event
- (timers/schedule #(st/emit! (dw/initialize-viewport size)))))))
+ (fn []
+ (let [node (mf/ref-val viewport-ref)
+ prnt (dom/get-parent node)
+ size (dom/get-client-size prnt)]
+ ;; We schedule the event so it fires after `initialize-page` event
+ (timers/schedule #(st/emit! (dw/initialize-viewport size)))))))
-(defn setup-cursor [cursor alt? panning drawing-tool drawing-path? path-editing?]
+(defn setup-cursor [cursor alt? ctrl? space? panning drawing-tool drawing-path? path-editing?]
(mf/use-effect
- (mf/deps @cursor @alt? panning drawing-tool drawing-path? path-editing?)
+ (mf/deps @cursor @alt? @ctrl? @space? panning drawing-tool drawing-path? path-editing?)
(fn []
(let [new-cursor
(cond
- panning (utils/get-cursor :hand)
+ (and @ctrl? @space?) (utils/get-cursor :zoom)
+ (or panning @space?) (utils/get-cursor :hand)
(= drawing-tool :comments) (utils/get-cursor :comments)
(= drawing-tool :frame) (utils/get-cursor :create-artboard)
(= drawing-tool :rect) (utils/get-cursor :create-rectangle)
@@ -80,16 +80,35 @@
(when (not= @cursor new-cursor)
(reset! cursor new-cursor))))))
-(defn setup-resize [layout viewport-ref]
- (let [on-resize (actions/on-resize viewport-ref)]
- (mf/use-layout-effect (mf/deps layout) on-resize)))
-
(defn setup-keyboard [alt? ctrl? space?]
(hooks/use-stream ms/keyboard-alt #(reset! alt? %))
(hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %))
(hooks/use-stream ms/keyboard-space #(reset! space? %)))
-(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom]
+(defn group-empty-space?
+ "Given a group `group-id` check if `hover-ids` contains any of its children. If it doesn't means
+ we're hovering over empty space for the group "
+ [group-id objects hover-ids]
+
+ (and (contains? #{:group :bool} (get-in objects [group-id :type]))
+
+ ;; If there are no children in the hover-ids we're in the empty side
+ (->> hover-ids
+ (remove #(contains? #{:group :bool} (get-in objects [% :type])))
+ (some #(cph/is-parent? objects % group-id))
+ (not))))
+
+(defn check-text-collision?
+ "Checks if he current position `pos` overlaps any of the text-nodes for the given `text-id`"
+ [objects pos text-id]
+ (and (= :text (get-in objects [text-id :type]))
+ (let [collisions
+ (->> (dom/query-all (str "#shape-" text-id " .text-node"))
+ (map dom/get-bounding-rect)
+ (map dom/bounding-rect->rect))]
+ (not (some #(gshr/contains-point? % pos) collisions)))))
+
+(defn setup-hover-shapes [page-id move-stream raw-position-ref objects transform selected ctrl? hover hover-ids hover-disabled? zoom]
(let [;; We use ref so we don't recreate the stream on a change
zoom-ref (mf/use-ref zoom)
ctrl-ref (mf/use-ref @ctrl?)
@@ -159,14 +178,21 @@
selected (mf/ref-val selected-ref)
- remove-xfm (mapcat #(cp/get-parents % objects))
+ remove-xfm (mapcat #(cph/get-parent-ids objects %))
remove-id? (cond-> (into #{} remove-xfm selected)
+ :always
+ (into (filter #(check-text-collision? objects (mf/ref-val raw-position-ref) %)) ids)
+
+ (not @ctrl?)
+ (into (filter #(group-empty-space? % objects ids)) ids)
+
@ctrl?
(into (filter is-group?) ids))
- ids (filterv (comp not remove-id?) ids)
- hover-shape (get objects (first ids))]
-
+ hover-shape (->> ids
+ (filter (comp not remove-id?))
+ (first)
+ (get objects))]
(reset! hover hover-shape)
(reset! hover-ids ids))))))
diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
index 90f120024c..590b392a28 100644
--- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
@@ -9,8 +9,8 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
- [app.common.types.interactions :as cti]
+ [app.common.pages.helpers :as cph]
+ [app.common.spec.interactions :as cti]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -210,7 +210,7 @@
(st/emit! (dw/start-move-overlay-pos index)))]
(when dest-shape
- (let [orig-frame (cp/get-frame orig-shape objects)
+ (let [orig-frame (cph/get-frame objects orig-shape)
marker-x (+ (:x orig-frame) (:x position))
marker-y (+ (:y orig-frame) (:y position))
width (:width dest-shape)
@@ -326,7 +326,7 @@
:objects objects
:hover-disabled? hover-disabled?}]))])))
(when (and shape
- (not (cp/unframed-shape? shape))
+ (not (cph/unframed-shape? shape))
(not (#{:move :rotate} current-transform)))
[:& interaction-handle {:key (:id shape)
:index nil
diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs
index f525bb31f8..95d58b0167 100644
--- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs
@@ -8,7 +8,7 @@
(:require
[app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.refs :as refs]
[app.util.object :as obj]
[app.util.path.format :as upf]
@@ -89,7 +89,7 @@
transform (mf/deref refs/current-transform)
outlines-ids (->> (set/union selected hover)
- (cp/clean-loops objects))
+ (cph/clean-loops objects))
show-outline? (fn [shape] (and (not (:hidden shape))
(not (:blocked shape))))
diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs
index 3c09e952f3..9fda55c82b 100644
--- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs
@@ -10,6 +10,7 @@
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dwc]
+ [app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.cursors :as cur]
@@ -25,7 +26,7 @@
(:import goog.events.EventType))
(defn format-viewbox [vbox]
- (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))
+ (str/join " " [(:x vbox 0)
(:y vbox 0)
(:width vbox 0)
(:height vbox 0)]))
@@ -101,14 +102,16 @@
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
- (st/emit! (dwc/pick-color-select true (kbd/shift? event)))))
+ (st/emit! (dwu/start-undo-transaction)
+ (dwc/pick-color-select true (kbd/shift? event)))))
handle-mouse-up-picker
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
- (st/emit! (dwc/stop-picker))
+ (st/emit! (dwu/commit-undo-transaction)
+ (dwc/stop-picker))
(modal/disallow-click-outside!)))
handle-image-load
diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs
new file mode 100644
index 0000000000..57a27c1063
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/viewport/rules.cljs
@@ -0,0 +1,271 @@
+;; 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.viewport.rules
+ (:require
+ [app.common.colors :as colors]
+ [app.common.data :as d]
+ [app.common.geom.shapes :as gsh]
+ [app.common.math :as mth]
+ [app.main.ui.hooks :as hooks]
+ [app.util.object :as obj]
+ [rumext.alpha :as mf]))
+
+(def rules-pos 15)
+(def rules-size 4)
+(def rules-width 1)
+(def rule-area-size 22)
+(def rule-area-half-size (/ rule-area-size 2))
+(def rules-background "var(--color-gray-50)")
+(def selection-area-color "var(--color-primary)")
+(def selection-area-opacity 0.3)
+(def over-number-size 50)
+(def over-number-opacity 0.7)
+
+(def font-size 12)
+(def font-family "worksans")
+
+;; ----------------
+;; RULES
+;; ----------------
+
+(defn- calculate-step-size
+ [zoom]
+ (cond
+ (< 0 zoom 0.008) 10000
+ (< 0.008 zoom 0.015) 5000
+ (< 0.015 zoom 0.04) 2500
+ (< 0.04 zoom 0.07) 1000
+ (< 0.07 zoom 0.2) 500
+ (< 0.2 zoom 0.5) 250
+ (< 0.5 zoom 1) 100
+ (<= 1 zoom 2) 50
+ (< 2 zoom 4) 25
+ (< 4 zoom 6) 10
+ (< 6 zoom 15) 5
+ (< 15 zoom 25) 2
+ (< 25 zoom) 1
+ :else 1))
+
+(defn get-clip-area
+ [vbox zoom axis]
+ (if (= axis :x)
+ (let [x (+ (:x vbox) (/ 25 zoom))
+ y (:y vbox)
+ width (- (:width vbox) (/ 21 zoom))
+ height (/ 25 zoom)]
+ {:x x :y y :width width :height height})
+
+ (let [x (:x vbox)
+ y (+ (:y vbox) (/ 25 zoom))
+ width (/ 25 zoom)
+ height (- (:height vbox) (/ 21 zoom))]
+ {:x x :y y :width width :height height})))
+
+(defn get-background-area
+ [vbox zoom axis]
+ (if (= axis :x)
+ (let [x (:x vbox)
+ y (:y vbox)
+ width (:width vbox)
+ height (/ rule-area-size zoom)]
+ {:x x :y y :width width :height height})
+
+ (let [x (:x vbox)
+ y (+ (:y vbox) (/ rule-area-size zoom))
+ width (/ rule-area-size zoom)
+ height (- (:height vbox) (/ 21 zoom))]
+ {:x x :y y :width width :height height})))
+
+(defn get-rule-params
+ [vbox axis]
+ (if (= axis :x)
+ (let [start (:x vbox)
+ end (+ start (:width vbox))]
+ {:start start :end end})
+
+ (let [start (:y vbox)
+ end (+ start (:height vbox))]
+ {:start start :end end})))
+
+(defn get-rule-axis
+ [val vbox zoom axis]
+ (let [rules-pos (/ rules-pos zoom)
+ rules-size (/ rules-size zoom)]
+ (if (= axis :x)
+ {:text-x val
+ :text-y (+ (:y vbox) (- rules-pos (/ 4 zoom)))
+ :line-x1 val
+ :line-y1 (+ (:y vbox) rules-pos (/ 2 zoom))
+ :line-x2 val
+ :line-y2 (+ (:y vbox) rules-pos (/ 2 zoom) rules-size)}
+
+ {:text-x (+ (:x vbox) (- rules-pos (/ 4 zoom)))
+ :text-y val
+ :line-x1 (+ (:x vbox) rules-pos (/ 2 zoom))
+ :line-y1 val
+ :line-x2 (+ (:x vbox) rules-pos (/ 2 zoom) rules-size)
+ :line-y2 val})))
+
+(mf/defc rules-axis
+ [{:keys [zoom vbox axis]}]
+ (let [rules-width (/ rules-width zoom)
+ step (calculate-step-size zoom)
+ clip-id (str "clip-rule-" (d/name axis))]
+
+
+ [:*
+ (let [{:keys [x y width height]} (get-background-area vbox zoom axis)]
+ [:rect {:x x :y y :width width :height height :style {:fill rules-background}}])
+
+ [:g.rules {:clipPath (str "url(#" clip-id ")")}
+
+ [:defs
+ [:clipPath {:id clip-id}
+ (let [{:keys [x y width height]} (get-clip-area vbox zoom axis)]
+ [:rect {:x x :y y :width width :height height}])]]
+
+
+
+ (let [{:keys [start end]} (get-rule-params vbox axis)
+ minv (max (mth/round start) -100000)
+ minv (* (mth/ceil (/ minv step)) step)
+ maxv (min (mth/round end) 100000)
+ maxv (* (mth/floor (/ maxv step)) step)]
+
+ (for [step-val (range minv (inc maxv) step)]
+ (let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]}
+ (get-rule-axis step-val vbox zoom axis)]
+ [:*
+ [:text {:key (str "text-" (d/name axis) "-" step-val)
+ :x text-x
+ :y text-y
+ :text-anchor "middle"
+ :dominant-baseline "middle"
+ :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
+ :style {:font-size (/ font-size zoom)
+ :font-family font-family
+ :fill colors/gray-30}}
+ (str (mth/round step-val))]
+
+ [:line {:key (str "line-" (d/name axis) "-" step-val)
+ :x1 line-x1
+ :y1 line-y1
+ :x2 line-x2
+ :y2 line-y2
+ :style {:stroke colors/gray-30
+ :stroke-width rules-width}}]])))]]))
+
+(mf/defc selection-area
+ [{:keys [vbox zoom selection-rect]}]
+ [:g.selection-area
+ [:g
+ [:rect {:x (:x selection-rect)
+ :y (:y vbox)
+ :width (:width selection-rect)
+ :height (/ rule-area-size zoom)
+ :style {:fill selection-area-color
+ :fill-opacity selection-area-opacity}}]
+
+ [:rect {:x (- (:x selection-rect) (/ over-number-size zoom))
+ :y (:y vbox)
+ :width (/ over-number-size zoom)
+ :height (/ rule-area-size zoom)
+ :style {:fill rules-background
+ :fill-opacity over-number-opacity}}]
+
+ [:text {:x (- (:x1 selection-rect) (/ 4 zoom))
+ :y (+ (:y vbox) (/ 12 zoom))
+ :text-anchor "end"
+ :dominant-baseline "middle"
+ :style {:font-size (/ font-size zoom)
+ :font-family font-family
+ :fill selection-area-color}}
+ (str (mth/round (:x1 selection-rect)))]
+
+ [:rect {:x (:x2 selection-rect)
+ :y (:y vbox)
+ :width (/ over-number-size zoom)
+ :height (/ rule-area-size zoom)
+ :style {:fill rules-background
+ :fill-opacity over-number-opacity}}]
+
+ [:text {:x (+ (:x2 selection-rect) (/ 4 zoom))
+ :y (+ (:y vbox) (/ 12 zoom))
+ :text-anchor "start"
+ :dominant-baseline "middle"
+ :style {:font-size (/ font-size zoom)
+ :font-family font-family
+ :fill selection-area-color}}
+ (str (mth/round (:x2 selection-rect)))]]
+
+ (let [center-x (+ (:x vbox) (/ rule-area-half-size zoom))
+ center-y (- (+ (:y selection-rect) (/ (:height selection-rect) 2)) (/ rule-area-half-size zoom))]
+
+ [:g {:transform (str "rotate(-90 " center-x "," center-y ")")}
+ [:rect {:x (- center-x (/ (:height selection-rect) 2) (/ rule-area-half-size zoom))
+ :y (- center-y (/ rule-area-half-size zoom))
+ :width (:height selection-rect)
+ :height (/ rule-area-size zoom)
+ :style {:fill selection-area-color
+ :fill-opacity selection-area-opacity}}]
+
+ [:rect {:x (- center-x (/ (:height selection-rect) 2) (/ rule-area-half-size zoom) (/ over-number-size zoom))
+ :y (- center-y (/ rule-area-half-size zoom))
+ :width (/ over-number-size zoom)
+ :height (/ rule-area-size zoom)
+ :style {:fill rules-background
+ :fill-opacity over-number-opacity}}]
+
+ [:rect {:x (+ (- center-x (/ (:height selection-rect) 2) (/ rule-area-half-size zoom) ) (:height selection-rect))
+ :y (- center-y (/ rule-area-half-size zoom))
+ :width (/ over-number-size zoom)
+ :height (/ rule-area-size zoom)
+ :style {:fill rules-background
+ :fill-opacity over-number-opacity}}]
+
+ [:text {:x (- center-x (/ (:height selection-rect) 2) (/ 15 zoom))
+ :y center-y
+ :text-anchor "end"
+ :dominant-baseline "middle"
+ :style {:font-size (/ font-size zoom)
+ :font-family font-family
+ :fill selection-area-color}}
+ (str (mth/round (:y2 selection-rect)))]
+
+ [:text {:x (+ center-x (/ (:height selection-rect) 2) )
+ :y center-y
+ :text-anchor "start"
+ :dominant-baseline "middle"
+ :style {:font-size (/ font-size zoom)
+ :font-family font-family
+ :fill selection-area-color}}
+ (str (mth/round (:y1 selection-rect)))]])])
+
+(mf/defc rules
+ {::mf/wrap-props false
+ ::mf/wrap [#(mf/memo' % (mf/check-props ["zoom" "vbox" "selected-shapes"]))]}
+ [props]
+ (let [zoom (obj/get props "zoom")
+ vbox (obj/get props "vbox")
+ selected-shapes (-> (obj/get props "selected-shapes")
+ (hooks/use-equal-memo))
+
+ selection-rect
+ (mf/use-memo
+ (mf/deps selected-shapes)
+ #(when (d/not-empty? selected-shapes)
+ (gsh/selection-rect selected-shapes)))]
+
+ (when (some? vbox)
+ [:g.rules {:pointer-events "none"}
+ [:& rules-axis {:zoom zoom :vbox vbox :axis :x}]
+ [:& rules-axis {:zoom zoom :vbox vbox :axis :y}]
+
+ (when (some? selection-rect)
+ [:& selection-area {:zoom zoom
+ :vbox vbox
+ :selection-rect selection-rect}])])))
diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs
new file mode 100644
index 0000000000..3de69d6b24
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs
@@ -0,0 +1,223 @@
+;; 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.viewport.scroll-bars
+ (:require
+ [app.common.geom.shapes :as gsh]
+ [app.common.geom.shapes.rect :as gpr]
+ [app.common.pages.helpers :as cph]
+ [app.main.data.workspace :as dw]
+ [app.main.store :as st]
+ [app.main.ui.workspace.viewport.utils :as utils]
+ [app.util.dom :as dom]
+ [rumext.alpha :as mf]))
+
+(def scroll-x 10)
+(def scroll-y 10)
+(def scroll-height (+ scroll-x 4))
+(def scroll-width (+ scroll-y 4))
+(def other-x 26)
+(def other-y 26)
+(def other-width 100)
+(def other-height 100)
+
+
+(mf/defc viewport-scrollbars
+ {::mf/wrap [mf/memo]}
+ [{:keys [objects viewport-ref zoom vbox]}]
+
+ (let [v-scrolling? (mf/use-state false)
+ h-scrolling? (mf/use-state false)
+ start-ref (mf/use-ref nil)
+ v-scrollbar-y-ref (mf/use-ref nil)
+ h-scrollbar-x-ref (mf/use-ref nil)
+ v-scrollbar-y-stored (mf/ref-val v-scrollbar-y-ref)
+ h-scrollbar-x-stored (mf/ref-val h-scrollbar-x-ref)
+ v-scrollbar-y-padding-ref (mf/use-ref nil)
+ h-scrollbar-x-padding-ref (mf/use-ref nil)
+ scrollbar-height-ref (mf/use-ref nil)
+ scrollbar-width-ref (mf/use-ref nil)
+ scrollbar-height-stored (mf/ref-val scrollbar-height-ref)
+ scrollbar-width-stored (mf/ref-val scrollbar-width-ref)
+ height-factor-ref (mf/use-ref nil)
+ width-factor-ref (mf/use-ref nil)
+ vbox-y-ref (mf/use-ref nil)
+ vbox-x-ref (mf/use-ref nil)
+
+ vbox-x (:x vbox)
+ vbox-y (:y vbox)
+
+ base-objects-rect (mf/with-memo [objects]
+ (-> objects
+ (cph/get-immediate-children)
+ (gsh/selection-rect)))
+
+ inv-zoom (/ 1 zoom)
+ vbox-height (- (:height vbox) (* inv-zoom scroll-height))
+ vbox-width (- (:width vbox) (* inv-zoom scroll-width))
+
+ ;; top space hidden because of the scroll
+ top-offset (-> (- vbox-y (:y base-objects-rect))
+ (max 0)
+ (* vbox-height)
+ (/ (:height base-objects-rect)))
+ ;; left space hidden because of the scroll
+ left-offset (-> (- vbox-x (:x base-objects-rect))
+ (max 0)
+ (* vbox-width)
+ (/ (:width base-objects-rect)))
+
+ ;; bottom space hidden because of the scroll
+ bottom-offset (-> (- (:y2 base-objects-rect) (+ vbox-y vbox-height))
+ (max 0)
+ (* vbox-height)
+ (/ (:height base-objects-rect)))
+
+ ;; right space hidden because of the scroll
+ right-offset (-> (- (:x2 base-objects-rect) (+ vbox-x vbox-width))
+ (max 0)
+ (* vbox-width)
+ (/ (:width base-objects-rect)))
+
+ show-v-scroll? (or @v-scrolling? (> top-offset 0) (> bottom-offset 0))
+ show-h-scroll? (or @h-scrolling? (> left-offset 0) (> right-offset 0))
+
+ v-scrollbar-x (+ vbox-x (:width vbox) (* inv-zoom (- scroll-x)))
+ v-scrollbar-y (+ vbox-y top-offset)
+
+ h-scrollbar-x (+ vbox-x left-offset)
+ h-scrollbar-y (+ vbox-y (:height vbox) (* inv-zoom (- scroll-y)))
+
+ scrollbar-height (-> (- (+ vbox-y vbox-height) bottom-offset v-scrollbar-y))
+ scrollbar-height (-> (cond
+ @v-scrolling? scrollbar-height-stored
+ :else scrollbar-height)
+ (max (* inv-zoom other-height)))
+
+ scrollbar-width (-> (- (+ vbox-x vbox-width) right-offset h-scrollbar-x))
+ scrollbar-width (-> (cond
+ @h-scrolling? scrollbar-width-stored
+ :else scrollbar-width)
+ (max (* inv-zoom other-width)))
+
+ v-scrollbar-y (-> (cond
+ @v-scrolling? (- v-scrollbar-y-stored (- (- vbox-y (mf/ref-val vbox-y-ref))))
+ :else v-scrollbar-y)
+ (max (+ vbox-y (* inv-zoom other-y))))
+
+ v-scrollbar-y (if (> (+ v-scrollbar-y scrollbar-height) (+ vbox-y vbox-height)) ;; the scroll bar is stick to the bottom
+ (-> (+ vbox-y vbox-height)
+ (- scrollbar-height))
+ v-scrollbar-y)
+
+ h-scrollbar-x (-> (cond
+ @h-scrolling? (- h-scrollbar-x-stored (- (- vbox-x (mf/ref-val vbox-x-ref))))
+ :else h-scrollbar-x)
+ (max (+ vbox-x (* inv-zoom other-x))))
+
+ h-scrollbar-x (if (> (+ h-scrollbar-x scrollbar-width) (+ vbox-x vbox-width)) ;; the scroll bar is stick to the right
+ (-> (+ vbox-x vbox-width)
+ (- scrollbar-width))
+ h-scrollbar-x)
+
+ on-mouse-move
+ (mf/use-callback
+ (mf/deps zoom v-scrolling?)
+ (fn [event axis]
+ (when-let [_ (or @v-scrolling? @h-scrolling?)]
+ (let [viewport (mf/ref-val viewport-ref)
+ start-pt (mf/ref-val start-ref)
+ current-pt (dom/get-client-position event)
+ current-pt-viewport (utils/translate-point-to-viewport-raw viewport zoom current-pt)
+ y-delta (/ (* (mf/ref-val height-factor-ref) (- (:y current-pt) (:y start-pt))) zoom)
+ x-delta (/ (* (mf/ref-val width-factor-ref) (- (:x current-pt) (:x start-pt))) zoom)
+ new-v-scrollbar-y (-> current-pt-viewport
+ (:y)
+ (+ (mf/ref-val v-scrollbar-y-padding-ref)))
+ new-h-scrollbar-x (-> current-pt-viewport
+ (:x)
+ (+ (mf/ref-val h-scrollbar-x-padding-ref)))
+ viewport-update (-> {}
+ (cond-> (= axis :y) (assoc :y #(+ % y-delta)))
+ (cond-> (= axis :x) (assoc :x #(+ % x-delta))))]
+ (mf/set-ref-val! vbox-y-ref vbox-y)
+ (mf/set-ref-val! vbox-x-ref vbox-x)
+ (st/emit! (dw/update-viewport-position viewport-update))
+ (mf/set-ref-val! v-scrollbar-y-ref new-v-scrollbar-y)
+ (mf/set-ref-val! h-scrollbar-x-ref new-h-scrollbar-x)
+ (mf/set-ref-val! start-ref current-pt)))))
+
+ on-mouse-down
+ (mf/use-callback
+ (mf/deps v-scrollbar-y scrollbar-height)
+ (fn [event axis]
+ (let [viewport (mf/ref-val viewport-ref)
+ start-pt (dom/get-client-position event)
+ new-v-scrollbar-y (-> (utils/translate-point-to-viewport-raw viewport zoom start-pt) :y)
+ new-h-scrollbar-x (-> (utils/translate-point-to-viewport-raw viewport zoom start-pt) :x)
+ v-scrollbar-y-padding (- v-scrollbar-y new-v-scrollbar-y)
+ h-scrollbar-x-padding (- h-scrollbar-x new-h-scrollbar-x)
+ vbox-rect {:x vbox-x
+ :y vbox-y
+ :x1 vbox-x
+ :y1 vbox-y
+ :x2 (+ vbox-x (:width vbox))
+ :y2 (+ vbox-y (:height vbox))
+ :width (:width vbox)
+ :height (:height vbox)}
+ containing-rect (gpr/join-selrects [base-objects-rect vbox-rect])
+ height-factor (/ (:height containing-rect) vbox-height)
+ width-factor (/ (:width containing-rect) vbox-width)]
+ (mf/set-ref-val! start-ref start-pt)
+ (mf/set-ref-val! v-scrollbar-y-padding-ref v-scrollbar-y-padding)
+ (mf/set-ref-val! h-scrollbar-x-padding v-scrollbar-y-padding)
+ (mf/set-ref-val! v-scrollbar-y-ref (+ new-v-scrollbar-y v-scrollbar-y-padding))
+ (mf/set-ref-val! h-scrollbar-x-ref (+ new-h-scrollbar-x h-scrollbar-x-padding))
+ (mf/set-ref-val! vbox-y-ref vbox-y)
+ (mf/set-ref-val! vbox-x-ref vbox-x)
+ (mf/set-ref-val! scrollbar-height-ref scrollbar-height)
+ (mf/set-ref-val! scrollbar-width-ref scrollbar-width)
+ (mf/set-ref-val! height-factor-ref height-factor)
+ (mf/set-ref-val! width-factor-ref width-factor)
+ (reset! v-scrolling? (= axis :y))
+ (reset! h-scrolling? (= axis :x)))))
+
+ on-mouse-up
+ (mf/use-callback
+ (mf/deps)
+ (fn [_]
+ (reset! v-scrolling? false)
+ (reset! h-scrolling? false)))]
+
+ [:*
+ (when show-v-scroll?
+ [:g.v-scroll
+ [:rect {:on-mouse-move #(on-mouse-move % :y)
+ :on-mouse-down #(on-mouse-down % :y)
+ :on-mouse-up #(on-mouse-up % :y)
+ :width (* inv-zoom 7)
+ :rx (* inv-zoom 3)
+ :ry (* inv-zoom 3)
+ :height scrollbar-height
+ :fill-opacity 0.4
+ :x v-scrollbar-x
+ :y v-scrollbar-y
+ :style {:stroke "white"
+ :stroke-width 0.15}}]])
+ (when show-h-scroll?
+ [:g.h-scroll
+ [:rect {:on-mouse-move #(on-mouse-move % :x)
+ :on-mouse-down #(on-mouse-down % :x)
+ :on-mouse-up #(on-mouse-up % :x)
+ :width scrollbar-width
+ :rx (* inv-zoom 3)
+ :ry (* inv-zoom 3)
+ :height (* inv-zoom 7)
+ :fill-opacity 0.4
+ :x h-scrollbar-x
+ :y h-scrollbar-y
+ :style {:stroke "white"
+ :stroke-width 0.15}}]])]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs
index cf990d062c..ca450b2a81 100644
--- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs
@@ -9,7 +9,7 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.refs :as refs]
[app.main.worker :as uw]
[beicon.core :as rx]
@@ -225,7 +225,7 @@
:frame-id (:id frame)
:include-frames? true
:rect rect})
- (rx/map #(cp/clean-loops @refs/workspace-page-objects %))
+ (rx/map #(cph/clean-loops @refs/workspace-page-objects %))
(rx/map #(set/difference % selected))
(rx/map #(->> % (map (partial get @refs/workspace-page-objects)))))
(rx/of nil))))]
diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
index ec95528eec..e5a63b11da 100644
--- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
@@ -9,7 +9,8 @@
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
+ [app.common.spec :as us]
[app.main.snap :as snap]
[app.util.geom.snap-points :as sp]
[beicon.core :as rx]
@@ -52,7 +53,7 @@
:opacity line-opacity}])
(defn get-snap
- [coord {:keys [shapes page-id filter-shapes modifiers]}]
+ [coord {:keys [shapes page-id remove-snap? modifiers]}]
(let [shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))
(->> shapes (first)))
@@ -68,7 +69,7 @@
(->> (sp/shape-snap-points shape)
(map #(vector frame-id %)))))
(rx/flat-map (fn [[frame-id point]]
- (->> (snap/get-snap-points page-id frame-id filter-shapes point coord)
+ (->> (snap/get-snap-points page-id frame-id remove-snap? point coord)
(rx/map #(vector point % coord)))))
(rx/reduce conj []))))
@@ -104,7 +105,7 @@
(hash-map coord fixedv (flip coord) maxv)]))))
(mf/defc snap-feedback
- [{:keys [shapes filter-shapes zoom modifiers] :as props}]
+ [{:keys [shapes remove-snap? zoom modifiers] :as props}]
(let [state (mf/use-state [])
subject (mf/use-memo #(rx/subject))
@@ -129,7 +130,7 @@
#(rx/dispose! sub))))
(mf/use-effect
- (mf/deps shapes filter-shapes modifiers)
+ (mf/deps shapes remove-snap? modifiers)
(fn []
(rx/push! subject props)))
@@ -151,30 +152,20 @@
(mf/defc snap-points
{::mf/wrap [mf/memo]}
[{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}]
+ (us/assert set? selected)
+ (let [shapes (into [] (keep (d/getf objects)) selected)
- (let [;; shapes (mf/deref (refs/objects-by-id selected))
- ;; filter-shapes (mf/deref refs/selected-shapes-with-children)
+ filter-shapes
+ (into selected (mapcat #(cph/get-children-ids objects %)) selected)
- shapes (->> selected
- (map #(get objects %))
- (filterv (comp not nil?)))
- filter-shapes (into #{}
- (comp (mapcat #(cp/get-object-with-children % objects))
- (map :id))
- selected)
-
- filter-shapes (fn [id]
- (if (= id :layout)
- (or (not (contains? layout :display-grid))
- (not (contains? layout :snap-grid)))
- (or (filter-shapes id)
- (not (contains? layout :dynamic-alignment)))))
+ remove-snap? (mf/with-memo [layout filter-shapes]
+ (snap/make-remove-snap layout filter-shapes))
shapes (if drawing [drawing] shapes)]
(when (or drawing transform)
[:& snap-feedback {:shapes shapes
:page-id page-id
- :filter-shapes filter-shapes
+ :remove-snap? remove-snap?
:zoom zoom
:modifiers modifiers}])))
diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs
index 088d175494..8730610705 100644
--- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs
@@ -148,12 +148,12 @@
(dom/remove-attribute node "transform")))))))
(defn format-viewbox [vbox]
- (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))
+ (str/join " " [(:x vbox 0)
(:y vbox 0)
(:width vbox 0)
(:height vbox 0)]))
-(defn translate-point-to-viewport [viewport zoom pt]
+(defn translate-point-to-viewport-raw [viewport zoom pt]
(let [vbox (.. ^js viewport -viewBox -baseVal)
brect (dom/get-bounding-rect viewport)
brect (gpt/point (d/parse-integer (:left brect))
@@ -162,8 +162,11 @@
zoom (gpt/point zoom)]
(-> (gpt/subtract pt brect)
(gpt/divide zoom)
- (gpt/add box)
- (gpt/round 0))))
+ (gpt/add box))))
+
+(defn translate-point-to-viewport [viewport zoom pt]
+ (-> (translate-point-to-viewport-raw viewport zoom pt)
+ (gpt/round 0)))
(defn get-cursor [cursor]
(case cursor
@@ -176,4 +179,7 @@
:pencil cur/pencil
:create-shape cur/create-shape
:duplicate cur/duplicate
+ :zoom cur/zoom
+ :zoom-in cur/zoom-in
+ :zooom-out cur/zoom-out
cur/pointer-inner))
diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
index 2a1e9df0f8..507d8fc677 100644
--- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
@@ -8,7 +8,7 @@
(:require
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
@@ -161,7 +161,7 @@
on-frame-enter (unchecked-get props "on-frame-enter")
on-frame-leave (unchecked-get props "on-frame-leave")
on-frame-select (unchecked-get props "on-frame-select")
- frames (cp/select-frames objects)]
+ frames (cph/get-frames objects)]
[:g.frame-titles
(for [frame frames]
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 37e75d1a9f..15cbf004ef 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -213,9 +213,20 @@
(.-innerText el)))
(defn query
- [^js el ^string query]
- (when (some? el)
- (.querySelector el query)))
+ ([^string query]
+ (query globals/document query))
+
+ ([^js el ^string query]
+ (when (some? el)
+ (.querySelector el query))))
+
+(defn query-all
+ ([^string query]
+ (query-all globals/document query))
+
+ ([^js el ^string query]
+ (when (some? el)
+ (.querySelectorAll el query))))
(defn get-client-position
[^js event]
@@ -246,6 +257,13 @@
:width (.-width ^js rect)
:height (.-height ^js rect)}))
+(defn bounding-rect->rect
+ [{:keys [left top width height]}]
+ {:x left
+ :y top
+ :width width
+ :height height})
+
(defn get-window-size
[]
{:width (.-innerWidth ^js js/window)
@@ -396,21 +414,19 @@
(defn scroll-into-view!
([^js element]
- (when (some? element)
- (.scrollIntoView element false)))
+ (scroll-into-view! element false))
- ([^js element scroll-top]
+ ([^js element options]
(when (some? element)
- (.scrollIntoView element scroll-top))))
+ (.scrollIntoView element options))))
(defn scroll-into-view-if-needed!
([^js element]
- (when (some? element)
- (.scrollIntoViewIfNeeded ^js element false)))
+ (scroll-into-view-if-needed! element false))
- ([^js element scroll-top]
+ ([^js element options]
(when (some? element)
- (.scrollIntoViewIfNeeded ^js element scroll-top))))
+ (.scrollIntoViewIfNeeded ^js element options))))
(defn is-in-viewport?
[^js element]
diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs
index 669a291130..8fccf5bf36 100644
--- a/frontend/src/app/util/geom/snap_points.cljs
+++ b/frontend/src/app/util/geom/snap_points.cljs
@@ -28,3 +28,9 @@
(case (:type shape)
:frame (-> shape :selrect frame-snap-points)
(into #{(gsh/center-shape shape)} (:points shape)))))
+
+(defn guide-snap-points
+ [guide]
+ (if (= :x (:axis guide))
+ #{(gpt/point (:position guide) 0)}
+ #{(gpt/point 0 (:position guide))}))
diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs
index dec08ba7c5..63cc626689 100644
--- a/frontend/src/app/util/import/parser.cljs
+++ b/frontend/src/app/util/import/parser.cljs
@@ -9,7 +9,7 @@
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
- [app.common.types.interactions :as cti]
+ [app.common.spec.interactions :as cti]
[app.common.uuid :as uuid]
[app.util.color :as uc]
[app.util.json :as json]
@@ -364,7 +364,11 @@
(let [fill (:fill svg-data)
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
gradient (when (str/starts-with? fill "url")
- (parse-gradient node fill))]
+ (parse-gradient node fill))
+ meta-fill-color (get-meta node :fill-color)
+ meta-fill-opacity (get-meta node :fill-opacity)
+ meta-fill-color-gradient (get-meta node :fill-color-gradient)]
+
(cond-> props
:always
(assoc :fill-color nil
@@ -380,7 +384,16 @@
:fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double))
(some? hide-fill-on-export)
- (assoc :hide-fill-on-export hide-fill-on-export))))
+ (assoc :hide-fill-on-export hide-fill-on-export)
+
+ (some? meta-fill-color)
+ (assoc :fill-color meta-fill-color
+ :fill-opacity (d/parse-double meta-fill-opacity))
+
+ (some? meta-fill-color-gradient)
+ (assoc :fill-color-gradient meta-fill-color-gradient
+ :fill-color nil
+ :fill-opacity nil))))
(defn add-stroke
[props node svg-data]
@@ -515,6 +528,20 @@
(let [flows-node (get-data node :penpot:flows)]
(->> flows-node :content (mapv parse-flow-node))))
+(defn parse-guide-node [node]
+ (let [attrs (-> node :attrs remove-penpot-prefix)]
+ (println attrs)
+ (let [id (uuid/next)]
+ [id
+ {:id id
+ :frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid))
+ :axis (-> attrs :axis keyword)
+ :position (-> attrs :position d/parse-double)}])))
+
+(defn parse-guides [node]
+ (let [guides-node (get-data node :penpot:guides)]
+ (->> guides-node :content (map parse-guide-node) (into {}))))
+
(defn extract-from-data
([node tag]
(extract-from-data node tag identity))
@@ -749,7 +776,9 @@
(add-rect-data node svg-data))
(cond-> (some? (get-in node [:attrs :penpot:media-id]))
- (add-image-data type node))
+ (->
+ (add-rect-data node svg-data)
+ (add-image-data type node)))
(cond-> (= :text type)
(add-text-data node))
@@ -764,7 +793,8 @@
grids (->> (parse-grids node)
(group-by :type)
(d/mapm (fn [_ v] (-> v first :params))))
- flows (parse-flows node)]
+ flows (parse-flows node)
+ guides (parse-guides node)]
(cond-> {}
(some? background)
(assoc-in [:options :background] background)
@@ -773,7 +803,10 @@
(assoc-in [:options :saved-grids] grids)
(d/not-empty? flows)
- (assoc-in [:options :flows] flows))))
+ (assoc-in [:options :flows] flows)
+
+ (d/not-empty? guides)
+ (assoc-in [:options :guides] guides))))
(defn parse-interactions
[node]
diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs
new file mode 100644
index 0000000000..8c279836d7
--- /dev/null
+++ b/frontend/src/app/util/snap_data.cljs
@@ -0,0 +1,251 @@
+;; 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.snap-data
+ "Data structure that holds and retrieves the data to make the snaps. Internaly
+ is implemented with a balanced binary tree that queries by range.
+ https://en.wikipedia.org/wiki/Range_tree"
+ (:require
+ [app.common.data :as d]
+ [app.common.pages.diff :as diff]
+ [app.common.pages.helpers :as cph]
+ [app.common.uuid :as uuid]
+ [app.util.geom.grid :as gg]
+ [app.util.geom.snap-points :as snap]
+ [app.util.range-tree :as rt]))
+
+(def snap-attrs [:frame-id :x :y :width :height :hidden :selrect :grids])
+
+;; PRIVATE FUNCTIONS
+
+(defn- make-insert-tree-data
+ "Inserts all data in it's corresponding axis bucket"
+ [shape-data axis]
+ (fn [tree]
+ (let [tree (or tree (rt/make-tree))
+
+ insert-data
+ (fn [tree data]
+ (rt/insert tree (get-in data [:pt axis]) data))]
+
+ (reduce insert-data tree shape-data))))
+
+(defn- make-delete-tree-data
+ "Removes all data in it's corresponding axis bucket"
+ [shape-data axis]
+ (fn [tree]
+ (let [tree (or tree (rt/make-tree))
+
+ remove-data
+ (fn [tree data]
+ (rt/remove tree (get-in data [:pt axis]) data))]
+
+ (reduce remove-data tree shape-data))))
+
+(defn- add-root-frame
+ [page-data]
+ (let [frame-id uuid/zero]
+
+ (-> page-data
+ (assoc-in [frame-id :x] (rt/make-tree))
+ (assoc-in [frame-id :y] (rt/make-tree)))))
+
+(defn- add-frame
+ [page-data frame]
+ (let [frame-id (:id frame)
+ parent-id (:parent-id frame)
+ frame-data (->> (snap/shape-snap-points frame)
+ (mapv #(array-map :type :shape
+ :id frame-id
+ :pt %)))
+
+ grid-x-data (->> (gg/grid-snap-points frame :x)
+ (mapv #(array-map :type :layout
+ :id frame-id
+ :pt %)))
+
+ grid-y-data (->> (gg/grid-snap-points frame :y)
+ (mapv #(array-map :type :layout
+ :id frame-id
+ :pt %)))]
+
+ (-> page-data
+ ;; Update root frame information
+ (assoc-in [uuid/zero :objects-data frame-id] frame-data)
+ (update-in [parent-id :x] (make-insert-tree-data frame-data :x))
+ (update-in [parent-id :y] (make-insert-tree-data frame-data :y))
+
+ ;; Update frame information
+ (assoc-in [frame-id :objects-data frame-id] (d/concat-vec frame-data grid-x-data grid-y-data))
+ (update-in [frame-id :x] #(or % (rt/make-tree)))
+ (update-in [frame-id :y] #(or % (rt/make-tree)))
+ (update-in [frame-id :x] (make-insert-tree-data (d/concat-vec frame-data grid-x-data) :x))
+ (update-in [frame-id :y] (make-insert-tree-data (d/concat-vec frame-data grid-y-data) :y)))))
+
+(defn- add-shape
+ [page-data shape]
+ (let [frame-id (:frame-id shape)
+ snap-points (snap/shape-snap-points shape)
+ shape-data (->> snap-points
+ (mapv #(array-map
+ :type :shape
+ :id (:id shape)
+ :pt %)))]
+ (-> page-data
+ (assoc-in [frame-id :objects-data (:id shape)] shape-data)
+ (update-in [frame-id :x] (make-insert-tree-data shape-data :x))
+ (update-in [frame-id :y] (make-insert-tree-data shape-data :y)))))
+
+
+(defn- add-guide
+ [page-data guide]
+
+ (let [guide-data (->> (snap/guide-snap-points guide)
+ (mapv #(array-map
+ :type :guide
+ :id (:id guide)
+ :pt %)))]
+ (if-let [frame-id (:frame-id guide)]
+ ;; Guide inside frame, we add the information only on that frame
+ (-> page-data
+ (assoc-in [frame-id :objects-data (:id guide)] guide-data)
+ (update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide))))
+
+ ;; Guide outside the frame. We add the information in the global guides data
+ (-> page-data
+ (assoc-in [:guides :objects-data (:id guide)] guide-data)
+ (update-in [:guides (:axis guide)] (make-insert-tree-data guide-data (:axis guide)))))))
+
+(defn- remove-frame
+ [page-data frame]
+ (let [frame-id (:id frame)
+ root-data (get-in page-data [uuid/zero :objects-data frame-id])]
+ (-> page-data
+ (d/dissoc-in [uuid/zero :objects-data frame-id])
+ (update-in [uuid/zero :x] (make-delete-tree-data root-data :x))
+ (update-in [uuid/zero :y] (make-delete-tree-data root-data :y))
+ (dissoc frame-id))))
+
+(defn- remove-shape
+ [page-data shape]
+
+ (let [frame-id (:frame-id shape)
+ shape-data (get-in page-data [frame-id :objects-data (:id shape)])]
+ (-> page-data
+ (d/dissoc-in [frame-id :objects-data (:id shape)])
+ (update-in [frame-id :x] (make-delete-tree-data shape-data :x))
+ (update-in [frame-id :y] (make-delete-tree-data shape-data :y)))))
+
+(defn- remove-guide
+ [page-data guide]
+ (if-let [frame-id (:frame-id guide)]
+ (let [guide-data (get-in page-data [frame-id :objects-data (:id guide)])]
+ (-> page-data
+ (d/dissoc-in [frame-id :objects-data (:id guide)])
+ (update-in [frame-id (:axis guide)] (make-delete-tree-data guide-data (:axis guide)))))
+
+ ;; Guide outside the frame. We add the information in the global guides data
+ (let [guide-data (get-in page-data [:guides :objects-data (:id guide)])]
+ (-> page-data
+ (d/dissoc-in [:guides :objects-data (:id guide)])
+ (update-in [:guides (:axis guide)] (make-delete-tree-data guide-data (:axis guide)))))))
+
+(defn- update-frame
+ [page-data [_ new-frame]]
+ (let [frame-id (:id new-frame)
+ root-data (get-in page-data [uuid/zero :objects-data frame-id])
+ frame-data (get-in page-data [frame-id :objects-data frame-id])]
+ (-> page-data
+ (update-in [uuid/zero :x] (make-delete-tree-data root-data :x))
+ (update-in [uuid/zero :y] (make-delete-tree-data root-data :y))
+ (update-in [frame-id :x] (make-delete-tree-data frame-data :x))
+ (update-in [frame-id :y] (make-delete-tree-data frame-data :y))
+ (add-frame new-frame))))
+
+(defn- update-shape
+ [page-data [old-shape new-shape]]
+ (-> page-data
+ (remove-shape old-shape)
+ (add-shape new-shape)))
+
+(defn- update-guide
+ [page-data [old-guide new-guide]]
+ (-> page-data
+ (remove-guide old-guide)
+ (add-guide new-guide)))
+
+;; PUBLIC API
+
+(defn make-snap-data
+ "Creates an empty snap index"
+ []
+ {})
+
+(defn add-page
+ "Adds page information"
+ [snap-data {:keys [objects options] :as page}]
+ (let [frames (cph/get-frames objects)
+ shapes (->> (vals (:objects page))
+ (remove cph/frame-shape?))
+ guides (vals (:guides options))
+
+ page-data
+ (as-> {} $
+ (add-root-frame $)
+ (reduce add-frame $ frames)
+ (reduce add-shape $ shapes)
+ (reduce add-guide $ guides))]
+ (assoc snap-data (:id page) page-data)))
+
+(defn update-page
+ "Updates a previously inserted page with new data"
+ [snap-data old-page page]
+
+ (if (contains? snap-data (:id page))
+ ;; Update page
+ (update snap-data (:id page)
+ (fn [page-data]
+ (let [{:keys [change-frame-shapes
+ change-frame-guides
+ removed-frames
+ removed-shapes
+ removed-guides
+ updated-frames
+ updated-shapes
+ updated-guides
+ new-frames
+ new-shapes
+ new-guides]}
+ (diff/calculate-page-diff old-page page snap-attrs)]
+
+ (as-> page-data $
+ (reduce update-shape $ change-frame-shapes)
+ (reduce remove-frame $ removed-frames)
+ (reduce remove-shape $ removed-shapes)
+ (reduce update-frame $ updated-frames)
+ (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)))))
+
+ ;; Page doesn't exist, we create a new entry
+ (add-page snap-data page)))
+
+(defn query
+ "Retrieve the shape data for the snaps in that range"
+ [snap-data page-id frame-id axis [from to]]
+
+ (d/concat-vec
+ (-> snap-data
+ (get-in [page-id frame-id axis])
+ (rt/range-query from to))
+
+ (-> snap-data
+ (get-in [page-id :guides axis])
+ (rt/range-query from to))))
diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs
index 5fd1ee0c44..8ce40754c5 100644
--- a/frontend/src/app/util/time.cljs
+++ b/frontend/src/app/util/time.cljs
@@ -6,20 +6,20 @@
(ns app.util.time
(:require
- ["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict]
- ["date-fns/locale/ar-SA" :default dateFnsLocalesAr]
- ["date-fns/locale/ca" :default dateFnsLocalesCa]
- ["date-fns/locale/de" :default dateFnsLocalesDe]
- ["date-fns/locale/el" :default dateFnsLocalesEl]
- ["date-fns/locale/en-US" :default dateFnsLocalesEnUs]
- ["date-fns/locale/es" :default dateFnsLocalesEs]
- ["date-fns/locale/fr" :default dateFnsLocalesFr]
- ["date-fns/locale/he" :default dateFnsLocalesHe]
- ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr]
- ["date-fns/locale/ro" :default dateFnsLocalesRo]
- ["date-fns/locale/ru" :default dateFnsLocalesRu]
- ["date-fns/locale/tr" :default dateFnsLocalesTr]
- ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn]
+ ["date-fns/formatDistanceToNowStrict" :as dateFnsFormatDistanceToNowStrict]
+ ["date-fns/locale/ar-SA" :as dateFnsLocalesAr]
+ ["date-fns/locale/ca" :as dateFnsLocalesCa]
+ ["date-fns/locale/de" :as dateFnsLocalesDe]
+ ["date-fns/locale/el" :as dateFnsLocalesEl]
+ ["date-fns/locale/en-US" :as dateFnsLocalesEnUs]
+ ["date-fns/locale/es" :as dateFnsLocalesEs]
+ ["date-fns/locale/fr" :as dateFnsLocalesFr]
+ ["date-fns/locale/he" :as dateFnsLocalesHe]
+ ["date-fns/locale/pt-BR" :as dateFnsLocalesPtBr]
+ ["date-fns/locale/ro" :as dateFnsLocalesRo]
+ ["date-fns/locale/ru" :as dateFnsLocalesRu]
+ ["date-fns/locale/tr" :as dateFnsLocalesTr]
+ ["date-fns/locale/zh-CN" :as dateFnsLocalesZhCn]
["luxon" :as lxn]
[app.util.object :as obj]
[cuerdas.core :as str]))
diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs
index 13b7d167f7..977c8d85ee 100644
--- a/frontend/src/app/worker/impl.cljs
+++ b/frontend/src/app/worker/impl.cljs
@@ -40,14 +40,13 @@
(defmethod handler :update-page-indices
[{:keys [page-id changes] :as message}]
- (let [old-objects (get-in @state [:pages-index page-id :objects])]
+ (let [old-page (get-in @state [:pages-index page-id])]
(swap! state ch/process-changes changes false)
- (let [new-objects (get-in @state [:pages-index page-id :objects])
+ (let [new-page (get-in @state [:pages-index page-id])
message (assoc message
- :objects new-objects
- :new-objects new-objects
- :old-objects old-objects)]
+ :old-page old-page
+ :new-page new-page)]
(handler (-> message
(assoc :cmd :selection/update-index)))
(handler (-> message
diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs
index 4f269dcc40..ad09e48bb2 100644
--- a/frontend/src/app/worker/import.cljs
+++ b/frontend/src/app/worker/import.cljs
@@ -283,7 +283,6 @@
(defn setup-interactions
[file]
-
(letfn [(add-interactions
[file [id interactions]]
(->> interactions
@@ -294,7 +293,6 @@
(let [interactions (:interactions file)
file (dissoc file :interactions)]
(->> interactions (reduce add-interactions file))))]
-
(-> file process-interactions)))
(defn resolve-media
diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs
index 3b9d922914..30879d60b8 100644
--- a/frontend/src/app/worker/selection.cljs
+++ b/frontend/src/app/worker/selection.cljs
@@ -9,6 +9,7 @@
[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.util.quadtree :as qdt]
[app.worker.impl :as impl]
@@ -86,7 +87,7 @@
changed-ids (into #{}
(comp (filter #(not= % uuid/zero))
(filter changes?)
- (mapcat #(into [%] (cp/get-children % new-objects))))
+ (mapcat #(into [%] (cph/get-children-ids new-objects %))))
(set/union (set (keys old-objects))
(set (keys new-objects))))
@@ -105,7 +106,7 @@
(assoc data :index index :z-index z-index)))
(defn- query-index
- [{index :index z-index :z-index} rect frame-id full-frame? include-frames? 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))
@@ -117,6 +118,7 @@
(or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape)
:frame include-frames?
+ (:bool :group) (not ignore-groups?)
true)
(or (not full-frame?)
@@ -170,8 +172,10 @@
nil))
(defmethod impl/handler :selection/update-index
- [{:keys [page-id old-objects new-objects] :as message}]
- (let [update-page-index
+ [{:keys [page-id old-page new-page] :as message}]
+ (let [old-objects (:objects old-page)
+ new-objects (:objects new-page)
+ update-page-index
(fn [index]
(let [old-bounds (:bounds index)
new-bounds (objects-bounds new-objects)]
@@ -187,10 +191,10 @@
nil)
(defmethod impl/handler :selection/query
- [{:keys [page-id rect frame-id reverse? full-frame? include-frames? clip-children?]
+ [{: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}]
(when-let [index (get @state page-id)]
- (query-index index rect frame-id full-frame? include-frames? clip-children? reverse?)))
+ (query-index index rect frame-id full-frame? include-frames? ignore-groups? clip-children? reverse?)))
(defmethod impl/handler :selection/query-z-index
[{:keys [page-id objects ids]}]
diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs
index 66a4dff837..da872c2f72 100644
--- a/frontend/src/app/worker/snaps.cljs
+++ b/frontend/src/app/worker/snaps.cljs
@@ -6,179 +6,32 @@
(ns app.worker.snaps
(:require
- [app.common.data :as d]
- [app.common.uuid :as uuid]
- [app.util.geom.grid :as gg]
- [app.util.geom.snap-points :as snap]
- [app.util.range-tree :as rt]
+ [app.util.snap-data :as sd]
[app.worker.impl :as impl]
- [clojure.set :as set]
[okulary.core :as l]))
(defonce state (l/atom {}))
-(defn process-shape [frame-id coord]
- (fn [shape]
- (let [points (when-not (:hidden shape) (snap/shape-snap-points shape))
- shape-data (->> points (mapv #(vector % (:id shape))))]
- (if (= (:id shape) frame-id)
- (into shape-data
-
- ;; The grid points are only added by the "root" of the coord-dat
- (->> (gg/grid-snap-points shape coord)
- (map #(vector % :layout))))
- shape-data))))
-
-(defn- add-coord-data
- "Initializes the range tree given the shapes"
- [data frame-id shapes coord]
- (letfn [(into-tree [tree [point _ :as data]]
- (rt/insert tree (coord point) data))]
- (->> shapes
- (mapcat (process-shape frame-id coord))
- (reduce into-tree (or data (rt/make-tree))))))
-
-(defn remove-coord-data
- [data frame-id shapes coord]
- (letfn [(remove-tree [tree [point _ :as data]]
- (rt/remove tree (coord point) data))]
- (->> shapes
- (mapcat (process-shape frame-id coord))
- (reduce remove-tree (or data (rt/make-tree))))))
-
-(defn aggregate-data
- ([objects]
- (aggregate-data objects (keys objects)))
-
- ([objects ids]
- (->> ids
- (filter #(contains? objects %))
- (map #(get objects %))
- (filter :frame-id)
- (group-by :frame-id)
- ;; Adds the frame
- (d/mapm #(conj %2 (get objects %1))))))
-
-(defn- initialize-snap-data
- "Initialize the snap information with the current workspace information"
- [objects]
- (let [shapes-data (aggregate-data objects)
-
- create-index
- (fn [frame-id shapes]
- {:x (-> (rt/make-tree) (add-coord-data frame-id shapes :x))
- :y (-> (rt/make-tree) (add-coord-data frame-id shapes :y))})]
- (d/mapm create-index shapes-data)))
-
-;; Attributes that will change the values of their snap
-(def snap-attrs [:x :y :width :height :hidden :selrect :grids])
-
-(defn- update-snap-data
- [snap-data old-objects new-objects]
-
- (let [changed? (fn [id]
- (let [oldv (get old-objects id)
- newv (get new-objects id)]
- ;; Check first without select-keys because is faster if they are
- ;; the same reference
- (and (not= oldv newv)
- (not= (select-keys oldv snap-attrs)
- (select-keys newv snap-attrs)))))
-
- is-deleted-frame? #(and (not= uuid/zero %)
- (contains? old-objects %)
- (not (contains? new-objects %))
- (= :frame (get-in old-objects [% :type])))
- is-new-frame? #(and (not= uuid/zero %)
- (contains? new-objects %)
- (not (contains? old-objects %))
- (= :frame (get-in new-objects [% :type])))
-
- changed-ids (into #{}
- (filter changed?)
- (set/union (set (keys old-objects))
- (set (keys new-objects))))
-
- to-delete (aggregate-data old-objects changed-ids)
- to-add (aggregate-data new-objects changed-ids)
-
- frames-to-delete (->> changed-ids (filter is-deleted-frame?))
- frames-to-add (->> changed-ids (filter is-new-frame?))
-
- delete-data
- (fn [snap-data [frame-id shapes]]
- (-> snap-data
- (update-in [frame-id :x] remove-coord-data frame-id shapes :x)
- (update-in [frame-id :y] remove-coord-data frame-id shapes :y)))
-
- add-data
- (fn [snap-data [frame-id shapes]]
- (-> snap-data
- (update-in [frame-id :x] add-coord-data frame-id shapes :x)
- (update-in [frame-id :y] add-coord-data frame-id shapes :y)))
-
- delete-frames
- (fn [snap-data frame-id]
- (dissoc snap-data frame-id))
-
- add-frames
- (fn [snap-data frame-id]
- (assoc snap-data frame-id {:x (rt/make-tree)
- :y (rt/make-tree)}))]
-
- (as-> snap-data $
- (reduce delete-data $ to-delete)
- (reduce add-frames $ frames-to-add)
- (reduce add-data $ to-add)
- (reduce delete-frames $ frames-to-delete))))
-
-;; (defn- log-state
-;; "Helper function to print a friendly version of the snap tree. Debugging purposes"
-;; []
-;; (let [process-frame-data #(d/mapm rt/as-map %)
-;; process-page-data #(d/mapm process-frame-data %)]
-;; (js/console.log "STATE" (clj->js (d/mapm process-page-data @state)))))
-
-(defn- index-page [state page-id objects]
- (let [snap-data (initialize-snap-data objects)]
- (assoc state page-id snap-data)))
-
-(defn- update-page [state page-id old-objects new-objects]
- (let [snap-data (get state page-id)
- snap-data (update-snap-data snap-data old-objects new-objects)]
- (assoc state page-id snap-data)))
-
;; Public API
(defmethod impl/handler :snaps/initialize-index
[{:keys [data] :as message}]
- ;; Create the index
- (letfn [(process-page [state page]
- (let [id (:id page)
- objects (:objects page)]
- (index-page state id objects)))]
- (swap! state #(reduce process-page % (vals (:pages-index data))))
- ;; (log-state)
- ;; Return nil so the worker will not answer anything back
- nil))
+
+ (let [pages (vals (:pages-index data))]
+ (reset! state (reduce sd/add-page (sd/make-snap-data) pages)))
+
+ nil)
(defmethod impl/handler :snaps/update-index
- [{:keys [page-id old-objects new-objects] :as message}]
- (swap! state update-page page-id old-objects new-objects)
-
- ;; Uncomment this to regenerate the index everytime
- #_(swap! state index-page page-id new-objects)
- ;; (log-state)
+ [{:keys [old-page new-page] :as message}]
+ (swap! state sd/update-page old-page new-page)
nil)
(defmethod impl/handler :snaps/range-query
- [{:keys [page-id frame-id coord ranges] :as message}]
- (letfn [(calculate-range [[from to]]
- (-> @state
- (get-in [page-id frame-id coord])
- (rt/range-query from to)))]
- (->> ranges
- (mapcat calculate-range)
- set ;; unique
- (into []))))
+ [{:keys [page-id frame-id axis ranges] :as message}]
+
+ (into []
+ (comp (mapcat #(sd/query @state page-id frame-id axis %))
+ (distinct))
+ ranges))
diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs
index d46e0b35c4..9c5cbbe07e 100644
--- a/frontend/src/debug.cljs
+++ b/frontend/src/debug.cljs
@@ -8,9 +8,10 @@
(:require
[app.common.data :as d]
[app.common.math :as mth]
- [app.common.pages :as cp]
+ [app.common.pages.helpers :as cph]
[app.common.transit :as t]
[app.common.uuid :as uuid]
+ [app.main.data.workspace :as dw]
[app.main.data.workspace.changes :as dwc]
[app.main.store :as st]
[app.util.object :as obj]
@@ -207,7 +208,7 @@
(show-component [shape objects]
(if (nil? (:shape-ref shape))
""
- (let [root-shape (cp/get-component-shape shape objects)
+ (let [root-shape (cph/get-component-shape objects shape)
component-id (when root-shape (:component-id root-shape))
component-file-id (when root-shape (:component-file root-shape))
component-file (when component-file-id (get libraries component-file-id nil))
@@ -270,3 +271,15 @@
(-> (p/let [response (js/fetch url)]
(.text response))
(p/then apply-changes)))
+
+(defn ^:export reset-viewport
+ []
+ (st/emit!
+ dw/reset-zoom
+ (dw/update-viewport-position {:x (constantly 0) :y (constantly 0)})))
+
+
+(defn ^:export hide-ui
+ []
+ (st/emit!
+ (dw/toggle-layout-flags :hide-ui)))
diff --git a/frontend/test/app/components_basic_test.cljs b/frontend/test/app/components_basic_test.cljs
index 6a22c61be0..7b8138e25b 100644
--- a/frontend/test/app/components_basic_test.cljs
+++ b/frontend/test/app/components_basic_test.cljs
@@ -197,13 +197,10 @@
"Renamed component"))
(rx/do
(fn [new-state]
- (let [file (dwlh/get-local-file new-state)
- component (cph/get-component
- (:component-id instance1)
- (:component-file instance1)
- file
- {})]
-
+ (let [libs (dwlh/get-libraries new-state)
+ component (cph/get-component libs
+ (:component-file instance1)
+ (:component-id instance1))]
(t/is (= (:name component)
"Renamed component")))))
@@ -274,13 +271,10 @@
new-state
(:id instance1))
- file (dwlh/get-local-file new-state)
- component (cph/get-component
- (:component-id instance1)
- (:component-file instance1)
- file
- {})]
-
+ libs (dwlh/get-libraries new-state)
+ component (cph/get-component libs
+ (:component-file instance1)
+ (:component-id instance1))]
(t/is (nil? component)))))
(rx/subs done #(throw %))))))
diff --git a/frontend/test/app/components_sync_test.cljs b/frontend/test/app/components_sync_test.cljs
index 033ab60c1c..29ea86996e 100644
--- a/frontend/test/app/components_sync_test.cljs
+++ b/frontend/test/app/components_sync_test.cljs
@@ -3,7 +3,6 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.geom.point :as gpt]
- [app.common.pages.helpers :as cph]
[app.main.data.workspace.changes :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.libraries-helpers :as dwlh]
diff --git a/frontend/test/app/test_helpers/libraries.cljs b/frontend/test/app/test_helpers/libraries.cljs
index b93f81f2a5..a49faa5187 100644
--- a/frontend/test/app/test_helpers/libraries.cljs
+++ b/frontend/test/app/test_helpers/libraries.cljs
@@ -7,7 +7,6 @@
[app.common.uuid :as uuid]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
- [app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries-helpers :as dwlh]
@@ -55,26 +54,22 @@
(defn resolve-instance
[state root-inst-id]
- (let [page (thp/current-page state)
- root-inst (cph/get-shape page root-inst-id)
- shapes-inst (cph/get-object-with-children
- root-inst-id
- (:objects page))]
-
+ (let [page (thp/current-page state)
+ root-inst (cph/get-shape page root-inst-id)
+ shapes-inst (cph/get-children-with-self (:objects page)
+ root-inst-id)]
;; Validate that the instance tree is well constructed
- (t/is (is-instance-root (first shapes-inst)))
+ (is-instance-root (first shapes-inst))
(run! is-instance-child (rest shapes-inst))
shapes-inst))
(defn resolve-noninstance
[state root-inst-id]
- (let [page (thp/current-page state)
- root-inst (cph/get-shape page root-inst-id)
- shapes-inst (cph/get-object-with-children
- root-inst-id
- (:objects page))]
-
+ (let [page (thp/current-page state)
+ root-inst (cph/get-shape page root-inst-id)
+ shapes-inst (cph/get-children-with-self (:objects page)
+ root-inst-id)]
;; Validate that the tree is not an instance
(run! is-noninstance shapes-inst)
@@ -82,31 +77,23 @@
(defn resolve-instance-and-main
[state root-inst-id]
- (let [page (thp/current-page state)
- root-inst (cph/get-shape page root-inst-id)
+ (let [page (thp/current-page state)
+ root-inst (cph/get-shape page root-inst-id)
- file (dwlh/get-local-file state)
- component (cph/get-component
- (:component-id root-inst)
- (:id file)
- file
- nil)
+ libs (dwlh/get-libraries state)
+ component (cph/get-component libs (:component-id root-inst))
- shapes-inst (cph/get-object-with-children
- root-inst-id
- (:objects page))
- shapes-main (cph/get-object-with-children
- (:shape-ref root-inst)
- (:objects component))
+ shapes-inst (cph/get-children-with-self (:objects page) root-inst-id)
+ shapes-main (cph/get-children-with-self (:objects component) (:shape-ref root-inst))
- unique-refs (into #{} (map :shape-ref shapes-inst))
+ unique-refs (into #{} (map :shape-ref) shapes-inst)
- main-exists? (fn [shape]
- (t/is (some #(= (:id %) (:shape-ref shape))
- shapes-main)))]
+ main-exists? (fn [shape]
+ (t/is (some #(= (:id %) (:shape-ref shape))
+ shapes-main)))]
;; Validate that the instance tree is well constructed
- (t/is (is-instance-root (first shapes-inst)))
+ (is-instance-root (first shapes-inst))
(run! is-instance-child (rest shapes-inst))
(run! is-noninstance shapes-main)
(t/is (= (count shapes-inst)
@@ -118,20 +105,11 @@
(defn resolve-component
[state component-id]
- (let [page (thp/current-page state)
-
- file (dwlh/get-local-file state)
- component (cph/get-component
- component-id
- (:id file)
- file
- nil)
-
- root-main (cph/get-component-root
- component)
- shapes-main (cph/get-object-with-children
- (:id root-main)
- (:objects component))]
+ (let [page (thp/current-page state)
+ libs (dwlh/get-libraries state)
+ component (cph/get-component libs component-id)
+ root-main (cph/get-component-root component)
+ shapes-main (cph/get-children-with-self (:objects component) (:id root-main))]
;; Validate that the component tree is well constructed
(run! is-noninstance shapes-main)
diff --git a/frontend/test/app/test_helpers/pages.cljs b/frontend/test/app/test_helpers/pages.cljs
index 9f3af5f33d..e16fed14b2 100644
--- a/frontend/test/app/test_helpers/pages.cljs
+++ b/frontend/test/app/test_helpers/pages.cljs
@@ -66,7 +66,7 @@
([state label type] (sample-shape state type {}))
([state label type props]
(let [page (current-page state)
- frame (cph/get-top-frame (:objects page))
+ frame (cph/get-frame (:objects page))
shape (-> (cp/make-minimal-shape type)
(gsh/setup {:x 0 :y 0 :width 1 :height 1})
(merge props))]
diff --git a/frontend/test/app/util/snap_data_test.cljs b/frontend/test/app/util/snap_data_test.cljs
new file mode 100644
index 0000000000..6982d4f6f5
--- /dev/null
+++ b/frontend/test/app/util/snap_data_test.cljs
@@ -0,0 +1,431 @@
+;; 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.snap-data-test
+ (:require
+ [app.common.uuid :as uuid]
+ [cljs.test :as t :include-macros true]
+ [cljs.pprint :refer [pprint]]
+ [app.common.pages.init :as init]
+ [app.common.file-builder :as fb]
+ [app.util.snap-data :as sd]))
+
+(t/deftest test-create-index
+ (t/testing "Create empty data"
+ (let [data (sd/make-snap-data)]
+ (t/is (some? data))))
+
+ (t/testing "Add empty page (only root-frame)"
+ (let [page (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/get-current-page))
+
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))]
+ (t/is (some? data))))
+
+ (t/testing "Create simple shape on root"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/create-rect
+ {:x 0
+ :y 0
+ :width 100
+ :height 100}))
+ page (fb/get-current-page file)
+
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ result-x (sd/query data (:id page) uuid/zero :x [0 100])]
+
+ (t/is (some? data))
+
+ ;; 3 = left side, center and right side
+ (t/is (= (count result-x) 3))
+
+ ;; Left side: two points
+ (t/is (= (first (nth result-x 0)) 0))
+
+ ;; Center one point
+ (t/is (= (first (nth result-x 1)) 50))
+
+ ;; Right side two points
+ (t/is (= (first (nth result-x 2)) 100))))
+
+ (t/testing "Add page with single empty frame"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard
+ {:x 0
+ :y 0
+ :width 100
+ :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
+
+ (t/is (some? data))
+ (t/is (= (count result-zero-x) 3))
+ (t/is (= (count result-frame-x) 3))))
+
+ (t/testing "Add page with some shapes inside frames"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard
+ {:x 0
+ :y 0
+ :width 100
+ :height 100}))
+ frame-id (:last-id file)
+
+ file (-> file
+ (fb/create-rect
+ {:x 25
+ :y 25
+ :width 50
+ :height 50})
+ (fb/close-artboard))
+
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
+
+ (t/is (some? data))
+ (t/is (= (count result-zero-x) 3))
+ (t/is (= (count result-frame-x) 5))))
+
+ (t/testing "Add a global guide"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-guide {:position 50 :axis :x})
+ (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])
+ result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
+
+ (t/is (some? data))
+ ;; We can snap in the root
+ (t/is (= (count result-zero-x) 1))
+ (t/is (= (count result-zero-y) 0))
+
+ ;; We can snap in the frame
+ (t/is (= (count result-frame-x) 1))
+ (t/is (= (count result-frame-y) 0))))
+
+ (t/testing "Add a frame guide"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+
+ file (-> file
+ (fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
+
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])
+ result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
+ (t/is (some? data))
+ ;; We can snap in the root
+ (t/is (= (count result-zero-x) 0))
+ (t/is (= (count result-zero-y) 0))
+
+ ;; We can snap in the frame
+ (t/is (= (count result-frame-x) 1))
+ (t/is (= (count result-frame-y) 0)))))
+
+(t/deftest test-update-index
+ (t/testing "Create frame on root and then remove it."
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard
+ {:x 0
+ :y 0
+ :width 100
+ :height 100})
+ (fb/close-artboard))
+
+ shape-id (:last-id file)
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ file (-> file
+ (fb/delete-object shape-id))
+
+ new-page (fb/get-current-page file)
+ data (sd/update-page data page new-page)
+
+ result-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-y (sd/query data (:id page) uuid/zero :y [0 100])]
+
+ (t/is (some? data))
+ (t/is (= (count result-x) 0))
+ (t/is (= (count result-y) 0))))
+
+ (t/testing "Create simple shape on root. Then remove it"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/create-rect
+ {:x 0
+ :y 0
+ :width 100
+ :height 100}))
+
+ shape-id (:last-id file)
+ page (fb/get-current-page file)
+
+ ;; frame-id (:last-id file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ file (fb/delete-object file shape-id)
+
+ new-page (fb/get-current-page file)
+ data (sd/update-page data page new-page)
+
+ result-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-y (sd/query data (:id page) uuid/zero :y [0 100])]
+
+ (t/is (some? data))
+ (t/is (= (count result-x) 0))
+ (t/is (= (count result-y) 0))))
+
+ (t/testing "Create shape inside frame, then remove it"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard
+ {:x 0
+ :y 0
+ :width 100
+ :height 100}))
+ frame-id (:last-id file)
+
+ file (fb/create-rect file {:x 25 :y 25 :width 50 :height 50})
+ shape-id (:last-id file)
+
+ file (fb/close-artboard file)
+
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data)
+ (sd/add-page page))
+
+ file (fb/delete-object file shape-id)
+ new-page (fb/get-current-page file)
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
+
+ (t/is (some? data))
+ (t/is (= (count result-zero-x) 3))
+ (t/is (= (count result-frame-x) 3))))
+
+ (t/testing "Create global guide then remove it"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-guide {:position 50 :axis :x}))
+
+ guide-id (:last-id file)
+
+ file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data) (sd/add-page page))
+
+ new-page (-> (fb/delete-guide file guide-id)
+ (fb/get-current-page))
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])
+ result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
+
+ (t/is (some? data))
+ ;; We can snap in the root
+ (t/is (= (count result-zero-x) 0))
+ (t/is (= (count result-zero-y) 0))
+
+ ;; We can snap in the frame
+ (t/is (= (count result-frame-x) 0))
+ (t/is (= (count result-frame-y) 0))))
+
+ (t/testing "Create frame guide then remove it"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard {:x 200 :y 200 :width 100 :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
+ guide-id (:last-id file)
+
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data) (sd/add-page page))
+
+ new-page (-> (fb/delete-guide file guide-id)
+ (fb/get-current-page))
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
+ result-frame-x (sd/query data (:id page) frame-id :x [0 100])
+ result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
+ (t/is (some? data))
+ ;; We can snap in the root
+ (t/is (= (count result-zero-x) 0))
+ (t/is (= (count result-zero-y) 0))
+
+ ;; We can snap in the frame
+ (t/is (= (count result-frame-x) 0))
+ (t/is (= (count result-frame-y) 0))))
+
+ (t/testing "Update frame coordinates"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-artboard
+ {:x 0
+ :y 0
+ :width 100
+ :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data) (sd/add-page page))
+
+ frame (fb/lookup-shape file frame-id)
+ new-frame (-> frame
+ (assoc :x 200 :y 200))
+
+ file (fb/update-object file frame new-frame)
+ new-page (fb/get-current-page file)
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
+ result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100])
+ result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])
+ result-frame-x-2 (sd/query data (:id page) frame-id :x [200 300])]
+
+ (t/is (some? data))
+ (t/is (= (count result-zero-x-1) 0))
+ (t/is (= (count result-frame-x-1) 0))
+ (t/is (= (count result-zero-x-2) 3))
+ (t/is (= (count result-frame-x-2) 3))))
+
+ (t/testing "Update shape coordinates"
+ (let [file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/create-rect
+ {:x 0
+ :y 0
+ :width 100
+ :height 100}))
+
+ shape-id (:last-id file)
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data) (sd/add-page page))
+
+ shape (fb/lookup-shape file shape-id)
+ new-shape (-> shape
+ (assoc :x 200 :y 200))
+
+ file (fb/update-object file shape new-shape)
+ new-page (fb/get-current-page file)
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-x-2 (sd/query data (:id page) uuid/zero :x [200 300])]
+
+ (t/is (some? data))
+ (t/is (= (count result-zero-x-1) 0))
+ (t/is (= (count result-zero-x-2) 3))))
+
+ (t/testing "Update global guide"
+ (let [guide {:position 50 :axis :x}
+ file (-> (fb/create-file "Test")
+ (fb/add-page {:name "Page-1"})
+ (fb/add-guide guide))
+
+ guide-id (:last-id file)
+ guide (assoc guide :id guide-id)
+
+ file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100})
+ (fb/close-artboard))
+
+ frame-id (:last-id file)
+ page (fb/get-current-page file)
+ data (-> (sd/make-snap-data) (sd/add-page page))
+
+ new-page (-> (fb/update-guide file (assoc guide :position 150))
+ (fb/get-current-page))
+
+ data (sd/update-page data page new-page)
+
+ result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
+ result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100])
+ result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100])
+ result-frame-y-1 (sd/query data (:id page) frame-id :y [0 100])
+
+ result-zero-x-2 (sd/query data (:id page) uuid/zero :x [0 200])
+ result-zero-y-2 (sd/query data (:id page) uuid/zero :y [0 200])
+ result-frame-x-2 (sd/query data (:id page) frame-id :x [0 200])
+ result-frame-y-2 (sd/query data (:id page) frame-id :y [0 200])
+ ]
+
+ (t/is (some? data))
+
+ (t/is (= (count result-zero-x-1) 0))
+ (t/is (= (count result-zero-y-1) 0))
+ (t/is (= (count result-frame-x-1) 0))
+ (t/is (= (count result-frame-y-1) 0))
+
+ (t/is (= (count result-zero-x-2) 1))
+ (t/is (= (count result-zero-y-2) 0))
+ (t/is (= (count result-frame-x-2) 1))
+ (t/is (= (count result-frame-y-2) 0)))))
diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po
index af16177e50..af65d41fe1 100644
--- a/frontend/translations/ca.po
+++ b/frontend/translations/ca.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
-"PO-Revision-Date: 2021-12-06 11:26+0000\n"
-"Last-Translator: Rubรฉn \n"
+"PO-Revision-Date: 2022-01-20 20:55+0000\n"
+"Last-Translator: Rubรฉn \n"
"Language-Team: Catalan "
"\n"
"Language: ca\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.10-dev\n"
+"X-Generator: Weblate 4.11-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -234,7 +234,7 @@ msgstr "+ Crea un equip nou"
#: src/app/main/ui/dashboard/sidebar.cljs
msgid "dashboard.default-team-name"
-msgstr "El vostre Penpot"
+msgstr "El meu Penpot"
#: src/app/main/ui/dashboard/sidebar.cljs
msgid "dashboard.delete-team"
@@ -267,10 +267,10 @@ msgid "dashboard.export-frames"
msgstr "Exporta les taules de treball a PDF..."
msgid "dashboard.export-multi"
-msgstr "Exporta %s fitxers"
+msgstr "Exporta %s fitxers de Penpot"
msgid "dashboard.export-single"
-msgstr "Exporta el fitxer"
+msgstr "Exporta el fitxer de Penpot"
msgid "dashboard.export.detail"
msgstr "* Pot incloure components, grร fics, colors i/o tipografies."
@@ -312,9 +312,19 @@ msgstr "Exporta els fitxers"
msgid "dashboard.fonts.deleted-placeholder"
msgstr "S'ha eliminat el tipus de lletra"
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.dismiss-all"
+msgstr "Descarta-ho tot"
+
msgid "dashboard.fonts.empty-placeholder"
msgstr "Encara no teniu cap tipus de lletra personalitzat instalยทlat."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.fonts-added"
+msgid_plural "dashboard.fonts.fonts-added"
+msgstr[0] "S'ha afegit 1 tipografia"
+msgstr[1] "S'han afegit %s tipografies"
+
#, markdown
msgid "dashboard.fonts.hero-text1"
msgstr ""
@@ -333,8 +343,12 @@ msgstr ""
"Penpot](https://penpot.app/terms.html). Tambรฉ podeu llegir sobre les "
"[llicรจncies dels tipus de lletra](https://www.typography.com/faq)."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.upload-all"
+msgstr "Puja-ho tot"
+
msgid "dashboard.import"
-msgstr "Importa fitxers"
+msgstr "Importa fitxers de Penpot"
msgid "dashboard.import.analyze-error"
msgstr "Vaja! No s'ha pogut importar aquest fitxer"
@@ -345,6 +359,9 @@ msgstr "S'ha produรฏt un problema en importar el fitxer i no s'ha importat."
msgid "dashboard.import.import-message"
msgstr "S'han importat %s fitxers correctament."
+msgid "dashboard.import.import-warning"
+msgstr "Alguns fitxers contenen objectes no vร lids que s'han eliminat."
+
msgid "dashboard.import.progress.process-colors"
msgstr "S'estan carregant els colors"
@@ -518,7 +535,7 @@ msgstr "S'ha mogut el projecte"
#: src/app/main/ui/dashboard/sidebar.cljs
msgid "dashboard.switch-team"
-msgstr "Canvia d'equip"
+msgstr "Canvieu d'equip"
#: src/app/main/ui/dashboard/team.cljs
msgid "dashboard.team-info"
@@ -562,7 +579,7 @@ msgstr "Nom"
#: src/app/main/ui/dashboard/search.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/libraries.cljs, src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs
msgid "dashboard.your-penpot"
-msgstr "El vostre Penpot"
+msgstr "El meu Penpot"
#: src/app/main/ui/confirm.cljs
msgid "ds.confirm-cancel"
@@ -580,6 +597,9 @@ msgstr "N'esteu segur?"
msgid "ds.updated-at"
msgstr "Actualitzat: %s"
+msgid "errors.auth.unable-to-login"
+msgstr "Sembla que no esteu autenticat o que la sessiรณ ha caducat."
+
#: src/app/main/data/workspace.cljs
msgid "errors.clipboard-not-implemented"
msgstr "El vostre navegador no pot fer aquesta operaciรณ"
@@ -660,6 +680,19 @@ msgstr ""
msgid "errors.registration-disabled"
msgstr "El registre estร desactivat."
+msgid "errors.team-leave.insufficient-members"
+msgstr ""
+"No hi ha suficients membres com per a abandonar l'equip, potser voleu "
+"eliminar-lo."
+
+msgid "errors.team-leave.member-does-not-exists"
+msgstr "El membre que intenteu assignar no existeix."
+
+msgid "errors.team-leave.owner-cant-leave"
+msgstr ""
+"El propietari no pot abandonar l'equip, heu de reassignar el rol de "
+"propietat."
+
msgid "errors.terms-privacy-agreement-invalid"
msgstr ""
"Heu d'acceptar les nostres condicions del servei i la polรญtica de "
@@ -2451,6 +2484,26 @@ msgstr "Acciรณ"
msgid "workspace.options.interaction-after-delay"
msgstr "Desprรฉs de"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation"
+msgstr "Animaciรณ"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-dissolve"
+msgstr "Dissol"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-none"
+msgstr "Cap"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-push"
+msgstr "Empenta"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-slide"
+msgstr "Lliscament"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-background"
msgstr "Afegeix una superposiciรณ de fons"
@@ -2475,6 +2528,14 @@ msgstr "Retard"
msgid "workspace.options.interaction-destination"
msgstr "Destinaciรณ"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-duration"
+msgstr "Durada"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-linear"
+msgstr "Lineal"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-mouse-enter"
msgstr "El ratolรญ entra"
@@ -2499,6 +2560,10 @@ msgstr "Navega a: %s"
msgid "workspace.options.interaction-none"
msgstr "(sense definir)"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-offset-effect"
+msgstr "Efecte de desplaรงament"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-on-click"
msgstr "En fer clic"
@@ -3000,6 +3065,10 @@ msgstr "Envia-ho darrere"
msgid "workspace.shape.menu.copy"
msgstr "Copia"
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.create-artboard-from-selection"
+msgstr "Selecciona a la taula de treball"
+
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.create-component"
msgstr "Crea un component"
@@ -3020,6 +3089,10 @@ msgstr "Elimina l'inici del flux"
msgid "workspace.shape.menu.detach-instance"
msgstr "Desconnecta la instร ncia"
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.detach-instances-in-bulk"
+msgstr "Desenganxa les instร ncies"
+
msgid "workspace.shape.menu.difference"
msgstr "Diferรจncia"
@@ -3127,7 +3200,7 @@ msgstr "Historial (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Capes (%s)"
+msgstr "Capes"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3143,7 +3216,7 @@ msgstr "Mapa del lloc"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Recursos (%s)"
+msgstr "Recursos"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/de.po b/frontend/translations/de.po
index f9389599f6..beafaf1319 100644
--- a/frontend/translations/de.po
+++ b/frontend/translations/de.po
@@ -3032,7 +3032,7 @@ msgstr "Verlauf (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Ebenen (%s)"
+msgstr "Ebenen"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3048,7 +3048,7 @@ msgstr "Sitemap"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Assets (%s)"
+msgstr "Assets"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/el.po b/frontend/translations/el.po
index 8ad56d4dba..f3100b1c49 100644
--- a/frontend/translations/el.po
+++ b/frontend/translations/el.po
@@ -2163,7 +2163,7 @@ msgstr "ฮฯฯฮฟฯฮนฮบฯ (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "ฯฯฯฯฯฮตฮนฯ (%s)"
+msgstr "ฯฯฯฯฯฮตฮนฯ"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -2179,7 +2179,7 @@ msgstr "ฮงฮฌฯฯฮทฯ ฮนฯฯฮฟฯฯฯฮฟฯ
"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "ฮฃฯฮฟฮนฯฮตฮฏฮฑ (%s)"
+msgstr "ฮฃฯฮฟฮนฯฮตฮฏฮฑ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 7143899ed1..6ea9663f6c 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -257,9 +257,8 @@ msgstr "You still have no files here"
#, markdown
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
-"There are no files here yet. If you want to try some templates you can go "
-"to the [Libraries & templates "
-"section](https://penpot.app/libraries-templates.html)"
+"Oh no! You have no files yet! If you want to try with some templates go "
+"to [Libraries & templates](https://penpot.app/libraries-templates.html)"
msgid "dashboard.export-frames"
msgstr "Export artboards to PDF..."
@@ -576,6 +575,10 @@ msgstr "Your name"
msgid "dashboard.your-penpot"
msgstr "Your Penpot"
+#: src/app/main/ui/confirm.cljs
+msgid "ds.component-subtitle"
+msgstr "Components to update:"
+
#: src/app/main/ui/confirm.cljs
msgid "ds.confirm-cancel"
msgstr "Cancel"
@@ -607,6 +610,9 @@ msgstr "Email already used"
msgid "errors.email-already-validated"
msgstr "Email already validated."
+msgid "errors.email-as-password"
+msgstr "You can't use your email as password"
+
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs
msgid "errors.email-has-permanent-bounces"
msgstr "The email ยซ%sยป has many permanent bounce reports."
@@ -966,6 +972,10 @@ msgstr "Info"
msgid "history.alert-message"
msgstr "You are seeing version %s"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.about-penpot"
+msgstr "About Penpot"
+
msgid "labels.accept"
msgstr "Accept"
@@ -1101,6 +1111,10 @@ msgstr "Give feedback"
msgid "labels.go-back"
msgstr "Go back"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.help-center"
+msgstr "Help Center"
+
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
msgid "labels.hide-resolved-comments"
msgstr "Hide resolved comments"
@@ -1128,6 +1142,10 @@ msgstr "Internal Error"
msgid "labels.language"
msgstr "Language"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.libraries-and-templates"
+msgstr "Libraries & Templates"
+
msgid "labels.link"
msgstr "Link"
@@ -1209,7 +1227,7 @@ msgstr "Password"
msgid "labels.permissions"
msgstr "Permissions"
-#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+#: src/app/main/ui/settings/sidebar.cljs
msgid "labels.profile"
msgstr "Profile"
@@ -1323,6 +1341,10 @@ msgstr "Workspace"
msgid "labels.write-new-comment"
msgstr "Write new comment"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.your-account"
+msgstr "Your account"
+
#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
msgid "media.loading"
msgstr "Loading imageโฆ"
@@ -1341,6 +1363,10 @@ msgstr ""
msgid "modals.add-shared-confirm.message"
msgstr "Add โ%sโ as Shared Library"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.big-nudge"
+msgstr "Big nudge"
+
#: src/app/main/ui/settings/change_email.cljs
msgid "modals.change-email.confirm-email"
msgstr "Verify new email"
@@ -1522,6 +1548,10 @@ msgstr "Are you sure you want to leave this team?"
msgid "modals.leave-confirm.title"
msgstr "Leaving team"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.nudge-title"
+msgstr "Nudge amount"
+
#: src/app/main/ui/dashboard/team.cljs
msgid "modals.promote-owner-confirm.accept"
msgstr "Promote"
@@ -1548,9 +1578,23 @@ msgstr ""
msgid "modals.remove-shared-confirm.message"
msgstr "Remove โ%sโ as Shared Library"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.small-nudge"
+msgstr "Small nudge"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component-in-bulk.hint"
+msgstr ""
+"You are about to update components in a shared library. This may affect "
+"other files that use it."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component-in-bulk.message"
+msgstr "Update components in a shared library"
+
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "modals.update-remote-component.accept"
-msgstr "Update component"
+msgstr "Update"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "modals.update-remote-component.cancel"
@@ -2021,6 +2065,10 @@ msgstr "Disable scale text"
msgid "workspace.header.menu.disable-snap-grid"
msgstr "Disable snap to grid"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-snap-guides"
+msgstr "Disable snap to guides"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.enable-dynamic-alignment"
msgstr "Enable dynamic alignment"
@@ -2033,6 +2081,10 @@ msgstr "Enable scale text"
msgid "workspace.header.menu.enable-snap-grid"
msgstr "Snap to grid"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-snap-guides"
+msgstr "Snap to guides"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.hide-artboard-names"
msgstr "Hide artboard names"
@@ -2057,6 +2109,26 @@ msgstr "Hide color palette"
msgid "workspace.header.menu.hide-rules"
msgstr "Hide rules"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-textpalette"
+msgstr "Hide fonts palette"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.edit"
+msgstr "Edit"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.file"
+msgstr "File"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.preferences"
+msgstr "Preferences"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.view"
+msgstr "View"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.select-all"
msgstr "Select all"
@@ -2085,6 +2157,10 @@ msgstr "Show color palette"
msgid "workspace.header.menu.show-rules"
msgstr "Show rules"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-textpalette"
+msgstr "Show fonts palette"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.reset-zoom"
msgstr "Reset"
@@ -2119,7 +2195,7 @@ msgstr "Fit - Scale down to fit"
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.zoom-fit-all"
-msgstr "Zoom to fil all"
+msgstr "Zoom to fit all"
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.zoom-full-screen"
@@ -2755,6 +2831,9 @@ msgstr "All corners"
msgid "workspace.options.radius.single-corners"
msgstr "Single corners"
+msgid "workspace.options.recent-fonts"
+msgstr "Recent"
+
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.rotation"
msgstr "Rotation"
@@ -3140,6 +3219,9 @@ msgstr "Group"
msgid "workspace.shape.menu.hide"
msgstr "Hide"
+msgid "workspace.shape.menu.hide-ui"
+msgstr "Show/Hide UI"
+
msgid "workspace.shape.menu.intersection"
msgstr "Intersection"
@@ -3162,6 +3244,10 @@ msgstr "Path"
msgid "workspace.shape.menu.reset-overrides"
msgstr "Reset overrides"
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.select-layer"
+msgstr "Select layer"
+
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.show"
msgstr "Show"
@@ -3188,6 +3274,10 @@ msgstr "Unlock"
msgid "workspace.shape.menu.unmask"
msgstr "Unmask"
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.update-components-in-bulk"
+msgstr "Update main components"
+
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.update-main"
msgstr "Update main component"
@@ -3198,7 +3288,7 @@ msgstr "History (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Layers (%s)"
+msgstr "Layers"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3214,7 +3304,7 @@ msgstr "Sitemap"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Assets (%s)"
+msgstr "Assets"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
@@ -3256,6 +3346,10 @@ msgstr "Rectangle (%s)"
msgid "workspace.toolbar.text"
msgstr "Text (%s)"
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.text-palette"
+msgstr "Typographies (%s)"
+
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
msgstr "There are no history changes so far"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 56b3340a4b..b5de29d457 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -261,9 +261,8 @@ msgstr "Todavรญa no hay ningรบn archivo aquรญ"
#, markdown
msgid "dashboard.empty-placeholder-drafts"
msgstr ""
-"Aรบn no tienes archivos. Si quieres probar alguna plantilla visita nuestra "
-"seccion de [Bibliotecas y "
-"plantillas](https://penpot.app/libraries-templates.html)"
+"ยกOh, no! ยกAรบn no tienes archivos! Si quieres probar con alguna plantilla ve a "
+"[Bibliotecas y plantillas](https://penpot.app/libraries-templates.html)"
msgid "dashboard.export-frames"
msgstr "Exportar tableros a PDF..."
@@ -582,6 +581,10 @@ msgstr "Tu nombre"
msgid "dashboard.your-penpot"
msgstr "Tu Penpot"
+#: src/app/main/ui/confirm.cljs
+msgid "ds.component-subtitle"
+msgstr "Componentes a actualizar:"
+
#: src/app/main/ui/confirm.cljs
msgid "ds.confirm-cancel"
msgstr "Cancelar"
@@ -610,6 +613,9 @@ msgstr "Este correo ya estรก en uso"
msgid "errors.email-already-validated"
msgstr "Este correo ya estรก validado."
+msgid "errors.email-as-password"
+msgstr "No puedes usar tu email como password"
+
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs
msgid "errors.email-has-permanent-bounces"
msgstr "El email ยซ%sยป tiene varios reportes de rebote permanente."
@@ -967,6 +973,10 @@ msgstr "Informaciรณn"
msgid "history.alert-message"
msgstr "Estรกs viendo la versiรณn %s"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.about-penpot"
+msgstr "Acerca de Penpot"
+
msgid "labels.accept"
msgstr "Aceptar"
@@ -1102,6 +1112,10 @@ msgstr "Danos tu opiniรณn"
msgid "labels.go-back"
msgstr "Volver"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.help-center"
+msgstr "Centro de ayuda"
+
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
msgid "labels.hide-resolved-comments"
msgstr "Ocultar comentarios resueltos"
@@ -1129,6 +1143,10 @@ msgstr "Error interno"
msgid "labels.language"
msgstr "Idioma"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.libraries-and-templates"
+msgstr "Bibliotecas y Plantillas"
+
msgid "labels.link"
msgstr "Enlace"
@@ -1210,7 +1228,7 @@ msgstr "Contraseรฑa"
msgid "labels.permissions"
msgstr "Permisos"
-#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+#: src/app/main/ui/settings/sidebar.cljs
msgid "labels.profile"
msgstr "Perfil"
@@ -1325,6 +1343,10 @@ msgstr "Espacio de trabajo"
msgid "labels.write-new-comment"
msgstr "Escribir un nuevo comentario"
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.your-account"
+msgstr "Tu cuenta"
+
#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
msgid "media.loading"
msgstr "Cargando imagenโฆ"
@@ -1343,6 +1365,10 @@ msgstr ""
msgid "modals.add-shared-confirm.message"
msgstr "Aรฑadir โ%sโ como Biblioteca Compartida"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.big-nudge"
+msgstr "Mรกximo"
+
#: src/app/main/ui/settings/change_email.cljs
msgid "modals.change-email.confirm-email"
msgstr "Verificar el nuevo correo"
@@ -1524,6 +1550,10 @@ msgstr "ยฟSeguro que quieres abandonar este equipo?"
msgid "modals.leave-confirm.title"
msgstr "Abandonando el equipo"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.nudge-title"
+msgstr "Desplazamiento"
+
#: src/app/main/ui/dashboard/team.cljs
msgid "modals.promote-owner-confirm.accept"
msgstr "Promocionar"
@@ -1550,9 +1580,23 @@ msgstr ""
msgid "modals.remove-shared-confirm.message"
msgstr "Aรฑadir โ%sโ como Biblioteca Compartida"
+#: src/app/main/ui/workspace/nudge.cljs
+msgid "modals.small-nudge"
+msgstr "Mรญnimo"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component-in-bulk.hint"
+msgstr ""
+"Vas a actualizar componentes en una librerรญa compartida. Esto puede afectar "
+"a otros archivos que la usen."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component-in-bulk.message"
+msgstr "Actualizar componentes en librerรญa"
+
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "modals.update-remote-component.accept"
-msgstr "Actualizar componente"
+msgstr "Actualizar"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "modals.update-remote-component.cancel"
@@ -2036,6 +2080,10 @@ msgstr "Desactivar escalar texto"
msgid "workspace.header.menu.disable-snap-grid"
msgstr "Desactivar alinear a la rejilla"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-snap-guides"
+msgstr "Desactivar alinear a las guias"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.enable-dynamic-alignment"
msgstr "Activar alineamiento dinรกmico"
@@ -2048,6 +2096,10 @@ msgstr "Activar escalar texto"
msgid "workspace.header.menu.enable-snap-grid"
msgstr "Alinear a la rejilla"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-snap-guides"
+msgstr "Alinear a las guias"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.hide-artboard-names"
msgstr "Ocultar nombres de tableros"
@@ -2072,6 +2124,26 @@ msgstr "Ocultar paleta de colores"
msgid "workspace.header.menu.hide-rules"
msgstr "Ocultar reglas"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-textpalette"
+msgstr "Ocultar paleta de textos"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.edit"
+msgstr "Editar"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.file"
+msgstr "Archivo"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.preferences"
+msgstr "Preferencias"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.option.view"
+msgstr "Ver"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.select-all"
msgstr "Seleccionar todo"
@@ -2100,6 +2172,10 @@ msgstr "Mostrar paleta de colores"
msgid "workspace.header.menu.show-rules"
msgstr "Mostrar reglas"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-textpalette"
+msgstr "Mostrar paleta de textos"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.reset-zoom"
msgstr "Restablecer"
@@ -2766,6 +2842,9 @@ msgstr "Todas las esquinas"
msgid "workspace.options.radius.single-corners"
msgstr "Esquinas individuales"
+msgid "workspace.options.recent-fonts"
+msgstr "Recientes"
+
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
msgid "workspace.options.rotation"
msgstr "Rotaciรณn"
@@ -3153,6 +3232,9 @@ msgstr "Agrupar"
msgid "workspace.shape.menu.hide"
msgstr "Ocultar"
+msgid "workspace.shape.menu.hide-ui"
+msgstr "Mostrar/Ocultar Interfaz"
+
msgid "workspace.shape.menu.intersection"
msgstr "Intersecciรณn"
@@ -3175,6 +3257,10 @@ msgstr "Path"
msgid "workspace.shape.menu.reset-overrides"
msgstr "Deshacer modificaciones"
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.select-layer"
+msgstr "Seleccionar capa"
+
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.show"
msgstr "Mostrar"
@@ -3201,6 +3287,10 @@ msgstr "Desbloquear"
msgid "workspace.shape.menu.unmask"
msgstr "Quitar mรกscara"
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.update-components-in-bulk"
+msgstr "Actualizar componentes"
+
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.update-main"
msgstr "Actualizar componente principal"
@@ -3211,7 +3301,7 @@ msgstr "Historial (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Capas (%s)"
+msgstr "Capas"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3227,7 +3317,7 @@ msgstr "Mapa del sitio"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Recursos (%s)"
+msgstr "Recursos"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
@@ -3269,6 +3359,10 @@ msgstr "Rectรกngulo (%s)"
msgid "workspace.toolbar.text"
msgstr "Texto (%s)"
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.text-palette"
+msgstr "Tipografรญas (%s)"
+
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
msgstr "Todavรญa no hay cambios en el histรณrico"
diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po
index 70f85c1fe9..69fc424656 100644
--- a/frontend/translations/fr.po
+++ b/frontend/translations/fr.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"PO-Revision-Date: 2021-08-10 23:33+0000\n"
+"PO-Revision-Date: 2022-01-20 20:55+0000\n"
"Last-Translator: Voxybuns \n"
"Language-Team: French "
"\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
-"X-Generator: Weblate 4.8-dev\n"
+"X-Generator: Weblate 4.11-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -91,6 +91,10 @@ msgstr "Se connecter via OpenID (SSO)"
msgid "auth.new-password"
msgstr "Saisissez un nouveau mot de passe"
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.newsletter-subscription"
+msgstr "Je souhaite m'abonner ร la newsletter de Penpot."
+
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.notifications.invalid-token-error"
msgstr "Le code de rรฉcupรฉration nโest pas valide."
@@ -171,6 +175,47 @@ msgstr ""
msgid "auth.verification-email-sent"
msgstr "Nous avons envoyรฉ un e-mail de vรฉrification ร "
+msgid "common.share-link.confirm-deletion-link-description"
+msgstr ""
+"รtes-vous certain de vouloir supprimer ce lien ? Si oui, plus personne ne "
+"pourra y accรฉder"
+
+msgid "common.share-link.get-link"
+msgstr "Obtenir le lien"
+
+msgid "common.share-link.link-copied-success"
+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"
msgstr "Ajouter une Bibliothรจque Partagรฉe"
@@ -210,12 +255,25 @@ msgstr "Dupliquer %s fichiers"
msgid "dashboard.empty-files"
msgstr "Vous nโavez encore aucun fichier ici"
+msgid "dashboard.export-frames"
+msgstr "Exporter les plans de travail comme PDF..."
+
msgid "dashboard.export-multi"
msgstr "Exporter %s fichiers"
msgid "dashboard.export-single"
msgstr "Exporter le fichier"
+msgid "dashboard.export.detail"
+msgstr ""
+"* Peut inclure les composants, รฉlรฉments graphiques, couleurs et/ou polices "
+"de caractรจre."
+
+msgid "dashboard.export.explain"
+msgstr ""
+"Un ou plusieurs fichiers que vous souhaitez exporter utilisent des "
+"bibliothรจques partagรฉes. Que voulez-vous faire avec leurs ressources ?"
+
msgid "dashboard.fonts.deleted-placeholder"
msgstr "Police supprimรฉe"
@@ -2535,7 +2593,7 @@ msgstr "Historique (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Calques (%s)"
+msgstr "Calques"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -2551,7 +2609,7 @@ msgstr "Plan du site"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Ressources (%s)"
+msgstr "Ressources"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/he.po b/frontend/translations/he.po
index 01956021b7..5d3be3b379 100644
--- a/frontend/translations/he.po
+++ b/frontend/translations/he.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"PO-Revision-Date: 2021-11-25 13:50+0000\n"
+"PO-Revision-Date: 2022-01-15 16:53+0000\n"
"Last-Translator: Yaron Shahrabani \n"
"Language-Team: Hebrew "
"\n"
@@ -10,7 +10,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
"n % 10 == 0) ? 2 : 3));\n"
-"X-Generator: Weblate 4.10-dev\n"
+"X-Generator: Weblate 4.10.1\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -259,10 +259,10 @@ msgid "dashboard.export-frames"
msgstr "ืืืฆืื ืืืืืช ืืืื ืืช ืึพPDFโฆ"
msgid "dashboard.export-multi"
-msgstr "ืืืฆืื ืงืืืฆื %s"
+msgstr "ืืืฆืื ืงืืืฆื %s ืฉื Penpot"
msgid "dashboard.export-single"
-msgstr "ืืืฆืื ืงืืืฅ"
+msgstr "ืืืฆืื ืงืืืฅ Penpot"
msgid "dashboard.export.detail"
msgstr "* ืขืฉืื ืืืืื ืจืืืืื, ืืจืคืืงื, ืฆืืขืื ื/ืื ืืืคืืืจืคืืืช."
@@ -296,9 +296,21 @@ msgstr "ืืืฆืื ืงืืฆืื"
msgid "dashboard.fonts.deleted-placeholder"
msgstr "ืืืืคื ื ืืืง"
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.dismiss-all"
+msgstr "ืืืชืขืื ืืืืื"
+
msgid "dashboard.fonts.empty-placeholder"
msgstr "ืขืืืื ืื ืืืชืงื ืื ืืฆืื ืืืคื ืื ืืฉืื."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.fonts-added"
+msgid_plural "dashboard.fonts.fonts-added"
+msgstr[0] "ื ืืกืฃ ืืืคื"
+msgstr[1] "ื ืืกืคื 2 ืืืคื ืื"
+msgstr[2] "ื ืืกืคื %s ืืืคื ืื"
+msgstr[3] "ื ืืกืคื %s ืืืคื ืื"
+
#, markdown
msgid "dashboard.fonts.hero-text1"
msgstr ""
@@ -315,8 +327,12 @@ msgstr ""
"Penpot](https://penpot.app/terms.html). ืืคืฉืจ ืื ืืงืจืื ืื ืขื [ืจืืฉืื "
"ืืืคื ืื](https://www.typography.com/faq)."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.upload-all"
+msgstr "ืืืขืืืช ืืืื"
+
msgid "dashboard.import"
-msgstr "ืืืืื ืงืืฆืื"
+msgstr "ืืืืื ืงืืืฆื Penpot"
msgid "dashboard.import.analyze-error"
msgstr "ืืืคืก! ืื ืืฆืืื ื ืืืืื ืืช ืืงืืืฅ ืืื"
@@ -327,6 +343,9 @@ msgstr "ืืืจืขื ืชืงืื ืืืืืื ืืงืืืฅ. ืืื ืื ืืืืื."
msgid "dashboard.import.import-message"
msgstr "%s ืงืืฆืื ืขืืจื ืืืืื ืืจืืื."
+msgid "dashboard.import.import-warning"
+msgstr "ืืืง ืืืงืืฆืื ืืืืื ืคืจืืืื ืฉืืืืื ืฉืืืกืจื."
+
msgid "dashboard.import.progress.process-colors"
msgstr "ืขืืืื ืฆืืขืื"
@@ -562,6 +581,9 @@ msgstr "ืืืืฉืื?"
msgid "ds.updated-at"
msgstr "ืขืืืื: %s"
+msgid "errors.auth.unable-to-login"
+msgstr "ื ืจืื ืฉืื ืขืืจืช ืืืืืช ืื ืฉืชืืงืฃ ืืืคืขืื ืคื."
+
#: src/app/main/data/workspace.cljs
msgid "errors.clipboard-not-implemented"
msgstr "ืืืคืืคื ืฉืื ืื ืืืื ืืืฆืข ืืช ืืคืขืืื ืืืืช"
@@ -636,6 +658,15 @@ msgstr "ืืืืขืืช ืืืืืดื ืืคืจืืคืื ืฉืื ืืืฉืชืงืืช (ืืืื
msgid "errors.registration-disabled"
msgstr "ืืืจืฉืื ืืืฉืืชืช ืืจืืข."
+msgid "errors.team-leave.insufficient-members"
+msgstr "ืืื ืืกืคืืง ืืืจืื ืืื ืืขืืื ืืช ืืฆืืืช, ืื ืจืื ืืืื ืขืืื ืืืืืง ืืืชื."
+
+msgid "errors.team-leave.member-does-not-exists"
+msgstr "ืืืืจ ืฉื ืืกืืช ืืืงืฆืืช ืื ืงืืื."
+
+msgid "errors.team-leave.owner-cant-leave"
+msgstr "ืืืขืืื ืื ืืืืืื ืืขืืื ืืช ืืงืืืฆื, ืขืืื ืืืขืืืจ ืืช ืชืคืงืื ืืืขืืืช."
+
msgid "errors.terms-privacy-agreement-invalid"
msgstr "ืขืืื ืืงืื ืืช ืชื ืื ืืฉืืจืืช ืืืช ืืืื ืืืช ืืคืจืืืืช."
@@ -1766,7 +1797,7 @@ msgid "viewer.frame-not-found"
msgstr "ืืื ืืืืื ืืช ืื ื ืืฆื."
msgid "viewer.header.comments-section"
-msgstr "ืืขืจืืช"
+msgstr "ืืขืจืืช (%s)"
#: src/app/main/ui/viewer/header.cljs
msgid "viewer.header.dont-show-interactions"
@@ -1784,7 +1815,7 @@ msgid "viewer.header.interactions"
msgstr "ืืื ืืจืืงืฆืืืช"
msgid "viewer.header.interactions-section"
-msgstr "ืืื ืืจืืงืฆืืืช"
+msgstr "ืืื ืืจืืงืฆืืืช (%s)"
#: src/app/main/ui/viewer/header.cljs
msgid "viewer.header.share.copy-link"
@@ -2401,6 +2432,26 @@ msgstr "ืคืขืืื"
msgid "workspace.options.interaction-after-delay"
msgstr "ืืืืจ ืืฉืืื"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation"
+msgstr "ืื ืคืฉื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-dissolve"
+msgstr "ืืชืืืกืกืืช"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-none"
+msgstr "ืืื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-push"
+msgstr "ืืืืคื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-slide"
+msgstr "ืืืืฉื"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-background"
msgstr "ืืืกืคืช ืฉืืืช ืจืงืข"
@@ -2425,6 +2476,38 @@ msgstr "ืืฉืืื"
msgid "workspace.options.interaction-destination"
msgstr "ืืขื"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-duration"
+msgstr "ืืฉื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing"
+msgstr "ืืืืงื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease"
+msgstr "ืงืื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in"
+msgstr "ืืืืงื ืคื ืืื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in-out"
+msgstr "ืืืืงื ืคื ืืื ืืืืฆื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-out"
+msgstr "ืืืืงื ืืืืฆื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-linear"
+msgstr "ืงืืื"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-in"
+msgstr "ืคื ืืื"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-mouse-enter"
msgstr "ืื ืืกืช ืขืืืจ"
@@ -2449,6 +2532,10 @@ msgstr "ื ืืืื ืื: %s"
msgid "workspace.options.interaction-none"
msgstr "(ืื ืืืืืจ)"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-offset-effect"
+msgstr "ืืคืงื ืืืื"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-on-click"
msgstr "ืืืืืฆื"
@@ -2465,6 +2552,10 @@ msgstr "ืคืชืืืช ืฉืืืช ืขื: %s"
msgid "workspace.options.interaction-open-url"
msgstr "ืคืชืืืช ืืชืืืช"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-out"
+msgstr "ืืืืฆื"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-pos-bottom-center"
msgstr "ืืชืืชืืช ืืืจืื"
@@ -2951,6 +3042,10 @@ msgstr "ืืจืืงื ืืืืืจ"
msgid "workspace.shape.menu.copy"
msgstr "ืืขืชืงื"
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.create-artboard-from-selection"
+msgstr "ืืืืจื ืืืื ืืืื ืืช"
+
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.create-component"
msgstr "ืืฆืืจืช ืจืืื"
@@ -2971,6 +3066,10 @@ msgstr "ืืืืงืช ืืชืืืช ืืจืืื"
msgid "workspace.shape.menu.detach-instance"
msgstr "ื ืืชืืง ืืืคืข"
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.detach-instances-in-bulk"
+msgstr "ืืคืจืืช ืืืคืขืื"
+
msgid "workspace.shape.menu.difference"
msgstr "ืืืื"
@@ -3078,7 +3177,7 @@ msgstr "ืืืกืืืจืื (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "ืฉืืืืช (%s)"
+msgstr "ืฉืืืืช"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3094,7 +3193,7 @@ msgstr "ืืคืช ืืชืจ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "ืืฉืืืื (%s)"
+msgstr "ืืฉืืืื"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po
index cfdf4e3207..cd0442519e 100644
--- a/frontend/translations/pt_BR.po
+++ b/frontend/translations/pt_BR.po
@@ -1844,7 +1844,7 @@ msgstr "Histรณrico (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Camadas (%s)"
+msgstr "Camadas"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po
index a16acbce51..26c1f7e93e 100644
--- a/frontend/translations/ro.po
+++ b/frontend/translations/ro.po
@@ -2417,7 +2417,7 @@ msgstr "Istoric (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Layere (%s)"
+msgstr "Layere"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -2433,7 +2433,7 @@ msgstr "Harta site-ului"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Obiecte (%s)"
+msgstr "Obiecte"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po
index 523f8cbd30..bb1120a81b 100644
--- a/frontend/translations/tr.po
+++ b/frontend/translations/tr.po
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
-"PO-Revision-Date: 2021-11-25 13:50+0000\n"
+"PO-Revision-Date: 2022-01-15 16:53+0000\n"
"Last-Translator: Oฤuz Ersen \n"
"Language-Team: Turkish "
"\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.10-dev\n"
+"X-Generator: Weblate 4.10.1\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -267,10 +267,10 @@ msgid "dashboard.export-frames"
msgstr "รalฤฑลma yรผzeylerini PDF olarak dฤฑลarฤฑ aktar..."
msgid "dashboard.export-multi"
-msgstr "%s dosyayฤฑ dฤฑลarฤฑ aktar"
+msgstr "Penpot %s dosyalarฤฑnฤฑ dฤฑลa aktar"
msgid "dashboard.export-single"
-msgstr "Dosyayฤฑ dฤฑลarฤฑ aktar"
+msgstr "Penpot dosyasฤฑnฤฑ dฤฑลa aktar"
msgid "dashboard.export.detail"
msgstr "* Bileลenleri, grafikleri, renkleri ve/veya tipografileri iรงerebilir."
@@ -310,9 +310,19 @@ msgstr "Dosyalarฤฑ dฤฑลarฤฑ aktar"
msgid "dashboard.fonts.deleted-placeholder"
msgstr "Yazฤฑ tipi silindi"
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.dismiss-all"
+msgstr "Hepsini kapat"
+
msgid "dashboard.fonts.empty-placeholder"
msgstr "Kurulu รถzel yazฤฑ tipiniz bulunmamaktadฤฑr."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.fonts-added"
+msgid_plural "dashboard.fonts.fonts-added"
+msgstr[0] "1 yazฤฑ tipi eklendi"
+msgstr[1] "%s yazฤฑ tipi eklendi"
+
#, markdown
msgid "dashboard.fonts.hero-text1"
msgstr ""
@@ -332,8 +342,12 @@ msgstr ""
"lisanslama](https://www.typography.com/faq) hakkฤฑnda daha fazla bilgi almak "
"isteyebilirsiniz."
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "dashboard.fonts.upload-all"
+msgstr "Tรผmรผnรผ karลฤฑya yรผkle"
+
msgid "dashboard.import"
-msgstr "Dosyalarฤฑ iรงeri aktar"
+msgstr "Penpot dosyalarฤฑnฤฑ iรงe aktar"
msgid "dashboard.import.analyze-error"
msgstr "Oops! Bu dosyayฤฑ iรงeri aktaramadฤฑk"
@@ -344,6 +358,9 @@ msgstr "Dosya iรงeri aktarฤฑlฤฑrken bir sorun oluลtu. Dosya iรงeri aktarฤฑlmad
msgid "dashboard.import.import-message"
msgstr "%s dosya baลarฤฑyla iรงeri aktarฤฑldฤฑ."
+msgid "dashboard.import.import-warning"
+msgstr "Bazฤฑ dosyalar kaldฤฑrฤฑlmฤฑล geรงersiz nesneler iรงeriyordu."
+
msgid "dashboard.import.progress.process-colors"
msgstr "Renkler iลleniyor"
@@ -579,6 +596,9 @@ msgstr "Emin misin?"
msgid "ds.updated-at"
msgstr "Gรผncellendi: %s"
+msgid "errors.auth.unable-to-login"
+msgstr "Kimliฤiniz doฤrulanmamฤฑล veya oturumun sรผresi dolmuล gibi gรถrรผnรผyor."
+
#: src/app/main/data/workspace.cljs
msgid "errors.clipboard-not-implemented"
msgstr "Tarayฤฑcฤฑn bu iลlemi gerรงekleลtiremiyor"
@@ -657,6 +677,15 @@ msgstr ""
msgid "errors.registration-disabled"
msgstr "Kayฤฑt olma ลu anda devre dฤฑลฤฑ."
+msgid "errors.team-leave.insufficient-members"
+msgstr "Takฤฑmdan ayrฤฑlmak iรงin yeterli รผye yok, onu silmek isteyebilirsiniz."
+
+msgid "errors.team-leave.member-does-not-exists"
+msgstr "Atamaya รงalฤฑลtฤฑฤฤฑnฤฑz รผye mevcut deฤil."
+
+msgid "errors.team-leave.owner-cant-leave"
+msgstr "Sahip takฤฑmdan ayrฤฑlamaz, sahip rolรผnรผ yeniden atamanฤฑz gerekir."
+
msgid "errors.terms-privacy-agreement-invalid"
msgstr "Hizmet ลartlarฤฑmฤฑzฤฑ ve gizlilik politikamฤฑzฤฑ kabul etmelisin."
@@ -1858,7 +1887,7 @@ msgstr "Yatay olarak ortaya hizala (%s)"
#: src/app/main/ui/workspace/sidebar/align.cljs
msgid "workspace.align.hdistribute"
-msgstr "Yatayda daฤฤฑt"
+msgstr "Yatayda daฤฤฑt (%s)"
#: src/app/main/ui/workspace/sidebar/align.cljs
msgid "workspace.align.hleft"
@@ -1878,7 +1907,7 @@ msgstr "Dikey olarak ortaya hizala (%s)"
#: src/app/main/ui/workspace/sidebar/align.cljs
msgid "workspace.align.vdistribute"
-msgstr "Dikeyde daฤฤฑt"
+msgstr "Dikeyde daฤฤฑt (%s)"
#: src/app/main/ui/workspace/sidebar/align.cljs
msgid "workspace.align.vtop"
@@ -2433,6 +2462,26 @@ msgstr "Eylem"
msgid "workspace.options.interaction-after-delay"
msgstr "Gecikmeden sonra"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation"
+msgstr "Canlandฤฑrma"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-dissolve"
+msgstr "รรถz"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-none"
+msgstr "Hiรงbiri"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-push"
+msgstr "ฤฐt"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-animation-slide"
+msgstr "Kaydฤฑr"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-background"
msgstr "Arka plan รผst katmanฤฑ ekle"
@@ -2457,6 +2506,38 @@ msgstr "Gecikme"
msgid "workspace.options.interaction-destination"
msgstr "Hedef"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-duration"
+msgstr "Sรผre"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing"
+msgstr "Yumuลatma"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease"
+msgstr "Yumuลat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in"
+msgstr "Yumuลak giriล"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-in-out"
+msgstr "Yumuลak giriล รงฤฑkฤฑล"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-ease-out"
+msgstr "Yumuลak รงฤฑkฤฑล"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-easing-linear"
+msgstr "Doฤrusal"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-in"
+msgstr "Giriล"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-mouse-enter"
msgstr "Fare giriลi"
@@ -2481,6 +2562,10 @@ msgstr "ลuraya gidin: %s"
msgid "workspace.options.interaction-none"
msgstr "(ayarlanmadฤฑ)"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-offset-effect"
+msgstr "Uzaklฤฑk efekti"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-on-click"
msgstr "Tฤฑklandฤฑฤฤฑnda"
@@ -2497,6 +2582,10 @@ msgstr "รst katmanฤฑ aรง: %s"
msgid "workspace.options.interaction-open-url"
msgstr "URL'yi aรง"
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.interaction-out"
+msgstr "รฤฑkฤฑล"
+
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
msgid "workspace.options.interaction-pos-bottom-center"
msgstr "Alt orta"
@@ -2985,6 +3074,10 @@ msgstr "Arkaya gรถnder"
msgid "workspace.shape.menu.copy"
msgstr "Kopyala"
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.create-artboard-from-selection"
+msgstr "รalฤฑลma yรผzeyi iรงin seรงim"
+
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.create-component"
msgstr "Bileลen oluลtur"
@@ -3005,6 +3098,10 @@ msgstr "Akฤฑล baลlangฤฑcฤฑnฤฑ sil"
msgid "workspace.shape.menu.detach-instance"
msgstr "รrneฤi ayฤฑr"
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.detach-instances-in-bulk"
+msgstr "รrnekleri ayฤฑr"
+
msgid "workspace.shape.menu.difference"
msgstr "Fark"
@@ -3112,7 +3209,7 @@ msgstr "Geรงmiล (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "Katmanlar (%s)"
+msgstr "Katmanlar"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -3128,7 +3225,7 @@ msgstr "Site haritasฤฑ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "Varlฤฑklar(%s)"
+msgstr "Varlฤฑklar"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po
index c01314468d..43cfecabd2 100644
--- a/frontend/translations/zh_CN.po
+++ b/frontend/translations/zh_CN.po
@@ -2527,7 +2527,7 @@ msgstr "ๅๅฒ๏ผ%s๏ผ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.sidebar.layers"
-msgstr "ๅพๅฑ๏ผ%s๏ผ"
+msgstr "ๅพๅฑ"
#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
msgid "workspace.sidebar.options.svg-attrs.title"
@@ -2543,7 +2543,7 @@ msgstr "็ซ็นๅฐๅพ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.assets"
-msgstr "็ด ๆ๏ผ%s๏ผ"
+msgstr "็ด ๆ"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.color-palette"
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 419622455f..bb501d3ea6 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3,20 +3,25 @@
"@babel/runtime-corejs3@^7.14.9":
- version "7.15.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.4.tgz#403139af262b9a6e8f9ba04a6fdcebf8de692bf1"
- integrity sha512-lWcAqKeB624/twtTc3w6w/2o9RqJPaNBhPGK6DKLSiwuVWC7WFkypWyNg+CpZoyJH0jVzv1uMtXZ/5/lQOLtCg==
+ version "7.17.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.2.tgz#fdca2cd05fba63388babe85d349b6801b008fd13"
+ integrity sha512-NcKtr2epxfIrNM4VOmPKO46TvDMCBhgi2CrSHaEarrz+Plk2K5r9QemmOFTGpZaoKnWoGH5MO+CzeRsih/Fcgg==
dependencies:
- core-js-pure "^3.16.0"
+ core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
- version "7.14.0"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
- integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
+ version "7.17.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
+ integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
dependencies:
regenerator-runtime "^0.13.4"
+"@colors/colors@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
+ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
+
"@cypress/request@^2.88.10":
version "2.88.10"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
@@ -50,9 +55,9 @@
lodash.once "^4.1.1"
"@dabh/diagnostics@^2.0.2":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
- integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
+ integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
dependencies:
colorspace "1.1.x"
enabled "2.0.x"
@@ -77,67 +82,67 @@
normalize-path "^2.0.1"
through2 "^2.0.3"
-"@sentry/browser@^6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.16.1.tgz#4270ab0fbd1de425e339b3e7a364feb09f470a87"
- integrity sha512-F2I5RL7RTLQF9CccMrqt73GRdK3FdqaChED3RulGQX5lH6U3exHGFxwyZxSrY4x6FedfBFYlfXWWCJXpLnFkow==
+"@sentry/browser@^6.17.4":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.17.9.tgz#62eac0cc3c7c788df6b4677fe9882d3974d84027"
+ integrity sha512-RsC8GBZmZ3YfBTaIOJ06RlFp5zG7BkUoquNJmf4YhRUZeihT9osrn8qUYGFWSV/UduwKUIlSGJA/rATWWhwPRQ==
dependencies:
- "@sentry/core" "6.16.1"
- "@sentry/types" "6.16.1"
- "@sentry/utils" "6.16.1"
+ "@sentry/core" "6.17.9"
+ "@sentry/types" "6.17.9"
+ "@sentry/utils" "6.17.9"
tslib "^1.9.3"
-"@sentry/core@6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.16.1.tgz#d9f7a75f641acaddf21b6aafa7a32e142f68f17c"
- integrity sha512-UFI0264CPUc5cR1zJH+S2UPOANpm6dLJOnsvnIGTjsrwzR0h8Hdl6rC2R/GPq+WNbnipo9hkiIwDlqbqvIU5vw==
+"@sentry/core@6.17.9":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.17.9.tgz#1c09f1f101207952566349a1921d46db670c8f62"
+ integrity sha512-14KalmTholGUtgdh9TklO+jUpyQ/D3OGkhlH1rnGQGoJgFy2eYm+s+MnUEMxFdGIUCz5kOteuNqYZxaDmFagpQ==
dependencies:
- "@sentry/hub" "6.16.1"
- "@sentry/minimal" "6.16.1"
- "@sentry/types" "6.16.1"
- "@sentry/utils" "6.16.1"
+ "@sentry/hub" "6.17.9"
+ "@sentry/minimal" "6.17.9"
+ "@sentry/types" "6.17.9"
+ "@sentry/utils" "6.17.9"
tslib "^1.9.3"
-"@sentry/hub@6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.16.1.tgz#526e19db51f4412da8634734044c605b936a7b80"
- integrity sha512-4PGtg6AfpqMkreTpL7ymDeQ/U1uXv03bKUuFdtsSTn/FRf9TLS4JB0KuTZCxfp1IRgAA+iFg6B784dDkT8R9eg==
+"@sentry/hub@6.17.9":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.17.9.tgz#f2c355088a49045e49feafb5356ca5d6e1e31d3c"
+ integrity sha512-34EdrweWDbBV9EzEFIXcO+JeoyQmKzQVJxpTKZoJA6PUwf2NrndaUdjlkDEtBEzjuLUTxhLxtOzEsYs1O6RVcg==
dependencies:
- "@sentry/types" "6.16.1"
- "@sentry/utils" "6.16.1"
+ "@sentry/types" "6.17.9"
+ "@sentry/utils" "6.17.9"
tslib "^1.9.3"
-"@sentry/minimal@6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.16.1.tgz#6a9506a92623d2ff1fc17d60989688323326772e"
- integrity sha512-dq+mI1EQIvUM+zJtGCVgH3/B3Sbx4hKlGf2Usovm9KoqWYA+QpfVBholYDe/H2RXgO7LFEefDLvOdHDkqeJoyA==
+"@sentry/minimal@6.17.9":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.17.9.tgz#0edca978097b3f56463ede028395d40adbf2ae84"
+ integrity sha512-T3PMCHcKk6lkZq6zKgANrYJJxXBXKOe+ousV1Fas1rVBMv7dtKfsa4itqQHszcW9shusPDiaQKIJ4zRLE5LKmg==
dependencies:
- "@sentry/hub" "6.16.1"
- "@sentry/types" "6.16.1"
+ "@sentry/hub" "6.17.9"
+ "@sentry/types" "6.17.9"
tslib "^1.9.3"
-"@sentry/tracing@^6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.16.1.tgz#32fba3e07748e9a955055afd559a65996acb7d71"
- integrity sha512-MPSbqXX59P+OEeST+U2V/8Hu/8QjpTUxTNeNyTHWIbbchdcMMjDbXTS3etCgajZR6Ro+DHElOz5cdSxH6IBGlA==
+"@sentry/tracing@^6.17.4":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.17.9.tgz#d4a6d96d88f10c9cd496e5b32f44d6e67d4c5dc7"
+ integrity sha512-5Rb/OS4ryNJLvz2nv6wyjwhifjy6veqaF9ffLrwFYij/WDy7m62ASBblxgeiI3fbPLX0aBRFWIJAq1vko26+AQ==
dependencies:
- "@sentry/hub" "6.16.1"
- "@sentry/minimal" "6.16.1"
- "@sentry/types" "6.16.1"
- "@sentry/utils" "6.16.1"
+ "@sentry/hub" "6.17.9"
+ "@sentry/minimal" "6.17.9"
+ "@sentry/types" "6.17.9"
+ "@sentry/utils" "6.17.9"
tslib "^1.9.3"
-"@sentry/types@6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.16.1.tgz#4917607115b30315757c2cf84f80bac5100b8ac0"
- integrity sha512-Wh354g30UsJ5kYJbercektGX4ZMc9MHU++1NjeN2bTMnbofEcpUDWIiKeulZEY65IC1iU+1zRQQgtYO+/hgCUQ==
+"@sentry/types@6.17.9":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.17.9.tgz#d579c33cde0301adaf8ff4762479ad017bf0dffa"
+ integrity sha512-xuulX6qUCL14ayEOh/h6FUIvZtsi1Bx34dSOaWDrjXUOJHJAM7214uiqW1GZxPJ13YuaUIubjTSfDmSQ9CBzTw==
-"@sentry/utils@6.16.1":
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.16.1.tgz#1b9e14c2831b6e8b816f7021b9876133bf2be008"
- integrity sha512-7ngq/i4R8JZitJo9Sl8PDnjSbDehOxgr1vsoMmerIsyRZ651C/8B+jVkMhaAPgSdyJ0AlE3O7DKKTP1FXFw9qw==
+"@sentry/utils@6.17.9":
+ version "6.17.9"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.17.9.tgz#425fe9af4e2d6114c2e9aaede75ccb6ddf91fbda"
+ integrity sha512-4eo9Z3JlJCGlGrQRbtZWL+L9NnlUXgTbfK3Lk7oO8D1ev8R5b5+iE6tZHTvU5rQRcq6zu+POT+tK5u9oxc/rnQ==
dependencies:
- "@sentry/types" "6.16.1"
+ "@sentry/types" "6.17.9"
tslib "^1.9.3"
"@sindresorhus/is@^0.14.0":
@@ -153,24 +158,24 @@
defer-to-connect "^1.0.1"
"@types/node@*":
- version "16.11.11"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234"
- integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==
+ version "17.0.18"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074"
+ integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==
"@types/node@^14.14.31":
- version "14.17.34"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.34.tgz#fe4b38b3f07617c0fa31ae923fca9249641038f0"
- integrity sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==
+ version "14.18.12"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24"
+ integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==
"@types/q@^1.5.1":
- version "1.5.4"
- resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
- integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
+ integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
-"@types/sinonjs__fake-timers@^6.0.2":
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
- integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
+"@types/sinonjs__fake-timers@8.1.1":
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
+ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
"@types/sizzle@^2.3.2":
version "2.3.3"
@@ -184,6 +189,11 @@
dependencies:
"@types/node" "*"
+"@xmldom/xmldom@^0.7.5":
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
+ integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -250,16 +260,6 @@ ansi-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
-ansi-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
- integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-
-ansi-regex@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
- integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
-
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -413,9 +413,9 @@ asn1.js@^5.2.0:
safer-buffer "^2.1.0"
asn1@~0.2.3:
- version "0.2.4"
- resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
- integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
+ integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
dependencies:
safer-buffer "~2.1.0"
@@ -464,22 +464,10 @@ async-settle@^1.0.0:
dependencies:
async-done "^1.2.2"
-async@^2.6.1:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
- integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
- dependencies:
- lodash "^4.17.14"
-
-async@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
- integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
-
-async@^3.2.0:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd"
- integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==
+async@^3.2.0, async@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
+ integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
@@ -496,13 +484,13 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
-autoprefixer@^10.4.1:
- version "10.4.1"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.1.tgz#1735959d6462420569bc42408016acbc56861c12"
- integrity sha512-B3ZEG7wtzXDRCEFsan7HmR2AeNsxdJB0+sEC0Hc5/c2NbhJqPwuZm+tn233GBVw82L+6CtD6IPSfVruwKjfV3A==
+autoprefixer@^10.4.2:
+ version "10.4.2"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
+ integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
dependencies:
browserslist "^4.19.1"
- caniuse-lite "^1.0.30001294"
+ caniuse-lite "^1.0.30001297"
fraction.js "^4.1.2"
normalize-range "^0.1.2"
picocolors "^1.0.0"
@@ -538,7 +526,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-js@^1.0.2:
+base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -590,7 +578,7 @@ blob-util@^2.0.2:
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
-bluebird@3.7.2:
+bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -660,11 +648,6 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
-browser-stdout@1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
- integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
-
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
@@ -766,15 +749,23 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
+buffer@^5.6.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
bytes@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
- integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
cache-base@^1.0.1:
version "1.0.1"
@@ -828,21 +819,21 @@ camelcase@^5.0.0:
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelcase@^6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
- integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
-caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001294:
- version "1.0.30001294"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz#4849f27b101fd59ddee3751598c663801032533d"
- integrity sha512-LiMlrs1nSKZ8qkNhpUf5KD0Al1KCBE3zaT7OLOwEkagXMEDij98SiOovn9wxVGQpklk9vVC/pUSqgYmkmKOS8g==
+caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
+ version "1.0.30001312"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
+ integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
-chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -859,23 +850,15 @@ chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
- integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
check-more-types@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2:
- version "3.5.2"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
- integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
@@ -935,9 +918,9 @@ class-utils@^0.3.5:
static-extend "^0.1.1"
clean-css@^4.x:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
- integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
+ integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==
dependencies:
source-map "~0.6.0"
@@ -958,15 +941,14 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
-cli-table3@~0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
- integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
+cli-table3@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
+ integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==
dependencies:
- object-assign "^4.1.0"
string-width "^4.2.0"
optionalDependencies:
- colors "^1.1.2"
+ colors "1.4.0"
cli-truncate@^2.1.0:
version "2.1.0"
@@ -985,14 +967,14 @@ cliui@^3.2.0:
strip-ansi "^3.0.1"
wrap-ansi "^2.0.0"
-cliui@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
- integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
- string-width "^2.1.1"
- strip-ansi "^4.0.0"
- wrap-ansi "^2.0.0"
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
clone-buffer@^1.0.0:
version "1.0.0"
@@ -1061,7 +1043,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -1085,10 +1067,10 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-color-string@^1.5.2:
- version "1.5.5"
- resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
- integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
+color-string@^1.6.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa"
+ integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
@@ -1098,30 +1080,30 @@ color-support@^1.1.3:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
-color@3.0.x:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
- integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
+color@^3.1.3:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
+ integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
dependencies:
- color-convert "^1.9.1"
- color-string "^1.5.2"
+ color-convert "^1.9.3"
+ color-string "^1.6.0"
colorette@^2.0.16:
version "2.0.16"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
-colors@^1.1.2, colors@^1.2.1:
+colors@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
colorspace@1.1.x:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
- integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
+ integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
dependencies:
- color "3.0.x"
+ color "^3.1.3"
text-hex "1.0.x"
combined-stream@^1.0.6, combined-stream@~1.0.6:
@@ -1131,11 +1113,6 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
-commander@2.15.1:
- version "2.15.1"
- resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
- integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
-
commander@^2.19.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -1179,9 +1156,9 @@ concat-with-sourcemaps@^1.0.0:
source-map "^0.6.1"
config-chain@^1.1.12:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
- integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
+ integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==
dependencies:
ini "^1.3.4"
proto-list "~1.2.1"
@@ -1214,9 +1191,9 @@ content-type@^1.0.4:
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
convert-source-map@^1.0.0, convert-source-map@^1.5.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
- integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
+ integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
dependencies:
safe-buffer "~5.1.1"
@@ -1233,15 +1210,15 @@ copy-props@^2.0.1:
each-props "^1.3.2"
is-plain-object "^5.0.0"
-core-js-pure@^3.16.0:
- version "3.17.3"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.17.3.tgz#98ea3587188ab7ef4695db6518eeb71aec42604a"
- integrity sha512-YusrqwiOTTn8058JDa0cv9unbXdIiIgcgI9gXso0ey4WgkFLd3lYlV9rp9n7nDCsYxXsMDTjA4m1h3T348mdlQ==
+core-js-pure@^3.20.2:
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51"
+ integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
core-js@^3.6.4:
- version "3.13.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.13.0.tgz#58ca436bf01d6903aee3d364089868d0d89fe58d"
- integrity sha512-iWDbiyha1M5vFwPFmQnvRv+tJzGbFAm6XimJUT0NgHYW3xZEs1SkCAcasWSVFxpI2Xb/V1DDJckq3v90+bQnog==
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
+ integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
core-util-is@1.0.2:
version "1.0.2"
@@ -1285,13 +1262,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
sha.js "^2.4.8"
cross-fetch@^3.0.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
- integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+ integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
- node-fetch "2.6.1"
+ node-fetch "2.6.7"
-cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -1348,7 +1325,7 @@ css-select@^2.0.0:
domutils "^1.7.0"
nth-check "^1.0.2"
-css-selector-parser@^1.3.0:
+css-selector-parser@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759"
integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==
@@ -1395,34 +1372,40 @@ csso@^4.0.2:
dependencies:
css-tree "^1.1.2"
-cssom@^0.3.4:
- version "0.3.8"
- resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
- integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+cssom@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
+ integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
csstype@^3.0.2:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
- integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
+ integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
-cypress@^9.2.0:
- version "9.2.0"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.2.0.tgz#727c20b4662167890db81d5f6ba615231835b17d"
- integrity sha512-Jn26Tprhfzh/a66Sdj9SoaYlnNX6Mjfmj5PHu2a7l3YHXhrgmavM368wjCmgrxC6KHTOv9SpMQGhAJn+upDViA==
+cypress-file-upload@^5.0.8:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
+ integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
+
+cypress@^9.5.0:
+ version "9.5.0"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.0.tgz#704a79f0d3d4e775f433334eb8f5ae065e3bea31"
+ integrity sha512-rC5QPolKsVjJ8QJZ7IeZ6HlKM4gswBGZc0XvoAJNL8urQCSL8zTX0A/ai/h35WfF47NQ0iSZnwIXBlHX3MOUIQ==
dependencies:
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"
"@types/node" "^14.14.31"
- "@types/sinonjs__fake-timers" "^6.0.2"
+ "@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
blob-util "^2.0.2"
- bluebird "3.7.2"
+ bluebird "^3.7.2"
+ buffer "^5.6.0"
cachedir "^2.3.0"
chalk "^4.1.0"
check-more-types "^2.24.0"
cli-cursor "^3.1.0"
- cli-table3 "~0.6.0"
+ cli-table3 "~0.6.1"
commander "^5.1.0"
common-tags "^1.8.0"
dayjs "^1.10.4"
@@ -1446,10 +1429,10 @@ cypress@^9.2.0:
pretty-bytes "^5.6.0"
proxy-from-env "1.0.0"
request-progress "^3.0.0"
+ semver "^7.3.2"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
- url "^0.11.0"
yauzl "^2.10.0"
d@1, d@^1.0.1:
@@ -1472,11 +1455,6 @@ date-fns@^2.28.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
-dateformat@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
- integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
-
dayjs@^1.10.4:
version "1.10.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
@@ -1491,13 +1469,6 @@ debug-fabulous@^1.0.0:
memoizee "0.4.X"
object-assign "4.X"
-debug@3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
-
debug@3.X, debug@^3.1.0, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@@ -1610,11 +1581,6 @@ detect-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
-diff@3.5.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
- integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -1721,9 +1687,9 @@ editorconfig@^0.15.3:
sigmund "^1.0.1"
electron-to-chromium@^1.4.17:
- version "1.4.30"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.30.tgz#0f75a1dce26dffbd5a0f7212e5b87fe0b61cbc76"
- integrity sha512-609z9sIMxDHg+TcR/VB3MXwH+uwtrYyeAwWc/orhnr90ixs6WVGSrt85CDLGUdNnLqCA7liv426V20EecjvflQ==
+ version "1.4.71"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6"
+ integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
elliptic@^6.5.3:
version "6.5.4"
@@ -1781,29 +1747,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.17.2, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
- version "1.18.3"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
- integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
- dependencies:
- call-bind "^1.0.2"
- es-to-primitive "^1.2.1"
- function-bind "^1.1.1"
- get-intrinsic "^1.1.1"
- has "^1.0.3"
- has-symbols "^1.0.2"
- is-callable "^1.2.3"
- is-negative-zero "^2.0.1"
- is-regex "^1.1.3"
- is-string "^1.0.6"
- object-inspect "^1.10.3"
- object-keys "^1.1.1"
- object.assign "^4.1.2"
- string.prototype.trimend "^1.0.4"
- string.prototype.trimstart "^1.0.4"
- unbox-primitive "^1.0.1"
-
-es-abstract@^1.19.1:
+es-abstract@^1.17.2, es-abstract@^1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==
@@ -1889,7 +1833,7 @@ escape-goat@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
@@ -1945,19 +1889,6 @@ execa@4.1.0:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
-execa@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
- integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
- dependencies:
- cross-spawn "^6.0.0"
- get-stream "^4.0.0"
- is-stream "^1.1.0"
- npm-run-path "^2.0.0"
- p-finally "^1.0.0"
- signal-exit "^3.0.0"
- strip-eof "^1.0.0"
-
executable@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -1986,11 +1917,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
homedir-polyfill "^1.0.1"
ext@^1.1.2:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
- integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52"
+ integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==
dependencies:
- type "^2.0.0"
+ type "^2.5.0"
extend-shallow@^2.0.1:
version "2.0.1"
@@ -2053,9 +1984,9 @@ extsprintf@1.3.0:
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
extsprintf@^1.2.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
- integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
+ integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
fancy-log@^1.3.2, fancy-log@^1.3.3:
version "1.3.3"
@@ -2082,11 +2013,6 @@ fast-levenshtein@^1.0.0:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9"
integrity sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=
-fast-safe-stringify@^2.0.4:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
- integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
-
fbjs-css-vars@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
@@ -2155,12 +2081,13 @@ find-up@^1.0.0:
path-exists "^2.0.0"
pinkie-promise "^2.0.0"
-find-up@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
- integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
- locate-path "^3.0.0"
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
findup-sync@^2.0.0:
version "2.0.0"
@@ -2238,9 +2165,9 @@ form-data@~2.3.2:
mime-types "^2.1.12"
fraction.js@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8"
- integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.3.tgz#be65b0f20762ef27e1e793860bc2dfb716e99e65"
+ integrity sha512-pUHWWt6vHzZZiQJcM6S/0PXfS+g6FM4BF5rj9wZyreivhQPdsh5PpE25VtSNxq80wHS5RfY51Ii+8Z0Zl/pmzg==
fragment-cache@^0.2.1:
version "0.2.1"
@@ -2304,6 +2231,11 @@ get-caller-file@^1.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
@@ -2313,7 +2245,7 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
-get-stream@^4.0.0, get-stream@^4.1.0:
+get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
@@ -2408,22 +2340,10 @@ glob-watcher@^5.0.3:
normalize-path "^3.0.0"
object.defaults "^1.1.0"
-glob@7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
- integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@^7.1.1, glob@^7.1.3:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+glob@^7.1.1, glob@^7.1.3, glob@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -2483,20 +2403,10 @@ got@^9.6.0:
to-readable-stream "^1.0.0"
url-parse-lax "^3.0.0"
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
- version "4.2.6"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
- integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-
-graceful-fs@^4.2.0:
- version "4.2.8"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
- integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
-
-growl@1.10.5:
- version "1.10.5"
- resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
- integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
+graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0:
+ version "4.2.9"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
+ integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
gulp-cli@^2.2.0:
version "2.3.0"
@@ -2569,17 +2479,16 @@ gulp-rename@^2.0.0:
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
integrity sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==
-gulp-sass@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-5.0.0.tgz#c338fc021e450a51ae977fea9014eda331ce66b7"
- integrity sha512-J0aH0/2N4+2szGCeut0ktGHK0Wg8L9uWivuigrl7xv+nhxozBQRAKLrhnDDaTa3FeUWYtgT8w4RlgdhRy5v16w==
+gulp-sass@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-5.1.0.tgz#bb3d9094f39a260f62a8d0a6797b95ab826f9663"
+ integrity sha512-7VT0uaF+VZCmkNBglfe1b34bxn/AfcssquLKVDYnCDJ3xNBaW7cUuI3p3BQmoKcoKFrs9jdzUxyb+u+NGfL4OQ==
dependencies:
- chalk "^4.1.1"
- lodash "^4.17.20"
+ lodash.clonedeep "^4.5.0"
+ picocolors "^1.0.0"
plugin-error "^1.0.1"
replace-ext "^2.0.0"
- strip-ansi "^6.0.0"
- transfob "^1.0.0"
+ strip-ansi "^6.0.1"
vinyl-sourcemaps-apply "^0.2.1"
gulp-sourcemaps@^3.0.0:
@@ -2733,15 +2642,10 @@ hasha@^2.2.0:
is-stream "^1.0.1"
pinkie-promise "^2.0.0"
-he@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
- integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
-
-highlight.js@^11.3.1:
- version "11.3.1"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291"
- integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw==
+highlight.js@^11.4.0:
+ version "11.4.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.4.0.tgz#34ceadd49e1596ee5aba3d99346cdfd4845ee05a"
+ integrity sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA==
hmac-drbg@^1.0.1:
version "1.0.1"
@@ -2804,7 +2708,7 @@ iconv-lite@^0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
-ieee754@^1.1.4:
+ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -2829,20 +2733,6 @@ immutable@~3.7.4:
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks=
-import-cwd@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
- integrity sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==
- dependencies:
- import-from "^3.0.0"
-
-import-from@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966"
- integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==
- dependencies:
- resolve-from "^5.0.0"
-
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@@ -2910,11 +2800,6 @@ invert-kv@^1.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
-invert-kv@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
- integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
-
is-absolute@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
@@ -2948,9 +2833,11 @@ is-arrayish@^0.3.1:
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-bigint@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
- integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+ integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+ dependencies:
+ has-bigints "^1.0.1"
is-binary-path@^1.0.0:
version "1.0.1"
@@ -2967,23 +2854,19 @@ is-binary-path@~2.1.0:
binary-extensions "^2.0.0"
is-boolean-object@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8"
- integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+ integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
dependencies:
call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
- integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
-
-is-callable@^1.2.4:
+is-callable@^1.1.4, is-callable@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -3002,10 +2885,10 @@ is-ci@^3.0.0:
dependencies:
ci-info "^3.2.0"
-is-core-module@^2.2.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491"
- integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==
+is-core-module@^2.8.1:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
+ integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
dependencies:
has "^1.0.3"
@@ -3024,9 +2907,11 @@ is-data-descriptor@^1.0.0:
kind-of "^6.0.0"
is-date-object@^1.0.1:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
- integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+ integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
is-descriptor@^0.1.0:
version "0.1.6"
@@ -3070,11 +2955,6 @@ is-fullwidth-code-point@^1.0.0:
dependencies:
number-is-nan "^1.0.0"
-is-fullwidth-code-point@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
- integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -3088,9 +2968,9 @@ is-glob@^3.1.0:
is-extglob "^2.1.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
- integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
@@ -3108,9 +2988,9 @@ is-negated-glob@^1.0.0:
integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=
is-negative-zero@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
- integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+ integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
is-npm@^5.0.0:
version "5.0.0"
@@ -3118,9 +2998,11 @@ is-npm@^5.0.0:
integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==
is-number-object@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb"
- integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
+ integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==
+ dependencies:
+ has-tostringtag "^1.0.0"
is-number@^3.0.0:
version "3.0.0"
@@ -3166,14 +3048,6 @@ is-promise@^2.2.2:
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
-is-regex@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
- integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
- dependencies:
- call-bind "^1.0.2"
- has-symbols "^1.0.2"
-
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@@ -3194,22 +3068,17 @@ is-shared-array-buffer@^1.0.1:
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
-is-stream@^1.0.1, is-stream@^1.1.0:
+is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
- integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
-is-string@^1.0.5, is-string@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
- integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
-
-is-string@^1.0.7:
+is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -3251,11 +3120,11 @@ is-valid-glob@^1.0.0:
integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=
is-weakref@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2"
- integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
dependencies:
- call-bind "^1.0.0"
+ call-bind "^1.0.2"
is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2"
@@ -3309,7 +3178,7 @@ js-beautify@^1.14.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-yaml@^3.12.0, js-yaml@^3.13.1:
+js-yaml@^3.13.1, js-yaml@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@@ -3337,11 +3206,6 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-json-schema@0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
- integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
-
json-schema@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
@@ -3374,13 +3238,13 @@ jsonfile@^6.0.1:
graceful-fs "^4.1.6"
jsprim@^1.2.2:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
- integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
+ integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
- json-schema "0.2.3"
+ json-schema "0.4.0"
verror "1.10.0"
jsprim@^2.0.2:
@@ -3477,9 +3341,9 @@ lazy-ass@^1.6.0:
integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
lazystream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
- integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
+ integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==
dependencies:
readable-stream "^2.0.5"
@@ -3490,13 +3354,6 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
-lcid@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
- integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
- dependencies:
- invert-kv "^2.0.0"
-
lead@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42"
@@ -3525,22 +3382,22 @@ liftoff@^3.1.0:
rechoir "^0.6.2"
resolve "^1.1.7"
-lilconfig@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
- integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+lilconfig@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082"
+ integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==
listr2@^3.8.3:
- version "3.13.5"
- resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.5.tgz#105a813f2eb2329c4aae27373a281d610ee4985f"
- integrity sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==
+ version "3.14.0"
+ resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e"
+ integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==
dependencies:
cli-truncate "^2.1.0"
colorette "^2.0.16"
log-update "^4.0.0"
p-map "^4.0.0"
rfdc "^1.3.0"
- rxjs "^7.4.0"
+ rxjs "^7.5.1"
through "^2.3.8"
wrap-ansi "^7.0.0"
@@ -3565,125 +3422,24 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
-locate-path@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
- integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
- p-locate "^3.0.0"
- path-exists "^3.0.0"
+ p-locate "^4.1.0"
-lodash._arraymap@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/lodash._arraymap/-/lodash._arraymap-3.0.0.tgz#1a8fd0f4c0df4b61dea076d717cdc97f0a3c3e66"
- integrity sha1-Go/Q9MDfS2HeoHbXF83Jfwo8PmY=
-
-lodash._basecallback@^3.0.0:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz#b7b2bb43dc2160424a21ccf26c57e443772a8e27"
- integrity sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=
- dependencies:
- lodash._baseisequal "^3.0.0"
- lodash._bindcallback "^3.0.0"
- lodash.isarray "^3.0.0"
- lodash.pairs "^3.0.0"
-
-lodash._baseeach@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz#cf8706572ca144e8d9d75227c990da982f932af3"
- integrity sha1-z4cGVyyhROjZ11InyZDamC+TKvM=
- dependencies:
- lodash.keys "^3.0.0"
-
-lodash._baseget@^3.0.0:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/lodash._baseget/-/lodash._baseget-3.7.2.tgz#1b6ae1d5facf3c25532350a13c1197cb8bb674f4"
- integrity sha1-G2rh1frPPCVTI1ChPBGXy4u2dPQ=
-
-lodash._baseisequal@^3.0.0:
- version "3.0.7"
- resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1"
- integrity sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=
- dependencies:
- lodash.isarray "^3.0.0"
- lodash.istypedarray "^3.0.0"
- lodash.keys "^3.0.0"
-
-lodash._bindcallback@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
- integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._getnative@^3.0.0:
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
- integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
-lodash._topath@^3.0.0:
- version "3.8.1"
- resolved "https://registry.yarnpkg.com/lodash._topath/-/lodash._topath-3.8.1.tgz#3ec5e2606014f4cb97f755fe6914edd8bfc00eac"
- integrity sha1-PsXiYGAU9MuX91X+aRTt2L/ADqw=
- dependencies:
- lodash.isarray "^3.0.0"
-
-lodash.isarguments@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
- integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=
-
-lodash.isarray@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
- integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=
-
-lodash.istypedarray@^3.0.0:
- version "3.0.6"
- resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62"
- integrity sha1-yaR3SYYHUB2OhJTSg7h8OSgc72I=
-
-lodash.keys@^3.0.0:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
- integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=
- dependencies:
- lodash._getnative "^3.0.0"
- lodash.isarguments "^3.0.0"
- lodash.isarray "^3.0.0"
-
-lodash.map@^3.0.0:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-3.1.4.tgz#b483acd1b786c5c7b492c495f7b5266229bc00c2"
- integrity sha1-tIOs0beGxce0ksSV97UmYim8AMI=
- dependencies:
- lodash._arraymap "^3.0.0"
- lodash._basecallback "^3.0.0"
- lodash._baseeach "^3.0.0"
- lodash.isarray "^3.0.0"
- lodash.keys "^3.0.0"
+lodash.clonedeep@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+ integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.once@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
-lodash.pairs@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/lodash.pairs/-/lodash.pairs-3.0.1.tgz#bbe08d5786eeeaa09a15c91ebf0dcb7d2be326a9"
- integrity sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=
- dependencies:
- lodash.keys "^3.0.0"
-
-lodash.pluck@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/lodash.pluck/-/lodash.pluck-3.1.2.tgz#b347f0374c0169f0eeb04d672d89cec8632c2231"
- integrity sha1-s0fwN0wBafDusE1nLYnOyGMsIjE=
- dependencies:
- lodash._baseget "^3.0.0"
- lodash._topath "^3.0.0"
- lodash.isarray "^3.0.0"
- lodash.map "^3.0.0"
-
-lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21:
+lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -3706,15 +3462,15 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
-logform@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
- integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
+logform@^2.3.2, logform@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe"
+ integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==
dependencies:
- colors "^1.2.1"
- fast-safe-stringify "^2.0.4"
+ "@colors/colors" "1.5.0"
fecha "^4.2.0"
ms "^2.1.1"
+ safe-stable-stringify "^2.3.1"
triple-beam "^1.3.0"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
@@ -3756,10 +3512,10 @@ lru-queue@^0.1.0:
dependencies:
es5-ext "~0.10.2"
-luxon@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.2.0.tgz#f5c4a234ba4016f792488b11aaed2d5bc14c888e"
- integrity sha512-LwmknessH4jVIseCsizUgveIHwlLv/RQZWC2uDSMfGJs7w8faPUi2JFxfyfMcTPrpNbChTem3Uz6IKRtn+LcIA==
+luxon@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045"
+ integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==
make-dir@^3.0.0:
version "3.1.0"
@@ -3775,13 +3531,6 @@ make-iterator@^1.0.0:
dependencies:
kind-of "^6.0.2"
-map-age-cleaner@^0.1.1:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
- integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
- dependencies:
- p-defer "^1.0.0"
-
map-cache@^0.2.0, map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -3799,10 +3548,10 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
-marked@^4.0.8:
- version "4.0.8"
- resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.8.tgz#ef127626ac65786460f9420d57cc8d5ffdcacbed"
- integrity sha512-dkpJMIlJpc833hbjjg8jraw1t51e/eKDoG8TFOgc5O0Z77zaYKigYekTDop5AplRoKFGIaoazhYEhGkMtU3IeA==
+marked@^4.0.12:
+ version "4.0.12"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d"
+ integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==
matchdep@^2.0.0:
version "2.0.0"
@@ -3833,15 +3582,6 @@ mdn-data@2.0.4:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
-mem@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
- integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
- dependencies:
- map-age-cleaner "^0.1.1"
- mimic-fn "^2.0.0"
- p-is-promise "^2.0.0"
-
memoizee@0.4.X:
version "0.4.15"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
@@ -3893,19 +3633,19 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.48.0:
- version "1.48.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
- integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
+mime-db@1.51.0:
+ version "1.51.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
+ integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
mime-types@^2.1.12, mime-types@~2.1.19:
- version "2.1.31"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
- integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
+ version "2.1.34"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
+ integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
dependencies:
- mime-db "1.48.0"
+ mime-db "1.51.0"
-mimic-fn@^2.0.0, mimic-fn@^2.1.0:
+mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
@@ -3925,18 +3665,13 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-minimatch@3.0.4, minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+minimatch@^3.0.4:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
-minimist@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
- integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
-
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
@@ -3950,14 +3685,7 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-mkdirp@0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
- integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
- dependencies:
- minimist "0.0.8"
-
-mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.1:
+mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -3969,23 +3697,6 @@ mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-mocha@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
- integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==
- dependencies:
- browser-stdout "1.3.1"
- commander "2.15.1"
- debug "3.1.0"
- diff "3.5.0"
- escape-string-regexp "1.0.5"
- glob "7.1.2"
- growl "1.10.5"
- he "1.1.1"
- minimatch "3.0.4"
- mkdirp "0.5.1"
- supports-color "5.4.0"
-
mousetrap@^1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
@@ -4006,12 +3717,7 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mustache@^3.0.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/mustache/-/mustache-3.2.1.tgz#89e78a9d207d78f2799b1e95764a25bf71a28322"
- integrity sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA==
-
-mustache@^4.0.1:
+mustache@^4.0.1, mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
@@ -4022,14 +3728,14 @@ mute-stdout@^1.0.0:
integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==
nan@^2.12.1:
- version "2.14.2"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
- integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
+ integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
-nanoid@^3.1.30:
- version "3.1.30"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
- integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+nanoid@^3.2.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
+ integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
nanomatch@^1.2.9:
version "1.2.13"
@@ -4063,10 +3769,12 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-node-fetch@2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
- integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+node-fetch@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+ dependencies:
+ whatwg-url "^5.0.0"
node-libs-browser@^2.2.1:
version "2.2.1"
@@ -4098,9 +3806,9 @@ node-libs-browser@^2.2.1:
vm-browserify "^1.0.1"
node-releases@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5"
- integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
+ integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
nodemon@^2.0.15:
version "2.0.15"
@@ -4186,13 +3894,6 @@ npm-run-all@^4.1.5:
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
-npm-run-path@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
- integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
- dependencies:
- path-key "^2.0.0"
-
npm-run-path@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -4231,15 +3932,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
- integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
-
object-inspect@^1.11.0, object-inspect@^1.9.0:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
- integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
+ integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
@@ -4274,13 +3970,13 @@ object.defaults@^1.0.0, object.defaults@^1.1.0:
isobject "^3.0.0"
object.getownpropertydescriptors@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7"
- integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e"
+ integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.0-next.2"
+ es-abstract "^1.19.1"
object.map@^1.0.0:
version "1.0.1"
@@ -4306,13 +4002,13 @@ object.reduce@^1.0.0:
make-iterator "^1.0.0"
object.values@^1.1.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
- integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+ integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.2"
+ es-abstract "^1.19.1"
once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0:
version "1.4.0"
@@ -4362,15 +4058,6 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
-os-locale@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
- integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
- dependencies:
- execa "^1.0.0"
- lcid "^2.0.0"
- mem "^4.0.0"
-
ospath@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
@@ -4381,34 +4068,19 @@ p-cancelable@^1.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
-p-defer@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
- integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
-
-p-finally@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
- integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-
-p-is-promise@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
- integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
-
-p-limit@^2.0.0:
+p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
-p-locate@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
- integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
- p-limit "^2.0.0"
+ p-limit "^2.2.0"
p-map@^4.0.0:
version "4.0.0"
@@ -4504,17 +4176,17 @@ path-exists@^2.0.0:
dependencies:
pinkie-promise "^2.0.0"
-path-exists@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
- integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-path-key@^2.0.0, path-key@^2.0.1:
+path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
@@ -4524,7 +4196,7 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-path-parse@^1.0.6:
+path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@@ -4593,15 +4265,20 @@ phantomjs-prebuilt@^2.1.16:
request-progress "^2.0.1"
which "^1.2.10"
+picocolors@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
+ integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
- integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pidtree@^0.3.0:
version "0.3.1"
@@ -4654,12 +4331,11 @@ postcss-clean@^1.2.2:
postcss "^6.x"
postcss-load-config@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.0.tgz#d39c47091c4aec37f50272373a6a648ef5e97829"
- integrity sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.3.tgz#21935b2c43b9a86e6581a576ca7ee1bde2bd1d23"
+ integrity sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw==
dependencies:
- import-cwd "^3.0.0"
- lilconfig "^2.0.3"
+ lilconfig "^2.0.4"
yaml "^1.10.2"
postcss-value-parser@^4.2.0:
@@ -4677,22 +4353,21 @@ postcss@^6.x:
supports-color "^5.4.0"
postcss@^7.0.16:
- version "7.0.35"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
- integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
+ version "7.0.39"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+ integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
dependencies:
- chalk "^2.4.2"
+ picocolors "^0.2.1"
source-map "^0.6.1"
- supports-color "^6.1.0"
-postcss@^8.4.5:
- version "8.4.5"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
- integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
+postcss@^8.4.6:
+ version "8.4.6"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
+ integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
dependencies:
- nanoid "^3.1.30"
+ nanoid "^3.2.0"
picocolors "^1.0.0"
- source-map-js "^1.0.1"
+ source-map-js "^1.0.2"
prepend-http@^2.0.0:
version "2.0.0"
@@ -4714,10 +4389,10 @@ pretty-hrtime@^1.0.0:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
-prettysize@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/prettysize/-/prettysize-1.1.0.tgz#c6c52f87161ff172ea435f375f99831dd9a97bb0"
- integrity sha512-U5Noa+FYV1dGkICyLJz8IWlDUehPF4Bk9tZRO8YqPhLA9EoiHuFqtnpWY2mvMjHh5eOLo82HipeLn4RIiSsGqQ==
+prettysize@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/prettysize/-/prettysize-2.0.0.tgz#902c02480d865d9cc0813011c9feb4fa02ce6996"
+ integrity sha512-VVtxR7sOh0VsG8o06Ttq5TrI1aiZKmC+ClSn4eBPaNf4SHr5lzbYW+kYGX3HocBL/MfpVrRfFZ9V3vCbLaiplg==
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
version "2.0.1"
@@ -4742,13 +4417,13 @@ promise@^7.1.1:
asap "~2.0.3"
prop-types@^15.7.2:
- version "15.7.2"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
- integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
- react-is "^16.8.1"
+ react-is "^16.13.1"
proto-list@~1.2.1:
version "1.2.4"
@@ -4840,9 +4515,9 @@ q@^1.1.2:
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qs@~6.5.2:
- version "6.5.2"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
- integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
+ integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
querystring-es3@^0.2.0:
version "0.2.1"
@@ -4893,7 +4568,7 @@ react-dom@~17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
-react-is@^16.8.1:
+react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -4958,7 +4633,7 @@ read-pkg@^3.0.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
+readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -5127,6 +4802,11 @@ require-main-filename@^1.0.1:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
resolve-dir@^1.0.0, resolve-dir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
@@ -5135,11 +4815,6 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1:
expand-tilde "^2.0.0"
global-modules "^1.0.0"
-resolve-from@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
- integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-
resolve-options@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131"
@@ -5153,12 +4828,13 @@ resolve-url@^0.2.1:
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.4.0:
- version "1.20.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
- integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+ version "1.22.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
+ integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
dependencies:
- is-core-module "^2.2.0"
- path-parse "^1.0.6"
+ is-core-module "^2.8.1"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
responselike@^1.0.2:
version "1.0.2"
@@ -5200,12 +4876,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
-rxjs@^7.4.0, rxjs@~7.4.0:
- version "7.4.0"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68"
- integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==
+rxjs@^7.5.1, rxjs@~7.5.2:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d"
+ integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==
dependencies:
- tslib "~2.1.0"
+ tslib "^2.1.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
@@ -5224,15 +4900,20 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
+safe-stable-stringify@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73"
+ integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==
+
"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-sass@^1.45.1:
- version "1.45.1"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.45.1.tgz#fa03951f924d1ba5762949567eaf660e608a1ab0"
- integrity sha512-pwPRiq29UR0o4X3fiQyCtrESldXvUQAAE0QmcJTpsI4kuHHcLzZ54M1oNBVIXybQv8QF2zfkpFcTxp8ta97dUA==
+sass@^1.49.7:
+ version "1.49.7"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.7.tgz#22a86a50552b9b11f71404dfad1b9ff44c6b0c49"
+ integrity sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -5275,7 +4956,7 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-semver@^7.3.4:
+semver@^7.3.2, semver@^7.3.4:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -5320,10 +5001,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.16.12:
- version "2.16.12"
- resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.16.12.tgz#8757b3079dadfff15ca09192f81eb69b5d25266d"
- integrity sha512-6JqOhN5X3n0IkxA/gSUcZ1lImwcW1LmpgzlaBDOC/u/pIysdNm0tiOxpOTEnExl9nKZBS/EYS7bXIIInywPJUA==
+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==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"
@@ -5357,53 +5038,9 @@ shebang-regex@^3.0.0:
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.6.1:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
- integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
-
-should-equal@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
- integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==
- dependencies:
- should-type "^1.4.0"
-
-should-format@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1"
- integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE=
- dependencies:
- should-type "^1.3.0"
- should-type-adaptors "^1.0.1"
-
-should-type-adaptors@^1.0.1:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a"
- integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==
- dependencies:
- should-type "^1.3.0"
- should-util "^1.0.0"
-
-should-type@^1.3.0, should-type@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3"
- integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=
-
-should-util@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.1.tgz#fb0d71338f532a3a149213639e2d32cbea8bcb28"
- integrity sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==
-
-should@^13.2.3:
- version "13.2.3"
- resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10"
- integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==
- dependencies:
- should-equal "^2.0.0"
- should-format "^3.0.3"
- should-type "^1.4.0"
- should-type-adaptors "^1.0.1"
- should-util "^1.0.0"
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
+ integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
side-channel@^1.0.4:
version "1.0.4"
@@ -5419,15 +5056,10 @@ sigmund@^1.0.1:
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
-signal-exit@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
- integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
-
signal-exit@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
- integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-swizzle@^0.2.2:
version "0.2.2"
@@ -5484,10 +5116,10 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
-"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
- integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+ integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-resolve@^0.5.0:
version "0.5.3"
@@ -5565,9 +5197,9 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
- version "3.0.9"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f"
- integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==
+ version "3.0.11"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95"
+ integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
@@ -5582,9 +5214,9 @@ sprintf-js@~1.0.2:
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sshpk@^1.14.1, sshpk@^1.7.0:
- version "1.16.1"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
- integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
+ integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
@@ -5659,14 +5291,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-string-width@^2.0.0, string-width@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
- integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^4.0.0"
-
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -5727,21 +5351,7 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
dependencies:
ansi-regex "^2.0.0"
-strip-ansi@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
- integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
- dependencies:
- ansi-regex "^3.0.0"
-
-strip-ansi@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
- integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
- dependencies:
- ansi-regex "^5.0.0"
-
-strip-ansi@^6.0.1:
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -5765,11 +5375,6 @@ strip-bom@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
-strip-eof@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
- integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
-
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@@ -5780,13 +5385,6 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-supports-color@5.4.0:
- version "5.4.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
- integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==
- dependencies:
- has-flag "^3.0.0"
-
supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -5794,13 +5392,6 @@ supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
- integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
- dependencies:
- has-flag "^3.0.0"
-
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -5815,6 +5406,11 @@ supports-color@^8.1.1:
dependencies:
has-flag "^4.0.0"
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
sver-compat@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8"
@@ -5824,33 +5420,29 @@ sver-compat@^1.5.0:
es6-symbol "^3.1.1"
svg-sprite@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/svg-sprite/-/svg-sprite-1.5.0.tgz#d0c0dd2f9aa1bc12ca649d7a8f301370b26e42ce"
- integrity sha512-0mE5BLY3K8wg3+HrYfzpiKbIM44IGcg8uINED8ri22EdQbLvGecOHjRtkrNAlphbiU5kyGyqoBlIaukL45fs2Q==
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/svg-sprite/-/svg-sprite-1.5.4.tgz#974fd4734ea00d9951ce335a453ab2b66551e2d1"
+ integrity sha512-3jeqAmQS4c4rAMzsQNBXo3+J/x65JIxaFl15wTyvrJdT/G0DzXd67oTMBxz5+lCb8ETsWjM6ZAyr/+R+BwXzag==
dependencies:
- async "^2.6.1"
- css-selector-parser "^1.3.0"
+ "@xmldom/xmldom" "^0.7.5"
+ async "^3.2.3"
+ css-selector-parser "^1.4.1"
cssmin "^0.4.3"
- cssom "^0.3.4"
- dateformat "^3.0.3"
- glob "^7.1.3"
- js-yaml "^3.12.0"
- lodash "^4.17.11"
- lodash.pluck "^3.1.2"
- mkdirp "^0.5.1"
- mocha "^5.2.0"
- mustache "^3.0.0"
+ cssom "^0.5.0"
+ glob "^7.2.0"
+ js-yaml "^3.14.1"
+ lodash "^4.17.21"
+ mkdirp "^0.5.5"
+ mustache "^4.2.0"
phantomjs-prebuilt "^2.1.16"
- prettysize "^1.1.0"
- should "^13.2.3"
- svgo "^1.1.1"
- vinyl "^2.2.0"
- winston "^3.1.0"
- xmldom "0.1.27"
- xpath "^0.0.27"
- yargs "^12.0.2"
+ prettysize "^2.0.0"
+ svgo "^1.3.2"
+ vinyl "^2.2.1"
+ winston "^3.5.1"
+ xpath "^0.0.32"
+ yargs "^15.4.1"
-svgo@^1.1.1:
+svgo@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
@@ -6019,12 +5611,12 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
-transfob@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/transfob/-/transfob-1.0.0.tgz#c7fc27a5b5430ad486267ae666d923f74a0ab320"
- integrity sha1-x/wnpbVDCtSGJnrmZtkj90oKsyA=
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-triple-beam@^1.2.0, triple-beam@^1.3.0:
+triple-beam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
@@ -6034,10 +5626,10 @@ tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
- integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
+tslib@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+ integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tty-browserify@0.0.0:
version "0.0.0"
@@ -6071,10 +5663,10 @@ type@^1.0.1:
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
-type@^2.0.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
- integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
+type@^2.5.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f"
+ integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==
typedarray-to-buffer@^3.1.5:
version "3.1.5"
@@ -6089,9 +5681,9 @@ typedarray@^0.0.6:
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
ua-parser-js@^0.7.18:
- version "0.7.28"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
- integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+ version "0.7.31"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
+ integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
ua-parser-js@^1.0.2:
version "1.0.2"
@@ -6355,7 +5947,7 @@ vinyl-sourcemaps-apply@^0.2.1:
dependencies:
source-map "^0.5.1"
-vinyl@^2.0.0, vinyl@^2.2.0:
+vinyl@^2.0.0, vinyl@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974"
integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==
@@ -6372,6 +5964,19 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -6414,28 +6019,30 @@ widest-line@^3.1.0:
dependencies:
string-width "^4.0.0"
-winston-transport@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
- integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
+winston-transport@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
+ integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
dependencies:
- readable-stream "^2.3.7"
- triple-beam "^1.2.0"
+ logform "^2.3.2"
+ readable-stream "^3.6.0"
+ triple-beam "^1.3.0"
-winston@^3.1.0:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
- integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
+winston@^3.5.1:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6"
+ integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w==
dependencies:
"@dabh/diagnostics" "^2.0.2"
- async "^3.1.0"
+ async "^3.2.3"
is-stream "^2.0.0"
- logform "^2.2.0"
+ logform "^2.4.0"
one-time "^1.0.0"
readable-stream "^3.4.0"
+ safe-stable-stringify "^2.3.1"
stack-trace "0.0.x"
triple-beam "^1.3.0"
- winston-transport "^4.4.0"
+ winston-transport "^4.5.0"
wrap-ansi@^2.0.0:
version "2.1.0"
@@ -6479,24 +6086,19 @@ write-file-atomic@^3.0.0:
typedarray-to-buffer "^3.1.5"
ws@^7.4.6:
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691"
- integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==
+ version "7.5.7"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
+ integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
-xmldom@0.1.27:
- version "0.1.27"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
- integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
-
-xpath@^0.0.27:
- version "0.0.27"
- resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"
- integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==
+xpath@^0.0.32:
+ version "0.0.32"
+ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"
+ integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==
xregexp@^5.0.1:
version "5.1.0"
@@ -6515,7 +6117,7 @@ y18n@^3.2.1:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
-"y18n@^3.2.1 || ^4.0.0":
+y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
@@ -6535,10 +6137,10 @@ yaml@^1.10.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-yargs-parser@^11.1.1:
- version "11.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
- integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+yargs-parser@^18.1.2:
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
@@ -6551,23 +6153,22 @@ yargs-parser@^5.0.1:
camelcase "^3.0.0"
object.assign "^4.1.0"
-yargs@^12.0.2:
- version "12.0.5"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
- integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+yargs@^15.4.1:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
- cliui "^4.0.0"
+ cliui "^6.0.0"
decamelize "^1.2.0"
- find-up "^3.0.0"
- get-caller-file "^1.0.1"
- os-locale "^3.0.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
require-directory "^2.1.1"
- require-main-filename "^1.0.1"
+ require-main-filename "^2.0.0"
set-blocking "^2.0.0"
- string-width "^2.0.0"
+ string-width "^4.2.0"
which-module "^2.0.0"
- y18n "^3.2.1 || ^4.0.0"
- yargs-parser "^11.1.1"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.2"
yargs@^7.1.0:
version "7.1.2"
diff --git a/version.txt b/version.txt
index fb03eed94c..08c36205db 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.11.2-beta
+1.12.0-beta