diff --git a/.gitignore b/.gitignore
index 0bf57fa62..4695ea70a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ node_modules
/frontend/out/
/frontend/.shadow-cljs
/frontend/resources/public/*
+/frontend/resources/fonts/experiments
/exporter/target
/exporter/.shadow-cljs
/docker/images/bundle*
diff --git a/CHANGES.md b/CHANGES.md
index 4d35b7109..3a0659912 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,14 +1,59 @@
# CHANGELOG #
+
## :rocket: Next
### :sparkles: New features
+
### :bug: Bugs fixed
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
+## 1.6.0-alpha
+
+### :sparkles: New features
+
+- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292).
+- Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527)
+- Add performance improvements on dashboard data loading.
+- Add performance improvements to indexes handling on workspace.
+- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292).
+- Transform shapes to path on double click
+- Translate automatic names of new files and projects.
+- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697).
+- New translations: Portuguese (Brazil) and Romanias.
+
+
+### :bug: Bugs fixed
+
+- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656).
+- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940).
+- Fix problem with imported SVG on editing paths [#971](https://github.com/penpot/penpot/issues/971)
+- Fix problem with color picker positioning
+- Fix order on color palette [#961](https://github.com/penpot/penpot/issues/961)
+- Fix issue when group creation leaves an empty group [#1724](https://tree.taiga.io/project/penpot/issue/1724)
+- Fix problem with :multiple for colors and typographies [#1668](https://tree.taiga.io/project/penpot/issue/1668)
+- Fix problem with locked shapes when change parents [#974](https://github.com/penpot/penpot/issues/974)
+
+### :arrow_up: Deps updates
+
+- Update exporter dependencies (puppeteer), that fixes some unexpected exceptions.
+- Update string manipulation library.
+
+
+### :boom: Breaking changes
+
+- The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this
+ configuration added scopes to the default set. Now it replaces it, so use with care, because
+ penpot requires at least `name` and `email` props found on the user info object.
+
+### :heart: Community contributions by (Thank you!)
+
+- Translations: Portuguese (Brazil) and Romanias.
+
+
## 1.5.4-alpha
### :bug: Bugs fixed
diff --git a/backend/deps.edn b/backend/deps.edn
index 5659d1a62..8032825c0 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -4,8 +4,8 @@
"jcenter" {:url "https://jcenter.bintray.com/"}}
:deps
{org.clojure/clojure {:mvn/version "1.10.3"}
- org.clojure/data.json {:mvn/version "2.2.1"}
- org.clojure/core.async {:mvn/version "1.3.610"}
+ org.clojure/data.json {:mvn/version "2.2.3"}
+ org.clojure/core.async {:mvn/version "1.3.618"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
org.clojure/clojurescript {:mvn/version "1.10.844"}
@@ -32,28 +32,28 @@
org.eclipse.jetty/jetty-servlet]}
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
- selmer/selmer {:mvn/version "1.12.33"}
+ selmer/selmer {:mvn/version "1.12.40"}
expound/expound {:mvn/version "0.8.9"}
com.cognitect/transit-clj {:mvn/version "1.0.324"}
- io.lettuce/lettuce-core {:mvn/version "6.1.1.RELEASE"}
+ io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.2"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"}
- com.github.seancorfield/next.jdbc {:mvn/version "1.1.646"}
- metosin/reitit-ring {:mvn/version "0.5.12"}
- metosin/jsonista {:mvn/version "0.3.1"}
+ com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"}
+ metosin/reitit-ring {:mvn/version "0.5.13"}
+ metosin/jsonista {:mvn/version "0.3.3"}
- org.postgresql/postgresql {:mvn/version "42.2.19"}
+ org.postgresql/postgresql {:mvn/version "42.2.20"}
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
- funcool/datoteka {:mvn/version "1.2.0"}
- funcool/promesa {:mvn/version "6.0.0"}
- funcool/cuerdas {:mvn/version "2020.03.26-3"}
+ funcool/datoteka {:mvn/version "2.0.0"}
+ funcool/promesa {:mvn/version "6.0.1"}
+ funcool/cuerdas {:mvn/version "2021.05.09-0"}
- buddy/buddy-core {:mvn/version "1.9.0"}
- buddy/buddy-hashers {:mvn/version "1.7.0"}
- buddy/buddy-sign {:mvn/version "3.3.0"}
+ buddy/buddy-core {:mvn/version "1.10.1"}
+ buddy/buddy-hashers {:mvn/version "1.8.1"}
+ buddy/buddy-sign {:mvn/version "3.4.1"}
lambdaisland/uri {:mvn/version "1.4.54"
:exclusions [org.clojure/data.json]}
@@ -69,7 +69,7 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"}
- software.amazon.awssdk/s3 {:mvn/version "2.16.44"}
+ software.amazon.awssdk/s3 {:mvn/version "2.16.62"}
;; exception printing
io.aviso/pretty {:mvn/version "0.1.37"}
@@ -78,9 +78,9 @@
:aliases
{:dev
{:extra-deps
- {com.bhauman/rebel-readline {:mvn/version "0.1.4"}
- org.clojure/tools.namespace {:mvn/version "1.1.0"}
- org.clojure/test.check {:mvn/version "1.1.0"}
+ {com.bhauman/rebel-readline {:mvn/version "RELEASE"}
+ org.clojure/tools.namespace {:mvn/version "RELEASE"}
+ org.clojure/test.check {:mvn/version "RELEASE"}
fipp/fipp {:mvn/version "0.6.23"}
criterium/criterium {:mvn/version "0.4.6"}
diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml
index eb38cb183..1b9dba567 100644
--- a/backend/resources/log4j2-devenv.xml
+++ b/backend/resources/log4j2-devenv.xml
@@ -12,6 +12,11 @@
+
+
+ tcp://localhost:45556
+
+
@@ -30,10 +35,12 @@
+
-
-
+
+
+
diff --git a/backend/scripts/repl b/backend/scripts/repl
index 32fd12d42..1f434abcf 100755
--- a/backend/scripts/repl
+++ b/backend/scripts/repl
@@ -2,7 +2,9 @@
export PENPOT_ASSERTS_ENABLED=true
-export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Xms512m -J-Xmx512m -J-Dlog4j2.configurationFile=log4j2-devenv.xml"
+export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
+# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
+# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index 0e9d4ac5d..30ca83d38 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -8,6 +8,8 @@
"A configuration management."
(:refer-clojure :exclude [get])
(:require
+ [app.common.data :as d]
+ [app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.version :as v]
[app.util.time :as dt]
@@ -16,7 +18,8 @@
[clojure.pprint :as pprint]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
- [environ.core :refer [env]]))
+ [environ.core :refer [env]]
+ [integrant.core :as ig]))
(prefer-method print-method
clojure.lang.IRecord
@@ -26,6 +29,16 @@
clojure.lang.IPersistentMap
clojure.lang.IDeref)
+(defmethod ig/init-key :default
+ [_ data]
+ (d/without-nils data))
+
+(defmethod ig/prep-key :default
+ [_ data]
+ (if (map? data)
+ (d/without-nils data)
+ data))
+
(def defaults
{:http-server-port 6060
:host "devenv"
@@ -34,8 +47,7 @@
:database-username "penpot"
:database-password "penpot"
- :default-blob-version 1
-
+ :default-blob-version 3
:loggers-zmq-uri "tcp://localhost:45556"
:asserts-enabled false
@@ -72,7 +84,6 @@
:allow-demo-users true
:registration-enabled true
- :registration-domain-whitelist ""
:telemetry-enabled false
:telemetry-uri "https://telemetry.penpot.app/"
@@ -87,6 +98,13 @@
:initial-project-skey "initial-project"
})
+(s/def ::audit-enabled ::us/boolean)
+(s/def ::audit-archive-enabled ::us/boolean)
+(s/def ::audit-archive-uri ::us/string)
+(s/def ::audit-archive-gc-enabled ::us/boolean)
+(s/def ::audit-archive-gc-max-age ::dt/duration)
+
+(s/def ::secret-key ::us/string)
(s/def ::allow-demo-users ::us/boolean)
(s/def ::asserts-enabled ::us/boolean)
(s/def ::assets-path ::us/string)
@@ -142,7 +160,7 @@
(s/def ::profile-complaint-threshold ::us/integer)
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
-(s/def ::registration-domain-whitelist ::us/string)
+(s/def ::registration-domain-whitelist ::us/set-of-str)
(s/def ::registration-enabled ::us/boolean)
(s/def ::rlimits-image ::us/integer)
(s/def ::rlimits-password ::us/integer)
@@ -162,14 +180,18 @@
(s/def ::storage-s3-bucket ::us/string)
(s/def ::storage-s3-region ::us/keyword)
(s/def ::telemetry-enabled ::us/boolean)
-(s/def ::telemetry-server-enabled ::us/boolean)
-(s/def ::telemetry-server-port ::us/integer)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::config
- (s/keys :opt-un [::allow-demo-users
+ (s/keys :opt-un [::secret-key
+ ::allow-demo-users
+ ::audit-enabled
+ ::audit-archive-enabled
+ ::audit-archive-uri
+ ::audit-archive-gc-enabled
+ ::audit-archive-gc-max-age
::asserts-enabled
::database-password
::database-uri
@@ -242,8 +264,6 @@
::storage-s3-bucket
::storage-s3-region
::telemetry-enabled
- ::telemetry-server-enabled
- ::telemetry-server-port
::telemetry-uri
::telemetry-referer
::telemetry-with-taiga
@@ -263,9 +283,17 @@
(defn- read-config
[]
- (->> (read-env "penpot")
- (merge defaults)
- (us/conform ::config)))
+ (try
+ (->> (read-env "penpot")
+ (merge defaults)
+ (us/conform ::config))
+ (catch Throwable e
+ (when (ex/ex-info? e)
+ (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
+ (println "Error on validating configuration:")
+ (println (:explain (ex-data e))
+ (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))
+ (throw e))))
(def version (v/parse (or (some-> (io/resource "version.txt")
(slurp)
diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj
index 32a91e8d6..b9886ec7e 100644
--- a/backend/src/app/db.clj
+++ b/backend/src/app/db.clj
@@ -200,6 +200,13 @@
(sql/insert table params opts)
(assoc opts :return-keys true))))
+(defn insert-multi!
+ ([ds table cols rows] (insert-multi! ds table cols rows nil))
+ ([ds table cols rows opts]
+ (exec! ds
+ (sql/insert-multi table cols rows opts)
+ (assoc opts :return-keys true))))
+
(defn update!
([ds table params where] (update! ds table params where nil))
([ds table params where opts]
@@ -326,6 +333,12 @@
(t/decode-str val)
val)))
+(defn inet
+ [ip-addr]
+ (doto (org.postgresql.util.PGobject.)
+ (.setType "inet")
+ (.setValue (str ip-addr))))
+
(defn tjson
"Encode as transit json."
[data]
diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj
index d2c92db38..6ee5d3073 100644
--- a/backend/src/app/db/sql.clj
+++ b/backend/src/app/db/sql.clj
@@ -32,6 +32,11 @@
(assoc :suffix "ON CONFLICT DO NOTHING"))]
(sql/for-insert table key-map opts))))
+(defn insert-multi
+ [table cols rows opts]
+ (let [opts (merge default-opts opts)]
+ (sql/for-insert-multi table cols rows opts)))
+
(defn select
([table where-params]
(select table where-params nil))
diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj
index d2e45ed01..6f0d85cc0 100644
--- a/backend/src/app/http/errors.clj
+++ b/backend/src/app/http/errors.clj
@@ -76,6 +76,7 @@
{:status 500
:body {:type :server-error
+ :code :assertion
:data (-> edata
(assoc :explain (explain-error edata))
(dissoc :data))}}))
@@ -103,6 +104,7 @@
:cause error)
{:status 500
:body {:type :server-error
+ :code :unexpected
:hint (ex-message error)
:data edata}}))))
@@ -132,7 +134,8 @@
:else
{:status 500
- :body {:type :server-timeout
+ :body {:type :server-error
+ :code :psql-exception
:hint (ex-message error)
:state state}})))
diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj
index 60e0a90c8..41a40ab18 100644
--- a/backend/src/app/http/oauth.clj
+++ b/backend/src/app/http/oauth.clj
@@ -98,10 +98,11 @@
res (http/send! req)]
(when (= 200 (:status res))
- (let [{:keys [name] :as data} (json/read-str (:body res) :key-fn keyword)]
- (-> data
- (assoc :backend (:name provider))
- (assoc :fullname name)))))
+ (let [info (json/read-str (:body res) :key-fn keyword)]
+ {:backend (:name provider)
+ :email (:email info)
+ :fullname (:name info)
+ :props (dissoc info :name :email)})))
(catch Exception e
(l/error :hint "unexpected exception on retrieve-user-info"
@@ -117,7 +118,8 @@
(retrieve-user-info cfg))]
(when-not info
(ex/raise :type :internal
- :code :unable-to-auth))
+ :code :unable-to-auth
+ :hint "no user info"))
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
@@ -138,16 +140,35 @@
(cond-> info
(some? (:invitation-token state))
- (assoc :invitation-token (:invitation-token state)))))
+ (assoc :invitation-token (:invitation-token state))
+
+ ;; If state token comes with props, merge them. The state token
+ ;; props can contain pm_ and utm_ prefixed query params.
+ (map? (:props state))
+ (update :props merge (:props state)))))
;; --- HTTP HANDLERS
+(defn extract-props
+ [params]
+ (reduce-kv (fn [params k v]
+ (let [sk (name k)]
+ (cond-> params
+ (or (str/starts-with? sk "pm_")
+ (str/starts-with? sk "pm-")
+ (str/starts-with? sk "utm_"))
+ (assoc (-> sk str/kebab keyword) v))))
+ {}
+ params))
+
(defn- auth-handler
- [{:keys [tokens] :as cfg} request]
- (let [invitation (get-in request [:params :invitation-token])
+ [{:keys [tokens] :as cfg} {:keys [params] :as request}]
+ (let [invitation (:invitation-token params)
+ props (extract-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
@@ -215,8 +236,7 @@
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
- :scopes (into #{"openid" "profile" "email" "name"}
- (cf/get :oidc-scopes #{}))
+ :scopes (cf/get :oidc-scopes #{"openid" "profile"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}]
@@ -238,9 +258,7 @@
[cfg]
(let [opts {:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)
- :scopes #{"email" "profile" "openid"
- "https://www.googleapis.com/auth/userinfo.email"
- "https://www.googleapis.com/auth/userinfo.profile"}
+ :scopes #{"openid" "email" "profile"}
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
@@ -256,8 +274,7 @@
[cfg]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
- :scopes #{"read:user"
- "user:email"}
+ :scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user"
diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj
index af0fd6d8f..d1b0170bb 100644
--- a/backend/src/app/http/session.clj
+++ b/backend/src/app/http/session.clj
@@ -106,7 +106,6 @@
;; --- STATE INIT: SESSION UPDATER
-(declare batch-events)
(declare update-sessions)
(s/def ::session map?)
@@ -129,7 +128,9 @@
(l/info :action "initialize session updater"
:max-batch-age (str (:max-batch-age cfg))
:max-batch-size (str (:max-batch-size cfg)))
- (let [input (batch-events cfg (::events-ch session))
+ (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."
@@ -149,36 +150,6 @@
:count result))
(recur))))))
-(defn- timeout-chan
- [cfg]
- (a/timeout (inst-ms (:max-batch-age cfg))))
-
-(defn- batch-events
- [cfg in]
- (let [out (a/chan)]
- (a/go-loop [tch (timeout-chan cfg)
- buf #{}]
- (let [[val port] (a/alts! [tch in])]
- (cond
- (identical? port tch)
- (if (empty? buf)
- (recur (timeout-chan cfg) buf)
- (do
- (a/>! out [:timeout buf])
- (recur (timeout-chan cfg) #{})))
-
- (nil? val)
- (a/close! out)
-
- (identical? port in)
- (let [buf (conj buf val)]
- (if (>= (count buf) (:max-batch-size cfg))
- (do
- (a/>! out [:size buf])
- (recur (timeout-chan cfg) #{}))
- (recur tch buf))))))
- out))
-
(defn- update-sessions
[{:keys [pool executor]} ids]
(aa/with-thread executor
diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj
new file mode 100644
index 000000000..bbffcf781
--- /dev/null
+++ b/backend/src/app/loggers/audit.clj
@@ -0,0 +1,232 @@
+;; 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.loggers.audit
+ "Services related to the user activity (audit log)."
+ (:require
+ [app.common.exceptions :as ex]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
+ [app.config :as cf]
+ [app.db :as db]
+ [app.util.async :as aa]
+ [app.util.http :as http]
+ [app.util.logging :as l]
+ [app.util.time :as dt]
+ [app.util.transit :as t]
+ [app.worker :as wrk]
+ [clojure.core.async :as a]
+ [clojure.spec.alpha :as s]
+ [integrant.core :as ig]
+ [lambdaisland.uri :as u]))
+
+(defn clean-props
+ [{:keys [profile-id] :as event}]
+ (letfn [(clean-common [props]
+ (-> props
+ (dissoc :session-id)
+ (dissoc :password)
+ (dissoc :old-password)
+ (dissoc :token)))
+
+ (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))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Collector
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Defines a service that collects the audit/activity log using
+;; internal database. Later this audit log can be transferred to
+;; an external storage and data cleared.
+
+(declare persist-events)
+(s/def ::enabled ::us/boolean)
+
+(defmethod ig/pre-init-spec ::collector [_]
+ (s/keys :req-un [::db/pool ::wrk/executor ::enabled]))
+
+(def event-xform
+ (comp
+ (filter :profile-id)
+ (map clean-props)))
+
+(defmethod ig/init-key ::collector
+ [_ {:keys [enabled] :as cfg}]
+ (when enabled
+ (l/info :msg "intializing audit collector")
+ (let [input (a/chan 1 event-xform)
+ buffer (aa/batch input {:max-batch-size 100
+ :max-batch-age (* 5 1000)
+ :init []})]
+ (a/go-loop []
+ (when-let [[type events] (a/row [event]
+ [(uuid/next)
+ (:name event)
+ (:type event)
+ (:profile-id event)
+ (db/tjson (:props event))])]
+
+ (aa/with-thread executor
+ (db/with-atomic [conn pool]
+ (db/insert-multi! conn :audit-log
+ [:id :name :type :profile-id :props]
+ (sequence (map event->row) events))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Archive Task
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; This is a task responsible to send the accomulated events to an
+;; external service for archival.
+
+(declare archive-events)
+
+(s/def ::uri ::us/string)
+(s/def ::tokens fn?)
+
+(defmethod ig/pre-init-spec ::archive-task [_]
+ (s/keys :req-un [::db/pool ::tokens ::enabled]
+ :opt-un [::uri]))
+
+(defmethod ig/init-key ::archive-task
+ [_ {:keys [uri enabled] :as cfg}]
+ (fn [_]
+ (when (and enabled (not uri))
+ (ex/raise :type :internal
+ :code :task-not-configured
+ :hint "archive task not configured, missing uri"))
+ (loop []
+ (let [res (archive-events cfg)]
+ (when (= res :continue)
+ (aa/thread-sleep 200)
+ (recur))))))
+
+(def sql:retrieve-batch-of-audit-log
+ "select * from audit_log
+ where archived_at is null
+ order by created_at asc
+ limit 100
+ for update skip locked;")
+
+(defn archive-events
+ [{:keys [pool uri tokens] :as cfg}]
+ (letfn [(decode-row [{:keys [props] :as row}]
+ (cond-> row
+ (db/pgobject? props)
+ (assoc :props (db/decode-transit-pgobject props))))
+
+ (row->event [{:keys [name type created-at profile-id props]}]
+ {:type type
+ :name name
+ :timestamp created-at
+ :profile-id profile-id
+ :props props})
+
+ (send [events]
+ (let [token (tokens :generate {:iss "authentication"
+ :iat (dt/now)
+ :uid uuid/zero})
+ body (t/encode {:events events})
+ headers {"content-type" "application/transit+json"
+ "origin" (cf/get :public-uri)
+ "cookie" (u/map->query-string {:auth-token token})}
+ params {:uri uri
+ :timeout 5000
+ :method :post
+ :headers headers
+ :body body}
+ resp (http/send! params)]
+ (when (not= (:status resp) 204)
+ (ex/raise :type :internal
+ :code :unable-to-send-events
+ :hint "unable to send events"
+ :context resp))))
+
+ (mark-as-archived [conn rows]
+ (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)"
+ (->> (map :id rows)
+ (into-array java.util.UUID)
+ (db/create-array conn "uuid"))]))]
+
+ (db/with-atomic [conn pool]
+ (let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log])
+
+ xform (comp (map decode-row)
+ (map row->event))
+ events (into [] xform rows)]
+ (l/debug :action "archive-events" :uri uri :events (count events))
+ (if (empty? events)
+ :empty
+ (do
+ (send events)
+ (mark-as-archived conn rows)
+ :continue))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; GC Task
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(declare clean-archived)
+
+(s/def ::max-age ::cf/audit-archive-gc-max-age)
+
+(defmethod ig/pre-init-spec ::archive-gc-task [_]
+ (s/keys :req-un [::db/pool ::enabled ::max-age]))
+
+(defmethod ig/init-key ::archive-gc-task
+ [_ cfg]
+ (fn [_]
+ (clean-archived cfg)))
+
+(def sql:clean-archived
+ "delete from audit_log
+ where archived_at is not null
+ and archived_at < now() - ?::interval")
+
+(defn- clean-archived
+ [{:keys [pool max-age]}]
+ (let [interval (db/interval max-age)
+ result (db/exec-one! pool [sql:clean-archived interval])
+ result (:next.jdbc/update-count result)]
+ (l/debug :action "clean archived audit log" :removed result)
+ result))
diff --git a/backend/src/app/loggers/loki.clj b/backend/src/app/loggers/loki.clj
index 607f06e3b..1bafb7c4f 100644
--- a/backend/src/app/loggers/loki.clj
+++ b/backend/src/app/loggers/loki.clj
@@ -31,16 +31,16 @@
[_ {:keys [receiver uri] :as cfg}]
(when uri
(l/info :msg "intializing loki reporter" :uri uri)
- (let [output (a/chan (a/sliding-buffer 1024))]
- (receiver :sub output)
+ (let [input (a/chan (a/sliding-buffer 1024))]
+ (receiver :sub input)
(a/go-loop []
- (let [msg (a/otf [data]
+ (let [input-file (fs/create-tempfile :prefix "penpot")
+ output-file (fs/path (str input-file ".otf"))
+ _ (with-open [out (io/output-stream input-file)]
+ (IOUtils/writeChunked ^bytes data ^OutputStream out)
+ (.flush ^OutputStream out))
+ res (sh/sh "fontforge" "-lang=ff" "-c"
+ (str/fmt "Open('%s'); Generate('%s')"
+ (str input-file)
+ (str output-file)))]
+ (when (zero? (:exit res))
+ (fs/slurp-bytes output-file))))
+
+
+ (otf->ttf [data]
+ (let [input-file (fs/create-tempfile :prefix "penpot")
+ output-file (fs/path (str input-file ".ttf"))
+ _ (with-open [out (io/output-stream input-file)]
+ (IOUtils/writeChunked ^bytes data ^OutputStream out)
+ (.flush ^OutputStream out))
+ res (sh/sh "fontforge" "-lang=ff" "-c"
+ (str/fmt "Open('%s'); Generate('%s')"
+ (str input-file)
+ (str output-file)))]
+ (when (zero? (:exit res))
+ (fs/slurp-bytes output-file))))
+
+ (ttf-or-otf->woff [data]
+ (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
+ output-file (fs/path (str input-file ".woff"))
+ _ (with-open [out (io/output-stream input-file)]
+ (IOUtils/writeChunked ^bytes data ^OutputStream out)
+ (.flush ^OutputStream out))
+ res (sh/sh "sfnt2woff" (str input-file))]
+ (when (zero? (:exit res))
+ (fs/slurp-bytes output-file))))
+
+ (ttf-or-otf->woff2 [data]
+ (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
+ output-file (fs/path (str input-file ".woff2"))
+ _ (with-open [out (io/output-stream input-file)]
+ (IOUtils/writeChunked ^bytes data ^OutputStream out)
+ (.flush ^OutputStream out))
+ res (sh/sh "woff2_compress" (str input-file))]
+ (when (zero? (:exit res))
+ (fs/slurp-bytes output-file))))
+
+ (woff->sfnt [data]
+ (let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
+ _ (with-open [out (io/output-stream input-file)]
+ (IOUtils/writeChunked ^bytes data ^OutputStream out)
+ (.flush ^OutputStream out))
+ res (sh/sh "woff2sfnt" (str input-file)
+ :out-enc :bytes)]
+ (when (zero? (:exit res))
+ (:out res))))
+
+ ;; Documented here:
+ ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
+ (get-sfnt-type [data]
+ (let [buff (bb/slice data 0 4)
+ type (bc/bytes->hex buff)]
+ (case type
+ "4f54544f" :otf
+ "00010000" :ttf
+ (ex/raise :type :internal
+ :code :unexpected-data
+ :hint "unexpected font data"))))
+
+ (gen-if-nil [val factory]
+ (if (nil? val)
+ (factory)
+ val))]
+
+ (let [current (into #{} (keys input))]
+ (cond
+ (contains? current "font/ttf")
+ (let [data (get input "font/ttf")]
+ (-> input
+ (update "font/otf" gen-if-nil #(ttf->otf data))
+ (update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
+ (assoc "font/woff2" (ttf-or-otf->woff2 data))))
+
+ (contains? current "font/otf")
+ (let [data (get input "font/otf")]
+ (-> input
+ (update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
+ (assoc "font/ttf" (otf->ttf data))
+ (assoc "font/woff2" (ttf-or-otf->woff2 data))))
+
+ (contains? current "font/woff")
+ (let [data (get input "font/woff")
+ sfnt (woff->sfnt data)]
+ (when-not sfnt
+ (ex/raise :type :validation
+ :code :invalid-woff-file
+ :hint "invalid woff file"))
+ (let [stype (get-sfnt-type sfnt)]
+ (cond-> input
+ true
+ (-> (assoc "font/woff" data)
+ (assoc "font/woff2" (ttf-or-otf->woff2 sfnt)))
+
+ (= stype :otf)
+ (-> (assoc "font/otf" sfnt)
+ (assoc "font/ttf" (otf->ttf sfnt)))
+
+ (= stype :ttf)
+ (-> (assoc "font/otf" (ttf->otf sfnt))
+ (assoc "font/ttf" sfnt)))))))))
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 18a8917c4..8e6350995 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -166,6 +166,15 @@
{:name "0052-del-legacy-user-and-team"
:fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
+
+ {:name "0053-add-team-font-variant-table"
+ :fn (mg/resource "app/migrations/sql/0053-add-team-font-variant-table.sql")}
+
+ {:name "0054-add-audit-log-table"
+ :fn (mg/resource "app/migrations/sql/0054-add-audit-log-table.sql")}
+
+ {:name "0055-mod-file-media-object-table"
+ :fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")}
])
diff --git a/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql b/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql
index 5427757cf..1787c5a70 100644
--- a/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql
+++ b/backend/src/app/migrations/sql/0015-improve-tasks-tables.sql
@@ -1,4 +1,4 @@
-DROP TABLE task;
+DROP TABLE IF EXISTS task;
CREATE TABLE task (
id uuid DEFAULT uuid_generate_v4(),
@@ -27,3 +27,11 @@ CREATE TABLE task_default partition OF task default;
CREATE INDEX task__scheduled_at__queue__idx
ON task (scheduled_at, queue)
WHERE status = 'new' or status = 'retry';
+
+ALTER TABLE task
+ ALTER COLUMN queue SET STORAGE external,
+ ALTER COLUMN name SET STORAGE external,
+ ALTER COLUMN props SET STORAGE external,
+ ALTER COLUMN status SET STORAGE external,
+ ALTER COLUMN error SET STORAGE external;
+
diff --git a/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql b/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql
index b36c2a205..caad9b33b 100644
--- a/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql
+++ b/backend/src/app/migrations/sql/0019-add-improved-scheduled-tasks.sql
@@ -1,4 +1,4 @@
-DROP TABLE scheduled_task;
+DROP TABLE IF EXISTS scheduled_task;
CREATE TABLE scheduled_task (
id text PRIMARY KEY,
@@ -22,3 +22,7 @@ CREATE TABLE scheduled_task_history (
CREATE INDEX scheduled_task_history__task_id__idx
ON scheduled_task_history(task_id);
+
+ALTER TABLE scheduled_task
+ ALTER COLUMN id SET STORAGE external,
+ ALTER COLUMN cron_expr SET STORAGE external;
diff --git a/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql b/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql
index 5cd38a5f4..6a9e7c8c4 100644
--- a/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql
+++ b/backend/src/app/migrations/sql/0041-mod-pg-storage-options.sql
@@ -27,17 +27,6 @@ ALTER TABLE comment_thread
ALTER COLUMN participants SET STORAGE external,
ALTER COLUMN page_name SET STORAGE external;
-ALTER TABLE task
- ALTER COLUMN queue SET STORAGE external,
- ALTER COLUMN name SET STORAGE external,
- ALTER COLUMN props SET STORAGE external,
- ALTER COLUMN status SET STORAGE external,
- ALTER COLUMN error SET STORAGE external;
-
-ALTER TABLE scheduled_task
- ALTER COLUMN id SET STORAGE external,
- ALTER COLUMN cron_expr SET STORAGE external;
-
ALTER TABLE http_session
ALTER COLUMN id SET STORAGE external,
ALTER COLUMN user_agent SET STORAGE external;
diff --git a/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql
new file mode 100644
index 000000000..e03d1da99
--- /dev/null
+++ b/backend/src/app/migrations/sql/0053-add-team-font-variant-table.sql
@@ -0,0 +1,43 @@
+CREATE TABLE team_font_variant (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+
+ team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
+ profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE,
+
+ created_at timestamptz NOT NULL DEFAULT now(),
+ modified_at timestamptz NOT NULL DEFAULT now(),
+ deleted_at timestamptz NULL DEFAULT NULL,
+
+ font_id uuid NOT NULL,
+ font_family text NOT NULL,
+ font_weight smallint NOT NULL,
+ font_style text NOT NULL,
+
+ otf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE,
+ ttf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE,
+ woff1_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE,
+ woff2_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL DEFERRABLE
+);
+
+CREATE INDEX team_font_variant_team_id_font_id_idx
+ ON team_font_variant (team_id, font_id);
+
+CREATE INDEX team_font_variant_profile_id_idx
+ ON team_font_variant (profile_id);
+
+CREATE INDEX team_font_variant_otf_file_id_idx
+ ON team_font_variant (otf_file_id);
+
+CREATE INDEX team_font_variant_ttf_file_id_idx
+ ON team_font_variant (ttf_file_id);
+
+CREATE INDEX team_font_variant_woff1_file_id_idx
+ ON team_font_variant (woff1_file_id);
+
+CREATE INDEX team_font_variant_woff2_file_id_idx
+ ON team_font_variant (woff2_file_id);
+
+ALTER TABLE team_font_variant
+ ALTER COLUMN font_family SET STORAGE external,
+ ALTER COLUMN font_style SET STORAGE external;
+
diff --git a/backend/src/app/migrations/sql/0054-add-audit-log-table.sql b/backend/src/app/migrations/sql/0054-add-audit-log-table.sql
new file mode 100644
index 000000000..b7097fde2
--- /dev/null
+++ b/backend/src/app/migrations/sql/0054-add-audit-log-table.sql
@@ -0,0 +1,25 @@
+CREATE TABLE audit_log (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+
+ name text NOT NULL,
+ type text NOT NULL,
+
+ created_at timestamptz DEFAULT clock_timestamp() NOT NULL,
+ archived_at timestamptz NULL,
+
+ profile_id uuid NOT NULL,
+ props jsonb,
+
+ PRIMARY KEY (created_at, profile_id)
+) PARTITION BY RANGE (created_at);
+
+ALTER TABLE audit_log
+ ALTER COLUMN name SET STORAGE external,
+ ALTER COLUMN type SET STORAGE external,
+ ALTER COLUMN props SET STORAGE external;
+
+CREATE INDEX audit_log_id_archived_at_idx ON audit_log (id, archived_at);
+
+CREATE TABLE audit_log_default (LIKE audit_log INCLUDING ALL);
+
+ALTER TABLE audit_log ATTACH PARTITION audit_log_default DEFAULT;
diff --git a/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql b/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql
new file mode 100644
index 000000000..7017cb8d9
--- /dev/null
+++ b/backend/src/app/migrations/sql/0055-mod-file-media-object-table.sql
@@ -0,0 +1,4 @@
+ALTER TABLE file_media_object
+ DROP CONSTRAINT file_media_object_thumbnail_id_fkey,
+ ADD CONSTRAINT file_media_object_thumbnail_id_fkey
+ FOREIGN KEY (thumbnail_id) REFERENCES storage_object (id) ON DELETE SET NULL;
diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj
index 8058c6577..f7a013f72 100644
--- a/backend/src/app/msgbus.clj
+++ b/backend/src/app/msgbus.clj
@@ -21,6 +21,7 @@
java.time.Duration
io.lettuce.core.RedisClient
io.lettuce.core.RedisURI
+ io.lettuce.core.api.StatefulConnection
io.lettuce.core.api.StatefulRedisConnection
io.lettuce.core.api.async.RedisAsyncCommands
io.lettuce.core.codec.ByteArrayCodec
@@ -130,6 +131,7 @@
;; --- REDIS BACKEND IMPL
+(declare impl-redis-open?)
(declare impl-redis-pub)
(declare impl-redis-sub)
(declare impl-redis-unsub)
@@ -162,7 +164,8 @@
(a/go-loop []
(when-let [val (a/> (sv/scan-ns 'app.rpc.queries.projects
'app.rpc.queries.files
'app.rpc.queries.teams
@@ -120,6 +136,7 @@
'app.rpc.queries.profile
'app.rpc.queries.recent-files
'app.rpc.queries.viewer
+ 'app.rpc.queries.fonts
'app.rpc.queries.svg)
(map (partial process-method cfg))
(into {}))))
@@ -132,7 +149,7 @@
:registry (get-in cfg [:metrics :registry])
:type :histogram
:help "Timing of mutation services."})
- cfg (assoc cfg ::mobj mobj)]
+ cfg (assoc cfg ::mobj mobj ::type "mutation")]
(->> (sv/scan-ns 'app.rpc.mutations.demo
'app.rpc.mutations.media
'app.rpc.mutations.profile
@@ -143,6 +160,7 @@
'app.rpc.mutations.teams
'app.rpc.mutations.management
'app.rpc.mutations.ldap
+ 'app.rpc.mutations.fonts
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))
@@ -150,9 +168,11 @@
(s/def ::storage some?)
(s/def ::session map?)
(s/def ::tokens fn?)
+(s/def ::audit (s/nilable fn?))
(defmethod ig/pre-init-spec ::rpc [_]
- (s/keys :req-un [::db/pool ::storage ::session ::tokens ::mtx/metrics ::rlm/rlimits]))
+ (s/keys :req-un [::storage ::session ::tokens ::audit
+ ::mtx/metrics ::rlm/rlimits ::db/pool]))
(defmethod ig/init-key ::rpc
[_ cfg]
diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj
index 2f0c583c2..a74f8b4f8 100644
--- a/backend/src/app/rpc/mutations/demo.clj
+++ b/backend/src/app/rpc/mutations/demo.clj
@@ -11,6 +11,7 @@
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
+ [app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile]
[app.setup.initial-data :as sid]
[app.util.services :as sv]
@@ -53,5 +54,6 @@
::wrk/conn conn
:profile-id id})
- {:email email
- :password password})))
+ (with-meta {:email email
+ :password password}
+ {::audit/profile-id id}))))
diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj
index 493be817b..d6fb24c59 100644
--- a/backend/src/app/rpc/mutations/files.clj
+++ b/backend/src/app/rpc/mutations/files.clj
@@ -228,16 +228,10 @@
{:id file-id}))
+;; --- MUTATION: update-file
+
;; A generic, Changes based (granular) file update method.
-(s/def ::changes
- (s/coll-of map? :kind vector?))
-
-(s/def ::session-id ::us/uuid)
-(s/def ::revn ::us/integer)
-(s/def ::update-file
- (s/keys :req-un [::id ::session-id ::profile-id ::revn ::changes]))
-
;; File changes that affect to the library, and must be notified
;; to all clients using it.
(defn library-change?
@@ -256,6 +250,31 @@
(declare send-notifications)
(declare update-file)
+(s/def ::changes
+ (s/coll-of map? :kind vector?))
+
+(s/def ::hint-origin ::us/keyword)
+(s/def ::hint-events
+ (s/every ::us/keyword :kind vector?))
+
+(s/def ::change-with-metadata
+ (s/keys :req-un [::changes]
+ :opt-un [::hint-origin
+ ::hint-events]))
+
+(s/def ::changes-with-metadata
+ (s/every ::change-with-metadata :kind vector?))
+
+(s/def ::session-id ::us/uuid)
+(s/def ::revn ::us/integer)
+(s/def ::update-file
+ (s/and
+ (s/keys :req-un [::id ::session-id ::profile-id ::revn]
+ :opt-un [::changes ::changes-with-metadata])
+ (fn [o]
+ (or (contains? o :changes)
+ (contains? o :changes-with-metadata)))))
+
(sv/defmethod ::update-file
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
@@ -265,7 +284,7 @@
(assoc params :file file)))))
(defn- update-file
- [{:keys [conn] :as cfg} {:keys [file changes session-id profile-id] :as params}]
+ [{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
@@ -274,15 +293,19 @@
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
- (let [file (-> file
- (update :revn inc)
- (update :data (fn [data]
- (-> data
- (blob/decode)
- (assoc :id (:id file))
- (pmg/migrate-data)
- (cp/process-changes changes)
- (blob/encode)))))]
+ (let [changes (if changes-with-metadata
+ (mapcat :changes changes-with-metadata)
+ changes)
+
+ file (-> file
+ (update :revn inc)
+ (update :data (fn [data]
+ (-> data
+ (blob/decode)
+ (assoc :id (:id file))
+ (pmg/migrate-data)
+ (cp/process-changes changes)
+ (blob/encode)))))]
;; Insert change to the xlog
(db/insert! conn :file-change
{:id (uuid/next)
@@ -300,7 +323,8 @@
:has-media-trimmed false}
{:id (:id file)})
- (let [params (assoc params :file file)]
+ (let [params (-> params (assoc :file file
+ :changes changes))]
;; Send asynchronous notifications
(send-notifications cfg params)
diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj
new file mode 100644
index 000000000..ca1d2263e
--- /dev/null
+++ b/backend/src/app/rpc/mutations/fonts.clj
@@ -0,0 +1,143 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.rpc.mutations.fonts
+ (:require
+ [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.services :as sv]
+ [app.util.time :as dt]
+ [app.worker :as wrk]
+ [clojure.spec.alpha :as s]))
+
+(declare create-font-variant)
+
+(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
+(def valid-style #{"normal" "italic"})
+
+(s/def ::id ::us/uuid)
+(s/def ::profile-id ::us/uuid)
+(s/def ::team-id ::us/uuid)
+(s/def ::name ::us/not-empty-string)
+(s/def ::weight valid-weight)
+(s/def ::style valid-style)
+(s/def ::font-id ::us/uuid)
+(s/def ::content-type ::media/font-content-type)
+(s/def ::data (s/map-of ::us/string any?))
+
+(s/def ::create-font-variant
+ (s/keys :req-un [::profile-id ::team-id ::data
+ ::font-id ::font-family ::font-weight ::font-style]))
+
+(sv/defmethod ::create-font-variant
+ [{: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))))
+
+(defn create-font-variant
+ [{:keys [conn storage] :as cfg} {:keys [data] :as params}]
+ (let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
+ storage (assoc storage :conn conn)
+ 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"}))]
+
+ (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)})))
+
+;; --- UPDATE FONT FAMILY
+
+(s/def ::update-font
+ (s/keys :req-un [::profile-id ::team-id ::id ::name]))
+
+(def sql:update-font
+ "update team_font_variant
+ set font_family = ?
+ where team_id = ?
+ and font_id = ?")
+
+(sv/defmethod ::update-font
+ [{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
+ (db/with-atomic [conn pool]
+ (teams/check-edition-permissions! conn profile-id team-id)
+ (db/exec-one! conn [sql:update-font name team-id id])
+ nil))
+
+;; --- DELETE FONT
+
+(s/def ::delete-font
+ (s/keys :req-un [::profile-id ::team-id ::id]))
+
+(sv/defmethod ::delete-font
+ [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
+ (db/with-atomic [conn pool]
+ (teams/check-edition-permissions! conn profile-id team-id)
+
+ (let [items (db/query conn :team-font-variant
+ {:font-id id :team-id team-id}
+ {:for-update true})]
+ (doseq [item items]
+ ;; Schedule object deletion
+ (wrk/submit! {::wrk/task :delete-object
+ ::wrk/delay cf/deletion-delay
+ ::wrk/conn conn
+ :id (:id item)
+ :type :team-font-variant}))
+
+ (db/update! conn :team-font-variant
+ {:deleted-at (dt/now)}
+ {:font-id id :team-id team-id})
+ nil)))
+
+;; --- DELETE FONT VARIANT
+
+(s/def ::delete-font-variant
+ (s/keys :req-un [::profile-id ::team-id ::id]))
+
+(sv/defmethod ::delete-font-variant
+ [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
+ (db/with-atomic [conn pool]
+ (teams/check-edition-permissions! conn profile-id team-id)
+
+ ;; Schedule object deletion
+ (wrk/submit! {::wrk/task :delete-object
+ ::wrk/delay cf/deletion-delay
+ ::wrk/conn conn
+ :id id
+ :type :team-font-variant})
+
+ (db/update! conn :team-font-variant
+ {:deleted-at (dt/now)}
+ {:id id :team-id team-id})
+ nil))
diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj
index 177d02964..cc38374b2 100644
--- a/backend/src/app/rpc/mutations/management.clj
+++ b/backend/src/app/rpc/mutations/management.clj
@@ -91,21 +91,21 @@
(def sql:retrieve-used-media-objects
"select fmo.*
from file_media_object as fmo
- inner join storage_object as o on (fmo.media_id = o.id)
+ inner join storage_object as so on (fmo.media_id = so.id)
where fmo.file_id = ?
- and o.deleted_at is null")
+ and so.deleted_at is null")
(defn duplicate-file
- [conn {:keys [profile-id file index project-id name]} {:keys [reset-shared-flag] :as opts}]
- (let [flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)])
- fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)])
+ [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
+ (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
+ fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
;; memo uniform creation/modification date
now (dt/now)
ignore (dt/plus now (dt/duration {:seconds 5}))
;; add to the index all file media objects.
- index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
+ index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
flibs-xf (comp
(map #(remap-id % index :file-id))
diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj
index 26dbac324..62241a48b 100644
--- a/backend/src/app/rpc/mutations/media.clj
+++ b/backend/src/app/rpc/mutations/media.clj
@@ -32,12 +32,15 @@
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
+
;; --- Create File Media object (upload)
(declare create-file-media-object)
(declare select-file)
-(s/def ::content ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::content (s/and ::media/upload (s/keys :req-un [::content-type])))
+
(s/def ::is-local ::us/boolean)
(s/def ::upload-file-media-object
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index 7e719b6c2..71c2a0d90 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -6,13 +6,14 @@
(ns app.rpc.mutations.profile
(:require
- [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.emails :as eml]
+ [app.http.oauth :refer [extract-props]]
+ [app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
@@ -59,9 +60,10 @@
(ex/raise :type :restriction
:code :registration-disabled))
- (when-not (email-domain-in-whitelist? (cfg/get :registration-domain-whitelist) (:email params))
- (ex/raise :type :validation
- :code :email-domain-is-not-allowed))
+ (when-let [domains (cfg/get :registration-domain-whitelist)]
+ (when-not (email-domain-in-whitelist? domains (:email params))
+ (ex/raise :type :validation
+ :code :email-domain-is-not-allowed)))
(when-not (:terms-privacy params)
(ex/raise :type :validation
@@ -101,7 +103,9 @@
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
- :before-complete (annotate-profile-register metrics profile)}))
+ :before-complete (annotate-profile-register metrics profile)
+ ::audit/props (:props profile)
+ ::audit/profile-id (:id profile)}))
;; If no token is provided, send a verification email
(let [vtoken (tokens :generate
@@ -129,17 +133,20 @@
:extra-data ptoken})
(with-meta profile
- {:before-complete (annotate-profile-register metrics profile)})))))
+ {:before-complete (annotate-profile-register metrics profile)
+ ::audit/props (:props profile)
+ ::audit/profile-id (:id profile)})))))
(defn email-domain-in-whitelist?
- "Returns true if email's domain is in the given whitelist or if given
- whitelist is an empty string."
- [whitelist email]
- (if (str/empty-or-nil? whitelist)
+ "Returns true if email's domain is in the given whitelist or if
+ given whitelist is an empty string."
+ [domains email]
+ (if (or (empty? domains)
+ (nil? domains))
true
- (let [domains (str/split whitelist #",\s*")
- domain (second (str/split email #"@" 2))]
- (contains? (set domains) domain))))
+ (let [[_ candidate] (-> (str/lower email)
+ (str/split #"@" 2))]
+ (contains? domains candidate))))
(def ^:private sql:profile-existence
"select exists (select * from profile
@@ -174,11 +181,12 @@
(defn create-profile
"Create the profile entry on the database with limited input
filling all the other fields with defaults."
- [conn {:keys [id fullname email password props is-active is-muted is-demo opts]
- :or {is-active false is-muted false is-demo false}}]
+ [conn {:keys [id fullname email password is-active is-muted is-demo opts]
+ :or {is-active false is-muted false is-demo false}
+ :as params}]
(let [id (or id (uuid/next))
is-active (if is-demo true is-active)
- props (db/tjson (or props {}))
+ props (-> params extract-props db/tjson)
password (derive-password password)
params {:id id
:fullname fullname
@@ -270,10 +278,12 @@
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta {:invitation-token token}
- {:transform-response ((:create session) (:id profile))}))
+ {:transform-response ((:create session) (:id profile))
+ ::audit/profile-id (:id profile)}))
(with-meta profile
- {:transform-response ((:create session) (:id profile))}))))))
+ {:transform-response ((:create session) (:id profile))
+ ::audit/profile-id (:id profile)}))))))
;; --- Mutation: Logout
@@ -298,35 +308,39 @@
[{:keys [pool metrics] :as cfg} params]
(db/with-atomic [conn pool]
(let [profile (-> (assoc cfg :conn conn)
- (login-or-register params))]
+ (login-or-register params))
+ props (merge
+ (select-keys profile [:backend :fullname :email])
+ (:props profile))]
(with-meta profile
- {:before-complete (annotate-profile-register metrics profile)}))))
+ {:before-complete (annotate-profile-register metrics profile)
+ ::audit/name (if (::created profile) "register" "login")
+ ::audit/props props
+ ::audit/profile-id (:id profile)}))))
(defn login-or-register
- [{:keys [conn] :as cfg} {:keys [email backend] :as params}]
- (letfn [(info->props [info]
- (dissoc info :name :fullname :email :backend))
-
- (info->lang [{:keys [locale] :as info}]
+ [{:keys [conn] :as cfg} {:keys [email] :as params}]
+ (letfn [(info->lang [{:keys [locale] :as info}]
(when (and (string? locale)
(not (str/blank? locale)))
locale))
- (create-profile [conn {:keys [email] :as info}]
- (db/insert! conn :profile
- {:id (uuid/next)
- :fullname (:fullname info)
- :email (str/lower email)
- :lang (info->lang info)
- :auth-backend backend
- :is-active true
- :password "!"
- :props (db/tjson (info->props info))
- :is-demo false}))
+ (create-profile [conn {:keys [fullname backend email props] :as info}]
+ (let [params {:id (uuid/next)
+ :fullname fullname
+ :email (str/lower email)
+ :lang (info->lang props)
+ :auth-backend backend
+ :is-active true
+ :password "!"
+ :props (db/tjson props)
+ :is-demo false}]
+ (-> (db/insert! conn :profile params)
+ (update :props db/decode-transit-pgobject))))
(update-profile [conn info profile]
- (let [props (d/merge (:props profile)
- (info->props info))]
+ (let [props (merge (:props profile)
+ (:props info))]
(db/update! conn :profile
{:props (db/tjson props)
:modified-at (dt/now)}
@@ -401,7 +415,9 @@
(declare update-profile-photo)
-(s/def ::file ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
+
(s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file]))
@@ -605,7 +621,7 @@
;; Schedule a complete deletion of profile
(wrk/submit! {::wrk/task :delete-profile
- ::wrk/dalay cfg/deletion-delay
+ ::wrk/delay cfg/deletion-delay
::wrk/conn conn
:profile-id profile-id})
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index c545d52b8..662d2dc35 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -249,7 +249,9 @@
(declare upload-photo)
-(s/def ::file ::media/upload)
+(s/def ::content-type ::media/image-content-type)
+(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
+
(s/def ::update-team-photo
(s/keys :req-un [::profile-id ::team-id ::file]))
diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj
index 9f0a0b3ff..3fa857892 100644
--- a/backend/src/app/rpc/queries/files.clj
+++ b/backend/src/app/rpc/queries/files.clj
@@ -6,12 +6,13 @@
(ns app.rpc.queries.files
(:require
- [app.common.exceptions :as ex]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
+ [app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as projects]
+ [app.rpc.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@@ -97,7 +98,13 @@
ppr.is_owner = true or
ppr.can_edit = true)
)
- select distinct f.*
+ select distinct
+ f.id,
+ f.project_id,
+ f.created_at,
+ f.modified_at,
+ f.name,
+ f.is_shared
from file as f
inner join projects as pr on (f.project_id = pr.id)
where f.name ilike ('%' || ? || '%')
@@ -109,14 +116,15 @@
(sv/defmethod ::search-files
[{:keys [pool] :as cfg} {:keys [profile-id team-id search-term] :as params}]
- (let [rows (db/exec! pool [sql:search-files
- profile-id team-id
- profile-id team-id
- search-term])]
- (into [] decode-row-xf rows)))
+ (db/exec! pool [sql:search-files
+ profile-id team-id
+ profile-id team-id
+ search-term]))
-;; --- Query: Project Files
+;; --- Query: Files
+
+;; DEPRECATED: should be removed probably on 1.6.x
(def ^:private sql:files
"select f.*
@@ -136,6 +144,29 @@
(into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
+;; --- Query: Project Files
+
+(def ^:private sql:project-files
+ "select f.id,
+ f.project_id,
+ f.created_at,
+ f.modified_at,
+ f.name,
+ f.is_shared
+ from file as f
+ where f.project_id = ?
+ and f.deleted_at is null
+ order by f.modified_at desc")
+
+(s/def ::project-files
+ (s/keys :req-un [::profile-id ::project-id]))
+
+(sv/defmethod ::project-files
+ [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
+ (with-open [conn (db/open pool)]
+ (projects/check-read-permissions! conn profile-id project-id)
+ (db/exec! conn [sql:project-files project-id])))
+
;; --- Query: File (By ID)
(defn retrieve-file
@@ -154,17 +185,50 @@
(retrieve-file conn id)))
(s/def ::page
- (s/keys :req-un [::profile-id ::id ::file-id]))
+ (s/keys :req-un [::profile-id ::file-id]))
+
+(defn remove-thumbnails-frames
+ "Removes from data the children for frames that have a thumbnail set up"
+ [data]
+ (let [filter-shape?
+ (fn [objects [id shape]]
+ (let [frame-id (:frame-id shape)]
+ (or (= id uuid/zero)
+ (= frame-id uuid/zero)
+ (not (some? (get-in objects [frame-id :thumbnail]))))))
+
+ ;; We need to remove from the attribute :shapes its childrens because
+ ;; they will not be sent in the data
+ remove-frame-children
+ (fn [[id shape]]
+ [id (cond-> shape
+ (some? (:thumbnail shape))
+ (assoc :shapes []))])
+
+ update-objects
+ (fn [objects]
+ (into {}
+ (comp (map remove-frame-children)
+ (filter (partial filter-shape? objects)))
+ objects))]
+
+ (update data :objects update-objects)))
(sv/defmethod ::page
- [{:keys [pool] :as cfg} {:keys [profile-id file-id id]}]
+ [{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}]
+ [{:keys [pool] :as cfg} {:keys [profile-id file-id]}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
- (let [file (retrieve-file conn file-id)]
- (get-in file [:data :pages-index id]))))
+ (let [file (retrieve-file conn file-id)
+ page-id (get-in file [:data :pages 0])]
+ (cond-> (get-in file [:data :pages-index page-id])
+ strip-thumbnails
+ (remove-thumbnails-frames)))))
;; --- Query: Shared Library Files
+;; DEPRECATED: and will be removed on 1.6.x
+
(def ^:private sql:shared-files
"select f.*
from file as f
@@ -183,11 +247,36 @@
(into [] decode-row-xf (db/exec! pool [sql:shared-files team-id])))
+;; --- Query: Shared Library Files
+
+(def ^:private sql:team-shared-files
+ "select f.id,
+ f.project_id,
+ f.created_at,
+ f.modified_at,
+ f.name,
+ f.is_shared
+ from file as f
+ inner join project as p on (p.id = f.project_id)
+ where f.is_shared = true
+ and f.deleted_at is null
+ and p.deleted_at is null
+ and p.team_id = ?
+ order by f.modified_at desc")
+
+(s/def ::team-shared-files
+ (s/keys :req-un [::profile-id ::team-id]))
+
+(sv/defmethod ::team-shared-files
+ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
+ (db/exec! pool [sql:team-shared-files team-id]))
+
+
;; --- Query: File Libraries used by a File
(def ^:private sql:file-libraries
"select fl.*,
- ? as is_indirect,
+
flr.synced_at as synced_at
from file as fl
inner join file_library_rel as flr on (flr.library_file_id = fl.id)
@@ -196,22 +285,13 @@
(defn retrieve-file-libraries
[conn is-indirect file-id]
- (let [direct-libraries
- (into [] decode-row-xf (db/exec! conn [sql:file-libraries is-indirect file-id]))
+ (let [libraries (->> (db/exec! conn [sql:file-libraries file-id])
+ (map #(assoc % :is-indirect is-indirect))
+ (into #{} decode-row-xf))]
+ (reduce #(into %1 (retrieve-file-libraries conn true %2))
+ libraries
+ (map :id libraries))))
- select-distinct
- (fn [used-libraries new-libraries]
- (remove (fn [new-library]
- (some #(= (:id %) (:id new-library)) used-libraries))
- new-libraries))]
-
- (reduce (fn [used-libraries library]
- (concat used-libraries
- (select-distinct
- used-libraries
- (retrieve-file-libraries conn true (:id library)))))
- direct-libraries
- direct-libraries)))
(s/def ::file-libraries
(s/keys :req-un [::profile-id ::file-id]))
@@ -222,31 +302,35 @@
(check-edition-permissions! conn profile-id file-id)
(retrieve-file-libraries conn false file-id)))
+;; --- QUERY: team-recent-files
-;; --- Query: Single File Library
+(def sql:team-recent-files
+ "with recent_files as (
+ select f.id,
+ f.project_id,
+ f.created_at,
+ f.modified_at,
+ f.name,
+ f.is_shared,
+ row_number() over w as row_num
+ from file as f
+ join project as p on (p.id = f.project_id)
+ where p.team_id = ?
+ and p.deleted_at is null
+ and f.deleted_at is null
+ window w as (partition by f.project_id order by f.modified_at desc)
+ order by f.modified_at desc
+ )
+ select * from recent_files where row_num <= 10;")
-;; TODO: this looks like is duplicate of `::file`
+(s/def ::team-recent-files
+ (s/keys :req-un [::profile-id ::team-id]))
-(def ^:private sql:file-library
- "select fl.*
- from file as fl
- where fl.id = ?")
-
-(defn retrieve-file-library
- [conn file-id]
- (let [rows (db/exec! conn [sql:file-library file-id])]
- (when-not (seq rows)
- (ex/raise :type :not-found))
- (first (sequence decode-row-xf rows))))
-
-(s/def ::file-library
- (s/keys :req-un [::profile-id ::file-id]))
-
-(sv/defmethod ::file-library
- [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
- (db/with-atomic [conn pool]
- (check-edition-permissions! conn profile-id file-id) ;; TODO: this should check read permissions
- (retrieve-file-library conn file-id)))
+(sv/defmethod ::team-recent-files
+ [{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
+ (with-open [conn (db/open pool)]
+ (teams/check-read-permissions! conn profile-id team-id)
+ (db/exec! conn [sql:team-recent-files team-id])))
;; --- Helpers
diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj
new file mode 100644
index 000000000..b25c780f5
--- /dev/null
+++ b/backend/src/app/rpc/queries/fonts.clj
@@ -0,0 +1,29 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.rpc.queries.fonts
+ (:require
+ [app.common.spec :as us]
+ [app.db :as db]
+ [app.rpc.queries.teams :as teams]
+ [app.util.services :as sv]
+ [clojure.spec.alpha :as s]))
+
+;; --- Query: Team Font Variants
+
+(s/def ::team-id ::us/uuid)
+(s/def ::profile-id ::us/uuid)
+(s/def ::team-font-variants
+ (s/keys :req-un [::profile-id ::team-id]))
+
+(sv/defmethod ::team-font-variants
+ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
+ (with-open [conn (db/open pool)]
+ (teams/check-read-permissions! conn profile-id team-id)
+ (db/query conn :team-font-variant
+ {:team-id team-id
+ :deleted-at nil})))
+
diff --git a/backend/src/app/rpc/queries/recent_files.clj b/backend/src/app/rpc/queries/recent_files.clj
index 51c1bbe3f..e878d34e6 100644
--- a/backend/src/app/rpc/queries/recent_files.clj
+++ b/backend/src/app/rpc/queries/recent_files.clj
@@ -13,6 +13,8 @@
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
+;; DEPRECATED: should be removed on 1.6.x
+
(def sql:recent-files
"with recent_files as (
select f.*, row_number() over w as row_num
diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj
index ec2c10a96..610e9e9ff 100644
--- a/backend/src/app/setup.clj
+++ b/backend/src/app/setup.clj
@@ -29,16 +29,26 @@
(initialize-instance-id! cfg)
(retrieve-all cfg))))
+(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] :as cfg}]
- (let [key (-> (bn/random-bytes 64)
- (bc/bytes->b64u)
- (bc/bytes->str))]
- (db/insert! conn :server-prop
- {:id "secret-key"
- :preload true
- :content (db/tjson key)}
- {:on-conflict-do-nothing true})))
+ [{: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}]
diff --git a/backend/src/app/setup/initial_data.clj b/backend/src/app/setup/initial_data.clj
index 5514e7103..6532a54e5 100644
--- a/backend/src/app/setup/initial_data.clj
+++ b/backend/src/app/setup/initial_data.clj
@@ -8,7 +8,7 @@
(:refer-clojure :exclude [load])
(:require
[app.common.uuid :as uuid]
- [app.config :as cfg]
+ [app.config :as cf]
[app.db :as db]
[app.rpc.mutations.management :refer [duplicate-file]]
[app.rpc.mutations.projects :refer [create-project create-project-role]]
@@ -36,7 +36,7 @@
([system project-id {:keys [skey project-name]
:or {project-name "Penpot Onboarding"}}]
(db/with-atomic [conn (:app.db/pool system)]
- (let [skey (or skey (cfg/get :initial-project-skey))
+ (let [skey (or skey (cf/get :initial-project-skey))
files (db/exec! conn [sql:file project-id])
flibs (db/exec! conn [sql:file-library-rel project-id])
fmeds (db/exec! conn [sql:file-media-object project-id])
@@ -65,7 +65,7 @@
(defn load-initial-project!
([conn profile] (load-initial-project! conn profile nil))
([conn profile opts]
- (let [skey (or (:skey opts) (cfg/get :initial-project-skey))
+ (let [skey (or (:skey opts) (cf/get :initial-project-skey))
data (retrieve-data conn skey)]
(when data
(let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} (:files data))
@@ -82,10 +82,16 @@
:role :owner})
(doseq [file (:files data)]
- (let [params {:profile-id (:id profile)
+ (let [flibs (filterv #(= (:id file) (:file-id %)) (:flibs data))
+ fmeds (filterv #(= (:id file) (:file-id %)) (:fmeds data))
+
+ params {:profile-id (:id profile)
:project-id (:id project)
:file file
- :index index}
+ :index index
+ :flibs flibs
+ :fmeds fmeds}
+
opts {:reset-shared-flag false}]
(duplicate-file conn params opts))))))))
diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj
index 27a44cbed..a28184df5 100644
--- a/backend/src/app/storage/impl.clj
+++ b/backend/src/app/storage/impl.clj
@@ -145,8 +145,8 @@
(make-output-stream [_ opts]
(throw (UnsupportedOperationException. "not implemented")))
- clojure.lang.Counted
- (count [_] size)))
+ clojure.lang.Counted
+ (count [_] size)))
(defn content
([data] (content data nil))
diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj
index bcd6e4a48..8a8335bcd 100644
--- a/backend/src/app/tasks/delete_object.clj
+++ b/backend/src/app/tasks/delete_object.clj
@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.db :as db]
+ [app.storage :as sto]
[app.util.logging :as l]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
@@ -24,7 +25,8 @@
(fn [{:keys [props] :as task}]
(us/verify ::props props)
(db/with-atomic [conn pool]
- (handle-deletion conn props))))
+ (let [cfg (assoc cfg :conn conn)]
+ (handle-deletion cfg props)))))
(s/def ::type ::us/keyword)
(s/def ::id ::us/uuid)
@@ -34,21 +36,32 @@
(fn [_ props] (:type props)))
(defmethod handle-deletion :default
- [_conn {:keys [type]}]
+ [_cfg {:keys [type]}]
(l/warn :hint "no handler found"
:type (d/name type)))
(defmethod handle-deletion :file
- [conn {:keys [id] :as props}]
+ [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from file where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id])))
(defmethod handle-deletion :project
- [conn {:keys [id] :as props}]
+ [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from project where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id])))
(defmethod handle-deletion :team
- [conn {:keys [id] :as props}]
+ [{:keys [conn]} {:keys [id] :as props}]
(let [sql "delete from team where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id])))
+
+(defmethod handle-deletion :team-font-variant
+ [{:keys [conn storage]} {:keys [id] :as props}]
+ (let [font (db/get-by-id conn :team-font-variant id {:uncheked true})
+ storage (assoc storage :conn conn)]
+ (when (:deleted-at font)
+ (db/delete! conn :team-font-variant {:id id})
+ (some->> (:woff1-file-id font) (sto/del-object storage))
+ (some->> (:woff2-file-id font) (sto/del-object storage))
+ (some->> (:otf-file-id font) (sto/del-object storage))
+ (some->> (:ttf-file-id font) (sto/del-object storage)))))
diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj
index d1bdc6751..8b8bc3d28 100644
--- a/backend/src/app/tasks/file_media_gc.clj
+++ b/backend/src/app/tasks/file_media_gc.clj
@@ -101,7 +101,10 @@
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj))
;; NOTE: deleting the file-media-object in the database
- ;; automatically marks as toched the referenced storage objects.
+ ;; automatically marks as toched 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)}))
nil))
diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj
index 564021c3e..44a232aeb 100644
--- a/backend/src/app/tasks/telemetry.clj
+++ b/backend/src/app/tasks/telemetry.clj
@@ -60,10 +60,9 @@
:uri (:uri cfg)
:headers {"content-type" "application/json"}
:body (json/encode-str data)})]
-
- (when (not= 200 (:status response))
+ (when (> (:status response) 206)
(ex/raise :type :internal
- :code :invalid-response-from-google
+ :code :invalid-response
:context {:status (:status response)
:body (:body response)}))))
diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj
index ecf7c84a4..bfa682ea6 100644
--- a/backend/src/app/tokens.clj
+++ b/backend/src/app/tokens.clj
@@ -51,11 +51,11 @@
claims))
(s/def ::secret-key ::us/string)
-(s/def ::sprops
+(s/def ::props
(s/keys :req-un [::secret-key]))
(defmethod ig/pre-init-spec ::tokens [_]
- (s/keys :req-un [::sprops]))
+ (s/keys :req-un [::props]))
(defn- generate-predefined
[cfg {:keys [iss profile-id] :as params}]
@@ -71,8 +71,8 @@
:hint "no predefined token")))
(defmethod ig/init-key ::tokens
- [_ {:keys [sprops] :as cfg}]
- (let [secret (derive-tokens-secret (:secret-key sprops))
+ [_ {:keys [props] :as cfg}]
+ (let [secret (derive-tokens-secret (:secret-key props))
cfg (assoc cfg ::secret secret)]
(fn [action params]
(case action
diff --git a/backend/src/app/util/async.clj b/backend/src/app/util/async.clj
index fb17e6a7e..984cbb167 100644
--- a/backend/src/app/util/async.clj
+++ b/backend/src/app/util/async.clj
@@ -60,3 +60,42 @@
(if (= executor ::default)
`(a/thread-call (^:once fn* [] (try ~@body (catch Exception e# e#))))
`(thread-call ~executor (^:once fn* [] ~@body))))
+
+(defn batch
+ [in {:keys [max-batch-size
+ max-batch-age
+ init]
+ :or {max-batch-size 200
+ max-batch-age (* 30 1000)
+ init #{}}
+ :as opts}]
+ (let [out (a/chan)]
+ (a/go-loop [tch (a/timeout max-batch-age) buf init]
+ (let [[val port] (a/alts! [tch in])]
+ (cond
+ (identical? port tch)
+ (if (empty? buf)
+ (recur (a/timeout max-batch-age) buf)
+ (do
+ (a/>! out [:timeout buf])
+ (recur (a/timeout max-batch-age) init)))
+
+ (nil? val)
+ (if (empty? buf)
+ (a/close! out)
+ (do
+ (a/offer! out [:timeout buf])
+ (a/close! out)))
+
+ (identical? port in)
+ (let [buf (conj buf val)]
+ (if (>= (count buf) max-batch-size)
+ (do
+ (a/>! out [:size buf])
+ (recur (a/timeout max-batch-age) init))
+ (recur tch buf))))))
+ out))
+
+(defn thread-sleep
+ [ms]
+ (Thread/sleep ms))
diff --git a/backend/src/app/util/logging.clj b/backend/src/app/util/logging.clj
index 5aaa409a8..9a08e66b3 100644
--- a/backend/src/app/util/logging.clj
+++ b/backend/src/app/util/logging.clj
@@ -60,8 +60,8 @@
^Object msg)))
(defmacro log
- [& {:keys [level cause ::logger ::async] :as props}]
- (let [props (dissoc props :level :cause ::logger ::async)
+ [& {:keys [level cause ::logger ::async ::raw] :as props}]
+ (let [props (dissoc props :level :cause ::logger ::async ::raw)
logger (or logger (str *ns*))
logger-sym (gensym "log")
level-sym (gensym "log")]
@@ -69,8 +69,12 @@
~level-sym (get-level ~level)]
(if (enabled? ~logger-sym ~level-sym)
~(if async
- `(send-off logging-agent (fn [_#] (write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props))))
- `(write-log! ~logger-sym ~level-sym ~cause (build-map-message ~props)))))))
+ `(send-off logging-agent
+ (fn [_#]
+ (let [message# (or ~raw (build-map-message ~props))]
+ (write-log! ~logger-sym ~level-sym ~cause message#))))
+ `(let [message# (or ~raw (build-map-message ~props))]
+ (write-log! ~logger-sym ~level-sym ~cause message#)))))))
(defmacro info
[& params]
diff --git a/backend/tests/app/tests/_files/font-1.otf b/backend/tests/app/tests/_files/font-1.otf
new file mode 100644
index 000000000..9326ec784
Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.otf differ
diff --git a/backend/tests/app/tests/_files/font-1.ttf b/backend/tests/app/tests/_files/font-1.ttf
new file mode 100644
index 000000000..cb2f33597
Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.ttf differ
diff --git a/backend/tests/app/tests/_files/font-1.woff b/backend/tests/app/tests/_files/font-1.woff
new file mode 100644
index 000000000..9607e1e19
Binary files /dev/null and b/backend/tests/app/tests/_files/font-1.woff differ
diff --git a/backend/tests/app/tests/_files/font-2.otf b/backend/tests/app/tests/_files/font-2.otf
new file mode 100644
index 000000000..2fbddd00e
Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.otf differ
diff --git a/backend/tests/app/tests/_files/font-2.woff b/backend/tests/app/tests/_files/font-2.woff
new file mode 100644
index 000000000..4edf9e60e
Binary files /dev/null and b/backend/tests/app/tests/_files/font-2.woff differ
diff --git a/backend/tests/app/tests/test_common_geom_shapes.clj b/backend/tests/app/tests/test_common_geom_shapes.clj
index b53d3ebc5..8019da994 100644
--- a/backend/tests/app/tests/test_common_geom_shapes.clj
+++ b/backend/tests/app/tests/test_common_geom_shapes.clj
@@ -52,7 +52,7 @@
(t/testing "Shape without modifiers should stay the same"
(t/are [type]
(let [shape-before (create-test-shape type)
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(= shape-before shape-after))
:rect :path))
@@ -61,7 +61,7 @@
(t/are [type]
(let [modifiers {:displacement (gmt/translate-matrix (gpt/point 10 -10))}]
(let [shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/is (not= shape-before shape-after))
(t/is (close? (get-in shape-before [:selrect :x])
@@ -82,7 +82,7 @@
(t/are [type]
(let [modifiers {:displacement (gmt/matrix)}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/are [prop]
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
@@ -95,7 +95,7 @@
:resize-vector (gpt/point 2 2)
:resize-transform (gmt/matrix)}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/is (not= shape-before shape-after))
(t/is (close? (get-in shape-before [:selrect :x])
@@ -117,7 +117,7 @@
:resize-vector (gpt/point 1 1)
:resize-transform (gmt/matrix)}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/are [prop]
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
@@ -130,7 +130,7 @@
:resize-vector (gpt/point 0 0)
:resize-transform (gmt/matrix)}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/is (> (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width])))
(t/is (> (get-in shape-after [:selrect :width]) 0))
@@ -144,7 +144,7 @@
(t/are [type]
(let [modifiers {:rotation 30}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/is (not= shape-before shape-after))
@@ -168,7 +168,7 @@
(t/are [type]
(let [modifiers {:rotation 0}
shape-before (create-test-shape type {:modifiers modifiers})
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(t/are [prop]
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
@@ -180,7 +180,7 @@
(let [modifiers {:displacement (gmt/matrix)}
shape-before (-> (create-test-shape type {:modifiers modifiers})
(assoc :selrect selrect))
- shape-after (gsh/transform-shape shape-before)]
+ shape-after (gsh/transform-shape shape-before {:round-coords? false})]
(= (:selrect shape-before) (:selrect shape-after)))
:rect {:x 0 :y 0 :width ##Inf :height ##Inf}
diff --git a/backend/tests/app/tests/test_common_pages_migrations.clj b/backend/tests/app/tests/test_common_pages_migrations.clj
new file mode 100644
index 000000000..4e8adebd6
--- /dev/null
+++ b/backend/tests/app/tests/test_common_pages_migrations.clj
@@ -0,0 +1,94 @@
+;; 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.tests.test-common-pages-migrations
+ (:require
+ [clojure.test :as t]
+ [clojure.pprint :refer [pprint]]
+ [promesa.core :as p]
+ [mockery.core :refer [with-mock]]
+ [app.common.data :as d]
+ [app.common.pages :as cp]
+ [app.common.pages.migrations :as cpm]
+ [app.common.uuid :as uuid]
+ [app.tests.helpers :as th]))
+
+(t/deftest test-migration-8-1
+ (let [page-id (uuid/custom 0 0)
+ objects [{:type :rect :id (uuid/custom 1 0)}
+ {:type :group
+ :id (uuid/custom 1 1)
+ :selrect {}
+ :shapes [(uuid/custom 1 2) (uuid/custom 1 0)]}
+ {:type :group
+ :id (uuid/custom 1 2)
+ :selrect {}
+ :shapes [(uuid/custom 1 3)]}
+ {:type :group
+ :id (uuid/custom 1 3)
+ :selrect {}
+ :shapes [(uuid/custom 1 4)]}
+ {:type :group
+ :id (uuid/custom 1 4)
+ :selrect {}
+ :shapes [(uuid/custom 1 5)]}
+ {:type :path :id (uuid/custom 1 5)}]
+
+ data {:pages-index {page-id {:objects (d/index-by :id objects)}}
+ :components {}
+ :version 7}
+
+ res (cpm/migrate-data data)]
+
+ (pprint data)
+ (pprint res)
+
+ (t/is (= (dissoc data :version)
+ (dissoc res :version)))))
+
+(t/deftest test-migration-8-2
+ (let [page-id (uuid/custom 0 0)
+ objects [{:type :rect :id (uuid/custom 1 0)}
+ {:type :group
+ :id (uuid/custom 1 1)
+ :selrect {}
+ :shapes [(uuid/custom 1 2) (uuid/custom 1 0)]}
+ {:type :group
+ :id (uuid/custom 1 2)
+ :selrect {}
+ :shapes [(uuid/custom 1 3)]}
+ {:type :group
+ :id (uuid/custom 1 3)
+ :selrect {}
+ :shapes [(uuid/custom 1 4)]}
+ {:type :group
+ :id (uuid/custom 1 4)
+ :selrect {}
+ :shapes []}
+ {:type :path :id (uuid/custom 1 5)}]
+
+ data {:pages-index {page-id {:objects (d/index-by :id objects)}}
+ :components {}
+ :version 7}
+
+ expct (-> data
+ (update-in [:pages-index page-id :objects] dissoc
+ (uuid/custom 1 2)
+ (uuid/custom 1 3)
+ (uuid/custom 1 4))
+ (update-in [:pages-index page-id :objects (uuid/custom 1 1) :shapes]
+ (fn [shapes]
+ (let [id (uuid/custom 1 2)]
+ (into [] (remove #(= id %)) shapes)))))
+
+ res (cpm/migrate-data data)]
+
+ (pprint res)
+ (pprint expct)
+
+ (t/is (= (dissoc expct :version)
+ (dissoc res :version)))
+ ))
diff --git a/backend/tests/app/tests/test_services_files.clj b/backend/tests/app/tests/test_services_files.clj
index 68f34eacb..248dc86f2 100644
--- a/backend/tests/app/tests/test_services_files.clj
+++ b/backend/tests/app/tests/test_services_files.clj
@@ -52,7 +52,7 @@
(t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result))))))
- (t/testing "query files"
+ (t/testing "query files (deprecated)"
(let [data {::th/type :files
:project-id proj-id
:profile-id (:id prof)}
@@ -67,6 +67,20 @@
(t/is (= "new name" (get-in result [0 :name])))
(t/is (= 1 (count (get-in result [0 :data :pages])))))))
+ (t/testing "query files"
+ (let [data {::th/type :project-files
+ :project-id proj-id
+ :profile-id (:id prof)}
+ out (th/query! data)]
+
+ ;; (th/print-result! out)
+ (t/is (nil? (:error out)))
+
+ (let [result (:result out)]
+ (t/is (= 1 (count result)))
+ (t/is (= file-id (get-in result [0 :id])))
+ (t/is (= "new name" (get-in result [0 :name]))))))
+
(t/testing "query single file without users"
(let [data {::th/type :file
:profile-id (:id prof)
diff --git a/backend/tests/app/tests/test_services_fonts.clj b/backend/tests/app/tests/test_services_fonts.clj
new file mode 100644
index 000000000..86836b71e
--- /dev/null
+++ b/backend/tests/app/tests/test_services_fonts.clj
@@ -0,0 +1,94 @@
+;; 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.tests.test-services-fonts
+ (:require
+ [app.common.uuid :as uuid]
+ [app.db :as db]
+ [app.http :as http]
+ [app.storage :as sto]
+ [app.tests.helpers :as th]
+ [clojure.java.io :as io]
+ [clojure.test :as t]
+ [datoteka.core :as fs]))
+
+(t/use-fixtures :once th/state-init)
+(t/use-fixtures :each th/database-reset)
+
+(t/deftest ttf-font-upload-1
+ (let [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)
+
+ ttfdata (-> (io/resource "app/tests/_files/font-1.ttf")
+ (fs/slurp-bytes))
+
+ params {::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}}
+ out (th/mutation! params)]
+
+ ;; (th/print-result! out)
+ (t/is (nil? (:error out)))
+ (let [result (:result out)]
+ (t/is (uuid? (:id result)))
+ (t/is (uuid? (:ttf-file-id result)))
+ (t/is (uuid? (:otf-file-id result)))
+ (t/is (uuid? (:woff1-file-id result)))
+ (t/is (uuid? (:woff2-file-id result)))
+ (t/are [k] (= (get params k)
+ (get result k))
+ :team-id
+ :font-id
+ :font-family
+ :font-weight
+ :font-style))))
+
+(t/deftest ttf-font-upload-2
+ (let [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)
+
+ data (-> (io/resource "app/tests/_files/font-1.woff")
+ (fs/slurp-bytes))
+
+ params {::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/woff" data}}
+ out (th/mutation! params)]
+
+ ;; (th/print-result! out)
+ (t/is (nil? (:error out)))
+ (let [result (:result out)]
+ (t/is (uuid? (:id result)))
+ (t/is (uuid? (:ttf-file-id result)))
+ (t/is (uuid? (:otf-file-id result)))
+ (t/is (uuid? (:woff1-file-id result)))
+ (t/is (uuid? (:woff2-file-id result)))
+ (t/are [k] (= (get params k)
+ (get result k))
+ :team-id
+ :font-id
+ :font-family
+ :font-weight
+ :font-style))))
+
+
+
+
+
diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/tests/app/tests/test_services_profile.clj
index 7ac20c849..c3daf5c14 100644
--- a/backend/tests/app/tests/test_services_profile.clj
+++ b/backend/tests/app/tests/test_services_profile.clj
@@ -179,10 +179,10 @@
))
(t/deftest registration-domain-whitelist
- (let [whitelist "gmail.com, hey.com, ya.ru"]
+ (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
(t/testing "allowed email domain"
(t/is (true? (profile/email-domain-in-whitelist? whitelist "username@ya.ru")))
- (t/is (true? (profile/email-domain-in-whitelist? "" "username@somedomain.com"))))
+ (t/is (true? (profile/email-domain-in-whitelist? #{} "username@somedomain.com"))))
(t/testing "not allowed email domain"
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc
index a46b306f2..649e29097 100644
--- a/common/app/common/data.cljc
+++ b/common/app/common/data.cljc
@@ -12,6 +12,7 @@
(:require
[linked.set :as lks]
[app.common.math :as mth]
+ [clojure.set :as set]
#?(:clj [cljs.analyzer.api :as aapi])
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
@@ -252,15 +253,22 @@
(map (fn [x] (f x) x) coll)))
(defn merge
- "A faster merge."
[& maps]
- (loop [res (transient (or (first maps) {}))
- maps (next maps)]
- (if (nil? maps)
- (persistent! res)
- (recur (reduce-kv assoc! res (first maps))
- (next maps)))))
+ (reduce conj (or (first maps) {}) (rest maps)))
+(defn distinct-xf
+ [f]
+ (fn [rf]
+ (let [seen (volatile! #{})]
+ (fn
+ ([] (rf))
+ ([result] (rf result))
+ ([result input]
+ (let [input* (f input)]
+ (if (contains? @seen input*)
+ result
+ (do (vswap! seen conj input*)
+ (rf result input)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion
@@ -448,3 +456,50 @@
kw (if (keyword? kw) (name kw) kw)]
(keyword (str prefix kw))))
+
+(defn tap
+ "Simpilar to the tap in rxjs but for plain collections"
+ [f coll]
+ (f coll)
+ coll)
+
+(defn map-diff
+ "Given two maps returns the diff of its attributes in a map where
+ the keys will be the attributes that change and the values the previous
+ and current value. For attributes which value is a map this will be recursive.
+
+ For example:
+ (map-diff {:a 1 :b 2 :c { :foo 1 :var 2}
+ {:a 2 :c { :foo 10 } :d 10)
+
+ => { :a [1 2]
+ :b [2 nil]
+ :c { :foo [1 10]
+ :var [2 nil]}
+ :d [nil 10] }
+
+ If both maps are identical the result will be an empty map
+ "
+ [m1 m2]
+
+ (let [m1ks (keys m1)
+ m2ks (keys m2)
+ keys (set/union m1ks m2ks)
+
+ diff-attr
+ (fn [diff key]
+
+ (let [v1 (get m1 key)
+ v2 (get m2 key)]
+ (cond
+ (= v1 v2)
+ diff
+
+ (and (map? v1) (map? v2))
+ (assoc diff key (map-diff v1 v2))
+
+ :else
+ (assoc diff key [(get m1 key) (get m2 key)]))))]
+
+ (->> keys
+ (reduce diff-attr {}))))
diff --git a/common/app/common/exceptions.cljc b/common/app/common/exceptions.cljc
index 4fe202efd..5818578a9 100644
--- a/common/app/common/exceptions.cljc
+++ b/common/app/common/exceptions.cljc
@@ -36,7 +36,7 @@
(defn try*
[f on-error]
- (try (f) (catch #?(:clj Exception :cljs :default) e (on-error e))))
+ (try (f) (catch #?(:clj Throwable :cljs :default) e (on-error e))))
;; http://clj-me.cgrand.net/2013/09/11/macros-closures-and-unexpected-object-retention/
;; Explains the use of ^:once metadata
diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc
index a93c20ad3..db0d51e29 100644
--- a/common/app/common/geom/shapes.cljc
+++ b/common/app/common/geom/shapes.cljc
@@ -162,7 +162,7 @@
:points points))))
(defn rotation-modifiers
- [center shape angle]
+ [shape center angle]
(let [displacement (let [shape-center (gco/center-shape shape)]
(-> (gmt/matrix)
(gmt/rotate angle center)
diff --git a/common/app/common/geom/shapes/intersect.cljc b/common/app/common/geom/shapes/intersect.cljc
index 0b6fbcd6f..2b55cb339 100644
--- a/common/app/common/geom/shapes/intersect.cljc
+++ b/common/app/common/geom/shapes/intersect.cljc
@@ -174,9 +174,17 @@
"Checks if the given rect overlaps with the path in any point"
[shape rect]
- (let [rect-points (gpr/rect->points rect)
+ (let [;; If paths are too complex the intersection is too expensive
+ ;; we fallback to check its bounding box otherwise the performance penalty
+ ;; is too big
+ ;; TODO: Look for ways to optimize this operation
+ simple? (> (count (:content shape)) 100)
+
+ rect-points (gpr/rect->points rect)
rect-lines (points->lines rect-points)
- path-lines (gpp/path->lines shape)
+ path-lines (if simple?
+ (points->lines (:points shape))
+ (gpp/path->lines shape))
start-point (-> shape :content (first) :params (gpt/point))]
(or (is-point-inside-nonzero? (first rect-points) path-lines)
diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc
index cf9845741..0c7b8183d 100644
--- a/common/app/common/geom/shapes/transforms.cljc
+++ b/common/app/common/geom/shapes/transforms.cljc
@@ -6,13 +6,15 @@
(ns app.common.geom.shapes.transforms
(:require
+ [app.common.attrs :as attrs]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
- [app.common.data :as d]))
+ [app.common.data :as d]
+ [app.common.text :as txt]))
;; --- Relative Movement
@@ -264,7 +266,7 @@
(defn apply-transform
"Given a new set of points transformed, set up the rectangle so it keeps
its properties. We adjust de x,y,width,height and create a custom transform"
- [shape transform]
+ [shape transform round-coords?]
;;
(let [points (-> shape :points (transform-points transform))
center (gco/center-points points)
@@ -288,6 +290,13 @@
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))
+ rect-shape (cond-> rect-shape
+ round-coords?
+ (-> (update :x mth/round)
+ (update :y mth/round)
+ (update :width mth/round)
+ (update :height mth/round)))
+
shape (cond
(= :path (:type shape))
(-> shape
@@ -295,11 +304,7 @@
:else
(-> shape
- (merge rect-shape)
- (update :x #(mth/precision % 0))
- (update :y #(mth/precision % 0))
- (update :width #(mth/precision % 0))
- (update :height #(mth/precision % 0))))]
+ (merge rect-shape)))]
(as-> shape $
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
@@ -328,17 +333,40 @@
(dissoc :modifiers))))
shape)))
-(defn transform-shape [shape]
- (let [shape (apply-displacement shape)
- center (gco/center-shape shape)
- modifiers (:modifiers shape)]
- (if (and modifiers center)
- (let [transform (modifiers->transform center modifiers)]
- (-> shape
- (set-flip modifiers)
- (apply-transform transform)
- (dissoc :modifiers)))
- shape)))
+(defn apply-text-resize
+ [shape orig-shape modifiers]
+ (if (and (= (:type shape) :text)
+ (:resize-scale-text modifiers))
+ (let [merge-attrs (fn [attrs]
+ (let [font-size (-> (get attrs :font-size 14)
+ (d/parse-double)
+ (* (-> modifiers :resize-vector :x))
+ (str)
+ )]
+ (attrs/merge attrs {:font-size font-size})))]
+ (update shape :content #(txt/transform-nodes
+ txt/is-text-node?
+ merge-attrs
+ %)))
+ shape))
+
+(defn transform-shape
+ ([shape]
+ (transform-shape shape nil))
+
+ ([shape {:keys [round-coords?]
+ :or {round-coords? true}}]
+ (let [shape (apply-displacement shape)
+ center (gco/center-shape shape)
+ modifiers (:modifiers shape)]
+ (if (and modifiers center)
+ (let [transform (modifiers->transform center modifiers)]
+ (-> shape
+ (set-flip modifiers)
+ (apply-transform transform round-coords?)
+ (apply-text-resize shape modifiers)
+ (dissoc :modifiers)))
+ shape))))
(defn update-group-viewbox
"Updates the viewbox for groups imported from SVG's"
@@ -387,5 +415,5 @@
;; need to remove the flip flags
(assoc :flip-x false)
(assoc :flip-y false)
- (apply-transform (gmt/matrix)))))
+ (apply-transform (gmt/matrix) true))))
diff --git a/common/app/common/media.cljc b/common/app/common/media.cljc
index df3a556ca..cbf1fb826 100644
--- a/common/app/common/media.cljc
+++ b/common/app/common/media.cljc
@@ -9,10 +9,10 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
-(def valid-media-types
- #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
-
-(def str-media-types (str/join "," valid-media-types))
+(def valid-font-types #{"font/ttf" "font/woff", "font/otf"})
+(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
+(def str-image-types (str/join "," valid-image-types))
+(def str-font-types (str/join "," valid-font-types))
(defn format->extension
[format]
@@ -65,3 +65,38 @@
::modified-at
::uri]))
+
+(defn parse-font-weight
+ [variant]
+ (cond
+ (re-seq #"(?i)(?:hairline|thin)" variant) 100
+ (re-seq #"(?i)(?:extra\s*light|ultra\s*light)" variant) 200
+ (re-seq #"(?i)(?:light)" variant) 300
+ (re-seq #"(?i)(?:normal|regular)" variant) 400
+ (re-seq #"(?i)(?:medium)" variant) 500
+ (re-seq #"(?i)(?:semi\s*bold|demi\s*bold)" variant) 600
+ (re-seq #"(?i)(?:extra\s*bold|ultra\s*bold)" variant) 800
+ (re-seq #"(?i)(?:bold)" variant) 700
+ (re-seq #"(?i)(?:extra\s*black|ultra\s*black)" variant) 950
+ (re-seq #"(?i)(?:black|heavy)" variant) 900
+ :else 400))
+
+(defn parse-font-style
+ [variant]
+ (if (re-seq #"(?i)(?:italic)" variant)
+ "italic"
+ "normal"))
+
+(defn font-weight->name
+ [weight]
+ (case weight
+ 100 "Hairline"
+ 200 "Extra Light"
+ 300 "Light"
+ 400 "Regular"
+ 500 "Medium"
+ 600 "Semi Bold"
+ 700 "Bold"
+ 800 "Extra Bold"
+ 900 "Black"
+ 950 "Extra Black"))
diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc
index 2776e8168..4ef70baba 100644
--- a/common/app/common/pages.cljc
+++ b/common/app/common/pages.cljc
@@ -11,6 +11,7 @@
[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]))
@@ -42,7 +43,6 @@
(d/export helpers/is-shape-grouped)
(d/export helpers/get-parent)
(d/export helpers/get-parents)
-(d/export helpers/generate-child-parent-index)
(d/export helpers/clean-loops)
(d/export helpers/calculate-invalid-targets)
(d/export helpers/valid-frame-target)
@@ -60,12 +60,18 @@
(d/export helpers/get-base-shape)
(d/export helpers/is-parent?)
(d/export helpers/get-index-in-parent)
-(d/export helpers/calculate-z-index)
-(d/export helpers/generate-child-all-parents-index)
(d/export helpers/parse-path-name)
(d/export helpers/merge-path-item)
(d/export helpers/compact-path)
(d/export helpers/compact-name)
+(d/export helpers/merge-modifiers)
+
+;; Indices
+(d/export indices/calculate-z-index)
+(d/export indices/update-z-index)
+(d/export indices/generate-child-all-parents-index)
+(d/export indices/generate-child-parent-index)
+(d/export indices/create-mask-index)
;; Process changes
(d/export changes/process-changes)
diff --git a/common/app/common/pages/common.cljc b/common/app/common/pages/common.cljc
index db1e9341c..9a39105bf 100644
--- a/common/app/common/pages/common.cljc
+++ b/common/app/common/pages/common.cljc
@@ -8,7 +8,7 @@
(:require
[app.common.uuid :as uuid]))
-(def file-version 6)
+(def file-version 8)
(def default-color "#b1b2b5") ;; $color-gray-20
(def root uuid/zero)
diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc
index 39597788d..87299b2a3 100644
--- a/common/app/common/pages/helpers.cljc
+++ b/common/app/common/pages/helpers.cljc
@@ -10,6 +10,7 @@
[app.common.geom.shapes :as gsh]
[app.common.spec :as us]
[app.common.uuid :as uuid]
+ [clojure.set :as set]
[cuerdas.core :as str]))
(defn walk-pages
@@ -160,27 +161,6 @@
(when parent-id
(lazy-seq (cons parent-id (get-parents parent-id objects))))))
-(defn generate-child-parent-index
- [objects]
- (reduce-kv
- (fn [index id obj]
- (assoc index id (:parent-id obj)))
- {} objects))
-
-(defn generate-child-all-parents-index
- "Creates an index where the key is the shape id and the value is a set
- with all the parents"
- ([objects]
- (generate-child-all-parents-index objects (vals objects)))
-
- ([objects shapes]
- (let [shape->parents
- (fn [shape]
- (->> (get-parents (:id shape) objects)
- (into [])))]
- (->> shapes
- (map #(vector (:id %) (shape->parents %)))
- (into {})))))
(defn clean-loops
"Clean a list of ids from circular references."
@@ -347,40 +327,7 @@
(reduce red-fn cur-idx (reverse (:shapes object)))))]
(into {} (rec-index '() uuid/zero))))
-(defn calculate-z-index
- "Given a collection of shapes calculates their z-index. Greater index
- means is displayed over other shapes with less index."
- [objects]
- (let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
- root-children (get-in objects [uuid/zero :shapes])
- num-frames (->> root-children (filter is-frame?) count)]
- (when (seq root-children)
- (loop [current (peek root-children)
- pending (pop root-children)
- current-idx (+ (count objects) num-frames -1)
- z-index {}]
-
- (let [children (->> (get-in objects [current :shapes]))
- children (cond
- (and (is-frame? current) (contains? z-index current))
- []
-
- (and (is-frame? current)
- (not (contains? z-index current)))
- (into [current] children)
-
- :else
- children)
- pending (into (vec pending) children)]
- (if (empty? pending)
- (assoc z-index current current-idx)
-
- (let []
- (recur (peek pending)
- (pop pending)
- (dec current-idx)
- (assoc z-index current current-idx)))))))))
(defn expand-region-selection
"Given a selection selects all the shapes between the first and last in
@@ -511,3 +458,12 @@
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
+(defn merge-modifiers
+ [objects modifiers]
+
+ (let [set-modifier
+ (fn [objects [id modifiers]]
+ (-> objects
+ (d/update-when id merge modifiers)))]
+ (->> modifiers
+ (reduce set-modifier objects))))
diff --git a/common/app/common/pages/indices.cljc b/common/app/common/pages/indices.cljc
new file mode 100644
index 000000000..fa3993799
--- /dev/null
+++ b/common/app/common/pages/indices.cljc
@@ -0,0 +1,110 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.common.pages.indices
+ (:require
+ [app.common.data :as d]
+ [app.common.geom.shapes :as gsh]
+ [app.common.pages.helpers :as helpers]
+ [app.common.uuid :as uuid]
+ [clojure.set :as set]))
+
+(defn calculate-frame-z-index [z-index frame-id objects]
+ (let [is-frame? (fn [id] (= :frame (get-in objects [id :type])))
+ frame-shapes (->> objects (vals) (filterv #(= (:frame-id %) frame-id)))
+ children (or (get-in objects [frame-id :shapes]) [])]
+
+ (if (empty? children)
+ z-index
+
+ (loop [current (peek children)
+ pending (pop children)
+ current-idx (count frame-shapes)
+ z-index z-index]
+
+ (let [children (get-in objects [current :shapes])
+ is-frame? (is-frame? current)
+ pending (if (not is-frame?)
+ (d/concat pending children)
+ pending)]
+
+ (if (empty? pending)
+ (-> z-index
+ (assoc current current-idx))
+
+ (recur (peek pending)
+ (pop pending)
+ (dec current-idx)
+ (assoc z-index current current-idx))))))))
+
+;; The z-index is really calculated per-frame. Every frame will have its own
+;; internal z-index. To calculate the "final" z-index we add the shape z-index with
+;; the z-index of its frame. This way we can update the z-index per frame without
+;; the need of recalculate all the frames
+(defn calculate-z-index
+ "Given a collection of shapes calculates their z-index. Greater index
+ means is displayed over other shapes with less index."
+ [objects]
+
+ (let [frames (helpers/select-frames objects)
+ z-index (calculate-frame-z-index {} uuid/zero objects)]
+ (->> frames
+ (map :id)
+ (reduce #(calculate-frame-z-index %1 %2 objects) z-index))))
+
+(defn update-z-index
+ "Updates the z-index given a set of ids to change and the old and new objects
+ representations"
+ [z-index changed-ids old-objects new-objects]
+
+ (let [old-frames (into #{} (map #(get-in old-objects [% :frame-id])) changed-ids)
+ new-frames (into #{} (map #(get-in new-objects [% :frame-id])) changed-ids)
+
+ changed-frames (set/union old-frames new-frames)
+
+ frames (->> (helpers/select-frames new-objects)
+ (map :id)
+ (filter #(contains? changed-frames %)))
+
+ z-index (calculate-frame-z-index z-index uuid/zero new-objects)]
+
+ (->> frames
+ (reduce #(calculate-frame-z-index %1 %2 new-objects) z-index))))
+
+(defn generate-child-parent-index
+ [objects]
+ (reduce-kv
+ (fn [index id obj]
+ (assoc index id (:parent-id obj)))
+ {} objects))
+
+(defn generate-child-all-parents-index
+ "Creates an index where the key is the shape id and the value is a set
+ with all the parents"
+ ([objects]
+ (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 {})))))
+
+(defn create-mask-index
+ "Retrieves the mask information for an object"
+ [objects parents-index]
+ (let [retrieve-masks
+ (fn [id parents]
+ (->> parents
+ (map #(get objects %))
+ (filter #(:masked-group? %))
+ ;; Retrieve the masking element
+ (mapv #(get objects (->> % :shapes first)))))]
+ (->> parents-index
+ (d/mapm retrieve-masks))))
diff --git a/common/app/common/pages/init.cljc b/common/app/common/pages/init.cljc
index 79e0b50de..5ca2b85f1 100644
--- a/common/app/common/pages/init.cljc
+++ b/common/app/common/pages/init.cljc
@@ -63,8 +63,6 @@
{:type :path
:name "Path"
- :fill-color "#000000"
- :fill-opacity 0
:stroke-style :solid
:stroke-alignment :center
:stroke-width 2
diff --git a/common/app/common/pages/migrations.cljc b/common/app/common/pages/migrations.cljc
index 588da5d95..8e93da804 100644
--- a/common/app/common/pages/migrations.cljc
+++ b/common/app/common/pages/migrations.cljc
@@ -163,3 +163,62 @@
(-> data
(update :components #(d/mapm update-container %))
(update :pages-index #(d/mapm update-container %)))))
+
+
+;; Remove interactions pointing to deleted frames
+(defmethod migrate 7
+ [data]
+ (letfn [(update-object [page _ object]
+ (d/update-when object :interactions
+ (fn [interactions]
+ (filterv #(get-in page [:objects (:destination %)])
+ interactions))))
+
+ (update-page [_ page]
+ (update page :objects #(d/mapm (partial update-object page) %)))]
+
+ (update data :pages-index #(d/mapm update-page %))))
+
+
+;; Remove groups without any shape, both in pages and components
+
+(defmethod migrate 8
+ [data]
+ (letfn [(clean-parents [obj deleted?]
+ (d/update-when obj :shapes
+ (fn [shapes]
+ (into [] (remove deleted?) shapes))))
+
+ (obj-is-empty? [obj]
+ (and (= (:type obj) :group)
+ (or (empty? (:shapes obj))
+ (nil? (:selrect obj)))))
+
+ (clean-objects [objects]
+ (loop [entries (seq objects)
+ deleted #{}
+ result objects]
+ (let [[id obj :as entry] (first entries)]
+ (if entry
+ (if (obj-is-empty? obj)
+ (recur (rest entries)
+ (conj deleted id)
+ (dissoc result id))
+ (recur (rest entries)
+ deleted
+ result))
+ [(count deleted)
+ (d/mapm #(clean-parents %2 deleted) result)]))))
+
+ (clean-container [_ container]
+ (loop [n 0
+ objects (:objects container)]
+ (let [[deleted objects] (clean-objects objects)]
+ (if (and (pos? deleted) (< n 1000))
+ (recur (inc n) objects)
+ (assoc container :objects objects)))))]
+
+ (-> data
+ (update :pages-index #(d/mapm clean-container %))
+ (d/update-when :components #(d/mapm clean-container %)))))
+
diff --git a/common/app/common/pages/spec.cljc b/common/app/common/pages/spec.cljc
index ffea43331..24c523fd0 100644
--- a/common/app/common/pages/spec.cljc
+++ b/common/app/common/pages/spec.cljc
@@ -90,6 +90,7 @@
;;; 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 ::safe-number))
@@ -98,13 +99,13 @@
(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?)
@@ -380,6 +381,7 @@
(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)
@@ -401,7 +403,8 @@
:internal.typography/font-style
:internal.typography/line-height
:internal.typography/letter-spacing
- :internal.typography/text-transform]))
+ :internal.typography/text-transform]
+ :opt-un [:internal.typography/path]))
(s/def :internal.file/pages
(s/coll-of ::uuid :kind vector?))
diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc
index eabce91ff..61c651136 100644
--- a/common/app/common/spec.cljc
+++ b/common/app/common/spec.cljc
@@ -6,7 +6,7 @@
(ns app.common.spec
"Data manipulation and query helper functions."
- (:refer-clojure :exclude [assert])
+ (:refer-clojure :exclude [assert bytes?])
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require
#?(:clj [clojure.spec.alpha :as s]
@@ -108,6 +108,20 @@
(s/def ::point gpt/point?)
(s/def ::id ::uuid)
+(defn bytes?
+ "Test if a first parameter is a byte
+ array or not."
+ [x]
+ (if (nil? x)
+ false
+ #?(:clj (= (Class/forName "[B")
+ (.getClass ^Object x))
+ :cljs (or (instance? js/Uint8Array x)
+ (instance? js/ArrayBuffer x)))))
+
+(s/def ::bytes bytes?)
+
+
(s/def ::safe-integer
#(and
(int? %)
@@ -123,29 +137,34 @@
;; --- SPEC: email
+(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
-(let [re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"
- cfn (fn [v]
- (if (string? v)
- (if-let [matches (re-seq re v)]
- (first matches)
- (do ::s/invalid))
- ::s/invalid))]
- (s/def ::email (s/conformer cfn str)))
-
+(s/def ::email
+ (s/conformer
+ (fn [v]
+ (if (string? v)
+ (if-let [matches (re-seq email-re v)]
+ (first matches)
+ (do ::s/invalid))
+ ::s/invalid))
+ str))
;; --- SPEC: set-of-str
-(letfn [(conformer [s]
- (cond
- (string? s) (into #{} (str/split s #"\s*,\s*"))
- (set? s) (if (every? string? s)
- s
- ::s/invalid)
- :else ::s/invalid))
- (unformer [s]
- (str/join "," s))]
- (s/def ::set-of-str (s/conformer conformer unformer)))
+(s/def ::set-of-str
+ (s/conformer
+ (fn [s]
+ (let [xform (comp
+ (filter string?)
+ (remove str/empty?)
+ (remove str/blank?))]
+ (cond
+ (string? s) (->> (str/split s #"\s*,\s*")
+ (into #{} xform))
+ (set? s) (into #{} xform s)
+ :else ::s/invalid)))
+ (fn [s]
+ (str/join "," s))))
;; --- Macros
diff --git a/common/app/common/uuid_impl.js b/common/app/common/uuid_impl.js
index d276ce516..791dd58ea 100644
--- a/common/app/common/uuid_impl.js
+++ b/common/app/common/uuid_impl.js
@@ -12,17 +12,13 @@ goog.provide("app.common.uuid_impl");
goog.scope(function() {
const core = cljs.core;
+ const global = goog.global;
const self = app.common.uuid_impl;
const fill = (() => {
- if (typeof window === "object" && typeof window.crypto !== "undefined") {
+ if (typeof global.crypto !== "undefined") {
return (buf) => {
- window.crypto.getRandomValues(buf);
- return buf;
- };
- } else if (typeof self === "object" && typeof self.crypto !== "undefined") {
- return (buf) => {
- self.crypto.getRandomValues(buf);
+ global.crypto.getRandomValues(buf);
return buf;
};
} else if (typeof require === "function") {
@@ -34,7 +30,7 @@ goog.scope(function() {
};
} else {
// FALLBACK
- console.warn("No high quality RNG available, switching back to Math.random.");
+ console.warn("No SRNG available, switching back to Math.random.");
return (buf) => {
for (let i = 0, r; i < buf.length; i++) {
diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile
index 9e90ed04d..f7e2eebdc 100644
--- a/docker/devenv/Dockerfile
+++ b/docker/devenv/Dockerfile
@@ -6,7 +6,7 @@ ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v14.16.1 \
CLOJURE_VERSION=1.10.3.822 \
CLJKONDO_VERSION=2021.04.23 \
- BABASHKA_VERSION=0.3.5 \
+ BABASHKA_VERSION=0.4.0 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml
index 9046c6dd8..b34f56b1c 100644
--- a/docker/devenv/docker-compose.yaml
+++ b/docker/devenv/docker-compose.yaml
@@ -38,6 +38,7 @@ services:
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}
+ - PENPOT_SECRET_KEY=super-secret-devenv-key
# STMP setup
- PENPOT_SMTP_ENABLED=true
- PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com
diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf
index eefd05cd0..ee7e37bb0 100644
--- a/docker/devenv/files/nginx.conf
+++ b/docker/devenv/files/nginx.conf
@@ -46,7 +46,7 @@ http {
listen 3449 default_server;
server_name _;
- client_max_body_size 5M;
+ client_max_body_size 20M;
charset utf-8;
proxy_http_version 1.1;
diff --git a/docker/images/files/config.js b/docker/images/files/config.js
index aac4fb709..8af727193 100644
--- a/docker/images/files/config.js
+++ b/docker/images/files/config.js
@@ -9,3 +9,4 @@
//var penpotOIDCClientID = "";
//var penpotLoginWithLDAP = ;
//var penpotRegistrationEnabled = ;
+//var penpotAnalyticsEnabled = ;
diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh
index b341649a7..51a118c5a 100644
--- a/docker/images/files/nginx-entrypoint.sh
+++ b/docker/images/files/nginx-entrypoint.sh
@@ -97,6 +97,14 @@ update_registration_enabled() {
fi
}
+update_registration_enabled() {
+ if [ -n "$PENPOT_ANALYTICS_ENABLED" ]; then
+ sed -i \
+ -e "s|^//var penpotAnalyticsEnabled = .*;|var penpotAnalyticsEnabled = $PENPOT_ANALYTICS_ENABLED;|g" \
+ "$1"
+ fi
+}
+
update_public_uri /var/www/app/js/config.js
update_demo_warning /var/www/app/js/config.js
update_allow_demo_users /var/www/app/js/config.js
@@ -106,5 +114,6 @@ update_github_client_id /var/www/app/js/config.js
update_oidc_client_id /var/www/app/js/config.js
update_login_with_ldap /var/www/app/js/config.js
update_registration_enabled /var/www/app/js/config.js
+update_analytics_enabled /var/www/app/js/config.js
exec "$@";
diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn
index 3a81cff0f..7a7ca5859 100644
--- a/exporter/shadow-cljs.edn
+++ b/exporter/shadow-cljs.edn
@@ -1,11 +1,13 @@
{:dependencies
- [[funcool/promesa "6.0.0"]
+ [[com.cognitect/transit-cljs "0.8.269"]
[danlentz/clj-uuid "0.1.9"]
+ [frankiesardo/linked "1.3.0"]
[funcool/cuerdas "2021.05.02-0"]
+ [funcool/promesa "6.0.0"]
+ [integrant/integrant "0.8.0"]
[lambdaisland/glogi "1.0.106"]
- [metosin/reitit-core "0.5.13"]
- [com.cognitect/transit-cljs "0.8.269"]
- [frankiesardo/linked "1.3.0"]]
+ [lambdaisland/uri "1.4.54"]
+ [metosin/reitit-core "0.5.13"]]
:source-paths ["src" "vendor" "../common"]
:jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"]
diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs
index 374464420..90f7cd00b 100644
--- a/exporter/src/app/browser.cljs
+++ b/exporter/src/app/browser.cljs
@@ -6,11 +6,17 @@
(ns app.browser
(:require
+ ["puppeteer-cluster" :as ppc]
+ [app.common.data :as d]
+ [app.config :as cf]
[lambdaisland.glogi :as log]
- [promesa.core :as p]
- ["puppeteer-cluster" :as ppc]))
+ [promesa.core :as p]))
-(def USER-AGENT
+;; --- BROWSER API
+
+(def default-timeout 30000)
+(def default-viewport {:width 1920 :height 1080 :scale 1})
+(def default-user-agent
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"))
@@ -20,15 +26,25 @@
(let [page (unchecked-get props "page")]
(f page)))))
-(defn emulate!
- [page {:keys [viewport user-agent scale]
- :or {user-agent USER-AGENT
- scale 1}}]
- (let [[width height] viewport]
- (.emulate ^js page #js {:viewport #js {:width width
- :height height
- :deviceScaleFactor scale}
- :userAgent user-agent})))
+(defn set-cookie!
+ [page {:keys [key value domain]}]
+ (.setCookie ^js page #js {:name key
+ :value value
+ :domain domain}))
+
+(defn configure-page!
+ [page {:keys [timeout cookie user-agent viewport]}]
+ (let [timeout (or timeout default-timeout)
+ user-agent (or user-agent default-user-agent)
+ viewport (d/merge default-viewport viewport)]
+ (p/do!
+ (.setViewport ^js page #js {:width (:width viewport)
+ :height (:height viewport)
+ :deviceScaleFactor (:scale viewport)})
+ (.setUserAgent ^js page user-agent)
+ (.setDefaultTimeout ^js page timeout)
+ (when cookie
+ (set-cookie! page cookie)))))
(defn navigate!
([page url] (navigate! page url nil))
@@ -40,10 +56,9 @@
[page ms]
(.waitForTimeout ^js page ms))
-
(defn wait-for
([page selector] (wait-for page selector nil))
- ([page selector {:keys [visible] :or {visible false}}]
+ ([page selector {:keys [visible timeout] :or {visible false timeout 10000}}]
(.waitForSelector ^js page selector #js {:visible visible})))
(defn screenshot
@@ -68,30 +83,39 @@
[frame selector]
(.$$ ^js frame selector))
-(defn set-cookie!
- [page {:keys [key value domain]}]
- (.setCookie ^js page #js {:name key
- :value value
- :domain domain}))
-(defn start!
- ([] (start! nil))
- ([{:keys [concurrency concurrency-strategy]
- :or {concurrency 10
- concurrency-strategy :incognito}}]
- (let [ccst (case concurrency-strategy
- :browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
- :incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
- :page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
- opts #js {:concurrency ccst
- :maxConcurrency concurrency
- :puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
- (.launch ^js ppc/Cluster opts))))
+;; --- BROWSER STATE
-(defn stop!
- [instance]
- (p/do!
- (.idle ^js instance)
- (.close ^js instance)
- (log/info :msg "shutdown headless browser")
- nil))
+(def instance (atom nil))
+
+(defn- create-browser
+ [concurrency strategy]
+ (let [strategy (case strategy
+ :browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
+ :incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
+ :page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
+ opts #js {:concurrency strategy
+ :maxConcurrency concurrency
+ :puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
+ (.launch ^js ppc/Cluster opts)))
+
+
+(defn init
+ []
+ (let [concurrency (cf/get :browser-concurrency)
+ strategy (cf/get :browser-strategy)]
+ (-> (create-browser concurrency strategy)
+ (p/then #(reset! instance %))
+ (p/catch (fn [error]
+ (log/error :msg "failed to initialize browser")
+ (js/console.error error))))))
+
+
+(defn stop
+ []
+ (if-let [instance @instance]
+ (p/do!
+ (.idle ^js instance)
+ (.close ^js instance)
+ (log/info :msg "shutdown headless browser"))
+ (p/resolved nil)))
diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs
index e9b83c23b..eb65382a4 100644
--- a/exporter/src/app/config.cljs
+++ b/exporter/src/app/config.cljs
@@ -5,22 +5,62 @@
;; Copyright (c) UXBOX Labs SL
(ns app.config
+ (:refer-clojure :exclude [get])
(:require
+ [app.common.data :as d]
["process" :as process]
[cljs.pprint]
- [cuerdas.core :as str]))
+ [cuerdas.core :as str]
+ [app.common.spec :as us]
+ [cljs.spec.alpha :as s]
+ [cljs.core :as c]
+ [lambdaisland.uri :as u]))
-(defn- keywordize
- [s]
- (-> (str/kebab s)
- (str/keyword)))
+(def defaults
+ {:public-uri "http://localhost:3449"
+ :http-server-port 6061
+ :browser-concurrency 5
+ :browser-strategy :incognito})
-(defonce env
- (let [env (unchecked-get process "env")]
- (persistent!
- (reduce #(assoc! %1 (keywordize %2) (unchecked-get env %2))
- (transient {})
- (js/Object.keys env)))))
+(s/def ::public-uri ::us/string)
+(s/def ::http-server-port ::us/integer)
+(s/def ::browser-concurrency ::us/integer)
+(s/def ::browser-strategy ::us/keyword)
-(defonce config
- {:public-uri (:penpot-public-uri env "http://localhost:3449")})
+(s/def ::config
+ (s/keys :opt-un [::public-uri
+ ::http-server-port
+ ::browser-concurrency
+ ::browser-strategy]))
+(defn- read-env
+ [prefix]
+ (let [env (unchecked-get process "env")
+ kwd (fn [s] (-> (str/kebab s) (str/keyword)))
+ prefix (str prefix "_")
+ len (count prefix)]
+ (reduce (fn [res key]
+ (let [val (unchecked-get env key)
+ key (str/lower key)]
+ (cond-> res
+ (str/starts-with? key prefix)
+ (assoc (kwd (subs key len)) val))))
+ {}
+ (js/Object.keys env))))
+
+(defn- prepare-config
+ []
+ (let [env (read-env "penpot")
+ env (d/without-nils env)
+ data (merge defaults env)]
+ (us/conform ::config data)))
+
+(def config
+ (atom (prepare-config)))
+
+
+(defn get
+ "A configuration getter."
+ ([key]
+ (c/get @config key))
+ ([key default]
+ (c/get @config key default)))
diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs
index 6bc699476..7fafd8393 100644
--- a/exporter/src/app/core.cljs
+++ b/exporter/src/app/core.cljs
@@ -21,10 +21,9 @@
(defn start
[& args]
(log/info :msg "initializing")
- (p/let [browser (bwr/start!)
- server (http/start! {:browser browser})]
- (reset! state {:http server
- :browser browser})))
+ (p/do!
+ (bwr/init)
+ (http/init)))
(def main start)
@@ -35,8 +34,6 @@
(log/info :msg "stoping")
(p/do!
- (when-let [instance (:browser @state)]
- (bwr/stop! instance))
- (when-let [instance (:http @state)]
- (http/stop! instance))
+ (bwr/stop)
+ (http/stop)
(done)))
diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs
index d0d32ebf8..4dca3cac4 100644
--- a/exporter/src/app/http.cljs
+++ b/exporter/src/app/http.cljs
@@ -6,29 +6,33 @@
(ns app.http
(:require
+ [app.config :as cf]
[app.http.export :refer [export-handler]]
- [app.http.thumbnail :refer [thumbnail-handler]]
[app.http.impl :as impl]
[lambdaisland.glogi :as log]
[promesa.core :as p]
[reitit.core :as r]))
(def routes
- [["/export/thumbnail" {:handler thumbnail-handler}]
- ["/export" {:handler export-handler}]])
+ [["/export" {:handler export-handler}]])
-(defn start!
- [extra]
- (log/info :msg "starting http server" :port 6061)
+(def instance (atom nil))
+
+(defn init
+ []
(let [router (r/router routes)
- handler (impl/handler router extra)
- server (impl/server handler)]
- (.listen server 6061)
- (p/resolved server)))
+ handler (impl/handler router)
+ server (impl/server handler)
+ port (cf/get :http-server-port 6061)]
+ (.listen server port)
+ (log/info :msg "starting http server" :port port)
+ (reset! instance server)))
-(defn stop!
- [server]
- (p/create (fn [resolve]
- (.close server (fn []
- (log/info :msg "shutdown http server")
- (resolve))))))
+(defn stop
+ []
+ (if-let [server @instance]
+ (p/create (fn [resolve]
+ (.close server (fn []
+ (log/info :msg "shutdown http server")
+ (resolve)))))
+ (p/resolved nil)))
diff --git a/exporter/src/app/http/export.cljs b/exporter/src/app/http/export.cljs
index 15ac3e8ba..8be2f5470 100644
--- a/exporter/src/app/http/export.cljs
+++ b/exporter/src/app/http/export.cljs
@@ -6,15 +6,15 @@
(ns app.http.export
(:require
- [app.http.export-bitmap :as bitmap]
- [app.http.export-svg :as svg]
+ [app.common.exceptions :as exc :include-macros true]
+ [app.common.spec :as us]
+ [app.renderer.bitmap :as rb]
+ [app.renderer.svg :as rs]
[app.zipfile :as zip]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lambdaisland.glogi :as log]
- [promesa.core :as p]
- [app.common.exceptions :as exc :include-macros true]
- [app.common.spec :as us]))
+ [promesa.core :as p]))
(s/def ::name ::us/string)
(s/def ::page-id ::us/uuid)
@@ -38,42 +38,44 @@
(declare attach-filename)
(defn export-handler
- [{:keys [params browser cookies] :as request}]
+ [{:keys [params cookies] :as request}]
(let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params)
token (.get ^js cookies "auth-token")]
(case (count exports)
- 0 (exc/raise :type :validation :code :missing-exports)
- 1 (handle-single-export
- request
- (assoc (first exports)
- :name name
- :token token
- :file-id file-id
- :page-id page-id
- :object-id object-id))
- (handle-multiple-export
- request
- (map (fn [item]
- (assoc item
- :name name
- :token token
- :file-id file-id
- :page-id page-id
- :object-id object-id)) exports)))))
+ 0 (exc/raise :type :validation
+ :code :missing-exports)
+
+ 1 (-> (first exports)
+ (assoc :name name)
+ (assoc :token token)
+ (assoc :file-id file-id)
+ (assoc :page-id page-id)
+ (assoc :object-id object-id)
+ (handle-single-export))
+
+ (->> exports
+ (map (fn [item]
+ (-> item
+ (assoc :name name)
+ (assoc :token token)
+ (assoc :file-id file-id)
+ (assoc :page-id page-id)
+ (assoc :object-id object-id))))
+ (handle-multiple-export)))))
(defn- handle-single-export
- [{:keys [browser]} params]
- (p/let [result (perform-export browser params)]
+ [params]
+ (p/let [result (perform-export params)]
{:status 200
:body (:content result)
:headers {"content-type" (:mime-type result)
"content-length" (:length result)}}))
(defn- handle-multiple-export
- [{:keys [browser]} exports]
+ [exports]
(let [proms (->> exports
(attach-filename)
- (map (partial perform-export browser)))]
+ (map perform-export))]
(-> (p/all proms)
(p/then (fn [results]
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
@@ -83,11 +85,11 @@
:body (.generateNodeStream ^js fzip)})))))
(defn- perform-export
- [browser params]
+ [params]
(case (:type params)
- :png (bitmap/export browser params)
- :jpeg (bitmap/export browser params)
- :svg (svg/export browser params)))
+ :png (rb/render params)
+ :jpeg (rb/render params)
+ :svg (rs/render params)))
(defn- find-filename-candidate
[params used]
diff --git a/exporter/src/app/http/export_bitmap.cljs b/exporter/src/app/http/export_bitmap.cljs
deleted file mode 100644
index cf17a5e6d..000000000
--- a/exporter/src/app/http/export_bitmap.cljs
+++ /dev/null
@@ -1,80 +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.http.export-bitmap
- (:require
- [cuerdas.core :as str]
- [app.browser :as bwr]
- [app.config :as cfg]
- [lambdaisland.glogi :as log]
- [cljs.spec.alpha :as s]
- [promesa.core :as p]
- [app.common.exceptions :as exc :include-macros true]
- [app.common.data :as d]
- [app.common.pages :as cp]
- [app.common.spec :as us])
- (:import
- goog.Uri))
-
-(defn screenshot-object
- [browser {:keys [file-id page-id object-id token scale type]}]
- (letfn [(handle [page]
- (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
- uri (doto (Uri. (:public-uri cfg/config))
- (.setPath "/")
- (.setFragment path))
- cookie {:domain (str (.getDomain uri)
- ":"
- (.getPort uri))
- :key "auth-token"
- :value token}]
- (log/info :uri (.toString uri))
- (screenshot page (.toString uri) cookie)))
-
- (screenshot [page uri cookie]
- (p/do!
- (bwr/emulate! page {:viewport [1920 1080]
- :scale scale})
- (bwr/set-cookie! page cookie)
- (bwr/navigate! page uri)
- (bwr/eval! page (js* "() => document.body.style.background = 'transparent'"))
- (p/let [dom (bwr/select page "#screenshot")]
- (case type
- :png (bwr/screenshot dom {:omit-background? true :type type})
- :jpeg (bwr/screenshot dom {:omit-background? false :type type})))))]
-
- (bwr/exec! browser handle)))
-
-(s/def ::name ::us/string)
-(s/def ::suffix ::us/string)
-(s/def ::type #{:jpeg :png})
-(s/def ::page-id ::us/uuid)
-(s/def ::file-id ::us/uuid)
-(s/def ::object-id ::us/uuid)
-(s/def ::scale ::us/number)
-(s/def ::token ::us/string)
-(s/def ::filename ::us/string)
-
-(s/def ::export-params
- (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
- :opt-un [::filename]))
-
-(defn export
- [browser params]
- (us/assert ::export-params params)
- (p/let [content (screenshot-object browser params)]
- {:content content
- :filename (or (:filename params)
- (str (:name params)
- (:suffix params "")
- (case (:type params)
- :png ".png"
- :jpeg ".jpg")))
- :length (alength content)
- :mime-type (case (:type params)
- :png "image/png"
- :jpeg "image/jpeg")}))
-
diff --git a/exporter/src/app/http/impl.cljs b/exporter/src/app/http/impl.cljs
index 15f00ba5b..3401194d4 100644
--- a/exporter/src/app/http/impl.cljs
+++ b/exporter/src/app/http/impl.cljs
@@ -13,25 +13,15 @@
[app.util.transit :as t]
[cuerdas.core :as str]
[lambdaisland.glogi :as log]
+ [lambdaisland.uri :as u]
[promesa.core :as p]
- [reitit.core :as r])
- (:import
- goog.Uri))
-
-(defn- query-params
- "Given goog.Uri, read query parameters into Clojure map."
- [^Uri uri]
- (let [^js q (.getQueryData uri)]
- (->> q
- (.getKeys)
- (map (juxt keyword #(.get q %)))
- (into {}))))
+ [reitit.core :as r]))
(defn- match
[router ctx]
- (let [uri (.parse Uri (unchecked-get ctx "originalUrl"))]
- (when-let [match (r/match-by-path router (.getPath ^js uri))]
- (assoc match :query-params (query-params uri)))))
+ (let [uri (u/uri (unchecked-get ctx "originalUrl"))]
+ (when-let [match (r/match-by-path router (:path uri))]
+ (assoc match :query-params (u/query-string->map (:query uri))))))
(defn- handle-error
[error request]
@@ -48,17 +38,21 @@
:headers {"content-type" "text/html"}
:body (str "" (:explain data) "
\n")}))
+ (and (= :internal type)
+ (= :browser-not-ready code))
+ {:status 503
+ :headers {"x-error" (t/encode data)}
+ :body ""}
+
:else
(do
(log/error :msg "Unexpected error"
:error error)
(js/console.error error)
{:status 500
- :headers {"x-metadata" (t/encode {:type :unexpected
- :message (ex-message error)})}
+ :headers {"x-error" (t/encode data)}
:body ""}))))
-
(defn- handle-response
[ctx {:keys [body headers status] :or {headers {} status 200}}]
(run! (fn [[k v]] (.set ^js ctx k v)) headers)
@@ -89,17 +83,16 @@
(t/decode))))))))
(defn- wrap-handler
- [f extra]
+ [f]
(fn [ctx]
(p/let [cookies (unchecked-get ctx "cookies")
headers (parse-headers ctx)
body (parse-body ctx)
- request (assoc extra
- :method (str/lower (unchecked-get ctx "method"))
- :body body
- :ctx ctx
- :headers headers
- :cookies cookies)]
+ request {:method (str/lower (unchecked-get ctx "method"))
+ :body body
+ :ctx ctx
+ :headers headers
+ :cookies cookies}]
(-> (p/do! (f request))
(p/then (fn [rsp]
(when (map? rsp)
@@ -131,10 +124,10 @@
(.createServer http @handler))
(defn handler
- [router extra]
+ [router]
(let [instance (doto (new koa)
(.use (-> (router-handler router)
- (wrap-handler extra))))]
+ (wrap-handler))))]
(specify! instance
cljs.core/IDeref
(-deref [_]
diff --git a/exporter/src/app/http/thumbnail.cljs b/exporter/src/app/http/thumbnail.cljs
deleted file mode 100644
index f351d67a4..000000000
--- a/exporter/src/app/http/thumbnail.cljs
+++ /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.http.thumbnail
- (:require
- [app.common.exceptions :as exc :include-macros true]
- [app.common.spec :as us]
- [app.http.export-bitmap :as bitmap]
- [cljs.spec.alpha :as s]
- [cuerdas.core :as str]
- [lambdaisland.glogi :as log]
- [promesa.core :as p]))
-
-(s/def ::page-id ::us/uuid)
-(s/def ::file-id ::us/uuid)
-(s/def ::object-id ::us/uuid)
-(s/def ::scale ::us/number)
-
-(s/def ::handler-params
- (s/keys :req-un [::page-id ::file-id ::object-id]))
-
-(declare handle-single-export)
-(declare handle-multiple-export)
-(declare perform-export)
-(declare attach-filename)
-
-(defn thumbnail-handler
- [{:keys [params browser cookies] :as request}]
- (let [{:keys [page-id file-id object-id]} (us/conform ::handler-params params)
- params {:token (.get ^js cookies "auth-token")
- :file-id file-id
- :page-id page-id
- :object-id object-id
- :scale 0.3
- :type :jpeg}]
- (p/let [content (bitmap/screenshot-object browser params)]
- {:status 200
- :body content
- :headers {"content-type" "image/jpeg"
- "content-length" (alength content)}})))
diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs
new file mode 100644
index 000000000..d80d53ab6
--- /dev/null
+++ b/exporter/src/app/renderer/bitmap.cljs
@@ -0,0 +1,95 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.renderer.bitmap
+ "A bitmap renderer."
+ (:require
+ [app.browser :as bw]
+ [app.common.data :as d]
+ [app.common.exceptions :as ex :include-macros true]
+ [app.common.pages :as cp]
+ [app.common.spec :as us]
+ [app.config :as cf]
+ [cljs.spec.alpha :as s]
+ [cuerdas.core :as str]
+ [lambdaisland.uri :as u]
+ [lambdaisland.glogi :as log]
+ [promesa.core :as p]))
+
+(defn create-cookie
+ [uri token]
+ (let [domain (str (:host uri)
+ (when (:port uri)
+ (str ":" (:port uri))))]
+ {:domain domain
+ :key "auth-token"
+ :value token}))
+
+(defn screenshot-object
+ [browser {:keys [file-id page-id object-id token scale type]}]
+ (letfn [(handle [page]
+ (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
+ uri (-> (u/uri (cf/get :public-uri))
+ (assoc :path "/")
+ (assoc :fragment path))
+ cookie (create-cookie uri token)]
+ (screenshot page (str uri) cookie)))
+
+ (screenshot [page uri cookie]
+ (log/info :uri uri)
+ (let [viewport {:width 1920
+ :height 1080
+ :scale scale}
+ options {:viewport viewport
+ :cookie cookie}]
+ (p/do!
+ (bw/configure-page! page options)
+ (bw/navigate! page uri)
+ (bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
+ (bw/wait-for page "#screenshot")
+ (p/let [dom (bw/select page "#screenshot")]
+ (case type
+ :png (bw/screenshot dom {:omit-background? true :type type})
+ :jpeg (bw/screenshot dom {:omit-background? false :type type}))))))]
+
+ (bw/exec! browser handle)))
+
+(s/def ::name ::us/string)
+(s/def ::suffix ::us/string)
+(s/def ::type #{:jpeg :png})
+(s/def ::page-id ::us/uuid)
+(s/def ::file-id ::us/uuid)
+(s/def ::object-id ::us/uuid)
+(s/def ::scale ::us/number)
+(s/def ::token ::us/string)
+(s/def ::filename ::us/string)
+
+(s/def ::render-params
+ (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
+ :opt-un [::filename]))
+
+(defn render
+ [params]
+ (us/assert ::render-params params)
+ (let [browser @bw/instance]
+ (when-not browser
+ (ex/raise :type :internal
+ :code :browser-not-ready
+ :hint "browser cluster is not initialized yet"))
+
+ (p/let [content (screenshot-object browser params)]
+ {:content content
+ :filename (or (:filename params)
+ (str (:name params)
+ (:suffix params "")
+ (case (:type params)
+ :png ".png"
+ :jpeg ".jpg")))
+ :length (alength content)
+ :mime-type (case (:type params)
+ :png "image/png"
+ :jpeg "image/jpeg")})))
+
diff --git a/exporter/src/app/http/export_svg.cljs b/exporter/src/app/renderer/svg.cljs
similarity index 81%
rename from exporter/src/app/http/export_svg.cljs
rename to exporter/src/app/renderer/svg.cljs
index 13b6bf946..64f298ca0 100644
--- a/exporter/src/app/http/export_svg.cljs
+++ b/exporter/src/app/renderer/svg.cljs
@@ -4,24 +4,24 @@
;;
;; Copyright (c) UXBOX Labs SL
-(ns app.http.export-svg
+(ns app.renderer.svg
(:require
["path" :as path]
["xml-js" :as xml]
- [app.browser :as bwr]
+ [app.browser :as bw]
[app.common.data :as d]
- [app.common.exceptions :as exc :include-macros true]
+ [app.common.exceptions :as ex :include-macros true]
[app.common.pages :as cp]
[app.common.spec :as us]
- [app.config :as cfg]
+ [app.config :as cf]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[clojure.walk :as walk]
[cuerdas.core :as str]
[lambdaisland.glogi :as log]
- [promesa.core :as p])
- (:import
- goog.Uri))
+ [lambdaisland.uri :as u]
+ [app.renderer.bitmap :refer [create-cookie]]
+ [promesa.core :as p]))
(log/set-level "app.http.export-svg" :trace)
@@ -67,7 +67,6 @@
(nil? d)
(str/empty? d)))))
-
(defn flatten-toplevel-svg-elements
"Flattens XML data structure if two nested top-side SVG elements found."
[item]
@@ -165,7 +164,9 @@
;; objects.
(let [vbox (-> (get-in result ["attributes" "viewBox"])
(parse-viewbox))
- transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y (/ width (:width vbox)) (/ height (:height vbox)))]
+ transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
+ (/ width (:width vbox))
+ (/ height (:height vbox)))]
(-> result
(assoc "name" "g")
(assoc "attributes" {})
@@ -212,8 +213,8 @@
(extract-single-node [node]
(log/trace :fn :extract-single-node)
- (p/let [attrs (bwr/eval! node extract-element-attrs)
- shot (bwr/screenshot node {:omit-background? true :type "png"})]
+ (p/let [attrs (bw/eval! node extract-element-attrs)
+ shot (bw/screenshot node {:omit-background? true :type "png"})]
{:id (unchecked-get attrs "id")
:x (unchecked-get attrs "x")
:y (unchecked-get attrs "y")
@@ -235,12 +236,12 @@
(process-text-nodes [page]
(log/trace :fn :process-text-nodes)
- (-> (bwr/select-all page "#screenshot foreignObject")
+ (-> (bw/select-all page "#screenshot foreignObject")
(p/then (fn [nodes] (p/all (map process-text-node nodes))))))
(extract-svg [page]
- (p/let [dom (bwr/select page "#screenshot")
- xmldata (bwr/eval! dom (fn [elem] (.-outerHTML ^js elem)))
+ (p/let [dom (bw/select page "#screenshot")
+ xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
nodes (process-text-nodes page)
nodes (d/index-by :id nodes)
result (replace-text-nodes xmldata nodes)]
@@ -252,31 +253,33 @@
result))
(render-in-page [page {:keys [uri cookie] :as rctx}]
- (p/do!
- (bwr/emulate! page {:viewport [1920 1080]
- :scale 4})
- (bwr/set-cookie! page cookie)
- (bwr/navigate! page uri)
- ;; (bwr/wait-for page "#screenshot foreignObject" {:visible true})
- (bwr/sleep page 2000)
- ;; (bwr/eval! page (js* "() => document.body.style.background = 'transparent'"))
- page))
+ (let [viewport {:width 1920
+ :height 1080
+ :scale 4}
+ options {:viewport viewport
+ :timeout 15000
+ :cookie cookie}]
+ (p/do!
+ (bw/configure-page! page options)
+ (bw/navigate! page uri)
+ (bw/wait-for page "#screenshot")
+ (bw/sleep page 2000)
+ ;; (bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
+ page)))
(handle [rctx page]
(p/let [page (render-in-page page rctx)]
(extract-svg page)))]
- (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
- uri (doto (Uri. (:public-uri cfg/config))
- (.setPath "/")
- (.setFragment path))
- rctx {:cookie {:domain (str (.getDomain uri) ":" (.getPort uri))
- :key "auth-token"
- :value token}
- :uri (.toString uri)}]
-
- (log/info :uri (.toString uri))
- (bwr/exec! browser (partial handle rctx)))))
+ (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
+ uri (-> (u/uri (cf/get :public-uri))
+ (assoc :path "/")
+ (assoc :fragment path))
+ cookie (create-cookie uri token)
+ rctx {:cookie cookie
+ :uri (str uri)}]
+ (log/info :uri (:uri rctx))
+ (bw/exec! browser (partial handle rctx)))))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
@@ -288,18 +291,25 @@
(s/def ::token ::us/string)
(s/def ::filename ::us/string)
-(s/def ::export-params
+(s/def ::render-params
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
:opt-un [::filename]))
-(defn export
- [browser params]
- (us/assert ::export-params params)
- (p/let [content (render-object browser params)]
- {:content content
- :filename (or (:filename params)
- (str (:name params)
- (:suffix params "")
- ".svg"))
- :length (alength content)
- :mime-type "image/svg+xml"}))
+(defn render
+ [params]
+ (us/assert ::render-params params)
+ (let [browser @bw/instance]
+ (when-not browser
+ (ex/raise :type :internal
+ :code :browser-not-ready
+ :hint "browser cluster is not initialized yet"))
+
+
+ (p/let [content (render-object browser params)]
+ {:content content
+ :filename (or (:filename params)
+ (str (:name params)
+ (:suffix params "")
+ ".svg"))
+ :length (alength content)
+ :mime-type "image/svg+xml"})))
diff --git a/exporter/src/app/util/transit.cljs b/exporter/src/app/util/transit.cljs
index 38fbe3d4b..6afaa015e 100644
--- a/exporter/src/app/util/transit.cljs
+++ b/exporter/src/app/util/transit.cljs
@@ -25,5 +25,5 @@
(defn encode
[data]
- (let [w (t/writer :json {:handlers +write-handlers+})]
+ (let [w (t/writer :json-verbose {:handlers +write-handlers+})]
(t/write w data)))
diff --git a/exporter/yarn.lock b/exporter/yarn.lock
index a122fe7ac..43907db23 100644
--- a/exporter/yarn.lock
+++ b/exporter/yarn.lock
@@ -11,9 +11,9 @@
regenerator-runtime "^0.13.4"
"@types/node@*":
- version "15.0.1"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a"
- integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA==
+ version "15.0.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67"
+ integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==
"@types/yauzl@^2.9.1":
version "2.9.1"
@@ -272,9 +272,9 @@ cookies@~0.8.0:
keygrip "~1.1.0"
core-js-pure@^3.0.0:
- version "3.11.2"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c"
- integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ==
+ version "3.12.1"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.12.1.tgz#934da8b9b7221e2a2443dc71dfa5bd77a7ea00b8"
+ integrity sha512-1cch+qads4JnDSWsvc7d6nzlKAippwjUlf6vykkTLW53VSV+NkE6muGBToAjEA8pG90cSfcud3JgVmW2ds5TaQ==
core-util-is@~1.0.0:
version "1.0.2"
@@ -492,9 +492,9 @@ get-stream@^5.1.0:
pump "^3.0.0"
glob@^7.1.3:
- version "7.1.6"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
- integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ version "7.1.7"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+ integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -618,9 +618,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-generator-function@^1.0.7:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b"
- integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ==
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c"
+ integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
@@ -982,9 +982,9 @@ puppeteer-cluster@^0.22.0:
debug "^4.1.1"
puppeteer@^9.1.0:
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.0.tgz#0530ed1f595088eefd078c8f1f7618d00f216a56"
- integrity sha512-+BWwEKYQ9oBTUcDYwfgnVPlHSEYqD4sXsMqQf70vSlTE6YIuXujc7zKgO3FyZNJYVrdrUppy/LLwGF1IRacQMQ==
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c"
+ integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==
dependencies:
debug "^4.1.0"
devtools-protocol "0.0.869402"
diff --git a/frontend/dev/cljs/user.cljs b/frontend/dev/cljs/user.cljs
deleted file mode 100644
index 2d251b6c7..000000000
--- a/frontend/dev/cljs/user.cljs
+++ /dev/null
@@ -1 +0,0 @@
-(ns cljs.user)
diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js
index ba8cedd86..428bbf9a7 100644
--- a/frontend/gulpfile.js
+++ b/frontend/gulpfile.js
@@ -17,6 +17,7 @@ const mkdirp = require("mkdirp");
const rimraf = require("rimraf");
const sass = require("sass");
const gettext = require("gettext-parser");
+const marked = require("marked");
const mapStream = require("map-stream");
const paths = {};
@@ -32,7 +33,7 @@ paths.dist = "./target/dist/";
// Templates
function readLocales() {
- const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_cn"];
+ const langs = ["ca", "de", "el", "en", "es", "fr", "tr", "ru", "zh_CN", "pt_BR", "ro"];
const result = {};
for (let lang of langs) {
@@ -45,17 +46,35 @@ function readLocales() {
for (let key of Object.keys(trdata)) {
if (key === "") continue;
+ const comments = trdata[key].comments || {};
if (l.isNil(result[key])) {
result[key] = {};
}
- const msgstr = trdata[key].msgstr;
- if (msgstr.length === 1) {
- result[key][lang] = msgstr[0];
+ const isMarkdown = l.includes(comments.flag, "markdown");
+
+ const msgs = trdata[key].msgstr;
+ if (msgs.length === 1) {
+ let message = msgs[0];
+ if (isMarkdown) {
+ message = marked.parseInline(message);
+ }
+
+ result[key][lang] = message;
} else {
- result[key][lang] = msgstr;
+ result[key][lang] = msgs.map((item) => {
+ if (isMarkdown) {
+ return marked.parseInline(item);
+ } else {
+ return item;
+ }
+ });
}
+ // if (key === "modals.delete-font.title") {
+ // console.dir(trdata[key], {depth:10});
+ // console.dir(result[key], {depth:10});
+ // }
}
}
diff --git a/frontend/package.json b/frontend/package.json
index dbc09258c..9e583d58a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,26 +27,31 @@
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^1.5.0",
"map-stream": "0.0.7",
+ "marked": "^2.0.3",
"mkdirp": "^1.0.4",
- "postcss": "^8.2.7",
+ "postcss": "^8.2.15",
"postcss-clean": "^1.2.2",
"rimraf": "^3.0.0",
"sass": "^1.32.8",
- "shadow-cljs": "^2.11.20"
+ "shadow-cljs": "2.12.6"
},
"dependencies": {
- "date-fns": "^2.21.1",
+ "date-fns": "^2.21.3",
"draft-js": "^0.11.7",
"highlight.js": "^10.6.0",
"js-beautify": "^1.13.5",
"luxon": "^1.26.0",
"mousetrap": "^1.6.5",
+ "opentype.js": "^1.3.3",
"randomcolor": "^0.6.2",
"react": "~17.0.1",
"react-dom": "~17.0.1",
- "rxjs": "~7.0.0-beta.12",
+ "react-virtualized": "^9.22.3",
+ "rxjs": "~7.0.1",
+ "sax": "^1.2.4",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
+ "ua-parser-js": "^0.7.28",
"xregexp": "^5.0.1"
}
}
diff --git a/frontend/resources/images/features/custom-fonts.gif b/frontend/resources/images/features/custom-fonts.gif
new file mode 100644
index 000000000..191dcf36c
Binary files /dev/null and b/frontend/resources/images/features/custom-fonts.gif differ
diff --git a/frontend/resources/images/features/performance.gif b/frontend/resources/images/features/performance.gif
new file mode 100644
index 000000000..afd4c582a
Binary files /dev/null and b/frontend/resources/images/features/performance.gif differ
diff --git a/frontend/resources/images/features/scale-text.gif b/frontend/resources/images/features/scale-text.gif
new file mode 100644
index 000000000..bc56494a6
Binary files /dev/null and b/frontend/resources/images/features/scale-text.gif differ
diff --git a/frontend/resources/images/features/shapes-to-path.gif b/frontend/resources/images/features/shapes-to-path.gif
new file mode 100644
index 000000000..cea102568
Binary files /dev/null and b/frontend/resources/images/features/shapes-to-path.gif differ
diff --git a/frontend/resources/styles/common/dependencies/colors.scss b/frontend/resources/styles/common/dependencies/colors.scss
index bd995bc8c..5a9c19c4d 100644
--- a/frontend/resources/styles/common/dependencies/colors.scss
+++ b/frontend/resources/styles/common/dependencies/colors.scss
@@ -23,6 +23,7 @@ $color-info: #59b9e2;
$color-ocean: #4285f4;
$color-component: #76B0B8;
$color-component-highlight: #00E0FF;
+$color-pink: #feecfc;
// Gray scale
$color-gray-10: #E3E3E3;
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index 0c9ee57b1..27c7c3610 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -63,6 +63,7 @@
@import "main/partials/dashboard-sidebar";
@import "main/partials/dashboard-team";
@import "main/partials/dashboard-settings";
+@import "main/partials/dashboard-fonts";
@import "main/partials/debug-icons-preview";
@import "main/partials/editable-label";
@import "main/partials/left-toolbar";
diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss
new file mode 100644
index 000000000..0f61a12a3
--- /dev/null
+++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss
@@ -0,0 +1,234 @@
+.dashboard-fonts {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .dashboard-installed-fonts {
+ max-width: 1000px;
+ width: 100%;
+ display: flex;
+ margin-top: $big;
+ flex-direction: column;
+
+ h3 {
+ font-size: $fs14;
+ color: $color-gray-30;
+ margin: $x-small;
+ }
+
+ .font-item {
+ color: $color-black;
+ }
+ }
+
+ .installed-fonts-header {
+ color: $color-gray-40;
+ display: flex;
+ height: 40px;
+ font-size: $fs12;
+ background-color: $color-white;
+ align-items: center;
+ padding: 0px $big;
+
+ > .family {
+ min-width: 200px;
+ width: 200px;
+ }
+
+ > .variants {
+ padding-left: 12px;
+ }
+
+ .search-input {
+ display: flex;
+ flex-grow: 1;
+ justify-content: flex-end;
+
+ input {
+ font-size: $fs12;
+ border: 1px solid $color-gray-30;
+ border-radius: $br-small;
+ width: 130px;
+ padding: $x-small;
+ margin: 0px;
+ }
+ }
+ }
+
+ .font-item {
+ margin-top: $big;
+ color: $color-gray-40;
+ font-size: $fs14;
+ background-color: $color-white;
+ display: flex;
+ min-width: 1000px;
+ width: 100%;
+ min-height: 97px;
+ align-items: center;
+ padding: $big;
+ justify-content: space-between;
+
+ &:not(:first-child) {
+ border-top: 1px solid $color-gray-10;
+ }
+
+ input {
+ border: 1px solid $color-gray-30;
+ border-radius: $br-small;
+ margin: 0px;
+ padding: $small;
+ font-size: $fs12;
+ }
+
+ > .family {
+ min-width: 200px;
+ width: 200px;
+ }
+
+ > .filenames {
+ min-width: 200px;
+ }
+
+ > .variants {
+ font-size: $fs14;
+ display: flex;
+ flex-wrap: wrap;
+ flex-grow: 1;
+
+ .variant {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ cursor: pointer;
+
+ .icon {
+ display: flex;
+ height: 16px;
+ width: 16px;
+ margin-left: 6px;
+ align-items: center;
+ svg {
+ fill: transparent;
+ width: 12px;
+ height: 12px;
+ transform: rotate(45deg);
+ }
+ }
+
+ &:hover {
+ .icon svg {
+ fill: $color-gray-30;
+ }
+ }
+
+ }
+ }
+
+
+ .filenames {
+ display: flex;
+ flex-direction: column;
+ font-size: $fs12;
+ }
+
+ .options {
+ display: flex;
+ justify-content: flex-end;
+
+ .icon {
+ width: $big;
+ cursor: pointer;
+ display: flex;
+ margin-left: 10px;
+ justify-content: center;
+ align-items: center;
+ svg {
+ width: 16px;
+ height: 16px;
+ }
+
+ &.close {
+ svg {
+ transform: rotate(45deg);
+ }
+ }
+
+ }
+ }
+ }
+
+ .dashboard-fonts-upload {
+ max-width: 1000px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+
+
+ .upload-button {
+ width: 100px;
+ }
+ }
+
+ .dashboard-fonts-hero {
+ font-size: $fs14;
+
+ padding: $x-big;
+ background-color: $color-white;
+ margin-top: $x-big;
+ display: flex;
+ justify-content: space-between;
+
+ .banner {
+ background-color: unset;
+
+ display: flex;
+
+ .icon {
+ display: flex;
+ align-items: center;
+ padding-left: 0px;
+ padding-right: 10px;
+ svg {
+ fill: $color-info;
+ }
+ }
+ }
+
+ .desc {
+ h2 {
+ margin-bottom: $medium;
+ color: $color-black;
+ }
+ width: 80%;
+ color: $color-gray-40;
+ }
+ }
+
+ .fonts-placeholder {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ max-width: 1000px;
+ width: 100%;
+ height: 161px;
+
+ border: 1px dashed $color-gray-20;
+ margin-top: 16px;
+
+
+ .icon {
+ svg {
+ fill: $color-gray-40;
+ width: 32px;
+ height: 32px;
+ }
+ }
+
+ .label {
+ color: $color-gray-40;
+ font-size: $fs14;
+ }
+ }
+}
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index d67985d15..bb0b12751 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -382,7 +382,7 @@
.modal-left {
align-items: center;
- background-color: $color-primary;
+ background-color: $color-pink;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
display: flex;
@@ -391,6 +391,10 @@
overflow: hidden;
padding: $x-big;
width: 230px;
+
+ &.welcome {
+ padding: 0;
+ }
}
.modal-right {
@@ -498,6 +502,7 @@
color: $color-black;
flex: 1;
flex-direction: column;
+ overflow: visible;
padding: $x-big 40px;
text-align: center;
diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss
index bcae65e66..4389cf8b6 100644
--- a/frontend/resources/styles/main/partials/sidebar-assets.scss
+++ b/frontend/resources/styles/main/partials/sidebar-assets.scss
@@ -164,6 +164,8 @@
.asset-title {
display: flex;
cursor: pointer;
+ font-size: $fs11;
+ text-transform: uppercase;
& .num-assets {
color: $color-gray-30;
@@ -371,14 +373,11 @@
// overflow-y: scroll;
// }
- .asset-list {
- margin-top: $medium;
- }
-
.asset-list-item {
display: flex;
align-items: center;
border: 1px solid transparent;
+ border-radius: $br-small;
margin-top: $x-small;
padding: 2px;
font-size: $fs12;
diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss
index 36da777ab..af2a9d22a 100644
--- a/frontend/resources/styles/main/partials/sidebar-element-options.scss
+++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss
@@ -2,8 +2,7 @@
// 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) 2015-2016 Andrey Antukh
-// Copyright (c) 2015-2016 Juan de la Cruz
+// Copyright (c) UXBOX Labs SL
.element-options {
display: flex;
@@ -809,9 +808,10 @@
left: 0;
position: absolute;
top: 0;
- width: calc(100% - 8px);
+ width: calc(100%);
opacity: 0.4;
z-index: 10;
+ display: flex;
}
.advanced-options-wrapper {
@@ -1061,37 +1061,200 @@
}
.multiple-typography {
- margin: 0.5rem;
- padding: 0.5rem;
- border: 1px dashed $color-gray-30;
- border-radius: 4px;
- display: flex;
- justify-content: space-between;
+ margin: 0.5rem;
+ padding: 0.5rem;
+ border: 1px dashed $color-gray-30;
+ border-radius: 4px;
+ display: flex;
+ justify-content: space-between;
- .multiple-typography-text,
- .multiple-typography-button {
- font-size: $fs13;
- display: flex;
- align-items: center;
+ .multiple-typography-text,
+ .multiple-typography-button {
+ font-size: $fs13;
+ display: flex;
+ align-items: center;
+ }
+
+ .multiple-typography-button {
+ cursor: pointer;
+ svg {
+ transition: fill 0.3s;
+ width: 16px;
+ height: 16px;
+ fill: $color-gray-10;
}
- .multiple-typography-button {
- cursor: pointer;
- svg {
- transition: fill 0.3s;
- width: 16px;
- height: 16px;
- fill: $color-gray-10;
- }
+ &:hover svg {
+ fill: $color-primary;
+ }
+ }
- &:hover svg {
- fill: $color-primary;
+ svg {
+ }
+
+ .multiple-typography-button:hover svg {
+ }
+}
+
+.font-selector {
+ background: $color-black;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: calc(100%);
+ z-index: 10;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .font-selector-dropdown {
+ background: #303236;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ height: 100%;
+ }
+
+ header {
+ padding: 15px 17px;
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ .backend-filters {
+ padding: $small $medium;
+ // width: 220px;
+ top: 40px;
+ right: 20px;
+ }
+ .backend-filter {
+ display: flex;
+ align-items: center;
+ padding: $small 0;
+ cursor: pointer;
+
+ .checkbox-icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: $medium;
+ height: $medium;
+ border: 1px solid $color-gray-30;
+ border-radius: $br-small;
+
+ svg {
+ width: 8px;
+ display: none;
+ height: 8px;
+ fill: $color-black;
}
+ }
+
+ .backend-name {
+ margin-left: $small;
+ color: $color-gray-50;
+ }
+
+ &.selected {
+ .checkbox-icon {
+ svg {
+ display: inherit;
+ }
+ }
+ }
+
+ }
+
+ input {
+ display: flex;
+ flex-grow: 1;
+ padding: 4px;
+ font-size: $fs12;
+ background: $color-gray-50;
+ border-radius: $br-small;
+ color: $color-gray-20;
+ border: 1px solid $color-gray-30;
+ margin: 0px;
+ }
+
+ .options {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 24px;
+ height: 24px;
+ margin-left: $small;
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $color-gray-20
+ }
+
+ &.active {
+ svg {
+ fill: $color-primary;
+ }
+ }
+ }
+ }
+
+ .fonts-list {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ position: relative;
+ -webkit-box-flex: 1;
+ flex: 1 1 auto;
+ }
+
+ hr {
+ margin-bottom: 0px;
+ margin-top: 0px;
+ }
+
+ .font-item {
+ padding-left: $big;
+ height: $x-big;
+ max-height: $x-big;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ color: $color-gray-10;
+
+ &.selected {
+ background-color: $color-black;
+ color: $color-primary;
+
+ .icon svg {fill: $color-primary;}
+ }
+
+ &:hover {
+ background-color: $color-primary;
+ color: $color-black;
+ }
+
+ .icon {
+ display: flex;
+ // justify-content: center;
+ align-items: center;
+ // border: 1px solid red;
+ width: $big
+ }
+
+ .label {
+ font-size: 12px;
}
svg {
+ fill: $color-gray-10;
+ width: 10px;
+ height: 10px;
}
-
- .multiple-typography-button:hover svg {
- }
+ }
}
+
+
diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss
index 57baeec1b..d2f630bb5 100644
--- a/frontend/resources/styles/main/partials/sidebar.scss
+++ b/frontend/resources/styles/main/partials/sidebar.scss
@@ -69,6 +69,7 @@ $width-settings-bar: 16rem;
height: 100%;
.tool-window {
+ position: relative;
border-bottom: 1px solid $color-gray-60;
display: flex;
flex-direction: column;
diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss
index d8e3b69ef..f167e9814 100644
--- a/frontend/resources/styles/main/partials/workspace.scss
+++ b/frontend/resources/styles/main/partials/workspace.scss
@@ -134,8 +134,8 @@
width: 100%;
display: grid;
- grid-template-rows: 20px 1fr;
- grid-template-columns: 20px 1fr;
+ grid-template-rows: 20px 100%;
+ grid-template-columns: 20px 100%;
}
.viewport {
@@ -145,6 +145,11 @@
overflow: hidden;
position: relative;
+ svg {
+ widht: 100%;
+ height: 100%;
+ }
+
.viewport-overlays {
position: absolute;
width: 100%;
@@ -164,12 +169,6 @@
}
}
- .selection-rect {
- fill: rgba(235, 215, 92, 0.1);
- stroke: #000000;
- stroke-width: 0.1px;
- }
-
.render-shapes {
position: absolute;
}
diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn
index da046cd80..d4b2b0a2e 100644
--- a/frontend/shadow-cljs.edn
+++ b/frontend/shadow-cljs.edn
@@ -1,25 +1,26 @@
{:http {:port 3448}
:nrepl {:port 3447}
- :jvm-opts ["-Xmx500m" "-Xms50m" "-XX:+UseSerialGC"]
+ :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
:dev-http {8888 "classpath:public"}
- :source-paths ["src", "vendor", "resources", "../common", "tests"]
+ :source-paths ["src", "vendor", "resources", "../common", "tests", "dev"]
:dependencies
[[binaryage/devtools "RELEASE"]
[environ/environ "1.2.0"]
- [metosin/reitit-core "0.5.12"]
+ [metosin/reitit-core "0.5.13"]
[expound/expound "0.8.9"]
[danlentz/clj-uuid "0.1.9"]
[frankiesardo/linked "1.3.0"]
- [funcool/beicon "2021.04.27-2"]
- [funcool/cuerdas "2020.03.26-3"]
+ [funcool/beicon "2021.04.29-0"]
+ [funcool/cuerdas "2021.05.09-0"]
[funcool/okulary "2020.04.14-0"]
- [funcool/potok "3.2.0"]
+ [funcool/potok "4.0.0"]
[funcool/promesa "6.0.0"]
- [funcool/rumext "2021.01.26-0"]
+ [funcool/rumext "2021.05.12-1"]
+ [funcool/tubax "2021.05.20-0"]
[lambdaisland/uri "1.4.54"
:exclusions [org.clojure/data.json]]
@@ -43,9 +44,11 @@
:worker {:entries [app.worker]
:web-worker true
:depends-on #{:shared}}}
+
:compiler-options
{:output-feature-set :es8
- :output-wrapper false}
+ :output-wrapper false
+ :warnings {:fn-deprecated false}}
:release
{:compiler-options
@@ -59,6 +62,10 @@
{:target :node-test
:output-to "target/tests.js"
:ns-regexp "^app.test-"
- :autorun true}}}
+ :autorun true
+ :compiler-options
+ {:output-feature-set :es8
+ :output-wrapper false
+ :warnings {:fn-deprecated false}}}}}
diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs
index 5f801d155..87dcf4431 100644
--- a/frontend/src/app/config.cljs
+++ b/frontend/src/app/config.cljs
@@ -76,6 +76,7 @@
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes"))
+(def analytics (obj/get global "penpotAnalyticsEnabled" false))
(def version (delay (parse-version global)))
(def target (delay (parse-target global)))
@@ -120,9 +121,10 @@
(defn resolve-file-media
([media]
(resolve-file-media media false))
- ([{:keys [id] :as media} thumnail?]
+
+ ([{:keys [id]} thumbnail?]
(str (cond-> (u/join public-uri "assets/by-file-media-id/")
- (true? thumnail?) (u/join (str id "/thumbnail"))
- (false? thumnail?) (u/join (str id))))))
+ (true? thumbnail?) (u/join (str id "/thumbnail"))
+ (false? thumbnail?) (u/join (str id))))))
diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs
index e80e545f8..dc2a58f96 100644
--- a/frontend/src/app/main.cljs
+++ b/frontend/src/app/main.cljs
@@ -9,7 +9,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
- [app.main.data.auth :as da]
+ [app.main.data.events :as ev]
[app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.repo :as rp]
@@ -28,6 +28,7 @@
[app.util.timers :as ts]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
+ [potok.core :as ptk]
[rumext.alpha :as mf]))
(log/initialize!)
@@ -52,18 +53,16 @@
(defn on-navigate
[router path]
(let [match (match-path router path)
- profile (:profile storage)
+ profile (:profile @storage)
nopath? (or (= path "") (= path "/"))
authed? (and (not (nil? profile))
(not= (:id profile) uuid/zero))]
(cond
(and nopath? authed? (nil? match))
- (->> (rp/query! :profile)
- (rx/subs (fn [profile]
- (if (not= uuid/zero profile)
- (st/emit! (rt/nav :dashboard-projects {:team-id (da/current-team-id profile)}))
- (st/emit! (rt/nav :auth-login))))))
+ (if (not= uuid/zero profile)
+ (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)}))
+ (st/emit! (rt/nav :auth-login)))
(and (not authed?) (nil? match))
(st/emit! (rt/nav :auth-login))
@@ -72,23 +71,42 @@
(st/emit! (dm/assign-exception {:type :not-found}))
:else
- (st/emit! #(assoc % :route match)))))
+ (st/emit! (rt/navigated match)))))
(defn init-ui
[]
(mf/mount (mf/element ui/app) (dom/get-element "app"))
(mf/mount (mf/element modal) (dom/get-element "modal")))
+
+(defn initialize
+ []
+ (letfn [(on-profile [profile]
+ (rx/of (rt/initialize-router ui/routes)
+ (rt/initialize-history on-navigate)))]
+ (ptk/reify ::initialize
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :session-id (uuid/next)))
+
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (rx/merge
+ (rx/of
+ (ptk/event ::ev/initialize)
+ (du/initialize-profile))
+ (->> stream
+ (rx/filter (ptk/type? ::du/profile-fetched))
+ (rx/take 1)
+ (rx/map deref)
+ (rx/mapcat on-profile)))))))
+
(defn ^:export init
[]
(i18n/init! cfg/translations)
(theme/init! cfg/themes)
- (st/init)
(init-ui)
-
- (st/emit! (rt/initialize-router ui/routes)
- (rt/initialize-history on-navigate)
- (du/fetch-profile-and-teams)))
+ (st/emit! (initialize)))
(defn reinit
[]
@@ -103,3 +121,4 @@
(defn ^:dev/after-load after-load
[]
(reinit))
+
diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs
deleted file mode 100644
index 694ff93b9..000000000
--- a/frontend/src/app/main/data/auth.cljs
+++ /dev/null
@@ -1,205 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) UXBOX Labs SL
-
-(ns app.main.data.auth
- (:require
- [app.common.spec :as us]
- [app.config :as cf]
- [app.main.data.messages :as dm]
- [app.main.data.modal :as modal]
- [app.main.data.users :as du]
- [app.main.repo :as rp]
- [app.main.store :refer [initial-state]]
- [app.util.i18n :as i18n :refer [tr]]
- [app.util.router :as rt]
- [app.util.storage :refer [storage]]
- [beicon.core :as rx]
- [cljs.spec.alpha :as s]
- [potok.core :as ptk]))
-
-(s/def ::email ::us/email)
-(s/def ::password string?)
-(s/def ::fullname string?)
-
-;; --- Current team for a profile
-
-(defn current-team-id
- [profile]
- (let [team-id (:current-team-id storage)]
- (or team-id (:default-team-id profile))))
-
-(defn set-current-team!
- [team-id]
- (swap! storage assoc :current-team-id team-id))
-
-;; --- Logged In
-
-(defn logged-in
- [profile]
- (ptk/reify ::logged-in
- ptk/WatchEvent
- (watch [this state stream]
- (let [team-id (current-team-id profile)
- props (:props profile)]
- (rx/concat
- (rx/of (du/profile-fetched profile))
- (rx/of (du/fetch-teams))
- (rx/of (rt/nav' :dashboard-projects {:team-id team-id}))
- (when-not (:onboarding-viewed props)
- (->> (rx/of (modal/show {:type :onboarding}))
- (rx/delay 1000))))))))
-
-;; --- Login
-
-(s/def ::login-params
- (s/keys :req-un [::email ::password]))
-
-(defn login
- [{:keys [email password] :as data}]
- (us/verify ::login-params data)
- (ptk/reify ::login
- ptk/UpdateEvent
- (update [_ state]
- (merge state (dissoc initial-state :route :router)))
-
- ptk/WatchEvent
- (watch [this state s]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)
- params {:email email
- :password password
- :scope "webapp"}]
- (->> (rx/timer 100)
- (rx/mapcat #(rp/mutation :login params))
- (rx/tap on-success)
- (rx/catch on-error)
- (rx/map logged-in))))))
-
-(defn login-from-token
- [{:keys [profile] :as tdata}]
- (ptk/reify ::login-from-token
- ptk/UpdateEvent
- (update [_ state]
- (merge state (dissoc initial-state :route :router)))
-
- ptk/WatchEvent
- (watch [this state s]
- (rx/of (logged-in profile)))))
-
-;; --- Logout
-
-(def clear-user-data
- (ptk/reify ::clear-user-data
- ptk/UpdateEvent
- (update [_ state]
- (select-keys state [:route :router :session-id :history]))
-
- ptk/WatchEvent
- (watch [_ state s]
- (->> (rp/mutation :logout)
- (rx/catch (constantly (rx/empty)))
- (rx/ignore)))
-
- ptk/EffectEvent
- (effect [_ state s]
- (reset! storage {})
- (i18n/reset-locale))))
-
-(defn logout
- []
- (ptk/reify ::logout
- ptk/WatchEvent
- (watch [_ state stream]
- (rx/of clear-user-data
- (rt/nav :auth-login)))))
-
-;; --- Register
-
-(s/def ::invitation-token ::us/not-empty-string)
-
-(s/def ::register
- (s/keys :req-un [::fullname ::password ::email]
- :opt-un [::invitation-token]))
-
-(defn register
- "Create a register event instance."
- [data]
- (s/assert ::register data)
- (ptk/reify ::register
- ptk/WatchEvent
- (watch [_ state stream]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)]
- (->> (rp/mutation :register-profile data)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-
-;; --- Request Account Deletion
-
-(defn request-account-deletion
- [params]
- (ptk/reify ::request-account-deletion
- ptk/WatchEvent
- (watch [_ state stream]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta params)]
- (->> (rp/mutation :delete-profile {})
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- Recovery Request
-
-(s/def ::recovery-request
- (s/keys :req-un [::email]))
-
-(defn request-profile-recovery
- [data]
- (us/verify ::recovery-request data)
- (ptk/reify ::request-profile-recovery
- ptk/WatchEvent
- (watch [_ state stream]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)]
-
- (->> (rp/mutation :request-profile-recovery data)
- (rx/tap on-success)
- (rx/catch on-error))))))
-
-;; --- Recovery (Password)
-
-(s/def ::token string?)
-(s/def ::recover-profile
- (s/keys :req-un [::password ::token]))
-
-(defn recover-profile
- [{:keys [token password] :as data}]
- (us/verify ::recover-profile data)
- (ptk/reify ::recover-profile
- ptk/WatchEvent
- (watch [_ state stream]
- (let [{:keys [on-error on-success]
- :or {on-error identity
- on-success identity}} (meta data)]
- (->> (rp/mutation :recover-profile data)
- (rx/tap on-success)
- (rx/catch (fn [err]
- (on-error)
- (rx/empty))))))))
-
-
-;; --- Create Demo Profile
-
-(def create-demo-profile
- (ptk/reify ::create-demo-profile
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/mutation :create-demo-profile {})
- (rx/map login)))))
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index bb56ba551..eacc98c3a 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -11,7 +11,9 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
+ [app.main.data.events :as ev]
[app.main.data.users :as du]
+ [app.main.data.fonts :as df]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.time :as dt]
@@ -53,156 +55,195 @@
(s/def ::file
(s/keys :req-un [::id
::name
- ::created-at
- ::modified-at
- ::project-id]))
+ ::project-id]
+ :opt-un [::created-at
+ ::modified-at]))
(s/def ::set-of-uuid
(s/every ::us/uuid :kind set?))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Data Fetching
+;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defn fetch-team
+(declare fetch-projects)
+
+(defn initialize
[{:keys [id] :as params}]
- (letfn [(fetched [team state]
- (update state :teams assoc id team))]
- (ptk/reify ::fetch-team
- ptk/WatchEvent
- (watch [_ state stream]
- (let [profile (:profile state)]
- (->> (rp/query :team params)
- (rx/map #(partial fetched %))))))))
+ (us/assert ::us/uuid id)
+ (ptk/reify ::initialize
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [prev-team-id (:current-team-id state)]
+ (cond-> state
+ (not= prev-team-id id)
+ (-> (assoc :current-team-id id)
+ (dissoc :dashboard-files)
+ (dissoc :dashboard-projects)
+ (dissoc :dashboard-recent-files)
+ (dissoc :dashboard-team-members)
+ (dissoc :dashboard-team-stats)))))
+
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (rx/merge
+ (ptk/watch (df/load-team-fonts id) state stream)
+ (ptk/watch (fetch-projects) state stream)
+ (ptk/watch (du/fetch-teams) state stream)
+ (ptk/watch (du/fetch-users {:team-id id}) state stream)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Data Fetching (context aware: current team)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; --- EVENT: fetch-team-members
+
+(defn team-members-fetched
+ [members]
+ (ptk/reify ::team-members-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :dashboard-team-members (d/index-by :id members)))))
(defn fetch-team-members
- [{:keys [id] :as params}]
- (us/assert ::us/uuid id)
- (letfn [(fetched [members state]
- (->> members
- (d/index-by :id)
- (assoc-in state [:team-members id])))]
- (ptk/reify ::fetch-team-members
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :team-members {:team-id id})
- (rx/map #(partial fetched %)))))))
-
-(defn fetch-team-stats
- [{:keys [id] :as team}]
- (us/assert ::us/uuid id)
+ []
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state stream]
- (let [fetched #(assoc-in %2 [:team-stats id] %1)]
- (->> (rp/query :team-stats {:team-id id})
- (rx/map #(partial fetched %)))))))
+ (let [team-id (:current-team-id state)]
+ (->> (rp/query :team-members {:team-id team-id})
+ (rx/map team-members-fetched))))))
-;; --- Fetch Projects
+;; --- EVENT: fetch-team-stats
-(defn fetch-projects
- [{:keys [team-id] :as params}]
- (us/assert ::us/uuid team-id)
- (letfn [(fetched [projects state]
- (assoc-in state [:projects team-id] (d/index-by :id projects)))]
- (ptk/reify ::fetch-projects
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :projects {:team-id team-id})
- (rx/map #(partial fetched %)))))))
+(defn team-stats-fetched
+ [stats]
+ (ptk/reify ::team-stats-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :dashboard-team-stats stats))))
-(defn fetch-bundle
- [{:keys [id] :as params}]
- (us/assert ::us/uuid id)
- (ptk/reify ::fetch-bundle
+(defn fetch-team-stats
+ []
+ (ptk/reify ::fetch-team-stats
ptk/WatchEvent
(watch [_ state stream]
- (let [profile (:profile state)]
- (rx/merge (ptk/watch (fetch-team params) state stream)
- (ptk/watch (fetch-projects {:team-id id}) state stream)
- (ptk/watch (du/fetch-users {:team-id id}) state stream))))))
+ (let [team-id (:current-team-id state)]
+ (->> (rp/query :team-stats {:team-id team-id})
+ (rx/map team-stats-fetched))))))
-;; --- Search Files
+;; --- EVENT: fetch-projects
-(s/def :internal.event.search-files/team-id ::us/uuid)
-(s/def :internal.event.search-files/search-term (s/nilable ::us/string))
+(defn projects-fetched
+ [projects]
+ (ptk/reify ::projects-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [projects (d/index-by :id projects)]
+ (assoc state :dashboard-projects projects)))))
-(s/def :internal.event/search-files
- (s/keys :req-un [:internal.event.search-files/search-term
- :internal.event.search-files/team-id]))
+(defn fetch-projects
+ []
+ (ptk/reify ::fetch-projects
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/query :projects {:team-id team-id})
+ (rx/map projects-fetched))))))
-(defn search-files
+;; --- EVENT: search
+
+(defn search-result-fetched
+ [result]
+ (ptk/reify ::search-result-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :dashboard-search-result result))))
+
+(s/def ::search-term (s/nilable ::us/string))
+(s/def ::search
+ (s/keys :req-un [::search-term ]))
+
+(defn search
[params]
- (us/assert :internal.event/search-files params)
- (letfn [(fetched [result state]
- (update state :dashboard-local
- assoc :search-result result))]
- (ptk/reify ::search-files
+ (us/assert ::search params)
+ (ptk/reify ::search
+ ptk/UpdateEvent
+ (update [_ state]
+ (dissoc state :dashboard-search-result))
+
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/query :search-files params)
+ (rx/map search-result-fetched))))))
+
+;; --- EVENT: files
+
+(defn files-fetched
+ [project-id files]
+ (letfn [(remove-project-files [files]
+ (reduce-kv (fn [result id file]
+ (cond-> result
+ (= (:project-id file) project-id) (dissoc id)))
+ files
+ files))]
+ (ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
- (update state :dashboard-local
- assoc :search-result nil))
-
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :search-files params)
- (rx/map #(partial fetched %)))))))
-
-;; --- Fetch Files
+ (update state :dashboard-files
+ (fn [state]
+ (let [state (remove-project-files state)]
+ (reduce #(assoc %1 (:id %2) %2) state files))))))))
(defn fetch-files
[{:keys [project-id] :as params}]
(us/assert ::us/uuid project-id)
- (letfn [(fetched [files state]
- (update state :files assoc project-id (d/index-by :id files)))]
- (ptk/reify ::fetch-files
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :files params)
- (rx/map #(partial fetched %)))))))
-
-;; --- Fetch Shared Files
-
-(defn fetch-shared-files
- [{:keys [team-id] :as params}]
- (us/assert ::us/uuid team-id)
- (letfn [(fetched [files state]
- (update state :shared-files assoc team-id (d/index-by :id files)))]
- (ptk/reify ::fetch-shared-files
- ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :shared-files {:team-id team-id})
- (rx/map #(partial fetched %)))))))
-
-;; --- Fetch recent files
-
-(declare recent-files-fetched)
-
-(defn fetch-recent-files
- [{:keys [team-id] :as params}]
- (us/assert ::us/uuid team-id)
- (ptk/reify ::fetch-recent-files
+ (ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
- (let [params {:team-id team-id}]
- (->> (rp/query :recent-files params)
- (rx/map #(recent-files-fetched team-id %)))))))
+ (->> (rp/query :project-files {:project-id project-id})
+ (rx/map #(files-fetched project-id %))))))
+
+;; --- EVENT: shared-files
+
+(defn shared-files-fetched
+ [files]
+ (ptk/reify ::shared-files-fetched
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :dashboard-shared-files (d/index-by :id files)))))
+
+(defn fetch-shared-files
+ []
+ (ptk/reify ::fetch-shared-files
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/query :team-shared-files {:team-id team-id})
+ (rx/map shared-files-fetched))))))
+
+;; --- EVENT: recent-files
(defn recent-files-fetched
- [team-id files]
+ [files]
(ptk/reify ::recent-files-fetched
ptk/UpdateEvent
(update [_ state]
- (let [projects (keys (get-in state [:projects team-id]))]
- (reduce (fn [state project-id]
- (let [files (filter #(= project-id (:project-id %)) files)]
- (-> state
- (update-in [:files project-id] merge (d/index-by :id files))
- (assoc-in [:recent-files project-id] (into #{} (map :id) files)))))
- state
- projects)))))
+ (let [files (d/index-by :id files)]
+ (-> state
+ (assoc :dashboard-recent-files files)
+ (update :dashboard-files d/merge files))))))
+(defn fetch-recent-files
+ []
+ (ptk/reify ::fetch-recent-files
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/query :team-recent-files {:team-id team-id})
+ (rx/map recent-files-fetched))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
@@ -218,31 +259,34 @@
:selected-project nil))))
(defn toggle-file-select
- [{:keys [file] :as params}]
+ [{:keys [id project-id] :as file}]
+ (us/assert ::file file)
(ptk/reify ::toggle-file-select
ptk/UpdateEvent
(update [_ state]
- (let [file-id (:id file)
- selected-project (get-in state [:dashboard-local
- :selected-project])]
- (if (or (nil? selected-project)
- (= selected-project (:project-id file)))
+ (let [selected-project-id (get-in state [:dashboard-local :selected-project])]
+ (if (or (nil? selected-project-id)
+ (= selected-project-id project-id))
(update state :dashboard-local
(fn [local]
(-> local
- (update :selected-files
- #(if (contains? % file-id)
- (disj % file-id)
- (conj % file-id)))
- (assoc :selected-project
- (:project-id file)))))
+ (update :selected-files #(if (contains? % id)
+ (disj % id)
+ (conj % id)))
+ (assoc :selected-project project-id))))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; --- Create Project
+;; --- EVENT: create-team
+
+(defn team-created
+ [team]
+ (ptk/reify ::team-created
+ IDeref
+ (-deref [_] team)))
(defn create-team
[{:keys [name] :as params}]
@@ -252,11 +296,14 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)]
(->> (rp/mutation! :create-team {:name name})
(rx/tap on-success)
+ (rx/map team-created)
(rx/catch on-error))))))
+;; --- EVENT: update-team
+
(defn update-team
[{:keys [id name] :as params}]
(us/assert ::team params)
@@ -271,75 +318,75 @@
(rx/ignore)))))
(defn update-team-photo
- [{:keys [file team-id] :as params}]
+ [{:keys [file] :as params}]
(us/assert ::di/file file)
- (us/assert ::us/uuid team-id)
(ptk/reify ::update-team-photo
ptk/WatchEvent
(watch [_ state stream]
(let [on-success di/notify-finished-loading
-
- on-error #(do (di/notify-finished-loading)
- (di/process-error %))
-
- prepare #(hash-map :file % :team-id team-id)]
+ on-error #(do (di/notify-finished-loading)
+ (di/process-error %))
+ team-id (:current-team-id state)
+ prepare #(hash-map :file % :team-id team-id)]
(di/notify-start-loading)
-
(->> (rx/of file)
(rx/map di/validate-file)
(rx/map prepare)
(rx/mapcat #(rp/mutation :update-team-photo %))
(rx/do on-success)
- (rx/map #(fetch-team %))
+ (rx/map du/fetch-teams)
(rx/catch on-error))))))
(defn update-team-member-role
- [{:keys [team-id role member-id] :as params}]
- (us/assert ::us/uuid team-id)
+ [{:keys [role member-id] :as params}]
(us/assert ::us/uuid member-id)
(us/assert ::us/keyword role)
(ptk/reify ::update-team-member-role
ptk/WatchEvent
(watch [_ state stream]
- (->> (rp/mutation! :update-team-member-role params)
- (rx/mapcat #(rx/of (fetch-team-members {:id team-id})
- (fetch-team {:id team-id})))))))
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/mutation! :update-team-member-role params)
+ (rx/mapcat (fn [_]
+ (rx/of (fetch-team-members)
+ (du/fetch-teams)))))))))
(defn delete-team-member
- [{:keys [team-id member-id] :as params}]
- (us/assert ::us/uuid team-id)
+ [{:keys [member-id] :as params}]
(us/assert ::us/uuid member-id)
(ptk/reify ::delete-team-member
ptk/WatchEvent
(watch [_ state stream]
- (->> (rp/mutation! :delete-team-member params)
- (rx/mapcat #(rx/of (fetch-team-members {:id team-id})
- (fetch-team {:id team-id})))))))
+ (let [team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
+ (->> (rp/mutation! :delete-team-member params)
+ (rx/mapcat (fn [_]
+ (rx/of (fetch-team-members)
+ (du/fetch-teams)))))))))
(defn leave-team
- [{:keys [id reassign-to] :as params}]
- (us/assert ::team params)
+ [{:keys [reassign-to] :as params}]
(us/assert (s/nilable ::us/uuid) reassign-to)
(ptk/reify ::leave-team
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)
+ team-id (:current-team-id state)]
(rx/concat
(when (uuid? reassign-to)
- (->> (rp/mutation! :update-team-member-role {:team-id id
+ (->> (rp/mutation! :update-team-member-role {:team-id team-id
:role :owner
:member-id reassign-to})
(rx/ignore)))
- (->> (rp/mutation! :leave-team {:id id})
+ (->> (rp/mutation! :leave-team {:id team-id})
(rx/tap on-success)
(rx/catch on-error)))))))
(defn invite-team-member
- [{:keys [team-id email role] :as params}]
- (us/assert ::us/uuid team-id)
+ [{:keys [email role] :as params}]
(us/assert ::us/email email)
(us/assert ::us/keyword role)
(ptk/reify ::invite-team-member
@@ -347,11 +394,15 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)
+ team-id (:current-team-id state)
+ params (assoc params :team-id team-id)]
(->> (rp/mutation! :invite-team-member params)
(rx/tap on-success)
(rx/catch on-error))))))
+;; --- EVENT: delete-team
+
(defn delete-team
[{:keys [id] :as params}]
(us/assert ::team params)
@@ -360,50 +411,68 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)]
(->> (rp/mutation! :delete-team {:id id})
(rx/tap on-success)
(rx/catch on-error))))))
+;; --- EVENT: create-project
+
+(defn- project-created
+ [{:keys [id] :as project}]
+ (ptk/reify ::project-created
+ IDeref
+ (-deref [_] project)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (assoc-in [:dashboard-projects id] project)
+ (assoc-in [:dashboard-local :project-for-edit] id)))))
+
(defn create-project
- [{:keys [team-id] :as params}]
- (us/assert ::us/uuid team-id)
- (letfn [(created [project state]
- (-> state
- (assoc-in [:projects team-id (:id project)] project)
- (assoc-in [:dashboard-local :project-for-edit] (:id project))))]
- (ptk/reify ::create-project
- ptk/WatchEvent
- (watch [_ state stream]
- (let [name (name (gensym "New Project "))
- {:keys [on-success on-error]
- :or {on-success identity
- on-error identity}} (meta params)]
- (->> (rp/mutation! :create-project {:name name :team-id team-id})
- (rx/tap on-success)
- (rx/map #(partial created %))
- (rx/catch on-error)))))))
+ []
+ (ptk/reify ::create-project
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [name (name (gensym (str (tr "dashboard.new-project-prefix") " ")))
+ team-id (:current-team-id state)
+ params {:name name
+ :team-id team-id}
+ {:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)]
+ (->> (rp/mutation! :create-project params)
+ (rx/tap on-success)
+ (rx/map project-created)
+ (rx/catch on-error))))))
+
+;; --- EVENT: duplicate-project
+
+(defn project-duplicated
+ [{:keys [id] :as project}]
+ (ptk/reify ::project-duplicated
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc-in state [:dashboard-projects id] project))))
(defn duplicate-project
[{:keys [id name] :as params}]
(us/assert ::us/uuid id)
- (letfn [(duplicated [project state]
- (-> state
- (assoc-in [:projects (:team-id project) (:id project)] project)))]
- (ptk/reify ::duplicate-project
- ptk/WatchEvent
- (watch [_ state stream]
- (let [{:keys [on-success on-error]
- :or {on-success identity
- on-error identity}} (meta params)
+ (ptk/reify ::duplicate-project
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [on-success on-error]
+ :or {on-success identity
+ on-error rx/throw}} (meta params)
- new-name (str name " " (tr "dashboard.copy-suffix"))]
+ new-name (str name " " (tr "dashboard.copy-suffix"))]
- (->> (rp/mutation! :duplicate-project {:project-id id
- :name new-name})
- (rx/tap on-success)
- (rx/map #(partial duplicated %))
- (rx/catch on-error)))))))
+ (->> (rp/mutation! :duplicate-project {:project-id id
+ :name new-name})
+ (rx/tap on-success)
+ (rx/map project-duplicated)
+ (rx/catch on-error))))))
(defn move-project
[{:keys [id team-id] :as params}]
@@ -414,7 +483,7 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)]
(->> (rp/mutation! :move-project {:project-id id
:team-id team-id})
@@ -422,21 +491,21 @@
(rx/catch on-error))))))
(defn toggle-project-pin
- [{:keys [id is-pinned team-id] :as params}]
- (us/assert ::project params)
+ [{:keys [id is-pinned team-id] :as project}]
+ (us/assert ::project project)
(ptk/reify ::toggle-project-pin
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:projects team-id id :is-pinned] (not is-pinned)))
+ (assoc-in state [:dashboard-projects id :is-pinned] (not is-pinned)))
ptk/WatchEvent
(watch [_ state stream]
- (let [project (get-in state [:projects team-id id])
+ (let [project (get-in state [:dashboard-projects id])
params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/mutation :update-project-pin params)
(rx/ignore))))))
-;; --- Rename Project
+;; --- EVENT: rename-project
(defn rename-project
[{:keys [id name team-id] :as params}]
@@ -445,7 +514,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (assoc-in [:projects team-id id :name] name)
+ (update-in [:dashboard-projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent
@@ -454,7 +523,7 @@
(->> (rp/mutation :rename-project params)
(rx/ignore))))))
-;; --- Delete Project (by id)
+;; --- EVENT: delete-project
(defn delete-project
[{:keys [id team-id] :as params}]
@@ -462,16 +531,21 @@
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
- (update-in state [:projects team-id] dissoc id))
+ (update state :dashboard-projects dissoc id))
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation :delete-project {:id id})
(rx/ignore)))))
-;; --- Delete File (by id)
+;; --- EVENT: delete-file
-(declare delete-file-result)
+(defn file-deleted
+ [team-id project-id]
+ (ptk/reify ::file-deleted
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:dashboard-projects project-id :count] dec))))
(defn delete-file
[{:keys [id project-id] :as params}]
@@ -480,33 +554,26 @@
ptk/UpdateEvent
(update [_ state]
(-> state
- (update-in [:files project-id] dissoc id)
- (update-in [:recent-files project-id] (fnil disj #{}) id)))
+ (d/update-when :dashboard-files dissoc id)
+ (d/update-when :dashboard-recent-files dissoc id)))
ptk/WatchEvent
(watch [_ state s]
(let [team-id (uuid/uuid (get-in state [:route :path-params :team-id]))]
(->> (rp/mutation :delete-file {:id id})
- (rx/map #(delete-file-result team-id project-id)))))))
-
-(defn delete-file-result
- [team-id project-id]
-
- (ptk/reify ::delete-file
- ptk/UpdateEvent
- (update [_ state]
- (-> state
- (update-in [:projects team-id project-id :count] dec)))))
+ (rx/map #(file-deleted team-id project-id)))))))
;; --- Rename File
(defn rename-file
- [{:keys [id name project-id] :as params}]
+ [{:keys [id name] :as params}]
(us/assert ::file params)
(ptk/reify ::rename-file
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:files project-id id :name] name))
+ (-> state
+ (d/update-in-when [:dashboard-files id :name] (constantly name))
+ (d/update-in-when [:dashboard-recent-files id :name] (constantly name))))
ptk/WatchEvent
(watch [_ state stream]
@@ -517,12 +584,14 @@
;; --- Set File shared
(defn set-file-shared
- [{:keys [id project-id is-shared] :as params}]
+ [{:keys [id is-shared] :as params}]
(us/assert ::file params)
(ptk/reify ::set-file-shared
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:files project-id id :is-shared] is-shared))
+ (-> state
+ (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
+ (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))))
ptk/WatchEvent
(watch [_ state stream]
@@ -530,10 +599,23 @@
(->> (rp/mutation :set-file-shared params)
(rx/ignore))))))
-;; --- Create File
+;; --- EVENT: create-file
(declare file-created)
+(defn file-created
+ [{:keys [id] :as file}]
+ (us/verify ::file file)
+ (ptk/reify ::file-created
+ IDeref
+ (-deref [_] file)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (assoc-in [:dashboard-files id] file)
+ (assoc-in [:dashboard-recent-files id] file)))))
+
(defn create-file
[{:keys [project-id] :as params}]
(us/assert ::us/uuid project-id)
@@ -542,9 +624,9 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)
+ on-error rx/throw}} (meta params)
- name (name (gensym "New File "))
+ name (name (gensym (str (tr "dashboard.new-file-prefix") " ")))
params (assoc params :name name)]
(->> (rp/mutation! :create-file params)
@@ -552,17 +634,7 @@
(rx/map file-created)
(rx/catch on-error))))))
-(defn file-created
- [{:keys [project-id id] :as file}]
- (us/verify ::file file)
- (ptk/reify ::file-created
- ptk/UpdateEvent
- (update [_ state]
- (-> state
- (assoc-in [:files project-id id] file)
- (update-in [:recent-files project-id] (fnil conj #{}) id)))))
-
-;; --- Duplicate File
+;; --- EVENT: duplicate-file
(defn duplicate-file
[{:keys [id name] :as params}]
@@ -573,7 +645,7 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)
+ on-error rx/throw}} (meta params)
new-name (str name " " (tr "dashboard.copy-suffix"))]
@@ -583,7 +655,7 @@
(rx/map file-created)
(rx/catch on-error))))))
-;; --- Move File
+;; --- EVENT: move-files
(defn move-files
[{:keys [ids project-id] :as params}]
@@ -594,10 +666,76 @@
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
- on-error identity}} (meta params)]
+ on-error rx/throw}} (meta params)]
- (->> (rp/mutation! :move-files {:ids ids
- :project-id project-id})
+ (->> (rp/mutation! :move-files {:ids ids :project-id project-id})
(rx/tap on-success)
(rx/catch on-error))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Navigation
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn go-to-workspace
+ [{:keys [id project-id] :as file}]
+ (us/assert ::file file)
+ (ptk/reify ::go-to-workspace
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [pparams {:project-id project-id :file-id id}]
+ (rx/of (rt/nav :workspace pparams))))))
+
+
+(defn go-to-files
+ [project-id]
+ (ptk/reify ::go-to-files
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-files {:team-id team-id
+ :project-id project-id}))))))
+
+(defn go-to-search
+ ([] (go-to-search nil))
+ ([term]
+ (ptk/reify ::go-to-search
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (if (empty? term)
+ (rx/of (rt/nav :dashboard-search
+ {:team-id team-id}))
+ (rx/of (rt/nav :dashboard-search
+ {:team-id team-id}
+ {:search-term term}))))))))
+
+(defn go-to-projects
+ ([]
+ (ptk/reify ::go-to-projects
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
+ ([team-id]
+ (ptk/reify ::go-to-projects
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (du/set-current-team! team-id)
+ (rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
+
+(defn go-to-team-members
+ []
+ (ptk/reify ::go-to-team-members
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-team-members {:team-id team-id}))))))
+
+(defn go-to-team-settings
+ []
+ (ptk/reify ::go-to-team-settings
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/of (rt/nav :dashboard-team-settings {:team-id team-id}))))))
diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs
new file mode 100644
index 000000000..ca4b99a61
--- /dev/null
+++ b/frontend/src/app/main/data/events.cljs
@@ -0,0 +1,247 @@
+;; 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.events
+ (:require
+ ["ua-parser-js" :as UAParser]
+ [app.common.data :as d]
+ [app.main.repo :as rp]
+ [app.config :as cf]
+ [app.util.globals :as g]
+ [app.util.http :as http]
+ [app.util.object :as obj]
+ [app.util.storage :refer [storage]]
+ [app.util.time :as dt]
+ [app.util.i18n :as i18n]
+ [beicon.core :as rx]
+ [lambdaisland.uri :as u]
+ [potok.core :as ptk]))
+
+;; Defines the maximum buffer size, after events start discarding.
+(def max-buffer-size 1024)
+
+;; Defines the maximum number of events that can go in a single batch.
+(def max-chunk-size 100)
+
+;; Defines the time window within events belong to the same session.
+(def session-timeout
+ (dt/duration {:minutes 30}))
+
+;; --- CONTEXT
+
+(defn- collect-context
+ []
+ (let [uagent (UAParser.)]
+ (d/merge
+ {:app-version (:full @cf/version)
+ :locale @i18n/locale}
+ (let [browser (.getBrowser uagent)]
+ {:browser (obj/get browser "name")
+ :browser-version (obj/get browser "version")})
+ (let [engine (.getEngine uagent)]
+ {:engine (obj/get engine "name")
+ :engine-version (obj/get engine "version")})
+ (let [os (.getOS uagent)
+ name (obj/get os "name")
+ version (obj/get os "version")]
+ {:os (str name " " version)
+ :os-version version})
+ (let [device (.getDevice uagent)]
+ (if-let [type (obj/get device "type")]
+ {:device-type type
+ :device-vendor (obj/get device "vendor")
+ :device-model (obj/get device "model")}
+ {:device-type "unknown"}))
+ (let [screen (obj/get g/window "screen")
+ orientation (obj/get screen "orientation")]
+ {:screen-width (obj/get screen "width")
+ :screen-height (obj/get screen "height")
+ :screen-color-depth (obj/get screen "colorDepth")
+ :screen-orientation (obj/get orientation "type")})
+ (let [cpu (.getCPU uagent)]
+ {:device-arch (obj/get cpu "architecture")}))))
+
+(def context
+ (atom (d/without-nils (collect-context))))
+
+(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
+
+;; --- EVENT TRANSLATION
+
+(defmulti ^:private process-event ptk/type)
+(defmethod process-event :default [_] nil)
+
+(defmethod process-event ::event
+ [event]
+ (let [data (deref event)]
+ (when (::name data)
+ (d/without-nils
+ {:type (::type data "action")
+ :name (::name data)
+ :context (::context data)
+ :props (dissoc data ::name ::type ::context)}))))
+
+(defmethod process-event :app.util.router/navigated
+ [event]
+ (let [match (deref event)
+ route (get-in match [:data :name])
+ props {:route (name route)
+ :team-id (get-in match [:path-params :team-id])
+ :file-id (get-in match [:path-params :file-id])
+ :project-id (get-in match [:path-params :project-id])}]
+ {:name "navigate"
+ :type "action"
+ :timestamp (dt/now)
+ :props (d/without-nils props)}))
+
+(defmethod process-event :app.main.data.users/logged-in
+ [event]
+ (let [data (deref event)
+ mdata (meta data)
+ props {:signin-source (::source mdata)
+ :email (:email data)
+ :auth-backend (:auth-backend data)
+ :fullname (:fullname data)
+ :is-muted (:is-muted data)
+ :default-team-id (str (:default-team-id data))
+ :default-project-id (str (:default-project-id data))}]
+ {:name "signin"
+ :type "identify"
+ :profile-id (:id data)
+ :props (d/without-nils props)}))
+
+(defmethod process-event :app.main.data.dashboard/project-created
+ [event]
+ (let [data (deref event)]
+ {:type "action"
+ :name "create-project"
+ :props {:id (:id data)
+ :team-id (:team-id data)}}))
+
+(defmethod process-event :app.main.data.dashboard/file-created
+ [event]
+ (let [data (deref event)]
+ {:type "action"
+ :name "create-file"
+ :props {:id (:id data)
+ :project-id (:project-id data)}}))
+
+(defmethod process-event :app.main.data.workspace/create-page
+ [event]
+ (let [data (deref event)]
+ {:type "action"
+ :name "create-page"
+ :props {:id (:id data)
+ :file-id (:file-id data)
+ :project-id (:project-id data)}}))
+
+(defn- event->generic-action
+ [event name]
+ {:type "action"
+ :name name
+ :props {}})
+
+(defmethod process-event :app.main.data.users/logout
+ [event]
+ (event->generic-action event "signout"))
+
+
+;; --- MAIN LOOP
+
+(defn- append-to-buffer
+ [buffer item]
+ (if (>= (count buffer) max-buffer-size)
+ buffer
+ (conj buffer item)))
+
+(defn- remove-from-buffer
+ [buffer items]
+ (into #queue [] (drop items) buffer))
+
+(defn- persist-events
+ [events]
+ (if (seq events)
+ (let [uri (u/join cf/public-uri "events")
+ params {:events events}]
+ (->> (http/send! {:uri uri
+ :method :post
+ :body (http/transit-data params)})
+ (rx/mapcat rp/handle-response)))
+ (rx/of nil)))
+
+(defmethod ptk/resolve ::persistence
+ [_ {:keys [buffer] :as params}]
+ (ptk/reify ::persistence
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [profile-id (:profile-id state)
+ events (into [] (take max-buffer-size) @buffer)]
+ (when (seq events)
+ (->> events
+ (filterv #(= profile-id (:profile-id %)))
+ (persist-events)
+ (rx/subs (fn [_]
+ (swap! buffer remove-from-buffer (count events))))))))))
+
+(defn initialize
+ []
+ (let [buffer (atom #queue [])]
+ (ptk/reify ::initialize
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (->> (rx/merge
+ (->> (rx/from-atom buffer)
+ (rx/filter #(pos? (count %)))
+ (rx/debounce 2000))
+ (->> stream
+ (rx/filter (ptk/type? :app.main.data.users/logout))
+ (rx/observe-on :async)))
+ (rx/map #(ptk/event ::persistence {:buffer buffer}))))
+
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [events (methods process-event)
+ session (atom nil)
+
+ profile (->> (rx/from-atom storage {:emit-current-value? true})
+ (rx/map :profile)
+ (rx/map :id)
+ (rx/dedupe))
+
+ source (->> stream
+ (rx/with-latest-from profile)
+ (rx/map (fn [result]
+ (let [event (aget result 0)
+ profile-id (aget result 1)
+ type (ptk/type event)
+ impl-fn (get events type)]
+ (when (fn? impl-fn)
+ (some-> (impl-fn event)
+ (update :profile-id #(or % profile-id)))))))
+ (rx/filter :profile-id)
+ (rx/map (fn [event]
+ (let [session* (or @session (dt/now))
+ context (-> @context
+ (d/merge (:context event))
+ (assoc :session session*))]
+ (reset! session session*)
+ (-> event
+ (assoc :timestamp (dt/now))
+ (assoc :context context)))))
+ (rx/share))]
+ (->> source
+ (rx/switch-map #(rx/timer (inst-ms session-timeout)))
+ (rx/subs #(reset! session nil)))
+
+ (->> source
+ (rx/subs (fn [event]
+ (swap! buffer append-to-buffer event)))))))))
+
+(defmethod ptk/resolve ::initialize
+ [_ params]
+ (if cf/analytics
+ (initialize)
+ (ptk/data-event ::initialize params)))
diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs
new file mode 100644
index 000000000..899285654
--- /dev/null
+++ b/frontend/src/app/main/data/fonts.cljs
@@ -0,0 +1,244 @@
+;; 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.fonts
+ (:require
+ ["opentype.js" :as ot]
+ [app.common.data :as d]
+ [app.common.spec :as us]
+ [app.common.media :as cm]
+ [app.common.uuid :as uuid]
+ [app.main.fonts :as fonts]
+ [app.main.repo :as rp]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.logging :as log]
+ [beicon.core :as rx]
+ [cljs.spec.alpha :as s]
+ [app.util.webapi :as wa]
+ [cuerdas.core :as str]
+ [potok.core :as ptk]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; General purpose events & IMPL
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn team-fonts-loaded
+ [fonts]
+ (letfn [;; Prepare font to the internal font database format.
+ (prepare-font [[id [item :as items]]]
+ {:id id
+ :name (:font-family item)
+ :family (:font-family item)
+ :variants (->> items
+ (map prepare-font-variant)
+ (sort-by variant-sort-fn)
+ (vec))})
+
+ (variant-sort-fn [item]
+ [(:weight item)
+ (if (= "normal" (:style item)) 1 2)])
+
+ (prepare-font-variant [item]
+ {:id (str (:font-style item) "-" (:font-weight item))
+ :name (str (cm/font-weight->name (:font-weight item))
+ (when (not= "normal" (:font-style item))
+ (str " " (str/capital (:font-style item)))))
+ :style (:font-style item)
+ :weight (str (:font-weight item))
+ ::fonts/woff1-file-id (:woff1-file-id item)
+ ::fonts/woff2-file-id (:woff2-file-id item)
+ ::fonts/ttf-file-id (:ttf-file-id item)
+ ::fonts/otf-file-id (:otf-file-id item)})
+
+ (adapt-font-id [variant]
+ (update variant :font-id #(str "custom-" %)))]
+
+ (ptk/reify ::team-fonts-loaded
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :dashboard-fonts (d/index-by :id fonts)))
+
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [fonts (->> fonts
+ (map adapt-font-id)
+ (group-by :font-id)
+ (mapv prepare-font))]
+ (fonts/register! :custom fonts))))))
+
+(defn load-team-fonts
+ [team-id]
+ (ptk/reify ::load-team-fonts
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (->> (rp/query :team-font-variants {:team-id team-id})
+ (rx/map team-fonts-loaded)))))
+
+(defn process-upload
+ "Given a seq of blobs and the team id, creates a ready-to-use fonts
+ map with temporal ID's associated to each font entry."
+ [blobs team-id]
+ (letfn [(prepare [{:keys [font type name data] :as params}]
+ (let [family (or (.getEnglishName ^js font "preferredFamily")
+ (.getEnglishName ^js font "fontFamily"))
+ variant (or (.getEnglishName ^js font "preferredSubfamily")
+ (.getEnglishName ^js font "fontSubfamily"))]
+ {:content {:data (js/Uint8Array. data)
+ :name name
+ :type type}
+ :font-family family
+ :font-weight (cm/parse-font-weight variant)
+ :font-style (cm/parse-font-style variant)}))
+
+ (join [res {:keys [content] :as font}]
+ (let [key-fn (juxt :font-family :font-weight :font-style)
+ existing (d/seek #(= (key-fn font) (key-fn %)) (vals res))]
+ (if existing
+ (update res
+ (:id existing)
+ (fn [existing]
+ (-> existing
+ (update :data assoc (:type content) (:data content))
+ (update :names conj (:name content)))))
+ (let [tmp-id (uuid/next)]
+ (assoc res tmp-id
+ (-> font
+ (assoc :id tmp-id)
+ (assoc :team-id team-id)
+ (assoc :names #{(:name content)})
+ (assoc :data {(:type content)
+ (:data content)})
+ (dissoc :content)))))))
+
+ (parse-mtype [mtype]
+ (case mtype
+ "application/vnd.oasis.opendocument.formula-template" "font/otf"
+ mtype))
+
+ (parse-font [{:keys [data] :as params}]
+ (try
+ (assoc params :font (ot/parse data))
+ (catch :default e
+ (log/warn :msg (str/fmt "skiping file %s, unsupported format" (:name params)))
+ nil)))
+
+ (read-blob [blob]
+ (->> (wa/read-file-as-array-buffer blob)
+ (rx/map (fn [data]
+ {:data data
+ :name (.-name blob)
+ :type (parse-mtype (.-type blob))}))))]
+
+ (->> (rx/from blobs)
+ (rx/mapcat read-blob)
+ (rx/map parse-font)
+ (rx/filter some?)
+ (rx/map prepare)
+ (rx/reduce join {}))))
+
+(defn- calculate-family-to-id-mapping
+ [existing]
+ (reduce #(assoc %1 (:font-family %2) (:font-id %2)) {} (vals existing)))
+
+(defn merge-and-group-fonts
+ "Function responsible to merge (and apropriatelly group) incoming
+ fonts (processed by `process-upload`) into existing fonts
+ in local state, preserving correct font-id references."
+ [current-fonts installed-fonts incoming-fonts]
+ (loop [famdb (-> (merge current-fonts installed-fonts)
+ (calculate-family-to-id-mapping))
+ items (vals incoming-fonts)
+ result current-fonts]
+ (if-let [{:keys [id font-family] :as item} (first items)]
+ (let [font-id (or (get famdb font-family)
+ (uuid/next))
+ font (assoc item :font-id font-id)]
+ (recur (assoc famdb font-family font-id)
+ (rest items)
+ (assoc result id font)))
+ result)))
+
+(defn rename-and-regroup
+ "Function responsible to rename a font in a local state and properly
+ regroup it to the apropriate `font-id` having in account current
+ fonts and installed fonts."
+ [current-fonts id name installed-fonts]
+ (let [famdb (-> (merge current-fonts installed-fonts)
+ (calculate-family-to-id-mapping))
+ font-id (or (get famdb name)
+ (uuid/next))]
+ (update current-fonts id (fn [font]
+ (-> font
+ (assoc :name name)
+ (assoc :font-id font-id))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dashboard related events
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn add-font
+ [font]
+ (ptk/reify ::add-font
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :dashboard-fonts assoc (:id font) font))))
+
+(defn update-font
+ [{:keys [id name] :as params}]
+ (us/assert ::us/uuid id)
+ (us/assert ::us/not-empty-string name)
+ (ptk/reify ::update-font
+ ptk/UpdateEvent
+ (update [_ state]
+ ;; Update all variants that has the same font-id with the new
+ ;; name in the local state.
+ (update state :dashboard-fonts
+ (fn [fonts]
+ (d/mapm (fn [_ font]
+ (cond-> font
+ (= id (:font-id font))
+ (assoc :font-family name)))
+ fonts))))
+
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/mutation! :update-font {:id id :name name :team-id team-id})
+ (rx/ignore))))))
+
+(defn delete-font
+ "Delete all variants related to the provided `font-id`."
+ [font-id]
+ (us/assert ::us/uuid font-id)
+ (ptk/reify ::delete-font
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :dashboard-fonts
+ (fn [variants]
+ (d/removem (fn [[id variant]]
+ (= (:font-id variant) font-id)) variants))))
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/mutation! :delete-font {:id font-id :team-id team-id})
+ (rx/ignore))))))
+
+(defn delete-font-variant
+ [id]
+ (us/assert ::us/uuid id)
+ (ptk/reify ::delete-font-variants
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :dashboard-fonts
+ (fn [variants]
+ (d/removem (fn [[_ variant]]
+ (= (:id variant) id))
+ variants))))
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [team-id (:current-team-id state)]
+ (->> (rp/mutation! :delete-font-variant {:id id :team-id team-id})
+ (rx/ignore))))))
diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs
index 27794fd77..695807cc2 100644
--- a/frontend/src/app/main/data/media.cljs
+++ b/frontend/src/app/main/data/media.cljs
@@ -51,7 +51,7 @@
(ex/raise :type :validation
:code :media-too-large
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
- (when-not (contains? cm/valid-media-types (.-type file))
+ (when-not (contains? cm/valid-image-types (.-type file))
(ex/raise :type :validation
:code :media-type-not-allowed
:hint (str/fmt "media type %s is not supported" (.-type file))))
diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs
index 850380517..e3eb7810d 100644
--- a/frontend/src/app/main/data/shortcuts.cljs
+++ b/frontend/src/app/main/data/shortcuts.cljs
@@ -5,14 +5,22 @@
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.shortcuts
+ (:refer-clojure :exclude [meta reset!])
(:require
["mousetrap" :as mousetrap]
+ [app.common.data :as d]
+ [app.common.spec :as us]
[app.config :as cfg]
- [app.util.logging :as log])
- (:refer-clojure :exclude [meta]))
+ [app.util.logging :as log]
+ [cljs.spec.alpha :as s]
+ [potok.core :as ptk]))
(log/set-level! :warn)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helpers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(def mac-command "\u2318")
(def mac-option "\u2325")
(def mac-delete "\u232B")
@@ -44,30 +52,8 @@
[shortcut]
(c-mod (a-mod shortcut)))
-(defn bind-shortcuts
- ([shortcuts-config]
- (bind-shortcuts
- shortcuts-config
- mousetrap/bind
- (fn [key cb]
- (fn [event]
- (log/debug :msg (str "Shortcut" key))
- (.preventDefault event)
- (cb event)))))
-
- ([shortcuts-config bind-fn cb-fn]
- (doseq [[key {:keys [command disabled fn type]}] shortcuts-config]
- (when-not disabled
- (if (vector? command)
- (doseq [cmd (seq command)]
- (bind-fn cmd (cb-fn key fn) type))
- (bind-fn command (cb-fn key fn) type))))))
-
-(defn remove-shortcuts
- []
- (mousetrap/reset))
-
-(defn meta [key]
+(defn meta
+ [key]
;; If the key is "+" we need to surround with quotes
;; otherwise will not be very readable
(let [key (if (and (not (cfg/check-platform? :macos))
@@ -80,37 +66,120 @@
"Ctrl+")
key)))
-(defn shift [key]
+(defn shift
+ [key]
(str
(if (cfg/check-platform? :macos)
mac-shift
"Shift+")
key))
-(defn alt [key]
+(defn alt
+ [key]
(str
(if (cfg/check-platform? :macos)
mac-option
"Alt+")
key))
-(defn meta-shift [key]
+(defn meta-shift
+ [key]
(-> key meta shift))
-(defn meta-alt [key]
+(defn meta-alt
+ [key]
(-> key meta alt))
-(defn supr []
+(defn supr
+ []
(if (cfg/check-platform? :macos)
mac-delete
"Supr"))
-(defn esc []
+(defn esc
+ []
(if (cfg/check-platform? :macos)
mac-esc
"Escape"))
-(defn enter []
+(defn enter
+ []
(if (cfg/check-platform? :macos)
mac-enter
"Enter"))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Events
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; --- EVENT: push
+
+(s/def ::tooltip ::us/string)
+(s/def ::fn fn?)
+
+(s/def ::command
+ (s/or :str ::us/string
+ :vec vector?))
+
+(s/def ::shortcut
+ (s/keys :req-un [::command]
+ :opt-un [::fn
+ ::tooltip]))
+
+(s/def ::shortcuts
+ (s/map-of ::us/keyword
+ ::shortcut))
+
+(defn- wrap-cb
+ [key cb]
+ (fn [event]
+ (log/debug :msg (str "Shortcut" key))
+ (.preventDefault event)
+ (cb event)))
+
+(defn- bind!
+ [shortcuts]
+ (->> shortcuts
+ (remove #(:disabled (second %)))
+ (run! (fn [[key {:keys [command fn type]}]]
+ (if (vector? command)
+ (run! #(mousetrap/bind % (wrap-cb key fn) type) command)
+ (mousetrap/bind command (wrap-cb key fn) type))))))
+
+(defn- reset!
+ ([]
+ (mousetrap/reset))
+ ([shortcuts]
+ (mousetrap/reset)
+ (bind! shortcuts)))
+
+(defn push-shortcuts
+ [key shortcuts]
+ (us/assert ::us/keyword key)
+ (us/assert ::shortcuts shortcuts)
+ (ptk/reify ::push-shortcuts
+ ptk/UpdateEvent
+ (update [_ state]
+ (-> state
+ (update :shortcuts (fnil conj '()) [key shortcuts])))
+
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [[key shortcuts] (peek (:shortcuts state))]
+ (reset! shortcuts)))))
+
+(defn pop-shortcuts
+ [key]
+ (ptk/reify ::pop-shortcuts
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :shortcuts (fn [shortcuts]
+ (let [current-key (first (peek shortcuts))]
+ (if (= key current-key)
+ (pop shortcuts)
+ shortcuts)))))
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [[key* shortcuts] (peek (:shortcuts state))]
+ (when (not= key key*)
+ (reset! shortcuts))))))
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index 283a9bad9..906df6b8d 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -10,7 +10,9 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.common.uuid :as uuid]
+ [app.main.data.events :as ev]
[app.main.data.media :as di]
+ [app.main.data.modal :as modal]
[app.main.data.messages :as dm]
[app.main.repo :as rp]
[app.main.store :as st]
@@ -24,7 +26,7 @@
[cuerdas.core :as str]
[potok.core :as ptk]))
-;; --- Common Specs
+;; --- COMMON SPECS
(s/def ::id ::us/uuid)
(s/def ::fullname ::us/string)
@@ -45,39 +47,61 @@
::lang
::theme]))
+;; --- HELPERS
+
+(defn get-current-team-id
+ [profile]
+ (let [team-id (::current-team-id @storage)]
+ (or team-id (:default-team-id profile))))
+
+(defn set-current-team!
+ [team-id]
+ (swap! storage assoc ::current-team-id team-id))
+
+;; --- EVENT: fetch-teams
+
+(defn teams-fetched
+ [teams]
+ (let [teams (d/index-by :id teams)]
+ (ptk/reify ::teams-fetched
+ IDeref
+ (-deref [_] teams)
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :teams teams)))))
+
(defn fetch-teams
[]
- (letfn [(on-fetched [state data]
- (let [teams (d/index-by :id data)]
- (assoc state :teams teams)))]
- (ptk/reify ::fetch-teams
- ptk/WatchEvent
- (watch [_ state s]
- (->> (rp/query! :teams)
- (rx/map (fn [data] #(on-fetched % data))))))))
+ (ptk/reify ::fetch-teams
+ ptk/WatchEvent
+ (watch [_ state s]
+ (->> (rp/query! :teams)
+ (rx/map teams-fetched)))))
+
+;; --- EVENT: fetch-profile
(defn profile-fetched
- [{:keys [fullname id] :as data}]
- (us/verify ::profile data)
+ [{:keys [id] :as profile}]
+ (us/verify ::profile profile)
(ptk/reify ::profile-fetched
IDeref
- (-deref [_] data)
+ (-deref [_] profile)
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :profile-id id)
- (assoc :profile data)))
+ (assoc :profile profile)))
ptk/EffectEvent
(effect [_ state stream]
(let [profile (:profile state)]
- (swap! storage assoc :profile profile)
- (i18n/set-locale! (:lang profile))
- (some-> (:theme profile)
- (theme/set-current-theme!))))))
-
-;; --- Fetch Profile
+ (when (not= uuid/zero (:id profile))
+ (swap! storage assoc :profile profile)
+ (i18n/set-locale! (:lang profile))
+ (some-> (:theme profile)
+ (theme/set-current-theme!)))))))
(defn fetch-profile
[]
@@ -87,12 +111,14 @@
(->> (rp/query! :profile)
(rx/map profile-fetched)))))
-(defn fetch-profile-and-teams
+;; --- EVENT: INITIALIZE PROFILE
+
+(defn initialize-profile
"Event used mainly on application bootstrap; it fetches the profile
and if and only if the fetched profile corresponds to an
authenticated user; proceed to fetch teams."
[]
- (ptk/reify ::fetch-profile-and-teams
+ (ptk/reify ::initialize-profile
ptk/WatchEvent
(watch [_ state stream]
(rx/merge
@@ -106,6 +132,115 @@
(rx/empty)
(rx/of (fetch-teams))))))))))
+;; --- EVENT: login
+
+(defn- logged-in
+ [profile]
+ (ptk/reify ::logged-in
+ IDeref
+ (-deref [_] profile)
+
+ ptk/WatchEvent
+ (watch [this state stream]
+ (let [team-id (get-current-team-id profile)
+ profile (with-meta profile
+ {::ev/source "login"})]
+ (->> (rx/concat
+ (rx/of (profile-fetched profile)
+ (fetch-teams))
+
+ (->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id}))
+ (rx/delay 1000))
+
+ (when-not (get-in profile [:props :onboarding-viewed])
+ (->> (rx/of (modal/show {:type :onboarding}))
+ (rx/delay 1000))))
+
+ (rx/observe-on :async))))))
+
+(s/def ::login-params
+ (s/keys :req-un [::email ::password]))
+
+(defn login
+ [{:keys [email password] :as data}]
+ (us/verify ::login-params data)
+ (ptk/reify ::login
+ ptk/WatchEvent
+ (watch [this state s]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)
+ params {:email email
+ :password password
+ :scope "webapp"}]
+ (->> (rx/timer 100)
+ (rx/mapcat #(rp/mutation :login params))
+ (rx/tap on-success)
+ (rx/catch on-error)
+ (rx/map (fn [profile]
+ (with-meta profile
+ {::ev/source "login"})))
+ (rx/map logged-in))))))
+
+(defn login-from-token
+ [{:keys [profile] :as tdata}]
+ (ptk/reify ::login-from-token
+ ptk/WatchEvent
+ (watch [this state s]
+ (rx/of (logged-in
+ (with-meta profile
+ {::ev/source "login-with-token"}))))))
+
+;; --- EVENT: logout
+
+(defn logged-out
+ []
+ (ptk/reify ::logged-out
+ ptk/UpdateEvent
+ (update [_ state]
+ (select-keys state [:route :router :session-id :history]))
+
+ ptk/WatchEvent
+ (watch [_ state s]
+ (rx/of (rt/nav :auth-login)))
+
+ ptk/EffectEvent
+ (effect [_ state s]
+ (reset! storage {})
+ (i18n/reset-locale))))
+
+(defn logout
+ []
+ (ptk/reify ::logout
+ ptk/WatchEvent
+ (watch [_ state s]
+ (->> (rp/mutation :logout)
+ (rx/delay-at-least 300)
+ (rx/catch (constantly (rx/of 1)))
+ (rx/map logged-out)))))
+
+;; --- EVENT: register
+
+(s/def ::invitation-token ::us/not-empty-string)
+
+(s/def ::register
+ (s/keys :req-un [::fullname ::password ::email]
+ :opt-un [::invitation-token]))
+
+(defn register
+ "Create a register event instance."
+ [data]
+ (s/assert ::register data)
+ (ptk/reify ::register
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [on-error on-success]
+ :or {on-error identity
+ on-success identity}} (meta data)]
+ (->> (rp/mutation :register-profile data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
;; --- Update Profile
(defn update-profile
@@ -231,3 +366,72 @@
(watch [_ state stream]
(->> (rp/query :team-users {:team-id team-id})
(rx/map #(partial fetched %)))))))
+
+;; --- EVENT: request-account-deletion
+
+(defn request-account-deletion
+ [params]
+ (ptk/reify ::request-account-deletion
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta params)]
+ (->> (rp/mutation :delete-profile {})
+ (rx/tap on-success)
+ (rx/delay-at-least 300)
+ (rx/catch (constantly (rx/of 1)))
+ (rx/map logged-out)
+ (rx/catch on-error))))))
+
+;; --- EVENT: request-profile-recovery
+
+(s/def ::request-profile-recovery
+ (s/keys :req-un [::email]))
+
+(defn request-profile-recovery
+ [data]
+ (us/verify ::request-profile-recovery data)
+ (ptk/reify ::request-profile-recovery
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+
+ (->> (rp/mutation :request-profile-recovery data)
+ (rx/tap on-success)
+ (rx/catch on-error))))))
+
+;; --- EVENT: recover-profile (Password)
+
+(s/def ::token string?)
+(s/def ::recover-profile
+ (s/keys :req-un [::password ::token]))
+
+(defn recover-profile
+ [{:keys [token password] :as data}]
+ (us/verify ::recover-profile data)
+ (ptk/reify ::recover-profile
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [on-error on-success]
+ :or {on-error rx/throw
+ on-success identity}} (meta data)]
+ (->> (rp/mutation :recover-profile data)
+ (rx/tap on-success)
+ (rx/catch (fn [err]
+ (on-error)
+ (rx/empty))))))))
+
+;; --- EVENT: crete-demo-profile
+
+(defn create-demo-profile
+ []
+ (ptk/reify ::create-demo-profile
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (->> (rp/mutation :create-demo-profile {})
+ (rx/map login)))))
+
+
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index 0546fe18b..5b70d9ce9 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -34,6 +34,7 @@
[app.main.data.workspace.undo :as dwu]
[app.main.repo :as rp]
[app.main.streams :as ms]
+ [app.main.worker :as uw]
[app.util.http :as http]
[app.util.i18n :as i18n]
[app.util.logging :as log]
@@ -72,6 +73,7 @@
:rules
:display-grid
:snap-grid
+ :scale-text
:dynamic-alignment})
(s/def ::layout-flags (s/coll-of ::layout-flag))
@@ -132,7 +134,7 @@
(or layout default-layout))))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(if (and layout-name (contains? layout-names layout-name))
(rx/of (ensure-layout layout-name))
(rx/of (ensure-layout :layers))))))
@@ -151,51 +153,48 @@
:workspace-presence {}))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/merge
(rx/of (dwp/fetch-bundle project-id file-id))
;; Initialize notifications (websocket connection) and the file persistence
(->> stream
(rx/filter (ptk/type? ::dwp/bundle-fetched))
- (rx/first)
- (rx/mapcat #(rx/of (dwn/initialize file-id)
- (dwp/initialize-file-persistence file-id))))
-
- ;; Initialize Indexes (webworker)
- (->> stream
- (rx/filter (ptk/type? ::dwp/bundle-fetched))
+ (rx/take 1)
(rx/map deref)
- (rx/map dwc/initialize-indices)
- (rx/first))
+ (rx/mapcat (fn [{:keys [project] :as bundle}]
+ (rx/merge
+ (rx/of (dwn/initialize file-id)
+ (dwp/initialize-file-persistence file-id)
+ (dwc/initialize-indices bundle))
- ;; Mark file initialized when indexes are ready
- (->> stream
- (rx/filter #(= ::dwc/index-initialized %))
- (rx/first)
- (rx/map (fn []
- (file-initialized project-id file-id))))
-
- ))))
+ (->> stream
+ (rx/filter #(= ::dwc/index-initialized %))
+ (rx/first)
+ (rx/map #(file-initialized bundle)))))))))))
(defn- file-initialized
- [project-id file-id]
+ [{:keys [file users project libraries] :as bundle}]
(ptk/reify ::file-initialized
ptk/UpdateEvent
(update [_ state]
- (update state :workspace-file
- (fn [file]
- (if (= (:id file) file-id)
- (assoc file :initialized true)
- file))))
+ (assoc state
+ :current-team-id (:team-id project)
+ :users (d/index-by :id users)
+ :workspace-undo {}
+ :workspace-project project
+ :workspace-file (assoc file :initialized true)
+ :workspace-data (:data file)
+ :workspace-libraries (d/index-by :id libraries)))
ptk/WatchEvent
- (watch [_ state stream]
- (let [ignore-until (get-in state [:workspace-file :ignore-sync-until])
+ (watch [it state stream]
+ (let [file-id (:id file)
+ ignore-until (:ignore-sync-until file)
needs-update? (some #(and (> (:modified-at %) (:synced-at %))
(or (not ignore-until)
(> (:modified-at %) ignore-until)))
- (vals (get state :workspace-libraries)))]
+ libraries)]
(when needs-update?
(rx/of (dwl/notify-sync-file file-id)))))))
@@ -211,39 +210,43 @@
:workspace-persistence))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/of (dwn/finalize file-id)
::dwp/finalize))))
+(declare go-to-page)
(defn initialize-page
[page-id]
+ (us/assert ::us/uuid page-id)
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
(let [;; we maintain a cache of page state for user convenience
;; with the exception of the selection; when user abandon
;; the current page, the selection is lost
- local (-> state
- (get-in [:workspace-cache page-id] workspace-local-default)
- (assoc :selected (d/ordered-set)))
- page (-> (get-in state [:workspace-data :pages-index page-id])
- (select-keys [:id :name]))]
- (assoc state
- :current-page-id page-id ; mainly used by events
- :trimmed-page page
- :workspace-local local)))))
+
+ page (get-in state [:workspace-data :pages-index page-id])
+ local (-> state
+ (get-in [:workspace-cache page-id] workspace-local-default)
+ (assoc :selected (d/ordered-set)))]
+ (-> state
+ (assoc :current-page-id page-id)
+ (assoc :trimmed-page (select-keys page [:id :name]))
+ (assoc :workspace-local local)
+ (update-in [:route :params :query] assoc :page-id (str page-id)))))))
(defn finalize-page
[page-id]
- (us/verify ::us/uuid page-id)
+ (us/assert ::us/uuid page-id)
(ptk/reify ::finalize-page
ptk/UpdateEvent
(update [_ state]
- (let [local (-> (:workspace-local state)
- (dissoc :edition)
- (dissoc :edit-path)
- (dissoc :selected))]
+ (let [page-id (or page-id (get-in state [:workspace-data :pages 0]))
+ local (-> (:workspace-local state)
+ (dissoc :edition)
+ (dissoc :edit-path)
+ (dissoc :selected))]
(-> state
(assoc-in [:workspace-cache page-id] local)
(dissoc :current-page-id :workspace-local :trimmed-page :workspace-drawing))))))
@@ -252,23 +255,31 @@
;; Workspace Page CRUD
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(def create-empty-page
- (ptk/reify ::create-empty-page
- ptk/WatchEvent
- (watch [this state stream]
- (let [id (uuid/next)
- pages (get-in state [:workspace-data :pages-index])
- unames (dwc/retrieve-used-names pages)
- name (dwc/generate-unique-name unames "Page")
+(defn create-page
+ [{:keys [file-id]}]
+ (let [id (uuid/next)]
+ (ptk/reify ::create-page
+ IDeref
+ (-deref [_]
+ {:id id :file-id file-id})
- rchange {:type :add-page
- :id id
- :name name}
- uchange {:type :del-page
- :id id}]
- (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true}))))))
+ ptk/WatchEvent
+ (watch [it state stream]
+ (let [pages (get-in state [:workspace-data :pages-index])
+ unames (dwc/retrieve-used-names pages)
+ name (dwc/generate-unique-name unames "Page")
-(defn duplicate-page [page-id]
+ rchange {:type :add-page
+ :id id
+ :name name}
+ uchange {:type :del-page
+ :id id}]
+ (rx/of (dch/commit-changes {:redo-changes [rchange]
+ :undo-changes [uchange]
+ :origin it})))))))
+
+(defn duplicate-page
+ [page-id]
(ptk/reify ::duplicate-page
ptk/WatchEvent
(watch [this state stream]
@@ -284,7 +295,9 @@
:page page}
uchange {:type :del-page
:id id}]
- (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchange]
+ :undo-changes [uchange]
+ :origin this}))))))
(s/def ::rename-page
(s/keys :req-un [::id ::name]))
@@ -295,7 +308,7 @@
(us/verify string? name)
(ptk/reify ::rename-page
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page (get-in state [:workspace-data :pages-index id])
rchg {:type :mod-page
:id id
@@ -303,7 +316,9 @@
uchg {:type :mod-page
:id id
:name (:name page)}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
(declare purge-page)
(declare go-to-file)
@@ -314,13 +329,15 @@
[id]
(ptk/reify ::delete-page
ptk/WatchEvent
- (watch [_ state s]
+ (watch [it state stream]
(let [page (get-in state [:workspace-data :pages-index id])
rchg {:type :del-page
:id id}
uchg {:type :add-page
:page page}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it})
(when (= id (:current-page-id state))
go-to-file))))))
@@ -338,7 +355,7 @@
(assoc-in state [:workspace-file :name] name))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [params {:id id :name name}]
(->> (rp/mutation :rename-file params)
(rx/ignore))))))
@@ -437,7 +454,7 @@
(defn start-panning []
(ptk/reify ::start-panning
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)]
(when-not (get-in state [:workspace-local :panning])
@@ -535,7 +552,7 @@
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
- #(impl-update-zoom % center (fn [z] (min (* z 1.1) 200)))))))
+ #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200)))))))
(defn decrease-zoom
[center]
@@ -543,7 +560,7 @@
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
- #(impl-update-zoom % center (fn [z] (max (* z 0.9) 0.01)))))))
+ #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01)))))))
(def reset-zoom
(ptk/reify ::reset-zoom
@@ -600,7 +617,7 @@
(us/verify ::shape-attrs attrs)
(ptk/reify ::update-shape
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/of (dch/update-shapes [id] #(merge % attrs))))))
(defn start-rename-shape
@@ -625,7 +642,7 @@
(us/verify ::shape-attrs attrs)
(ptk/reify ::update-selected-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [selected (wsh/lookup-selected state)]
(rx/from (map #(update-shape % attrs) selected))))))
@@ -663,7 +680,7 @@
"Deselect all and remove all selected shapes."
(ptk/reify ::delete-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [selected (wsh/lookup-selected state)]
(rx/of (dwc/delete-shapes selected)
(dws/deselect-all))))))
@@ -675,9 +692,9 @@
(defn vertical-order-selected
[loc]
(us/verify ::loc loc)
- (ptk/reify ::vertical-order-selected-shpes
+ (ptk/reify ::vertical-order-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
@@ -709,7 +726,9 @@
:index (cp/position-on-parent id objects)}))
selected)]
;; TODO: maybe missing the :reg-objects event?
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
;; --- Change Shape Order (D&D Ordering)
@@ -884,7 +903,7 @@
(ptk/reify ::relocate-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -972,25 +991,30 @@
[[] [] []]
ids)
- [rchanges uchanges] (relocate-shapes-changes objects
- parents
- parent-id
- page-id
- to-index
- ids
- groups-to-delete
- groups-to-unmask
- shapes-to-detach
- shapes-to-reroot
- shapes-to-deroot)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ [rchanges uchanges]
+ (relocate-shapes-changes objects
+ parents
+ parent-id
+ page-id
+ to-index
+ ids
+ groups-to-delete
+ groups-to-unmask
+ shapes-to-detach
+ shapes-to-reroot
+ shapes-to-deroot)
+
+ ]
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/expand-collapse parent-id))))))
(defn relocate-selected-shapes
[parent-id to-index]
(ptk/reify ::relocate-selected-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [selected (wsh/lookup-selected state)]
(rx/of (relocate-shapes selected parent-id to-index))))))
@@ -999,7 +1023,7 @@
[]
(ptk/reify ::start-editing-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [selected (wsh/lookup-selected state)]
(if-not (= 1 (count selected))
(rx/empty)
@@ -1027,7 +1051,7 @@
[id index]
(ptk/reify ::relocate-pages
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [cidx (-> (get-in state [:workspace-data :pages])
(d/index-of id))
rchg {:type :mov-page
@@ -1036,7 +1060,9 @@
uchg {:type :mov-page
:id id
:index cidx}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
;; --- Shape / Selection Alignment and Distribution
@@ -1048,7 +1074,7 @@
(us/verify ::gal/align-axis axis)
(ptk/reify :align-objects
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
@@ -1079,7 +1105,7 @@
(us/verify ::gal/dist-axis axis)
(ptk/reify :align-objects
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
@@ -1097,7 +1123,7 @@
[id lock]
(ptk/reify ::set-shape-proportion-lock
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(letfn [(assign-proportions [shape]
(if-not lock
(assoc shape :proportion-lock false)
@@ -1118,7 +1144,7 @@
(us/verify ::position position)
(ptk/reify ::update-position
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (get objects id)
@@ -1140,12 +1166,16 @@
(s/assert ::shape-attrs flags)
(ptk/reify ::update-shape-flags
ptk/WatchEvent
- (watch [_ state stream]
- (letfn [(update-fn [obj]
- (cond-> obj
- (boolean? blocked) (assoc :blocked blocked)
- (boolean? hidden) (assoc :hidden hidden)))]
- (rx/of (dch/update-shapes-recursive [id] update-fn))))))
+ (watch [it state stream]
+ (let [update-fn
+ (fn [obj]
+ (cond-> obj
+ (boolean? blocked) (assoc :blocked blocked)
+ (boolean? hidden) (assoc :hidden hidden)))
+
+ objects (wsh/lookup-page-objects state)
+ ids (d/concat [id] (cp/get-children id objects))]
+ (rx/of (dch/update-shapes ids update-fn))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1156,29 +1186,41 @@
[project-id]
(ptk/reify ::navigate-to-project
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-ids (get-in state [:projects project-id :pages])
params {:project project-id :page (first page-ids)}]
(rx/of (rt/nav :workspace/page params))))))
(defn go-to-page
- [page-id]
- (us/verify ::us/uuid page-id)
- (ptk/reify ::go-to-page
- ptk/WatchEvent
- (watch [_ state stream]
- (let [project-id (get-in state [:workspace-project :id])
- file-id (get-in state [:workspace-file :id])
- pparams {:file-id file-id :project-id project-id}
- qparams {:page-id page-id}]
- (rx/of (rt/nav :workspace pparams qparams))))))
+ ([]
+
+ (ptk/reify ::go-to-page
+ ptk/WatchEvent
+ (watch [it state stream]
+ (let [project-id (:current-project-id state)
+ file-id (:current-file-id state)
+ page-id (get-in state [:workspace-data :pages 0])
+
+ pparams {:file-id file-id :project-id project-id}
+ qparams {:page-id page-id}]
+ (rx/of (rt/nav :workspace pparams qparams))))))
+ ([page-id]
+ (us/verify ::us/uuid page-id)
+ (ptk/reify ::go-to-page
+ ptk/WatchEvent
+ (watch [it state stream]
+ (let [project-id (:current-project-id state)
+ file-id (:current-file-id state)
+ pparams {:file-id file-id :project-id project-id}
+ qparams {:page-id page-id}]
+ (rx/of (rt/nav :workspace pparams qparams)))))))
(defn go-to-layout
[layout]
(us/verify ::layout-flag layout)
(ptk/reify ::go-to-layout
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-file :id])
page-id (get-in state [:current-page-id])
@@ -1189,7 +1231,7 @@
(def go-to-file
(ptk/reify ::go-to-file
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [{:keys [id project-id data] :as file} (:workspace-file state)
page-id (get-in data [:pages 0])
pparams {:project-id project-id :file-id id}
@@ -1202,7 +1244,7 @@
([{:keys [file-id page-id]}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [{:keys [current-file-id current-page-id]} state
params {:file-id (or file-id current-file-id)
:page-id (or page-id current-page-id)}]
@@ -1214,11 +1256,20 @@
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
- (watch [_ state stream]
- (let [team-id (or team-id (get-in state [:workspace-project :team-id]))]
+ (watch [it state stream]
+ (when-let [team-id (or team-id (:current-team-id state))]
(rx/of ::dwp/force-persist
(rt/nav :dashboard-projects {:team-id team-id})))))))
+(defn go-to-dashboard-fonts
+ []
+ (ptk/reify ::go-to-dashboard
+ ptk/WatchEvent
+ (watch [it state stream]
+ (let [team-id (:current-team-id state)]
+ (rx/of ::dwp/force-persist
+ (rt/nav :dashboard-fonts {:team-id team-id}))))))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1244,7 +1295,7 @@
(us/verify ::cp/minimal-shape shape)
(ptk/reify ::show-shape-context-menu
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [selected (wsh/lookup-selected state)]
(rx/concat
(when-not (selected (:id shape))
@@ -1264,7 +1315,27 @@
(defn copy-selected
[]
- (letfn [;; Retrieve all ids of selected shapes with corresponding
+ (letfn [;; Sort objects so they have the same relative ordering
+ ;; when pasted later.
+ (sort-selected [state data]
+ (let [selected (:selected data)
+ page-id (:current-page-id state)
+ objects (get-in state [:workspace-data
+ :pages-index
+ page-id
+ :objects])]
+ (->> (uw/ask! {:cmd :selection/query-z-index
+ :page-id page-id
+ :objects objects
+ :ids selected})
+ (rx/map (fn [z-indexes]
+ (assoc data :selected
+ (->> (d/zip selected z-indexes)
+ (sort-by second)
+ (map first)
+ (into (d/ordered-set)))))))))
+
+ ;; Retrieve all ids of selected shapes with corresponding
;; children; this is needed because each shape should be
;; processed one by one because of async events (data url
;; fetching).
@@ -1316,7 +1387,7 @@
(ptk/reify ::copy-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cp/clean-loops objects))
@@ -1329,6 +1400,7 @@
(->> (rx/from (seq (vals pdata)))
(rx/merge-map (partial prepare-object objects selected))
(rx/reduce collect-data initial)
+ (rx/mapcat (partial sort-selected state))
(rx/map t/encode)
(rx/map wapi/write-to-clipboard)
(rx/catch on-copy-error)
@@ -1342,7 +1414,7 @@
(def paste
(ptk/reify ::paste
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(try
(let [clipboard-str (wapi/read-from-clipboard)
@@ -1381,7 +1453,7 @@
[event in-viewport?]
(ptk/reify ::paste-from-event
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(try
(let [objects (wsh/lookup-page-objects state)
paste-data (wapi/read-from-paste-event event)
@@ -1499,7 +1571,7 @@
change)))
;; Procceed with the standard shape paste procediment.
- (do-paste [state mouse-pos media]
+ (do-paste [it state mouse-pos media]
(let [media-idx (d/index-by :prev-id media)
page-id (:current-page-id state)
@@ -1545,19 +1617,21 @@
(map #(get-in % [:obj :id]))
(into (d/ordered-set)))]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes selected))))]
(ptk/reify ::paste-shape
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [file-id (:current-file-id state)
mouse-pos (deref ms/mouse-position)]
(if (= file-id (:file-id data))
- (do-paste state mouse-pos [])
+ (do-paste it state mouse-pos [])
(->> (rx/from images)
(rx/merge-map (partial upload-media file-id))
(rx/reduce conj [])
- (rx/mapcat (partial do-paste state mouse-pos)))))))))
+ (rx/mapcat (partial do-paste it state mouse-pos)))))))))
(defn as-content [text]
@@ -1573,7 +1647,7 @@
(s/assert string? text)
(ptk/reify ::paste-text
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [id (uuid/next)
{:keys [x y]} @ms/mouse-position
width (max 8 (min (* 7 (count text)) 700))
@@ -1602,7 +1676,7 @@
(s/assert string? text)
(ptk/reify ::paste-svg
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [position (deref ms/mouse-position)
file-id (:current-file-id state)]
(->> (dwp/parse-svg ["svg" text])
@@ -1612,7 +1686,7 @@
[image]
(ptk/reify ::paste-bin-impl
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [file-id (get-in state [:workspace-file :id])
params {:file-id file-id
:blobs [image]
@@ -1637,7 +1711,7 @@
[]
(ptk/reify ::start-create-interaction
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
@@ -1674,7 +1748,7 @@
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -1702,20 +1776,20 @@
[color]
(ptk/reify ::change-canvas-color
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (get state :current-page-id)
options (wsh/lookup-page-options state page-id)
previus-color (:background options)]
(rx/of (dch/commit-changes
- [{:type :set-option
- :page-id page-id
- :option :background
- :value (:color color)}]
- [{:type :set-option
- :page-id page-id
- :option :background
- :value previus-color}]
- {:commit-local? true}))))))
+ {:redo-changes [{:type :set-option
+ :page-id page-id
+ :option :background
+ :value (:color color)}]
+ :undo-changes [{:type :set-option
+ :page-id page-id
+ :option :background
+ :value previus-color}]
+ :origin it}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1735,6 +1809,7 @@
(d/export dwt/update-dimensions)
(d/export dwt/flip-horizontal-selected)
(d/export dwt/flip-vertical-selected)
+(d/export dwt/selected-to-path)
;; Persistence
@@ -1758,12 +1833,10 @@
(d/export dws/duplicate-selected)
(d/export dws/handle-selection)
(d/export dws/select-inside-group)
-;;(d/export dws/select-last-layer)
(d/export dwd/select-for-drawing)
(d/export dwc/clear-edition-mode)
(d/export dwc/add-shape)
(d/export dwc/start-edition-mode)
-#_(d/export dwc/start-path-edit)
;; Groups
diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs
index 7153223b3..b7be2c2eb 100644
--- a/frontend/src/app/main/data/workspace/changes.cljs
+++ b/frontend/src/app/main/data/workspace/changes.cljs
@@ -11,7 +11,9 @@
[app.common.pages.spec :as spec]
[app.common.spec :as us]
[app.main.data.workspace.undo :as dwu]
+ [app.main.data.workspace.state-helpers :as wsh]
[app.main.worker :as uw]
+ [app.main.store :as st]
[app.util.logging :as log]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
@@ -30,123 +32,75 @@
(def commit-changes? (ptk/type? ::commit-changes))
-(defn- generate-operations
- ([ma mb] (generate-operations ma mb false))
- ([ma mb undo?]
- (let [ops (let [ma-keys (set (keys ma))
- mb-keys (set (keys mb))
- added (set/difference mb-keys ma-keys)
- removed (set/difference ma-keys mb-keys)
- both (set/intersection ma-keys mb-keys)]
- (d/concat
- (mapv #(array-map :type :set :attr % :val (get mb %)) added)
- (mapv #(array-map :type :set :attr % :val nil) removed)
- (loop [items (seq both)
- result []]
- (if items
- (let [k (first items)
- vma (get ma k)
- vmb (get mb k)]
- (if (= vma vmb)
- (recur (next items) result)
- (recur (next items)
- (conj result {:type :set
- :attr k
- :val vmb
- :ignore-touched undo?}))))
- result))))]
- (if undo?
- (conj ops {:type :set-touched :touched (:touched mb)})
- ops))))
+(defn- generate-operation
+ "Given an object old and new versions and an attribute will append into changes
+ the set and undo operations"
+ [changes attr old new]
+ (let [old-val (get old attr)
+ new-val (get new attr)]
+ (if (= old-val new-val)
+ changes
+ (-> changes
+ (update :rops conj {:type :set :attr attr :val new-val})
+ (update :uops conj {:type :set :attr attr :val old-val :ignore-touched true})))))
+
+(defn- update-shape-changes
+ "Calculate the changes and undos to be done when a function is applied to a
+ single object"
+ [changes page-id objects update-fn attrs id]
+ (let [old-obj (get objects id)
+ new-obj (update-fn old-obj)
+
+ attrs (or attrs (d/concat #{} (keys old-obj) (keys new-obj)))
+
+ {rops :rops uops :uops}
+ (reduce #(generate-operation %1 %2 old-obj new-obj)
+ {:rops [] :uops []}
+ attrs)
+
+ uops (cond-> uops
+ (not (empty? uops))
+ (conj {:type :set-touched :touched (:touched old-obj)}))
+
+ change {:type :mod-obj :page-id page-id :id id}]
+
+ (cond-> changes
+ (not (empty? rops))
+ (update :rch conj (assoc change :operations rops))
+
+ (not (empty? uops))
+ (update :uch conj (assoc change :operations uops)))))
(defn update-shapes
([ids f] (update-shapes ids f nil))
- ([ids f {:keys [reg-objects?] :or {reg-objects? false}}]
+ ([ids f {:keys [reg-objects? save-undo? keys]
+ :or {reg-objects? false save-undo? true attrs nil}}]
+
(us/assert ::coll-of-uuid ids)
(us/assert fn? f)
+
(ptk/reify ::update-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
- objects (get-in state [:workspace-data :pages-index page-id :objects])
- objects' (get-in state [:workspace-file :data :pages-index page-id :objects])
- reg-objects {:type :reg-objects :page-id page-id :shapes (vec ids)}]
- (loop [ids (seq ids)
- rch []
- uch []]
- (if (nil? ids)
- (rx/of (let [has-rch? (not (empty? rch))
- has-uch? (not (empty? uch))
- rch (cond-> rch (and has-rch? reg-objects?) (conj reg-objects))
- uch (cond-> uch (and has-rch? reg-objects?) (conj reg-objects))]
- (when (and has-rch? has-uch?)
- (commit-changes rch uch {:commit-local? true}))))
+ objects (wsh/lookup-page-objects state)
+ reg-objects {:type :reg-objects :page-id page-id :shapes (vec ids)}
- (let [id (first ids)
- obj1 (get objects id)
- obj2 (f obj1)
- obj3 (get objects' id)
- rch-operations (generate-operations obj1 obj2)
- uch-operations (generate-operations obj2 obj3 true)
- rchg {:type :mod-obj
- :page-id page-id
- :operations rch-operations
- :id id}
- uchg {:type :mod-obj
- :page-id page-id
- :operations uch-operations
- :id id}]
- (recur (next ids)
- (if (empty? rch-operations) rch (conj rch rchg))
- (if (empty? uch-operations) uch (conj uch uchg)))))))))))
+ {redo-changes :rch undo-changes :uch}
+ (reduce #(update-shape-changes %1 page-id objects f keys %2)
+ {:rch [] :uch []} ids)]
-(defn update-shapes-recursive
- [ids f]
- (us/assert ::coll-of-uuid ids)
- (us/assert fn? f)
- (letfn [(impl-get-children [objects id]
- (cons id (cp/get-children id objects)))
+ (when-not (empty? redo-changes)
+ (let [redo-changes (cond-> redo-changes
+ reg-objects? (conj reg-objects))
- (impl-gen-changes [objects page-id ids]
- (loop [sids (seq ids)
- cids (seq (impl-get-children objects (first sids)))
- rchanges []
- uchanges []]
- (cond
- (nil? sids)
- [rchanges uchanges]
+ undo-changes (cond-> undo-changes
+ reg-objects? (conj reg-objects))]
- (nil? cids)
- (recur (next sids)
- (seq (impl-get-children objects (first (next sids))))
- rchanges
- uchanges)
-
- :else
- (let [id (first cids)
- obj1 (get objects id)
- obj2 (f obj1)
- rops (generate-operations obj1 obj2)
- uops (generate-operations obj2 obj1 true)
- rchg {:type :mod-obj
- :page-id page-id
- :operations rops
- :id id}
- uchg {:type :mod-obj
- :page-id page-id
- :operations uops
- :id id}]
- (recur sids
- (next cids)
- (conj rchanges rchg)
- (conj uchanges uchg))))))]
- (ptk/reify ::update-shapes-recursive
- ptk/WatchEvent
- (watch [_ state stream]
- (let [page-id (:current-page-id state)
- objects (get-in state [:workspace-data :pages-index page-id :objects])
- [rchanges uchanges] (impl-gen-changes objects page-id (seq ids))]
- (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))))
+ (rx/of (commit-changes {:redo-changes redo-changes
+ :undo-changes undo-changes
+ :origin it
+ :save-undo? save-undo?})))))))))
(defn update-indices
[page-id changes]
@@ -158,67 +112,63 @@
:changes changes}))))
(defn commit-changes
- ([changes undo-changes]
- (commit-changes changes undo-changes {}))
- ([changes undo-changes {:keys [save-undo?
- commit-local?
- file-id]
- :or {save-undo? true
- commit-local? false}
- :as opts}]
- (us/assert ::cp/changes changes)
- (us/assert ::cp/changes undo-changes)
- (log/debug :msg "commit-changes"
- :js/changes changes
- :js/undo-changes undo-changes)
+ [{:keys [redo-changes undo-changes origin save-undo? file-id]
+ :or {save-undo? true}}]
- (let [error (volatile! nil)]
- (ptk/reify ::commit-changes
- cljs.core/IDeref
- (-deref [_] {:file-id file-id :changes changes})
- ptk/UpdateEvent
- (update [_ state]
- (let [current-file-id (get state :current-file-id)
- file-id (or file-id current-file-id)
- path1 (if (= file-id current-file-id)
- [:workspace-file :data]
- [:workspace-libraries file-id :data])
- path2 (if (= file-id current-file-id)
- [:workspace-data]
- [:workspace-libraries file-id :data])]
- (try
- (us/assert ::spec/changes changes)
- (let [state (update-in state path1 cp/process-changes changes false)]
- (cond-> state
- commit-local? (update-in path2 cp/process-changes changes false)))
- (catch :default e
- (vreset! error e)
- state))))
+ (log/debug :msg "commit-changes"
+ :js/redo-changes redo-changes
+ :js/undo-changes undo-changes)
- ptk/WatchEvent
- (watch [_ state stream]
- (when-not @error
- (let [;; adds page-id to page changes (that have the `id` field instead)
- add-page-id
- (fn [{:keys [id type page] :as change}]
- (cond-> change
- (page-change? type)
- (assoc :page-id (or id (:id page)))))
+ (let [error (volatile! nil)]
+ (ptk/reify ::commit-changes
+ cljs.core/IDeref
+ (-deref [_]
+ {:file-id file-id
+ :hint-events @st/last-events
+ :hint-origin (ptk/type origin)
+ :changes redo-changes})
- changes-by-pages
- (->> changes
- (map add-page-id)
- (remove #(nil? (:page-id %)))
- (group-by :page-id))
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [current-file-id (get state :current-file-id)
+ file-id (or file-id current-file-id)
+ path (if (= file-id current-file-id)
- process-page-changes
- (fn [[page-id changes]]
- (update-indices page-id changes))]
- (rx/concat
- (rx/from (map process-page-changes changes-by-pages))
+ [:workspace-data]
+ [:workspace-libraries file-id :data])]
+ (try
+ (us/assert ::spec/changes redo-changes)
+ (us/assert ::spec/changes undo-changes)
+ (update-in state path cp/process-changes redo-changes false)
- (when (and save-undo? (seq undo-changes))
- (let [entry {:undo-changes undo-changes
- :redo-changes changes}]
- (rx/of (dwu/append-undo entry))))))))))))
+ (catch :default e
+ (vreset! error e)
+ state))))
+
+ ptk/WatchEvent
+ (watch [it state stream]
+ (when-not @error
+ (let [;; adds page-id to page changes (that have the `id` field instead)
+ add-page-id
+ (fn [{:keys [id type page] :as change}]
+ (cond-> change
+ (page-change? type)
+ (assoc :page-id (or id (:id page)))))
+
+ changes-by-pages
+ (->> redo-changes
+ (map add-page-id)
+ (remove #(nil? (:page-id %)))
+ (group-by :page-id))
+
+ process-page-changes
+ (fn [[page-id changes]]
+ (update-indices page-id redo-changes))]
+ (rx/concat
+ (rx/from (map process-page-changes changes-by-pages))
+
+ (when (and save-undo? (seq undo-changes))
+ (let [entry {:undo-changes undo-changes
+ :redo-changes redo-changes}]
+ (rx/of (dwu/append-undo entry)))))))))))
diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs
index c9f8bfcd8..3bffa6bdb 100644
--- a/frontend/src/app/main/data/workspace/common.cljs
+++ b/frontend/src/app/main/data/workspace/common.cljs
@@ -40,7 +40,7 @@
[{:keys [file] :as bundle}]
(ptk/reify ::setup-selection-index
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [msg {:cmd :initialize-indices
:file-id (:id file)
:data (:data file)}]
@@ -112,7 +112,7 @@
(def undo
(ptk/reify ::undo
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
;; Editors handle their own undo's
@@ -123,12 +123,15 @@
(when-not (or (empty? items) (= index -1))
(let [changes (get-in items [index :undo-changes])]
(rx/of (dwu/materialize-undo changes (dec index))
- (dch/commit-changes changes [] {:save-undo? false}))))))))))
+ (dch/commit-changes {:redo-changes changes
+ :undo-changes []
+ :save-undo? false
+ :origin it}))))))))))
(def redo
(ptk/reify ::redo
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when-not (or (some? edition) (not-empty drawing))
@@ -138,7 +141,10 @@
(when-not (or (empty? items) (= index (dec (count items))))
(let [changes (get-in items [(inc index) :redo-changes])]
(rx/of (dwu/materialize-undo changes (inc index))
- (dch/commit-changes changes [] {:save-undo? false}))))))))))
+ (dch/commit-changes {:redo-changes changes
+ :undo-changes []
+ :origin it
+ :save-undo? false}))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shapes
@@ -174,7 +180,7 @@
(assoc-in state [:workspace-local :selected] ids))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)]
(rx/of (expand-all-parents ids objects))))))
@@ -196,7 +202,7 @@
state)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)]
(->> stream
(rx/filter interrupt?)
@@ -276,7 +282,7 @@
(us/verify ::shape-attrs attrs)
(ptk/reify ::add-shape
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -296,7 +302,9 @@
(assoc :name name)))]
(rx/concat
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (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))
@@ -305,7 +313,7 @@
(defn move-shapes-into-frame [frame-id shapes]
(ptk/reify ::move-shapes-into-frame
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(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})
@@ -329,15 +337,16 @@
:page-id page-id
:index index
:shapes [shape-id]})))]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
-
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn delete-shapes
[ids]
(us/assert (s/coll-of ::us/uuid) ids)
(ptk/reify ::delete-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -366,6 +375,12 @@
#{}
ids)
+ interacting-shapes
+ (filter (fn [shape]
+ (let [interactions (:interactions shape)]
+ (some ids (map :destination interactions))))
+ (vals objects))
+
rchanges
(d/concat
(reduce (fn [res id]
@@ -391,7 +406,18 @@
:operations [{:type :set
:attr :masked-group?
:val false}])
- groups-to-unmask))
+ groups-to-unmask)
+ (map #(array-map
+ :type :mod-obj
+ :page-id page-id
+ :id (:id %)
+ :operations [{:type :set
+ :attr :interactions
+ :val (vec (remove (fn [interaction]
+ (contains? ids (:destination interaction)))
+ (:interactions %)))}])
+ interacting-shapes))
+
uchanges
(d/concat
@@ -430,14 +456,23 @@
:operations [{:type :set
:attr :masked-group?
:val true}])
- groups-to-unmask))]
+ groups-to-unmask)
+ (map #(array-map
+ :type :mod-obj
+ :page-id page-id
+ :id (:id %)
+ :operations [{:type :set
+ :attr :interactions
+ :val (:interactions %)}])
+ interacting-shapes))]
;; (println "================ rchanges")
;; (cljs.pprint/pprint rchanges)
;; (println "================ uchanges")
;; (cljs.pprint/pprint uchanges)
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
-
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
;; --- Add shape to Workspace
@@ -450,7 +485,7 @@
[type frame-x frame-y data]
(ptk/reify ::create-and-add-shape
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [{:keys [width height]} data
[vbc-x vbc-y] (viewport-center state)
@@ -470,7 +505,7 @@
[image {:keys [x y]}]
(ptk/reify ::image-uploaded
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [{:keys [name width height id mtype]} image
shape {:name name
:width width
diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs
index 553b31038..503d6bee9 100644
--- a/frontend/src/app/main/data/workspace/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/drawing.cljs
@@ -30,7 +30,12 @@
(ptk/reify ::select-for-drawing
ptk/UpdateEvent
(update [_ state]
- (update state :workspace-drawing assoc :tool tool :object data))
+ (-> state
+ (update :workspace-drawing assoc :tool tool :object data)
+ ;; When changing drawing tool disable "scale text" mode
+ ;; automatically, to help users that ignore how this
+ ;; mode works.
+ (update :workspace-layout disj :scale-text)))
ptk/WatchEvent
(watch [_ state stream]
diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs
index 6e37c1d93..d9e3879a7 100644
--- a/frontend/src/app/main/data/workspace/drawing/box.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/box.cljs
@@ -87,14 +87,14 @@
(->> ms/mouse-position
(rx/filter #(> (gpt/distance % initial) 2))
- (rx/with-latest vector ms/mouse-position-ctrl)
+ (rx/with-latest vector ms/mouse-position-shift)
(rx/switch-map
(fn [[point :as current]]
(->> (snap/closest-snap-point page-id [shape] layout zoom point)
(rx/map #(conj current %)))))
(rx/map
- (fn [[_ ctrl? point]]
- #(update-drawing % point ctrl?)))
+ (fn [[_ shift? point]]
+ #(update-drawing % point shift?)))
(rx/take-until stoper))
(rx/of common/handle-finish-drawing))))))
diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs
index f31d5be14..92db09960 100644
--- a/frontend/src/app/main/data/workspace/drawing/common.cljs
+++ b/frontend/src/app/main/data/workspace/drawing/common.cljs
@@ -28,7 +28,6 @@
(watch [_ state stream]
(let [shape (get-in state [:workspace-drawing :object])]
(rx/concat
- (rx/of clear-drawing)
(when (:initialized? shape)
(let [page-id (:current-page-id state)
shape-click-width (case (:type shape)
@@ -65,4 +64,8 @@
:page-id page-id
:rect (:selrect shape)})
(rx/map #(dwc/move-shapes-into-frame (:id shape) %)))
- (rx/empty))))))))))
+ (rx/empty)))))
+
+ ;; Delay so the mouse event can read the drawing state
+ (->> (rx/of clear-drawing)
+ (rx/delay 0)))))))
diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs
index 13a327103..e405c2364 100644
--- a/frontend/src/app/main/data/workspace/grid.cljs
+++ b/frontend/src/app/main/data/workspace/grid.cljs
@@ -40,7 +40,7 @@
(us/assert ::us/uuid frame-id)
(ptk/reify ::add-frame-grid
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
data (get-in state [:workspace-data :pages-index page-id])
params (or (get-in data [:options :saved-grids :square])
@@ -56,29 +56,30 @@
[frame-id index]
(ptk/reify ::set-frame-grid
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))
(defn set-frame-grid
[frame-id index data]
(ptk/reify ::set-frame-grid
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data))))))
(defn set-default-grid
[type params]
(ptk/reify ::set-default-grid
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [pid (:current-page-id state)
prev-value (get-in state [:workspace-data :pages-index pid :options :saved-grids type])]
- (rx/of (dch/commit-changes [{:type :set-option
- :page-id pid
- :option [:saved-grids type]
- :value params}]
- [{:type :set-option
- :page-id pid
- :option [:saved-grids type]
- :value prev-value}]
- {:commit-local? true}))))))
+ (rx/of (dch/commit-changes
+ {:redo-changes [{:type :set-option
+ :page-id pid
+ :option [:saved-grids type]
+ :value params}]
+ :undo-changes [{:type :set-option
+ :page-id pid
+ :option [:saved-grids type]
+ :value prev-value}]
+ :origin it}))))))
diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs
index f34e1475c..849fdde94 100644
--- a/frontend/src/app/main/data/workspace/groups.cljs
+++ b/frontend/src/app/main/data/workspace/groups.cljs
@@ -32,31 +32,97 @@
(gsh/setup selrect)
(assoc :shapes (mapv :id shapes)))))
+(defn get-empty-groups
+ "Retrieve emtpy groups after group creation"
+ [objects parent-id shapes]
+ (let [ids (cp/clean-loops objects (into #{} (map :id) shapes))
+ parents (->> ids
+ (reduce #(conj %1 (cp/get-parent %2 objects))
+ #{}))]
+ (loop [current-id (first parents)
+ to-check (rest parents)
+ removed-id? ids
+ result #{}]
+
+ (if-not current-id
+ ;; Base case, no next element
+ result
+
+ (let [group (get objects current-id)]
+ (if (and (not= :frame (:type group))
+ (not= current-id parent-id)
+ (empty? (remove removed-id? (:shapes group))))
+
+ ;; Adds group to the remove and check its parent
+ (let [to-check (d/concat [] to-check [(cp/get-parent current-id objects)]) ]
+ (recur (first to-check)
+ (rest to-check)
+ (conj removed-id? current-id)
+ (conj result current-id)))
+
+ ;; otherwise recur
+ (recur (first to-check)
+ (rest to-check)
+ removed-id?
+ result)))))))
+
(defn prepare-create-group
- [page-id shapes prefix keep-name]
+ [objects page-id shapes prefix keep-name]
(let [group (make-group shapes prefix keep-name)
+ frame-id (:frame-id (first shapes))
+ parent-id (:parent-id (first shapes))
rchanges [{:type :add-obj
:id (:id group)
:page-id page-id
- :frame-id (:frame-id (first shapes))
- :parent-id (:parent-id (first shapes))
+ :frame-id frame-id
+ :parent-id parent-id
:obj group
:index (::index (first shapes))}
+
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes (mapv :id shapes)}]
- uchanges (conj
- (mapv (fn [obj] {:type :mov-objects
- :page-id page-id
- :parent-id (:parent-id obj)
- :index (::index obj)
- :shapes [(:id obj)]})
- shapes)
- {:type :del-obj
- :id (:id group)
- :page-id page-id})]
+ uchanges (-> (mapv
+ (fn [obj]
+ {:type :mov-objects
+ :page-id page-id
+ :parent-id (:parent-id obj)
+ :index (::index obj)
+ :shapes [(:id obj)]}) shapes)
+ (conj
+ {:type :del-obj
+ :id (:id group)
+ :page-id page-id}))
+
+ ids-to-delete (get-empty-groups objects parent-id shapes)
+
+ delete-group
+ (fn [changes id]
+ (-> changes
+ (conj {:type :del-obj
+ :id id
+ :page-id page-id})))
+
+ add-deleted-group
+ (fn [changes id]
+ (let [obj (-> (get objects id)
+ (d/without-keys [:shapes]))]
+
+ (d/concat [{:type :add-obj
+ :id id
+ :page-id page-id
+ :frame-id (:frame-id obj)
+ :parent-id (:parent-id obj)
+ :obj obj
+ :index (::index obj)}] changes)))
+
+ rchanges (->> ids-to-delete
+ (reduce delete-group rchanges))
+
+ uchanges (->> ids-to-delete
+ (reduce add-deleted-group uchanges))]
[group rchanges uchanges]))
(defn prepare-remove-group
@@ -100,21 +166,24 @@
(def group-selected
(ptk/reify ::group-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(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)
shapes (shapes-for-grouping objects selected)]
(when-not (empty? shapes)
- (let [[group rchanges uchanges] (prepare-create-group page-id shapes "Group-" false)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (let [[group rchanges uchanges]
+ (prepare-create-group objects page-id shapes "Group-" false)]
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(def ungroup-selected
(ptk/reify ::ungroup-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
@@ -124,12 +193,14 @@
(= (:type group) :group))
(let [[rchanges uchanges]
(prepare-remove-group page-id group objects)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))))
(def mask-group
(ptk/reify ::mask-group
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
@@ -142,7 +213,7 @@
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
- (prepare-create-group page-id shapes "Group-" true))
+ (prepare-create-group objects page-id shapes "Group-" true))
rchanges (d/concat rchanges
[{:type :mod-obj
@@ -178,13 +249,15 @@
:page-id page-id
:shapes [(:id group)]})]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(def unmask-group
(ptk/reify ::unmask-group
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)]
@@ -211,7 +284,9 @@
:page-id page-id
:shapes [(:id group)]}]]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set (:id group))))))))))
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index c0370fdb6..5e076b766 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -86,23 +86,26 @@
(us/assert ::cp/color color)
(ptk/reify ::add-color
ptk/WatchEvent
- (watch [_ state s]
+ (watch [it state s]
(let [rchg {:type :add-color
:color color}
uchg {:type :del-color
:id id}]
(rx/of #(assoc-in % [:workspace-local :color-for-rename] id)
- (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))))
-
+ (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it})))))))
(defn add-recent-color
[color]
(us/assert ::cp/recent-color color)
(ptk/reify ::add-recent-color
ptk/WatchEvent
- (watch [_ state s]
+ (watch [it state s]
(let [rchg {:type :add-recent-color
:color color}]
- (rx/of (dch/commit-changes [rchg] [] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes []
+ :origin it}))))))
(def clear-color-for-rename
(ptk/reify ::clear-color-for-rename
@@ -116,13 +119,17 @@
(us/assert ::us/uuid file-id)
(ptk/reify ::update-color
ptk/WatchEvent
- (watch [_ state stream]
- (let [prev (get-in state [:workspace-data :colors id])
+ (watch [it state stream]
+ (let [[path name] (cp/parse-path-name (:name color))
+ color (assoc color :path path :name name)
+ prev (get-in state [:workspace-data :colors id])
rchg {:type :mod-color
:color color}
uchg {:type :mod-color
:color prev}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it})
(sync-file (:current-file-id state) file-id))))))
(defn delete-color
@@ -130,26 +137,30 @@
(us/assert ::us/uuid id)
(ptk/reify ::delete-color
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [prev (get-in state [:workspace-data :colors id])
rchg {:type :del-color
:id id}
uchg {:type :add-color
:color prev}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
(defn add-media
[{:keys [id] :as media}]
(us/assert ::cp/media-object media)
(ptk/reify ::add-media
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [obj (select-keys media [:id :name :width :height :mtype])
rchg {:type :add-media
:object obj}
uchg {:type :del-media
:id id}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
(defn rename-media
[id new-name]
@@ -157,7 +168,7 @@
(us/assert ::us/string new-name)
(ptk/reify ::rename-media
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [object (get-in state [:workspace-data :media id])
[path name] (cp/parse-path-name new-name)
@@ -171,20 +182,24 @@
:name (:name object)
:path (:path object)}}]]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn delete-media
[{:keys [id] :as params}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-media
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [prev (get-in state [:workspace-data :media id])
rchg {:type :del-media
:id id}
uchg {:type :add-media
:object prev}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
(defn add-typography
([typography] (add-typography typography true))
@@ -193,12 +208,14 @@
(us/assert ::cp/typography typography)
(ptk/reify ::add-typography
ptk/WatchEvent
- (watch [_ state s]
+ (watch [it state s]
(let [rchg {:type :add-typography
:typography typography}
uchg {:type :del-typography
:id (:id typography)}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it})
#(cond-> %
edit?
(assoc-in [:workspace-local :rename-typography] (:id typography))))))))))
@@ -209,13 +226,17 @@
(us/assert ::us/uuid file-id)
(ptk/reify ::update-typography
ptk/WatchEvent
- (watch [_ state stream]
- (let [prev (get-in state [:workspace-data :typographies (:id typography)])
+ (watch [it state stream]
+ (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)])
rchg {:type :mod-typography
:typography typography}
uchg {:type :mod-typography
:typography prev}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it})
(sync-file (:current-file-id state) file-id))))))
(defn delete-typography
@@ -223,19 +244,21 @@
(us/assert ::us/uuid id)
(ptk/reify ::delete-typography
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [prev (get-in state [:workspace-data :typographies id])
rchg {:type :del-typography
:id id}
uchg {:type :add-typography
:typography prev}]
- (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes [rchg]
+ :undo-changes [uchg]
+ :origin it}))))))
(def add-component
"Add a new component to current file library, from the currently selected shapes."
(ptk/reify ::add-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -244,7 +267,9 @@
(let [[group rchanges uchanges]
(dwlh/generate-add-component selected objects page-id file-id)]
(when-not (empty? rchanges)
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set (:id group))))))))))
(defn rename-component
@@ -254,7 +279,7 @@
(us/assert ::us/string new-name)
(ptk/reify ::rename-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [[path name] (cp/parse-path-name new-name)
component (get-in state [:workspace-data :components id])
objects (get component :objects)
@@ -275,14 +300,16 @@
:path (:path component)
:objects objects}]]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn duplicate-component
"Create a new component copied from the one with the given id."
[{:keys [id] :as params}]
(ptk/reify ::duplicate-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [component (cp/get-component id
(:current-file-id state)
(dwlh/get-local-file state)
@@ -303,7 +330,9 @@
uchanges [{:type :del-component
:id (:id new-shape)}]]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn delete-component
"Delete the component with the given id, from the current file library."
@@ -311,7 +340,7 @@
(us/assert ::us/uuid id)
(ptk/reify ::delete-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [component (get-in state [:workspace-data :components id])
rchanges [{:type :del-component
@@ -323,7 +352,9 @@
:path (:path component)
:shapes (vals (:objects component))}]]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn instantiate-component
"Create a new shape in the current page, from the component with the given id
@@ -334,7 +365,7 @@
(us/assert ::us/point position)
(ptk/reify ::instantiate-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [local-library (dwlh/get-local-file state)
libraries (get state :workspace-libraries)
component (cp/get-component component-id file-id local-library libraries)
@@ -400,7 +431,9 @@
:ignore-touched true})
new-shapes)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set (:id new-shape))))))))
(defn detach-component
@@ -410,7 +443,7 @@
(us/assert ::us/uuid id)
(ptk/reify ::detach-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shapes (cp/get-object-with-children id objects)
@@ -463,14 +496,16 @@
:val (:touched obj)}]})
shapes)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)
(ptk/reify ::nav-to-component-file
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [file (get-in state [:workspace-libraries file-id])
pparams {:project-id (:project-id file)
:file-id (:id file)}
@@ -499,7 +534,7 @@
(us/assert ::us/uuid id)
(ptk/reify ::reset-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(log/info :msg "RESET-COMPONENT of shape" :id (str id))
(let [local-library (dwlh/get-local-file state)
libraries (dwlh/get-libraries state)
@@ -516,7 +551,9 @@
rchanges
local-library))
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it}))))))
(defn update-component
"Modify the component linked to the shape with the given id, in the
@@ -531,7 +568,7 @@
(us/assert ::us/uuid id)
(ptk/reify ::update-component
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(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)
@@ -571,12 +608,14 @@
file))
(rx/of (when (seq local-rchanges)
- (dch/commit-changes local-rchanges local-uchanges
- {:commit-local? true
+ (dch/commit-changes {:redo-changes local-rchanges
+ :undo-changes local-uchanges
+ :origin it
:file-id (:id local-library)}))
(when (seq rchanges)
- (dch/commit-changes rchanges uchanges
- {:commit-local? true
+ (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it
:file-id file-id})))))))
(declare sync-file-2nd-stage)
@@ -597,7 +636,7 @@
state))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(log/info :msg "SYNC-FILE"
:file (dwlh/pretty-file file-id state)
:library (dwlh/pretty-file library-id state))
@@ -625,8 +664,10 @@
(rx/concat
(rx/of (dm/hide-tag :sync-dialog))
(when rchanges
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true
- :file-id file-id})))
+ (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.
@@ -655,7 +696,7 @@
(us/assert ::us/uuid library-id)
(ptk/reify ::sync-file-2nd-stage
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(log/info :msg "SYNC-FILE (2nd stage)"
:file (dwlh/pretty-file file-id state)
:library (dwlh/pretty-file library-id state))
@@ -668,8 +709,10 @@
(log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes
rchanges
file))
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true
- :file-id file-id})))))))
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it
+ :file-id file-id})))))))
(def ignore-sync
(ptk/reify ::ignore-sync
@@ -678,7 +721,7 @@
(assoc-in state [:workspace-file :ignore-sync-until] (dt/now)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rp/mutation :ignore-sync
{:file-id (get-in state [:workspace-file :id])
:date (dt/now)}))))
@@ -688,7 +731,7 @@
(us/assert ::us/uuid file-id)
(ptk/reify ::notify-sync-file
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %))
(vals (get state :workspace-libraries)))
do-update #(do (apply st/emit! (map (fn [library]
diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
index 01d8a61a8..c59050916 100644
--- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs
@@ -131,7 +131,7 @@
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
- (dwg/prepare-create-group page-id shapes "Component-" true))
+ (dwg/prepare-create-group objects page-id shapes "Component-" true))
[new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id)
diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs
index 33a5c2b52..57f2af43b 100644
--- a/frontend/src/app/main/data/workspace/path.cljs
+++ b/frontend/src/app/main/data/workspace/path.cljs
@@ -18,6 +18,7 @@
(d/export drawing/start-path-from-point)
(d/export drawing/close-path-drag-start)
(d/export drawing/change-edit-mode)
+(d/export drawing/reset-last-handler)
;; Edition
(d/export edition/start-move-handler)
diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs
index 15c5c6c9b..8461b2e68 100644
--- a/frontend/src/app/main/data/workspace/path/changes.cljs
+++ b/frontend/src/app/main/data/workspace/path/changes.cljs
@@ -21,9 +21,9 @@
[objects page-id shape old-content new-content]
(us/verify ::spec/content old-content)
(us/verify ::spec/content new-content)
- (let [shape-id (:id shape)
- frame-id (:frame-id shape)
- parent-id (:parent-id shape)
+ (let [shape-id (:id shape)
+ frame-id (:frame-id shape)
+ parent-id (:parent-id shape)
parent-index (cp/position-on-parent shape-id objects)
[old-points old-selrect] (helpers/content->points+selrect shape old-content)
@@ -72,7 +72,6 @@
(defn save-path-content
([]
(save-path-content {}))
-
([{:keys [preserve-move-to] :or {preserve-move-to false}}]
(ptk/reify ::save-path-content
ptk/UpdateEvent
@@ -85,15 +84,17 @@
(assoc-in state (st/get-path state :content) content)))
ptk/WatchEvent
- (watch [_ state stream]
- (let [objects (wsh/lookup-page-objects state)
- id (get-in state [:workspace-local :edition])
+ (watch [it state stream]
+ (let [objects (wsh/lookup-page-objects state)
+ page-id (:current-page-id state)
+ id (get-in state [:workspace-local :edition])
old-content (get-in state [:workspace-local :edit-path id :old-content])]
(if (some? old-content)
- (let [shape (get-in state (st/get-path state))
- page-id (:current-page-id state)
+ (let [shape (get-in state (st/get-path state))
[rch uch] (generate-path-changes objects page-id shape old-content (:content shape))]
- (rx/of (dch/commit-changes rch uch {:commit-local? true})))
+ (rx/of (dch/commit-changes {:redo-changes rch
+ :undo-changes uch
+ :origin it})))
(rx/empty)))))))
diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs
index 1931a8d27..75c2f2202 100644
--- a/frontend/src/app/main/data/workspace/path/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/path/drawing.cljs
@@ -360,3 +360,13 @@
(and id (= :move mode)) (rx/of (common/finish-path "change-edit-mode"))
(and id (= :draw mode)) (rx/of (start-draw-mode))
:else (rx/empty))))))
+
+(defn reset-last-handler
+ []
+ (ptk/reify ::reset-last-handler
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [id (st/get-path-id state)]
+ (-> state
+ (assoc-in [:workspace-local :edit-path id :prev-handler] nil))))))
+
diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs
index b2217ea5e..9f5b3b639 100644
--- a/frontend/src/app/main/data/workspace/path/edition.cljs
+++ b/frontend/src/app/main/data/workspace/path/edition.cljs
@@ -47,7 +47,7 @@
(defn apply-content-modifiers []
(ptk/reify ::apply-content-modifiers
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)
id (st/get-path-id state)
@@ -65,9 +65,13 @@
[rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)]
(if (empty? new-content)
- (rx/of (dch/commit-changes rch uch {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rch
+ :undo-changes uch
+ :origin it})
dwc/clear-edition-mode)
- (rx/of (dch/commit-changes rch uch {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rch
+ :undo-changes uch
+ :origin it})
(selection/update-selection point-change)
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))))
@@ -133,7 +137,7 @@
[position shift?]
(ptk/reify ::start-move-path-point
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [id (get-in state [:workspace-local :edition])
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
selected? (contains? selected-points position)]
@@ -147,7 +151,7 @@
[start-position]
(ptk/reify ::drag-selected-points
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [stopper (->> stream (rx/filter ms/mouse-up?))
id (get-in state [:workspace-local :edition])
snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled])
@@ -202,7 +206,7 @@
state)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [id (get-in state [:workspace-local :edition])
current-move (get-in state [:workspace-local :edit-path id :current-move])]
(if (= same-event current-move)
@@ -236,7 +240,7 @@
[index prefix]
(ptk/reify ::start-move-handler
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [id (get-in state [:workspace-local :edition])
cx (d/prefix-keyword prefix :x)
cy (d/prefix-keyword prefix :y)
@@ -292,7 +296,7 @@
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [mode (get-in state [:workspace-local :edit-path id :edit-mode])]
(rx/concat
(rx/of (undo/start-path-undo))
@@ -322,5 +326,5 @@
(update-in (st/get-path state :content) upt/split-segments #{from-p to-p} t))))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/of (changes/save-path-content {:preserve-move-to true})))))
diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs
index 2470f2bb3..b88ee5268 100644
--- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs
@@ -40,8 +40,8 @@
:command "p"
:fn #(st/emit! (drp/change-edit-mode :draw))}
- :add-node {:tooltip "+"
- :command "+"
+ :add-node {:tooltip (ds/shift "+")
+ :command "shift++"
:fn #(st/emit! (drp/add-node))}
:delete-node {:tooltip (ds/supr)
@@ -88,7 +88,30 @@
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:fn #(st/emit! (drp/redo-path))}
+ ;; ZOOM
+
+ :increase-zoom {:tooltip "+"
+ :command "+"
+ :fn #(st/emit! (dw/increase-zoom nil))}
+
+ :decrease-zoom {:tooltip "-"
+ :command "-"
+ :fn #(st/emit! (dw/decrease-zoom nil))}
+
+ :reset-zoom {:tooltip (ds/shift "0")
+ :command "shift+0"
+ :fn #(st/emit! dw/reset-zoom)}
+
+ :fit-all {:tooltip (ds/shift "1")
+ :command "shift+1"
+ :fn #(st/emit! dw/zoom-to-fit-all)}
+
+ :zoom-selected {:tooltip (ds/shift "2")
+ :command "shift+2"
+ :fn #(st/emit! dw/zoom-to-selected-shape)}
+
;; Arrow movement
+
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
:command "shift+up"
:fn #(st/emit! (drp/move-selected :up true))}
diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs
index a6771a54f..ddd2ed3f2 100644
--- a/frontend/src/app/main/data/workspace/path/tools.cljs
+++ b/frontend/src/app/main/data/workspace/path/tools.cljs
@@ -25,7 +25,7 @@
([points tool-fn]
(ptk/reify ::process-path-tool
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)
id (st/get-path-id state)
page-id (:current-page-id state)
@@ -37,7 +37,9 @@
(let [new-content (-> (tool-fn (:content shape) points)
(ups/close-subpaths))
[rch uch] (changes/generate-path-changes objects page-id shape (:content shape) new-content)]
- (rx/of (dch/commit-changes rch uch {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rch
+ :undo-changes uch
+ :origin it})
(when (empty? new-content)
dwc/clear-edition-mode)))))))))
diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs
index 7b5d4433b..3228e0e5e 100644
--- a/frontend/src/app/main/data/workspace/persistence.cljs
+++ b/frontend/src/app/main/data/workspace/persistence.cljs
@@ -14,13 +14,16 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
+ [app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.messages :as dm]
- [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.changes :as dch]
+ [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.selection :as dws]
+ [app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.svg-upload :as svg]
+ [app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.avatars :as avatars]
@@ -33,9 +36,11 @@
[app.util.uri :as uu]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
+ [clojure.set :as set]
[cuerdas.core :as str]
[potok.core :as ptk]
- [promesa.core :as p]))
+ [promesa.core :as p]
+ [tubax.core :as tubax]))
(declare persist-changes)
(declare persist-sychronous-changes)
@@ -86,9 +91,11 @@
(rx/tap on-dirty)
(rx/buffer-until notifier)
(rx/filter (complement empty?))
- (rx/map (fn [buf] {:file-id file-id
- :changes (into [] (mapcat :changes) buf)}))
- (rx/map persist-changes)
+ (rx/map (fn [buf]
+ (->> (into [] (comp (map #(assoc % :id (uuid/next)))
+ (map #(assoc % :file-id file-id)))
+ buf)
+ (persist-changes file-id))))
(rx/tap on-saving)
(rx/take-until (rx/delay 100 stoper)))
(->> stream
@@ -109,27 +116,25 @@
(on-saved))))))))
(defn persist-changes
- [{:keys [file-id changes]}]
+ [file-id changes]
(us/verify ::us/uuid file-id)
(ptk/reify ::persist-changes
ptk/UpdateEvent
(update [_ state]
- (let [conj (fnil conj [])
- chng {:id (uuid/next)
- :changes changes}]
- (update-in state [:workspace-persistence :queue] conj chng)))
+ (let [conj (fnil conj [])
+ into* (fnil into [])]
+ (update-in state [:workspace-persistence :queue] into* changes)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [sid (:session-id state)
file (get state :workspace-file)
queue (get-in state [:workspace-persistence :queue] [])
- xf-cat (comp (mapcat :changes))
params {:id (:id file)
:revn (:revn file)
:session-id sid
- :changes (into [] xf-cat queue)}
+ :changes-with-metadata (into [] queue)}
ids (into #{} (map :id) queue)
@@ -172,7 +177,7 @@
(us/verify ::us/uuid file-id)
(ptk/reify ::persist-synchronous-changes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [sid (:session-id state)
file (get-in state [:workspace-libraries file-id])
@@ -211,8 +216,7 @@
(if (= file-id (:current-file-id state))
(-> state
(update-in [:workspace-file :revn] max revn)
- (update :workspace-data cp/process-changes changes)
- (update-in [:workspace-file :data] cp/process-changes changes))
+ (update :workspace-data cp/process-changes changes))
(-> state
(update-in [:workspace-libraries file-id :revn] max revn)
(update-in [:workspace-libraries file-id :data]
@@ -256,34 +260,20 @@
[project-id file-id]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(->> (rx/zip (rp/query :file {:id file-id})
(rp/query :team-users {:file-id file-id})
(rp/query :project {:id project-id})
(rp/query :file-libraries {:file-id file-id}))
- (rx/first)
- (rx/map (fn [bundle] (apply bundle-fetched bundle)))))))
-
-(defn- bundle-fetched
- [file users project libraries]
- (ptk/reify ::bundle-fetched
- IDeref
- (-deref [_]
- {:file file
- :users users
- :project project
- :libraries libraries})
-
- ptk/UpdateEvent
- (update [_ state]
- (assoc state
- :users (d/index-by :id users)
- :workspace-undo {}
- :workspace-project project
- :workspace-file file
- :workspace-data (:data file)
- :workspace-libraries (d/index-by :id libraries)))))
-
+ (rx/take 1)
+ (rx/map (fn [[file users project libraries]]
+ {:file file
+ :users users
+ :project project
+ :libraries libraries}))
+ (rx/mapcat (fn [{:keys [project] :as bundle}]
+ (rx/of (ptk/data-event ::bundle-fetched bundle)
+ (df/load-team-fonts (:team-id project)))))))))
;; --- Set File shared
@@ -296,7 +286,7 @@
(assoc-in state [:workspace-file :is-shared] is-shared))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [params {:id id :is-shared is-shared}]
(->> (rp/mutation :set-file-shared params)
(rx/ignore))))))
@@ -311,8 +301,8 @@
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
- (watch [_ state stream]
- (->> (rp/query :shared-files params)
+ (watch [it state stream]
+ (->> (rp/query :team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))
(defn shared-files-fetched
@@ -331,7 +321,7 @@
[file-id library-id]
(ptk/reify ::link-file-to-library
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
params {:file-id file-id
:library-id library-id}]
@@ -343,44 +333,24 @@
[file-id library-id]
(ptk/reify ::unlink-file-from-library
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [unlinked #(d/dissoc-in % [:workspace-libraries library-id])
params {:file-id file-id
:library-id library-id}]
(->> (rp/mutation :unlink-file-from-library params)
(rx/map (constantly unlinked)))))))
-;; --- Fetch Pages
-
-(declare page-fetched)
-
-(defn fetch-page
- [page-id]
- (us/verify ::us/uuid page-id)
- (ptk/reify ::fetch-pages
- ptk/WatchEvent
- (watch [_ state s]
- (->> (rp/query :page {:id page-id})
- (rx/map page-fetched)))))
-
-(defn page-fetched
- [{:keys [id] :as page}]
- (us/verify ::page page)
- (ptk/reify ::page-fetched
- IDeref
- (-deref [_] page)
-
- ptk/UpdateEvent
- (update [_ state]
- (assoc-in state [:workspace-pages id] page))))
-
;; --- Upload File Media objects
(defn parse-svg
[[name text]]
- (->> (rp/query! :parsed-svg {:data text})
- (rx/map #(assoc % :name name))))
+ (try
+ (->> (rx/of (-> (tubax/xml->clj text)
+ (assoc :name name))))
+
+ (catch :default err
+ (rx/throw {:type :svg-parser}))))
(defn fetch-svg [name uri]
(->> (http/send! {:method :get :uri uri :mode :no-cors})
@@ -499,7 +469,7 @@
(us/assert ::process-media-objects params)
(ptk/reify ::process-media-objects
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
@@ -546,7 +516,7 @@
(us/assert ::clone-media-objects-params params)
(ptk/reify ::clone-media-objects
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
@@ -573,3 +543,109 @@
(update-in [:workspace-file :pages] #(filterv (partial not= id) %))
(update :workspace-pages dissoc id)))
+(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail))
+
+(defn remove-thumbnails
+ [ids]
+ (ptk/reify ::remove-thumbnails
+ ptk/WatchEvent
+ (watch [_ state stream]
+ ;; Removes the thumbnail while it's regenerated
+ (rx/of (dch/update-shapes
+ ids
+ #(dissoc % :thumbnail)
+ {:save-undo? false})))))
+
+(defn update-frame-thumbnail
+ [frame-id]
+ (ptk/event ::update-frame-thumbnail {:frame-id frame-id}))
+
+(defn- extract-frame-changes
+ "Process a changes set in a commit to extract the frames that are channging"
+ [[event [old-objects new-objects]]]
+ (let [changes (-> event deref :changes)
+
+ extract-ids
+ (fn [{type :type :as change}]
+ (case type
+ :add-obj [(:id change)]
+ :mod-obj [(:id change)]
+ :del-obj [(:id change)]
+ :reg-objects (:shapes change)
+ :mov-objects (:shapes change)
+ []))
+
+ get-frame-id
+ (fn [id]
+ (let [shape (or (get new-objects id)
+ (get old-objects id))]
+
+ (or (and (= :frame (:type shape)) id)
+ (:frame-id shape))))
+
+ ;; Extracts the frames and then removes nils and the root frame
+ xform (comp (mapcat extract-ids)
+ (map get-frame-id)
+ (remove nil?)
+ (filter #(not= uuid/zero %)))]
+
+ (into #{} xform changes)))
+
+(defn thumbnail-change?
+ "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process"
+ [event]
+ (let [changes (-> event deref :changes)
+
+ is-thumbnail-op?
+ (fn [{type :type attr :attr}]
+ (and (= type :set)
+ (= attr :thumbnail)))
+
+ is-thumbnail-change?
+ (fn [change]
+ (and (= (:type change) :mod-obj)
+ (->> change :operations (every? is-thumbnail-op?))))]
+
+ (->> changes (every? is-thumbnail-change?))))
+
+(defn watch-state-changes []
+ (ptk/reify ::watch-state-changes
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [stopper (->> stream
+ (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
+ (= ::watch-state-changes (ptk/type %)))))
+
+ objects-stream (->> (rx/concat
+ (rx/of nil)
+ (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}))
+ ;; We need to keep the old-objects so we can check the frame for the
+ ;; deleted objects
+ (rx/buffer 2 1))
+
+ frame-changes (->> stream
+ (rx/filter dch/commit-changes?)
+ (rx/filter (comp not thumbnail-change?))
+ (rx/with-latest-from objects-stream)
+ (rx/map extract-frame-changes))
+
+ frames (-> state wsh/lookup-page-objects cp/select-frames)
+ no-thumb-frames (->> frames
+ (filter (comp nil? :thumbnail))
+ (mapv :id))]
+
+ (rx/concat
+ (->> (rx/from no-thumb-frames)
+ (rx/map #(update-frame-thumbnail %)))
+
+ ;; We remove the thumbnails inmediately but defer their generation
+ (rx/merge
+ (->> frame-changes
+ (rx/take-until stopper)
+ (rx/map #(remove-thumbnails %)))
+
+ (->> frame-changes
+ (rx/take-until stopper)
+ (rx/buffer-until (->> frame-changes (rx/debounce 1000)))
+ (rx/flat-map #(reduce set/union %))
+ (rx/map #(update-frame-thumbnail %)))))))))
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index f73e22467..52d36ee35 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -378,11 +378,12 @@
(def duplicate-selected
(ptk/reify ::duplicate-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
delta (gpt/point 0 0)
+
unames (dwc/retrieve-used-names objects)
rchanges (->> (prepare-duplicate-changes objects page-id unames selected delta)
@@ -396,7 +397,9 @@
(map #(get-in % [:obj :id]))
(into (d/ordered-set)))]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(select-shapes selected))))))
(defn change-hover-state
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 2ca3c32cc..7d2fe9bf4 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -33,7 +33,7 @@
:toggle-assets {:tooltip (ds/alt "I")
:command (ds/a-mod "i")
:fn #(st/emit! (dw/go-to-layout :assets))}
-
+
:toggle-history {:tooltip (ds/alt "H")
:command (ds/a-mod "h")
:fn #(st/emit! (dw/go-to-layout :document-history))}
@@ -45,7 +45,7 @@
:toggle-rules {:tooltip (ds/meta-shift "R")
:command (ds/c-mod "shift+r")
:fn #(st/emit! (dw/toggle-layout-flags :rules))}
-
+
:select-all {:tooltip (ds/meta "A")
:command (ds/c-mod "a")
:fn #(st/emit! (dw/select-all))}
@@ -61,7 +61,11 @@
:toggle-alignment {:tooltip (ds/meta "\\")
:command (ds/c-mod "\\")
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
-
+
+ :toggle-scale-text {:tooltip "K"
+ :command "k"
+ :fn #(st/emit! (dw/toggle-layout-flags :scale-text))}
+
:increase-zoom {:tooltip "+"
:command "+"
:fn #(st/emit! (dw/increase-zoom nil))}
@@ -69,7 +73,7 @@
:decrease-zoom {:tooltip "-"
:command "-"
:fn #(st/emit! (dw/decrease-zoom nil))}
-
+
:group {:tooltip (ds/meta "G")
:command (ds/c-mod "g")
:fn #(st/emit! dw/group-selected)}
@@ -155,8 +159,8 @@
:command "c"
:fn #(st/emit! (dwd/select-for-drawing :comments))}
- :insert-image {:tooltip "K"
- :command "k"
+ :insert-image {:tooltip (ds/shift "K")
+ :command "shift+k"
:fn #(-> "image-upload" dom/get-element dom/click)}
:copy {:tooltip (ds/meta "C")
@@ -169,7 +173,8 @@
:paste {:tooltip (ds/meta "V")
:disabled true
- :command (ds/c-mod "v")}
+ :command (ds/c-mod "v")
+ :fn (constantly nil)}
:delete {:tooltip (ds/supr)
:command ["del" "backspace"]
diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs
index c7efb8627..0710aba55 100644
--- a/frontend/src/app/main/data/workspace/state_helpers.cljs
+++ b/frontend/src/app/main/data/workspace/state_helpers.cljs
@@ -6,7 +6,8 @@
(ns app.main.data.workspace.state-helpers
(:require
- [app.common.data :as d]))
+ [app.common.data :as d]
+ [app.common.pages :as cp]))
(defn lookup-page-objects
([state]
@@ -25,10 +26,18 @@
(get-in state [:workspace-data :components component-id :objects])))
(defn lookup-selected
- [state]
- (let [selected (get-in state [:workspace-local :selected])
- objects (lookup-page-objects state)
- is-present? (fn [id] (contains? objects id))]
- (into (d/ordered-set)
- (filter is-present?)
- selected)))
+ ([state]
+ (lookup-selected state nil))
+
+ ([state {:keys [omit-blocked?]
+ :or {omit-blocked? false}}]
+ (let [objects (lookup-page-objects state)
+ selected (->> (get-in state [:workspace-local :selected])
+ (cp/clean-loops objects))
+ selectable? (fn [id]
+ (and (contains? objects id)
+ (or (not omit-blocked?)
+ (not (get-in objects [id :blocked] false)))))]
+ (into (d/ordered-set)
+ (filter selectable?)
+ selected))))
diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs
index 6d34597f3..f2828eb3f 100644
--- a/frontend/src/app/main/data/workspace/svg_upload.cljs
+++ b/frontend/src/app/main/data/workspace/svg_upload.cljs
@@ -387,7 +387,7 @@
[svg-data file-id position]
(ptk/reify ::svg-uploaded
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
;; Once the SVG is uploaded, we need to extract all the bitmap
;; images and upload them separatelly, then proceed to create
;; all shapes.
@@ -414,7 +414,7 @@
[svg-data {:keys [x y] :as position}]
(ptk/reify ::create-svg-shapes
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(try
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -464,7 +464,9 @@
rchanges (conj rchanges reg-objects-action)]
- (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})
+ (rx/of (dch/commit-changes {:redo-changes rchanges
+ :undo-changes uchanges
+ :origin it})
(dwc/select-shapes (d/ordered-set root-id))))
(catch :default e
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 707b06d54..e726917de 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -23,6 +23,7 @@
[app.main.snap :as snap]
[app.main.store :as st]
[app.main.streams :as ms]
+ [app.util.path.shapes-to-path :as ups]
[beicon.core :as rx]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
@@ -72,6 +73,27 @@
:bottom-left [ex sy])]
(gpt/point x y)))
+(defn- fix-init-point
+ "Fix the initial point so the resizes are accurate"
+ [initial handler shape]
+ (let [{:keys [x y width height]} (:selrect shape)
+ {:keys [rotation]} shape
+ rotation (or rotation 0)]
+ (if (= rotation 0)
+ (cond-> initial
+ (contains? #{:left :top-left :bottom-left} handler)
+ (assoc :x x)
+
+ (contains? #{:right :top-right :bottom-right} handler)
+ (assoc :x (+ x width))
+
+ (contains? #{:top :top-right :top-left} handler)
+ (assoc :y y)
+
+ (contains? #{:bottom :bottom-right :bottom-left} handler)
+ (assoc :y (+ y height)))
+ initial)))
+
(defn finish-transform []
(ptk/reify ::finish-transform
ptk/UpdateEvent
@@ -81,11 +103,20 @@
;; -- RESIZE
(defn start-resize
[handler initial ids shape]
- (letfn [(resize [shape initial resizing-shapes [point lock? point-snap]]
+ (letfn [(resize [shape initial resizing-shapes layout [point lock? point-snap]]
(let [{:keys [width height]} (:selrect shape)
{:keys [rotation]} shape
+ rotation (or rotation 0)
+
+ initial (fix-init-point initial handler shape)
+
shapev (-> (gpt/point width height))
+ scale-text (:scale-text layout)
+
+ ;; Force lock if the scale text mode is active
+ lock? (or lock? scale-text)
+
;; Vector modifiers depending on the handler
handler-modif (let [[x y] (handler-modifiers handler)] (gpt/point x y))
@@ -119,6 +150,7 @@
{:resize-vector scalev
:resize-origin origin
:resize-transform shape-transform
+ :resize-scale-text scale-text
:resize-transform-inverse shape-transform-inverse}
false))))
@@ -135,7 +167,7 @@
(assoc-in [:workspace-local :transform] :resize)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [initial-position @ms/mouse-position
stoper (rx/filter ms/mouse-up? stream)
layout (:workspace-layout state)
@@ -154,7 +186,7 @@
(rx/switch-map (fn [[point :as current]]
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
(rx/map #(conj current %)))))
- (rx/mapcat (partial resize shape initial-position resizing-shapes))
+ (rx/mapcat (partial resize shape initial-position resizing-shapes layout))
(rx/take-until stoper))
(rx/of (apply-modifiers ids)
(finish-transform))))))))
@@ -169,7 +201,7 @@
(assoc-in [:workspace-local :transform] :rotate)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes)
group-center (gsh/center-selrect group)
@@ -208,30 +240,31 @@
[]
(ptk/reify ::start-move-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [initial (deref ms/mouse-position)
- selected (wsh/lookup-selected state)
+ selected (wsh/lookup-selected state {:omit-blocked? true})
stopper (rx/filter ms/mouse-up? stream)]
- (->> ms/mouse-position
- (rx/take-until stopper)
- (rx/map #(gpt/to-vec initial %))
- (rx/map #(gpt/length %))
- (rx/filter #(> % 1))
- (rx/take 1)
- (rx/with-latest vector ms/mouse-position-alt)
- (rx/mapcat
- (fn [[_ alt?]]
- (if alt?
- ;; When alt is down we start a duplicate+move
- (rx/of (start-move-duplicate initial)
- dws/duplicate-selected)
- ;; Otherwise just plain old move
- (rx/of (start-move initial selected))))))))))
+ (when-not (empty? selected)
+ (->> ms/mouse-position
+ (rx/take-until stopper)
+ (rx/map #(gpt/to-vec initial %))
+ (rx/map #(gpt/length %))
+ (rx/filter #(> % 1))
+ (rx/take 1)
+ (rx/with-latest vector ms/mouse-position-alt)
+ (rx/mapcat
+ (fn [[_ alt?]]
+ (if alt?
+ ;; When alt is down we start a duplicate+move
+ (rx/of (start-move-duplicate initial)
+ dws/duplicate-selected)
+ ;; Otherwise just plain old move
+ (rx/of (start-move initial selected)))))))))))
(defn start-move-duplicate [from-position]
(ptk/reify ::start-move-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(->> stream
(rx/filter (ptk/type? ::dws/duplicate-selected))
(rx/first)
@@ -240,13 +273,14 @@
(defn calculate-frame-for-move [ids]
(ptk/reify ::calculate-frame-for-move
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(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)
moving-shapes (->> ids
+ (cp/clean-loops objects)
(map #(get objects %))
(remove #(= (:frame-id %) frame-id)))
@@ -265,9 +299,11 @@
:index (cp/get-index-in-parent objects (:id shape))
:shapes [(:id shape)]})))]
- (when-not (empty? rch)
+ (when-not (empty? uch)
(rx/of dwu/pop-undo-into-transaction
- (dch/commit-changes rch uch {:commit-local? true})
+ (dch/commit-changes {:redo-changes rch
+ :undo-changes uch
+ :origin it})
(dwu/commit-undo-transaction)
(dwc/expand-collapse frame-id)))))))
@@ -281,10 +317,11 @@
(assoc-in [:workspace-local :transform] :move)))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
- ids (if (nil? ids) (wsh/lookup-selected state) ids)
+ selected (wsh/lookup-selected state {:omit-blocked? true})
+ ids (if (nil? ids) selected ids)
shapes (mapv #(get objects %) ids)
stopper (rx/filter ms/mouse-up? stream)
layout (get state :workspace-layout)
@@ -295,12 +332,15 @@
(rx/take-until stopper)
(rx/map #(gpt/to-vec from-position %)))
- snap-delta (->> position
- (rx/throttle 20)
- (rx/switch-map
- (fn [pos]
- (->> (snap/closest-snap-move page-id shapes objects layout zoom pos)
- (rx/map #(vector pos %))))))]
+ snap-delta (rx/concat
+ ;; We send the nil first so the stream is not waiting for the first value
+ (rx/of nil)
+ (->> position
+ (rx/throttle 20)
+ (rx/switch-map
+ (fn [pos]
+ (->> (snap/closest-snap-move page-id shapes objects layout zoom pos)
+ (rx/map #(vector pos %)))))))]
(if (empty? shapes)
(rx/empty)
(rx/concat
@@ -309,8 +349,7 @@
(rx/map snap/correct-snap-point)
(rx/map start-local-displacement))
- (rx/of (set-modifiers ids)
- (apply-modifiers ids)
+ (rx/of (apply-modifiers ids {:set-modifiers? true})
(calculate-frame-for-move ids)
(finish-transform)))))))))
@@ -359,9 +398,9 @@
state))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(if (= same-event (get-in state [:workspace-local :current-move-selected]))
- (let [selected (wsh/lookup-selected state)
+ (let [selected (wsh/lookup-selected state {:omit-blocked? true})
move-events (->> stream
(rx/filter (ptk/type? ::move-selected))
(rx/filter #(= direction (deref %))))
@@ -379,8 +418,7 @@
(rx/map start-local-displacement))
(rx/of (move-selected direction shift?)))
- (rx/of (set-modifiers selected)
- (apply-modifiers selected)
+ (rx/of (apply-modifiers selected {:set-modifiers? true})
(finish-transform))))
(rx/empty))))))
@@ -399,6 +437,8 @@
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
+ ids (->> ids (into #{} (remove #(get-in objects [% :blocked] false))))
+
not-frame-id?
(fn [shape-id]
(let [shape (get objects shape-id)]
@@ -413,40 +453,41 @@
ids-with-children (concat ids (mapcat #(cp/get-children % objects)
(filter not-frame-id? ids)))]
- (d/update-in-when state [:workspace-data :pages-index page-id :objects]
- #(reduce update-shape % ids-with-children)))))))
+ (update state :workspace-modifiers
+ #(reduce update-shape % ids-with-children)))))))
;; Set-rotation is custom because applies different modifiers to each
;; shape adjusting their position.
(defn set-rotation
- ([delta-rotation shapes]
- (set-rotation delta-rotation shapes (-> shapes gsh/selection-rect gsh/center-selrect)))
+ ([angle shapes]
+ (set-rotation angle shapes (-> shapes gsh/selection-rect gsh/center-selrect)))
- ([delta-rotation shapes center]
- (letfn [(rotate-shape [objects angle shape center]
- (update-in objects [(:id shape) :modifiers] merge (gsh/rotation-modifiers center shape angle)))
+ ([angle shapes center]
+ (ptk/reify ::set-rotation
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [objects (wsh/lookup-page-objects state)
+ id->obj #(get objects %)
+ get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects)))
- (rotate-around-center [objects angle center shapes]
- (reduce #(rotate-shape %1 angle %2 center) objects shapes))
+ shapes (->> shapes (into [] (remove #(get % :blocked false))))
- (set-rotation [objects]
- (let [id->obj #(get objects %)
- get-children (fn [shape] (map id->obj (cp/get-children (:id shape) objects)))
- shapes (concat shapes (mapcat get-children shapes))]
- (rotate-around-center objects delta-rotation center shapes)))]
+ shapes (->> shapes (mapcat get-children) (concat shapes))
- (ptk/reify ::set-rotation
- ptk/UpdateEvent
- (update [_ state]
- (let [page-id (:current-page-id state)]
- (d/update-in-when state [:workspace-data :pages-index page-id :objects] set-rotation)))))))
+ update-shape
+ (fn [modifiers shape]
+ (let [rotate-modifiers (gsh/rotation-modifiers shape center angle)]
+ (assoc-in modifiers [(:id shape) :modifiers] rotate-modifiers)))]
+ (-> state
+ (update :workspace-modifiers
+ #(reduce update-shape % shapes))))))))
(defn increase-rotation [ids rotation]
(ptk/reify ::increase-rotation
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
@@ -458,18 +499,46 @@
(rx/of (apply-modifiers ids)))))))
(defn apply-modifiers
- [ids]
- (us/verify (s/coll-of uuid?) ids)
- (ptk/reify ::apply-modifiers
- ptk/WatchEvent
- (watch [_ state stream]
- (let [objects (wsh/lookup-page-objects state)
- children-ids (->> ids (mapcat #(cp/get-children % objects)))
- ids-with-children (d/concat [] children-ids ids)]
- (rx/of (dwu/start-undo-transaction)
- (dch/update-shapes ids-with-children gsh/transform-shape {:reg-objects? true})
- (clear-local-transform)
- (dwu/commit-undo-transaction))))))
+ ([ids]
+ (apply-modifiers ids nil))
+
+ ([ids {:keys [set-modifiers?]
+ :or {set-modifiers? false}}]
+ (us/verify (s/coll-of uuid?) ids)
+ (ptk/reify ::apply-modifiers
+ ptk/WatchEvent
+ (watch [it state stream]
+ (let [objects (wsh/lookup-page-objects state)
+ children-ids (->> ids (mapcat #(cp/get-children % objects)))
+ ids-with-children (d/concat [] children-ids ids)
+
+ state (if set-modifiers?
+ (ptk/update (set-modifiers ids) state)
+ state)
+ object-modifiers (get state :workspace-modifiers)]
+
+ (rx/of (dwu/start-undo-transaction)
+ (dch/update-shapes
+ ids-with-children
+ (fn [shape]
+ (-> shape
+ (merge (get object-modifiers (:id shape)))
+ (gsh/transform-shape)))
+ {:reg-objects? true
+ ;; Attributes that can change in the transform. This way we don't have to check
+ ;; all the attributes
+ :attrs [:selrect :points
+ :x :y
+ :width :height
+ :content
+ :transform
+ :transform-inverse
+ :rotation
+ :flip-x
+ :flip-y]
+ })
+ (clear-local-transform)
+ (dwu/commit-undo-transaction)))))))
;; --- Update Dimensions
@@ -508,7 +577,7 @@
#(reduce update-shape % ids))))
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
ids (d/concat [] ids (mapcat #(cp/get-children % objects) ids))]
@@ -517,9 +586,9 @@
(defn flip-horizontal-selected []
(ptk/reify ::flip-horizontal-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)
- selected (wsh/lookup-selected state)
+ selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected)
selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape)))
origin (gpt/point (:x selrect) (+ (:y selrect) (/ (:height selrect) 2)))]
@@ -534,9 +603,9 @@
(defn flip-vertical-selected []
(ptk/reify ::flip-vertical-selected
ptk/WatchEvent
- (watch [_ state stream]
+ (watch [it state stream]
(let [objects (wsh/lookup-page-objects state)
- selected (wsh/lookup-selected state)
+ selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected)
selrect (gsh/selection-rect (->> shapes (map gsh/transform-shape)))
origin (gpt/point (+ (:x selrect) (/ (:width selrect) 2)) (:y selrect))]
@@ -561,4 +630,13 @@
ptk/UpdateEvent
(update [_ state]
(-> state
+ (dissoc :workspace-modifiers)
(update :workspace-local dissoc :modifiers :current-move-selected)))))
+
+(defn selected-to-path
+ []
+ (ptk/reify ::selected-to-path
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [ids (wsh/lookup-selected state {:omit-blocked? true})]
+ (rx/of (dch/update-shapes ids ups/convert-to-path))))))
diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs
index f0e752556..a63225870 100644
--- a/frontend/src/app/main/data/workspace/undo.cljs
+++ b/frontend/src/app/main/data/workspace/undo.cljs
@@ -27,8 +27,8 @@
;; Undo / Redo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(s/def ::undo-changes ::cp/changes)
-(s/def ::redo-changes ::cp/changes)
+(s/def ::undo-changes ::spec/changes)
+(s/def ::redo-changes ::spec/changes)
(s/def ::undo-entry
(s/keys :req-un [::undo-changes ::redo-changes]))
diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs
index 1569144f3..aa5e08c21 100644
--- a/frontend/src/app/main/exports.cljs
+++ b/frontend/src/app/main/exports.cljs
@@ -121,7 +121,7 @@
(mf/defc page-svg
{::mf/wrap [mf/memo]}
- [{:keys [data width height] :as props}]
+ [{:keys [data width height thumbnails?] :as props}]
(let [objects (:objects data)
root (get objects uuid/zero)
shapes (->> (:shapes root)
@@ -146,11 +146,23 @@
:xmlns "http://www.w3.org/2000/svg"}
[:& background {:vbox dim :color background-color}]
(for [item shapes]
- (if (= (:type item) :frame)
- [:& frame-wrapper {:shape item
- :key (:id item)}]
- [:& shape-wrapper {:shape item
- :key (:id item)}]))]))
+ (let [frame? (= (:type item) :frame)]
+ (cond
+ (and frame? thumbnails? (some? (:thumbnail item)))
+ [:image {:xlinkHref (:thumbnail item)
+ :x (:x item)
+ :y (:y item)
+ :width (:width item)
+ :height (:height item)
+ ;; DEBUG
+ ;; :style {:filter "sepia(1)"}
+ }]
+ frame?
+ [:& frame-wrapper {:shape item
+ :key (:id item)}]
+ :else
+ [:& shape-wrapper {:shape item
+ :key (:id item)}])))]))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs
index 27a208b9b..e9030abb3 100644
--- a/frontend/src/app/main/fonts.cljs
+++ b/frontend/src/app/main/fonts.cljs
@@ -8,16 +8,22 @@
"Fonts management and loading logic."
(:require-macros [app.main.fonts :refer [preload-gfonts]])
(:require
+ [app.config :as cf]
[app.common.data :as d]
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.timers :as ts]
+ [app.util.logging :as log]
+ [lambdaisland.uri :as u]
+ [goog.events :as gev]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[okulary.core :as l]
[promesa.core :as p]))
+(log/set-level! :trace)
+
(def google-fonts
(preload-gfonts "fonts/gfonts.2020.04.23.json"))
@@ -38,22 +44,23 @@
{:id "blackitalic" :name "black (italic)" :weight "900" :style "italic"}]}])
(defonce fontsdb (l/atom {}))
-(defonce fontsview (l/atom {}))
+(defonce fonts (l/atom []))
-(defn- materialize-fontsview
- [db]
- (reset! fontsview (reduce-kv (fn [acc k v]
- (assoc acc k (sort-by :name v)))
- {}
- (group-by :backend (vals db)))))
(add-watch fontsdb "main"
(fn [_ _ _ db]
- (ts/schedule #(materialize-fontsview db))))
+ (->> (vals db)
+ (sort-by :name)
+ (map-indexed #(assoc %2 :index %1))
+ (vec)
+ (reset! fonts))))
(defn register!
[backend fonts]
- (let [fonts (map #(assoc % :backend backend) fonts)]
- (swap! fontsdb #(merge % (d/index-by :id fonts)))))
+ (swap! fontsdb
+ (fn [db]
+ (let [db (reduce-kv #(cond-> %1 (= backend (:backend %3)) (dissoc %2)) db db)
+ fonts (map #(assoc % :backend backend) fonts)]
+ (merge db (d/index-by :id fonts))))))
(register! :builtin local-fonts)
(register! :google google-fonts)
@@ -67,13 +74,15 @@
(defn resolve-fonts
[backend]
- (get @fontsview backend))
+ (get @fonts backend))
-;; --- Fonts Loader
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; FONTS LOADING
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce loaded (l/atom #{}))
-(defn- create-link-node
+(defn- create-link-element
[uri]
(let [node (.createElement js/document "link")]
(unchecked-set node "href" uri)
@@ -81,32 +90,106 @@
(unchecked-set node "type" "text/css")
node))
-(defn gfont-url [family variants]
+(defn- create-style-element
+ [css]
+ (let [node (.createElement js/document "style")]
+ (unchecked-set node "innerHTML" css)
+ node))
+
+(defn- load-font-css!
+ "Creates a link element and attaches it to the dom for correctly
+ load external css resource."
+ [url on-loaded]
+ (let [node (create-link-element url)
+ head (.-head ^js js/document)]
+ (gev/listenOnce node "load" (fn [event]
+ (when (fn? on-loaded)
+ (on-loaded))))
+ (dom/append-child! head node)))
+
+(defn- add-font-css!
+ "Creates a style element and attaches it to the dom."
+ [css]
+ (let [head (.-head ^js js/document)]
+ (->> (create-style-element css)
+ (dom/append-child! head))))
+
+;; --- LOADER: BUILTIN
+
+(defmulti ^:private load-font :backend)
+
+(defmethod load-font :default
+ [{:keys [backend] :as font}]
+ (log/warn :msg "no implementation found for" :backend backend))
+
+(defmethod load-font :builtin
+ [{:keys [id ::on-loaded] :as font}]
+ (log/debug :action "load-font" :font-id id :backend "builtin")
+ ;; (js/console.log "[debug:fonts]: loading builtin font" id)
+ (when (fn? on-loaded)
+ (on-loaded id)))
+
+;; --- LOADER: GOOGLE
+
+(defn generate-gfonts-url
+ [{:keys [family variants]}]
(let [base (str "https://fonts.googleapis.com/css?family=" family)
variants (str/join "," (map :id variants))]
(str base ":" variants "&display=block")))
-(defmulti ^:private load-font :backend)
-
-(defmethod load-font :builtin
- [{:keys [id ::on-loaded] :as font}]
- (js/console.log "[debug:fonts]: loading builtin font" id)
- (when (fn? on-loaded)
- (on-loaded id)))
-
(defmethod load-font :google
[{:keys [id family variants ::on-loaded] :as font}]
(when (exists? js/window)
- (js/console.log "[debug:fonts]: loading google font" id)
- (let [node (create-link-node (gfont-url family variants))]
- (.addEventListener node "load" (fn [event] (when (fn? on-loaded)
- (on-loaded id))))
- (.append (.-head js/document) node)
+ (log/debug :action "load-font" :font-id id :backend "google")
+ (let [url (generate-gfonts-url font)]
+ (load-font-css! url (partial on-loaded id))
nil)))
-(defmethod load-font :default
- [{:keys [backend] :as font}]
- (js/console.warn "no implementation found for" backend))
+;; --- LOADER: CUSTOM
+
+(def font-css-template
+ "@font-face {
+ font-family: '%(family)s';
+ font-style: %(style)s;
+ font-weight: %(weight)s;
+ font-display: block;
+ src: url(%(woff2-uri)s) format('woff2'),
+ url(%(woff1-uri)s) format('woff'),
+ url(%(ttf-uri)s) format('ttf'),
+ url(%(otf-uri)s) format('otf');
+ }")
+
+(defn- asset-id->uri
+ [asset-id]
+ (str (u/join cf/public-uri "assets/by-id/" asset-id)))
+
+(defn generate-custom-font-variant-css
+ [family variant]
+ (str/fmt font-css-template
+ {:family family
+ :style (:style variant)
+ :weight (:weight variant)
+ :woff2-uri (asset-id->uri (::woff2-file-id variant))
+ :woff1-uri (asset-id->uri (::woff1-file-id variant))
+ :ttf-uri (asset-id->uri (::ttf-file-id variant))
+ :otf-uri (asset-id->uri (::otf-file-id variant))}))
+
+(defn- generate-custom-font-css
+ [{:keys [family variants] :as font}]
+ (->> variants
+ (map #(generate-custom-font-variant-css family %))
+ (str/join "\n")))
+
+(defmethod load-font :custom
+ [{:keys [id family variants ::on-loaded] :as font}]
+ (when (exists? js/window)
+ (js/console.log "[debug:fonts]: loading custom font" id)
+ (let [css (generate-custom-font-css font)]
+ (add-font-css! css))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; LOAD API
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ensure-loaded!
([id]
@@ -119,7 +202,8 @@
(load-font (assoc font ::on-loaded on-loaded))
(swap! loaded conj id)))))
-(defn ready [cb]
+(defn ready
+ [cb]
(-> (obj/get-in js/document ["fonts" "ready"])
(p/then cb)))
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 5f3b09cc8..db33ad5aa 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -1,3 +1,4 @@
+
;; 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/.
@@ -44,6 +45,33 @@
(def dashboard-local
(l/derived :dashboard-local st/state))
+(def dashboard-fonts
+ (l/derived :dashboard-fonts st/state))
+
+(def dashboard-projects
+ (l/derived :dashboard-projects st/state))
+
+(def dashboard-files
+ (l/derived :dashboard-files st/state))
+
+(def dashboard-shared-files
+ (l/derived :dashboard-shared-files st/state))
+
+(def dashboard-search-result
+ (l/derived :dashboard-search-result st/state))
+
+(def dashboard-team
+ (l/derived (fn [state]
+ (let [team-id (:current-team-id state)]
+ (get-in state [:teams team-id])))
+ st/state))
+
+(def dashboard-team-stats
+ (l/derived :dashboard-team-stats st/state))
+
+(def dashboard-team-members
+ (l/derived :dashboard-team-members st/state))
+
(def dashboard-selected-project
(l/derived (fn [state]
(get-in state [:dashboard-local :selected-project]))
@@ -51,17 +79,14 @@
(def dashboard-selected-files
(l/derived (fn [state]
- (get-in state [:dashboard-local :selected-files] #{}))
- st/state))
-
-(def dashboard-selected-file-objs
- (l/derived (fn [state]
- (let [dashboard-local (get state :dashboard-local)
- selected-project (get dashboard-local :selected-project)
- selected-files (get dashboard-local :selected-files #{})]
- (map #(get-in state [:files selected-project %])
- selected-files)))
- st/state))
+ (let [get-file #(get-in state [:dashboard-files %])
+ sim-file #(select-keys % [:id :name :project-id])
+ selected (get-in state [:dashboard-local :selected-files])
+ xform (comp (map get-file)
+ (map sim-file))]
+ (->> (into #{} xform selected)
+ (d/index-by :id))))
+ st/state =))
;; ---- Workspace refs
@@ -127,16 +152,17 @@
(def workspace-file
(l/derived (fn [state]
- (when-let [file (:workspace-file state)]
+ (let [file (:workspace-file state)
+ data (:workspace-data state)]
(-> file
(dissoc :data)
- (assoc :pages (get-in file [:data :pages])))))
+ (assoc :pages (:pages data)))))
st/state =))
(def workspace-file-colors
(l/derived (fn [state]
- (when-let [file (:workspace-file state)]
- (->> (get-in file [:data :colors])
+ (when-let [file (:workspace-data state)]
+ (->> (:colors file)
(d/mapm #(assoc %2 :file-id (:id file))))))
st/state))
@@ -147,8 +173,8 @@
(def workspace-file-typography
(l/derived (fn [state]
- (when-let [file (:workspace-file state)]
- (get-in file [:data :typographies])))
+ (when-let [file (:workspace-data state)]
+ (:typographies file)))
st/state))
(def workspace-project
@@ -184,7 +210,10 @@
st/state))
(def workspace-page-objects
- (l/derived :objects workspace-page))
+ (l/derived wsh/lookup-page-objects st/state =))
+
+(def workspace-modifiers
+ (l/derived :workspace-modifiers st/state))
(def workspace-page-options
(l/derived :options workspace-page))
@@ -203,12 +232,21 @@
(l/derived #(get % id) workspace-page-objects))
(defn objects-by-id
- [ids]
- (l/derived (fn [objects]
- (into [] (comp (map #(get objects %))
- (remove nil?))
- ids))
- workspace-page-objects =))
+ ([ids]
+ (objects-by-id ids nil))
+
+ ([ids {:keys [with-modifiers?]
+ :or { with-modifiers? false }}]
+ (l/derived (fn [state]
+ (let [objects (wsh/lookup-page-objects state)
+ modifiers (:workspace-modifiers state)
+ objects (cond-> objects
+ with-modifiers?
+ (cp/merge-modifiers modifiers))
+ xform (comp (map #(get objects %))
+ (remove nil?))]
+ (into [] xform ids)))
+ st/state =)))
(def selected-data
(l/derived #(let [selected (wsh/lookup-selected %)
diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs
index 2a35a53f1..36d935ffb 100644
--- a/frontend/src/app/main/repo.cljs
+++ b/frontend/src/app/main/repo.cljs
@@ -15,7 +15,7 @@
[beicon.core :as rx]
[cuerdas.core :as str]))
-(defn- handle-response
+(defn handle-response
[{:keys [status body] :as response}]
(cond
(= 204 status)
@@ -110,14 +110,6 @@
:response-type :blob})
(rx/mapcat handle-response)))
-(defmethod query :parsed-svg
- [id params]
- (->> (http/send! {:method :post
- :uri (u/join base-uri "api/rpc/query/" (name id))
- :body (http/transit-data params)})
- (rx/map http/conditional-decode-transit)
- (rx/mapcat handle-response)))
-
(derive :upload-file-media-object ::multipart-upload)
(derive :update-profile-photo ::multipart-upload)
(derive :update-team-photo ::multipart-upload)
diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs
index e73f4c777..b8e700b6b 100644
--- a/frontend/src/app/main/snap.cljs
+++ b/frontend/src/app/main/snap.cljs
@@ -19,7 +19,7 @@
[beicon.core :as rx]
[clojure.set :as set]))
-(defonce ^:private snap-accuracy 10)
+(defonce ^:private snap-accuracy 5)
(defonce ^:private snap-path-accuracy 10)
(defonce ^:private snap-distance-accuracy 10)
@@ -284,7 +284,7 @@
([matches other]
(let [merge-coord
(fn [matches other]
-
+
(let [matches (into {} matches)
other (into {} other)
keys (set/union (keys matches) (keys other))]
@@ -305,7 +305,7 @@
(if (< (mth/abs cur-val) (mth/abs other-val))
current
other))
-
+
min-match-coord
(fn [matches]
(if (and (seq matches) (not (empty? matches)))
@@ -337,13 +337,15 @@
"Snaps a position given an old snap to a different position. We use this to provide a temporal
snap while the new is being processed."
[[position [snap-pos snap-delta]]]
- (let [dx (if (not= 0 (:x snap-delta))
- (- (+ (:x snap-pos) (:x snap-delta)) (:x position))
- 0)
- dy (if (not= 0 (:y snap-delta))
- (- (+ (:y snap-pos) (:y snap-delta)) (:y position))
- 0)]
+ (if (some? snap-delta)
+ (let [dx (if (not= 0 (:x snap-delta))
+ (- (+ (:x snap-pos) (:x snap-delta)) (:x position))
+ 0)
+ dy (if (not= 0 (:y snap-delta))
+ (- (+ (:y snap-pos) (:y snap-delta)) (:y position))
+ 0)]
- (cond-> position
- (<= (mth/abs dx) snap-accuracy) (update :x + dx)
- (<= (mth/abs dy) snap-accuracy) (update :y + dy))))
+ (cond-> position
+ (<= (mth/abs dx) snap-accuracy) (update :x + dx)
+ (<= (mth/abs dy) snap-accuracy) (update :y + dy)))
+ position))
diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs
index db162221f..f4f15b941 100644
--- a/frontend/src/app/main/store.cljs
+++ b/frontend/src/app/main/store.cljs
@@ -7,16 +7,13 @@
(ns app.main.store
(:require-macros [app.main.store])
(:require
- [beicon.core :as rx]
- [okulary.core :as l]
- [potok.core :as ptk]
- [cuerdas.core :as str]
[app.common.data :as d]
[app.common.pages :as cp]
- [app.common.pages.helpers :as helpers]
- [app.common.uuid :as uuid]
- [app.util.storage :refer [storage]]
- [app.util.debug :refer [debug? debug-exclude-events logjs]]))
+ [app.util.debug :refer [debug? debug-exclude-events logjs]]
+ [beicon.core :as rx]
+ [cuerdas.core :as str]
+ [okulary.core :as l]
+ [potok.core :as ptk]))
(enable-console-print!)
@@ -26,11 +23,20 @@
(defonce state (ptk/store {:resolve ptk/resolve}))
(defonce stream (ptk/input-stream state))
-(defn ^boolean is-logged?
- [pdata]
- (and (some? pdata)
- (uuid? (:id pdata))
- (not= uuid/zero (:id pdata))))
+(defonce last-events
+ (let [buffer (atom #queue [])
+ remove #{:potok.core/undefined
+ :app.main.data.workspace.notifications/handle-pointer-update}]
+ (->> stream
+ (rx/filter ptk/event?)
+ (rx/map ptk/type)
+ (rx/filter (complement remove))
+ (rx/map str)
+ (rx/dedupe)
+ (rx/buffer 20 1)
+ (rx/subs #(reset! buffer %)))
+
+ buffer))
(when *assert*
(defonce debug-subscription
@@ -53,19 +59,12 @@
[& events]
#(apply ptk/emit! state events))
-(def initial-state
- {:session-id (uuid/next)
- :profile (:profile storage)})
-
-(defn init
- "Initialize the state materialization."
- ([] (init {}))
- ([props]
- (emit! #(merge % initial-state props))))
-
(defn ^:export dump-state []
(logjs "state" @state))
+(defn ^:export dump-buffer []
+ (logjs "state" @last-events))
+
(defn ^:export get-state [str-path]
(let [path (->> (str/split str-path " ")
(map d/read-string))]
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 9098fea1d..601b89ecc 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -12,8 +12,9 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
- [app.main.data.auth :refer [logout]]
+ [app.main.data.users :as du]
[app.main.data.messages :as dm]
+ [app.main.data.events :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.auth :refer [auth]]
@@ -90,6 +91,8 @@
["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects]
["/search" :dashboard-search]
+ ["/fonts" :dashboard-fonts]
+ ["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries]
["/projects/:project-id" :dashboard-files]]
@@ -104,82 +107,85 @@
(mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route] :as props}]
- [:& (mf/provider ctx/current-route) {:value route}
- (case (get-in route [:data :name])
- (:auth-login
- :auth-register
- :auth-register-success
- :auth-recovery-request
- :auth-recovery)
- [:& auth {:route route}]
+ (let [{:keys [data params]} route]
+ [:& (mf/provider ctx/current-route) {:value route}
+ (case (:name data)
+ (:auth-login
+ :auth-register
+ :auth-register-success
+ :auth-recovery-request
+ :auth-recovery)
+ [:& auth {:route route}]
- :auth-verify-token
- [:& verify-token {:route route}]
+ :auth-verify-token
+ [:& verify-token {:route route}]
- (:settings-profile
- :settings-password
- :settings-options
- :settings-feedback)
- [:& settings/settings {:route route}]
+ (:settings-profile
+ :settings-password
+ :settings-options
+ :settings-feedback)
+ [:& settings/settings {:route route}]
- :debug-icons-preview
- (when *assert*
- [:div.debug-preview
- [:h1 "Cursors"]
- [:& c/debug-preview]
- [:h1 "Icons"]
- [:& i/debug-icons-preview]
- ])
+ :debug-icons-preview
+ (when *assert*
+ [:div.debug-preview
+ [:h1 "Cursors"]
+ [:& c/debug-preview]
+ [:h1 "Icons"]
+ [:& i/debug-icons-preview]
+ ])
- (:dashboard-search
- :dashboard-projects
- :dashboard-files
- :dashboard-libraries
- :dashboard-team-members
- :dashboard-team-settings)
- [:*
- #_[:div.modal-wrapper
- [:& app.main.ui.onboarding/release-notes-modal {:version "1.5"}]]
- [:& dashboard {:route route}]]
+ (:dashboard-search
+ :dashboard-projects
+ :dashboard-files
+ :dashboard-libraries
+ :dashboard-fonts
+ :dashboard-font-providers
+ :dashboard-team-members
+ :dashboard-team-settings)
+ [:*
+ #_[:div.modal-wrapper
+ [:& app.main.ui.onboarding/release-notes-modal {:version "1.6"}]]
+ [:& dashboard {:route route}]]
- :viewer
- (let [index (get-in route [:query-params :index])
- token (get-in route [:query-params :token])
- section (get-in route [:query-params :section] :interactions)
- file-id (get-in route [:path-params :file-id])
- page-id (get-in route [:path-params :page-id])]
- [:& fs/fullscreen-wrapper {}
- (if (= section :handoff)
- [:& handoff {:page-id page-id
- :file-id file-id
- :index index
- :token token}]
- [:& viewer-page {:page-id page-id
- :file-id file-id
- :section section
- :index index
- :token token}])])
+ :viewer
+ (let [index (get-in route [:query-params :index])
+ token (get-in route [:query-params :token])
+ section (get-in route [:query-params :section] :interactions)
+ file-id (get-in route [:path-params :file-id])
+ page-id (get-in route [:path-params :page-id])]
+ [:& fs/fullscreen-wrapper {}
+ (if (= section :handoff)
+ [:& handoff {:page-id page-id
+ :file-id file-id
+ :index index
+ :token token}]
+ [:& viewer-page {:page-id page-id
+ :file-id file-id
+ :section section
+ :index index
+ :token token}])])
- :render-object
- (do
- (let [file-id (uuid (get-in route [:path-params :file-id]))
- page-id (uuid (get-in route [:path-params :page-id]))
- object-id (uuid (get-in route [:path-params :object-id]))]
- [:& render/render-object {:file-id file-id
- :page-id page-id
- :object-id object-id}]))
+ :render-object
+ (do
+ (let [file-id (uuid (get-in route [:path-params :file-id]))
+ page-id (uuid (get-in route [:path-params :page-id]))
+ object-id (uuid (get-in route [:path-params :object-id]))]
+ [:& render/render-object {:file-id file-id
+ :page-id page-id
+ :object-id object-id}]))
- :workspace
- (let [project-id (uuid (get-in route [:params :path :project-id]))
- file-id (uuid (get-in route [:params :path :file-id]))
- page-id (uuid (get-in route [:params :query :page-id]))
- layout-name (get-in route [:params :query :layout])]
- [:& workspace/workspace {:project-id project-id
- :file-id file-id
- :page-id page-id
- :layout-name (keyword layout-name)
- :key file-id}])
- nil)])
+ :workspace
+ (let [project-id (some-> params :path :project-id uuid)
+ file-id (some-> params :path :file-id uuid)
+ page-id (some-> params :query :page-id uuid)
+ layout (some-> params :query :layout keyword)]
+ [:& workspace/workspace {:project-id project-id
+ :file-id file-id
+ :page-id page-id
+ :layout-name layout
+ :key file-id}])
+ nil)]))
(mf/defc app
[]
@@ -210,7 +216,7 @@
;; all profile data and redirect the user to the login page.
(defmethod ptk/handle-error :authentication
[error]
- (ts/schedule (st/emitf (logout))))
+ (ts/schedule (st/emitf (du/logout))))
;; Error that happens on an active bussines model validation does not
;; passes an validation (example: profile can't leave a team). From
@@ -220,8 +226,9 @@
[error]
(ts/schedule
(st/emitf
- (dm/show {:content "Unexpected validation error (server side)."
- :type :error})))
+ (dm/show {:content "Unexpected validation error."
+ :type :error
+ :timeout 3000})))
;; Print to the console some debug info.
(js/console.group "Validation Error")
@@ -233,62 +240,95 @@
(js/console.error explain)))
(js/console.groupEnd "Validation Error"))
+;; Error on parsing an SVG
+(defmethod ptk/handle-error :svg-parser
+ [error]
+ (ts/schedule
+ (st/emitf
+ (dm/show {:content "SVG is invalid or malformed"
+ :type :error
+ :timeout 3000}))))
+
;; This is a pure frontend error that can be caused by an active
;; assertion (assertion that is preserved on production builds). From
;; the user perspective this should be treated as internal error.
(defmethod ptk/handle-error :assertion
- [{:keys [data stack message context] :as error}]
- (ts/schedule
- (st/emitf (dm/show {:content "Internal error: assertion."
- :type :error})))
+ [{:keys [data stack message hint context] :as error}]
+ (let [message (or message hint)
+ context (str/fmt "ns: '%s'\nname: '%s'\nfile: '%s:%s'"
+ (:ns context)
+ (:name context)
+ (str cfg/public-uri "js/cljs-runtime/" (:file context))
+ (:line context))]
+ (ts/schedule
+ (st/emitf
+ (dm/show {:content "Internal error: assertion."
+ :type :error
+ :timeout 3000})
+ (ptk/event ::ev/event
+ {::ev/type "exception"
+ ::ev/name "assertion-error"
+ :message message
+ :context context
+ :trace stack})))
- ;; Print to the console some debugging info
- (js/console.group message)
- (js/console.info (str/format "ns: '%s'\nname: '%s'\nfile: '%s:%s'"
- (:ns context)
- (:name context)
- (str cfg/public-uri "/js/cljs-runtime/" (:file context))
- (:line context)))
- (js/console.groupCollapsed "Stack Trace")
- (js/console.info stack)
- (js/console.groupEnd "Stack Trace")
- (js/console.error (with-out-str (expound/printer data)))
- (js/console.groupEnd message))
+ ;; Print to the console some debugging info
+ (js/console.group message)
+ (js/console.info context)
+ (js/console.groupCollapsed "Stack Trace")
+ (js/console.info stack)
+ (js/console.groupEnd "Stack Trace")
+ (js/console.error (with-out-str (expound/printer data)))
+ (js/console.groupEnd message)))
;; This happens when the backed server fails to process the
;; request. This can be caused by an internal assertion or any other
;; uncontrolled error.
(defmethod ptk/handle-error :server-error
- [{:keys [data] :as error}]
- (ts/schedule
- (st/emitf (dm/show
- {:content "Something wrong has happened (on backend)."
- :type :error})))
+ [{:keys [data hint] :as error}]
+ (let [hint (or hint (:hint data) (:message data))
+ info (with-out-str (pprint (dissoc data :explain)))
+ expl (:explain data)]
+ (ts/schedule
+ (st/emitf
+ (dm/show {:content "Something wrong has happened (on backend)."
+ :type :error
+ :timeout 3000})
+ (ptk/event ::ev/event
+ {::ev/type "exception"
+ ::ev/name "server-error"
+ :hint hint
+ :info info
+ :explain expl})))
- (js/console.group "Internal Server Error:")
- (js/console.error "hint:" (or (:hint data) (:message data)))
- (js/console.info
- (with-out-str
- (pprint (dissoc data :explain))))
- (when-let [explain (:explain data)]
- (js/console.error explain))
- (js/console.groupEnd "Internal Server Error:"))
+ (js/console.group "Internal Server Error:")
+ (js/console.error "hint:" hint)
+ (js/console.info info)
+ (when expl (js/console.error expl))
+ (js/console.groupEnd "Internal Server Error:")))
(defmethod ptk/handle-error :default
[error]
(if (instance? ExceptionInfo error)
(ptk/handle-error (ex-data error))
- (do
+ (let [stack (.-stack error)
+ hint (or (ex-message error)
+ (:hint error)
+ (:message error))]
(ts/schedule
- (st/emitf (dm/assign-exception error)))
+ (st/emitf
+ (dm/assign-exception error)
+ (ptk/event ::ev/event
+ {::ev/type "exception"
+ ::ev/name "unexpected-error"
+ :message hint
+ :trace (.-stack error)})))
(js/console.group "Internal error:")
- (js/console.log "hint:" (or (ex-message error)
- (:hint error)
- (:message error)))
+ (js/console.log "hint:" hint)
(ex/ignoring
(js/console.error (clj->js error))
- (js/console.error "stack:" (.-stack error)))
+ (js/console.error "stack:" stack))
(js/console.groupEnd "Internal error:"))))
(defonce uncaught-error-handler
diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs
index f5c01f034..06b56dca4 100644
--- a/frontend/src/app/main/ui/auth.cljs
+++ b/frontend/src/app/main/ui/auth.cljs
@@ -7,7 +7,6 @@
(ns app.main.ui.auth
(:require
[app.common.uuid :as uuid]
- [app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.repo :as rp]
@@ -19,7 +18,6 @@
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.forms :as fm]
- [app.util.storage :refer [cache]]
[app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt]
[app.util.timers :as ts]
@@ -56,8 +54,7 @@
[:& recovery-request-page {:locale locale}]
:auth-recovery
- [:& recovery-page {:locale locale
- :params (:query-params route)}])
+ [:& recovery-page {:locale locale :params params}])
[:div.terms-login
[:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"]
[:span "and"]
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 5dd40cecc..3b809e986 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -6,22 +6,22 @@
(ns app.main.ui.auth.login
(:require
- [cljs.spec.alpha :as s]
- [beicon.core :as rx]
- [rumext.alpha :as mf]
- [app.config :as cfg]
[app.common.spec :as us]
- [app.main.ui.icons :as i]
- [app.main.data.auth :as da]
+ [app.config :as cfg]
+ [app.main.data.messages :as dm]
+ [app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.store :as st]
- [app.main.ui.messages :as msgs]
- [app.main.data.messages :as dm]
[app.main.ui.components.forms :as fm]
- [app.util.object :as obj]
+ [app.main.ui.icons :as i]
+ [app.main.ui.messages :as msgs]
[app.util.dom :as dom]
[app.util.i18n :refer [tr t]]
- [app.util.router :as rt]))
+ [app.util.object :as obj]
+ [app.util.router :as rt]
+ [beicon.core :as rx]
+ [cljs.spec.alpha :as s]
+ [rumext.alpha :as mf]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
@@ -45,7 +45,7 @@
(rx/subs (fn [profile]
(if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
- (st/emit! (da/logged-in profile))))
+ (st/emit! (du/login-from-token {:profile profile}))))
(fn [{:keys [type code] :as error}]
(cond
(and (= type :restriction)
@@ -72,7 +72,7 @@
(reset! error nil)
(let [params (with-meta (:clean-data @form)
{:on-error on-error})]
- (st/emit! (da/login params)))))
+ (st/emit! (du/login params)))))
on-submit-ldap
(mf/use-callback
@@ -149,15 +149,13 @@
[:div.links
[:div.link-entry
- [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
- :tab-index "5"}
+ [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
(tr "auth.forgot-password")]]
(when cfg/registration-enabled
[:div.link-entry
[:span (tr "auth.register") " "]
- [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))
- :tab-index "6"}
+ [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
(tr "auth.register-submit")]])]
[:& login-buttons {:params params}]
@@ -166,6 +164,5 @@
[:div.links.demo
[:div.link-entry
[:span (tr "auth.create-demo-profile") " "]
- [:a {:on-click (st/emitf da/create-demo-profile)
- :tab-index "6"}
+ [:a {:on-click (st/emitf (du/create-demo-profile))}
(tr "auth.create-demo-account")]]])]])
diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs
index f437b7fe5..704005986 100644
--- a/frontend/src/app/main/ui/auth/recovery.cljs
+++ b/frontend/src/app/main/ui/auth/recovery.cljs
@@ -6,18 +6,18 @@
(ns app.main.ui.auth.recovery
(:require
- [cljs.spec.alpha :as s]
- [cuerdas.core :as str]
- [rumext.alpha :as mf]
- [app.main.ui.icons :as i]
[app.common.spec :as us]
- [app.main.data.auth :as uda]
[app.main.data.messages :as dm]
+ [app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
+ [app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]]
- [app.util.router :as rt]))
+ [app.util.router :as rt]
+ [cljs.spec.alpha :as s]
+ [cuerdas.core :as str]
+ [rumext.alpha :as mf]))
(s/def ::password-1 ::us/not-empty-string)
(s/def ::password-2 ::us/not-empty-string)
@@ -54,7 +54,7 @@
:on-success on-success}
params {:token (get-in @form [:clean-data :token])
:password (get-in @form [:clean-data :password-2])}]
- (st/emit! (uda/recover-profile (with-meta params mdata)))))
+ (st/emit! (du/recover-profile (with-meta params mdata)))))
(mf/defc recovery-form
[{:keys [locale params] :as props}]
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index 2d8ae58d1..637bbc852 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -7,8 +7,8 @@
(ns app.main.ui.auth.recovery-request
(:require
[app.common.spec :as us]
- [app.main.data.auth :as uda]
[app.main.data.messages :as dm]
+ [app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
@@ -59,7 +59,7 @@
params (with-meta cdata
{:on-success #(on-success cdata %)
:on-error #(on-error cdata %)})]
- (st/emit! (uda/request-profile-recovery params)))))]
+ (st/emit! (du/request-profile-recovery params)))))]
[:& fm/form {:on-submit on-submit
:form form}
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index a8321a6a0..97c1aec2e 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -8,7 +8,6 @@
(:require
[app.common.spec :as us]
[app.config :as cfg]
- [app.main.data.auth :as da]
[app.main.data.users :as du]
[app.main.data.messages :as dm]
[app.main.store :as st]
@@ -93,7 +92,7 @@
(let [data (with-meta (:clean-data @form)
{:on-error (partial on-error form)
:on-success (partial on-success form)})]
- (st/emit! (da/register data)))))]
+ (st/emit! (du/register data)))))]
[:& fm/form {:on-submit on-submit
@@ -158,7 +157,7 @@
(when cfg/allow-demo-users
[:div.link-entry
[:span (tr "auth.create-demo-profile") " "]
- [:a {:on-click #(st/emit! da/create-demo-profile)
+ [:a {:on-click #(st/emit! (du/create-demo-profile))
:tab-index "5"}
(tr "auth.create-demo-account")]])
diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs
index b24521f09..3e0bf5323 100644
--- a/frontend/src/app/main/ui/auth/verify_token.cljs
+++ b/frontend/src/app/main/ui/auth/verify_token.cljs
@@ -7,7 +7,6 @@
(ns app.main.ui.auth.verify-token
(:require
[app.common.uuid :as uuid]
- [app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.repo :as rp]
@@ -21,7 +20,6 @@
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
- [app.util.storage :refer [cache]]
[app.util.timers :as ts]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
@@ -33,7 +31,7 @@
[data]
(let [msg (tr "dashboard.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
- (st/emit! (da/login-from-token data))))
+ (st/emit! (du/login-from-token data))))
(defmethod handle-token :change-email
[data]
@@ -44,7 +42,7 @@
(defmethod handle-token :auth
[tdata]
- (st/emit! (da/login-from-token tdata)))
+ (st/emit! (du/login-from-token tdata)))
(defmethod handle-token :team-invitation
[tdata]
diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs
index 50cfbfbde..075d99544 100644
--- a/frontend/src/app/main/ui/components/context_menu.cljs
+++ b/frontend/src/app/main/ui/components/context_menu.cljs
@@ -11,7 +11,6 @@
[app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i]
[app.common.uuid :as uuid]
- [app.util.data :refer [classnames]]
[app.util.dom :as dom]
[app.util.object :as obj]))
@@ -22,18 +21,18 @@
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop")
- (let [open? (gobj/get props "show")
- on-close (gobj/get props "on-close")
- options (gobj/get props "options")
+ (let [open? (gobj/get props "show")
+ on-close (gobj/get props "on-close")
+ options (gobj/get props "options")
is-selectable (gobj/get props "selectable")
- selected (gobj/get props "selected")
- top (gobj/get props "top" 0)
- left (gobj/get props "left" 0)
- fixed? (gobj/get props "fixed?" false)
- min-width? (gobj/get props "min-width?" false)
+ selected (gobj/get props "selected")
+ top (gobj/get props "top" 0)
+ left (gobj/get props "left" 0)
+ fixed? (gobj/get props "fixed?" false)
+ min-width? (gobj/get props "min-width?" false)
- local (mf/use-state {:offset 0
- :levels nil})
+ local (mf/use-state {:offset 0
+ :levels nil})
on-local-close
(mf/use-callback
@@ -81,13 +80,13 @@
(when (and open? (some? (:levels @local)))
[:> dropdown' props
- [:div.context-menu {:class (classnames :is-open open?
- :fixed fixed?
- :is-selectable is-selectable)
+ [:div.context-menu {:class (dom/classnames :is-open open?
+ :fixed fixed?
+ :is-selectable is-selectable)
:style {:top (+ top (:offset @local))
:left left}}
(let [level (-> @local :levels peek)]
- [:ul.context-menu-items {:class (classnames :min-width min-width?)
+ [:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
:ref check-menu-offscreen}
(when-let [parent-option (:parent-option level)]
[:*
@@ -103,8 +102,7 @@
(if (= option-name :separator)
[:li.separator]
[:li.context-menu-item
- {:class (classnames :is-selected (and selected
- (= option-name selected)))
+ {:class (dom/classnames :is-selected (and selected (= option-name selected)))
:key option-name}
(if-not sub-options
[:a.context-menu-action {:on-click option-handler}
diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs
index 82738e985..54e33925f 100644
--- a/frontend/src/app/main/ui/components/dropdown.cljs
+++ b/frontend/src/app/main/ui/components/dropdown.cljs
@@ -13,7 +13,7 @@
[props]
(let [children (gobj/get props "children")
on-close (gobj/get props "on-close")
- ref (gobj/get props "container")
+ ref (gobj/get props "container")
on-click
(fn [event]
diff --git a/frontend/src/app/main/ui/components/editable_select.cljs b/frontend/src/app/main/ui/components/editable_select.cljs
index e9de807d7..27fe4d897 100644
--- a/frontend/src/app/main/ui/components/editable_select.cljs
+++ b/frontend/src/app/main/ui/components/editable_select.cljs
@@ -44,17 +44,18 @@
(fn [node]
;; There is a problem when changing the state in this callback that
;; produces the dropdown to close in the same event
- (timers/schedule
- #(when-let [bounds (when node (dom/get-bounding-rect node))]
- (let [{window-height :height} (dom/get-window-size)
- {:keys [left top height]} bounds
- bottom (when (< (- window-height top) 300) (- window-height top))
- top (when (>= (- window-height top) 300) (+ top height))]
- (swap! state
- assoc
- :left left
- :top top
- :bottom bottom)))))]
+ (when node
+ (timers/schedule
+ #(when-let [bounds (when node (dom/get-bounding-rect node))]
+ (let [{window-height :height} (dom/get-window-size)
+ {:keys [left top height]} bounds
+ bottom (when (< (- window-height top) 300) (- window-height top))
+ top (when (>= (- window-height top) 300) (+ top height))]
+ (swap! state
+ assoc
+ :left left
+ :top top
+ :bottom bottom))))))]
(mf/use-effect
(mf/deps value)
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 105ec5663..dc14df9cd 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -18,6 +18,7 @@
[app.main.ui.dashboard.files :refer [files-section]]
[app.main.ui.dashboard.libraries :refer [libraries-page]]
[app.main.ui.dashboard.projects :refer [projects-section]]
+ [app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
[app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
@@ -50,14 +51,6 @@
(uuid-str? project-id)
(assoc :project-id (uuid project-id)))))
-(defn- team-ref
- [id]
- (l/derived (l/in [:teams id]) st/state))
-
-(defn- projects-ref
- [team-id]
- (l/derived (l/in [:projects team-id]) st/state))
-
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
[:div.dashboard-content {:on-click (st/emitf (dd/clear-selected-files))}
@@ -65,6 +58,12 @@
:dashboard-projects
[:& projects-section {:team team :projects projects}]
+ :dashboard-fonts
+ [:& fonts-page {:team team}]
+
+ :dashboard-font-providers
+ [:& font-providers-page {:team team}]
+
:dashboard-files
(when project
[:& files-section {:team team :project project}])
@@ -94,16 +93,15 @@
team-id (:team-id params)
search-term (:search-term params)
- projects-ref (mf/use-memo (mf/deps team-id) #(projects-ref team-id))
- team-ref (mf/use-memo (mf/deps team-id) #(team-ref team-id))
+ teams (mf/deref refs/teams)
+ team (get teams team-id)
- team (mf/deref team-ref)
- projects (mf/deref projects-ref)
+ projects (mf/deref refs/dashboard-projects)
project (get projects project-id)]
(mf/use-effect
(mf/deps team-id)
- (st/emitf (dd/fetch-bundle {:id team-id})))
+ (st/emitf (dd/initialize {:id team-id})))
(mf/use-effect
(mf/deps)
@@ -115,23 +113,31 @@
(not= "0.0" (:main @cf/version)))
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)})))))))
- [:& (mf/provider ctx/current-file-id) {:value nil}
- [:& (mf/provider ctx/current-team-id) {:value team-id}
- [:& (mf/provider ctx/current-project-id) {:value project-id}
- [:& (mf/provider ctx/current-page-id) {:value nil}
+ [:& (mf/provider ctx/current-team-id) {:value team-id}
+ [:& (mf/provider ctx/current-project-id) {:value project-id}
- [:section.dashboard-layout
- [:& sidebar {:team team
- :projects projects
- :project project
- :profile profile
- :section section
- :search-term search-term}]
+ ;; NOTE: dashboard events and other related functions assumes
+ ;; that the team is a implicit context variable that is
+ ;; available using react context or accessing
+ ;; the :current-team-id on the state. We set the key to the
+ ;; team-id becase we want to completly refresh all the
+ ;; components on team change. Many components assumess that the
+ ;; team is already set so don't put the team into mf/deps.
+ (when team
+ [:section.dashboard-layout {:key (:id team)}
+ [:& sidebar
+ {:team team
+ :projects projects
+ :project project
+ :profile profile
+ :section section
+ :search-term search-term}]
(when (and team (seq projects))
- [:& dashboard-content {:projects projects
- :profile profile
- :project project
- :section section
- :search-term search-term
- :team team}])]]]]]))
+ [:& dashboard-content
+ {:projects projects
+ :profile profile
+ :project project
+ :section section
+ :search-term search-term
+ :team team}])])]]))
diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs
index 6eec7fc75..48703fdf2 100644
--- a/frontend/src/app/main/ui/dashboard/comments.cljs
+++ b/frontend/src/app/main/ui/dashboard/comments.cljs
@@ -10,7 +10,6 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.config :as cfg]
- [app.main.data.auth :as da]
[app.main.data.dashboard :as dd]
[app.main.data.workspace :as dw]
[app.main.data.workspace.comments :as dwcm]
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index 5b8f80644..f24a8d412 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -19,6 +19,33 @@
[beicon.core :as rx]
[rumext.alpha :as mf]))
+(defn get-project-name
+ [project]
+ (if (:is-default project)
+ (tr "labels.drafts")
+ (:name project)))
+
+(defn get-team-name
+ [team]
+ (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team)))
+
+(defn group-by-team
+ "Group projects by team."
+ [projects]
+ (reduce (fn [teams project]
+ (update teams
+ (:team-id project)
+ #(if (nil? %)
+ {:id (:team-id project)
+ :name (:team-name project)
+ :is-default (:is-default-team project)
+ :projects [project]}
+ (update % :projects conj project))))
+ {}
+ projects))
+
(mf/defc file-menu
[{:keys [files show? on-edit on-menu-close top left navigate?] :as props}]
(assert (seq files) "missing `files` prop")
@@ -26,8 +53,8 @@
(assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
(assert (boolean? navigate?) "missing `navigate?` prop")
- (let [top (or top 0)
- left (or left 0)
+ (let [top (or top 0)
+ left (or left 0)
file (first files)
file-count (count files)
@@ -35,157 +62,113 @@
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
+
current-team (get @teams current-team-id)
- other-teams (remove #(= (:id %) current-team-id)
- (vals @teams))
+ other-teams (remove #(= (:id %) current-team-id) (vals @teams))
+
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
- project-name (fn [project]
- (if (:is-default project)
- (tr "labels.drafts")
- (:name project)))
-
- team-name (fn [team]
- (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team)))
-
on-new-tab
- (mf/use-callback
- (mf/deps file)
- (fn [event]
- (let [pparams {:project-id (:project-id file)
- :file-id (:id file)}
- qparams {:page-id (first (get-in file [:data :pages]))}]
- (st/emit! (rt/nav-new-window :workspace pparams qparams)))))
+ (fn [event]
+ (let [pparams {:project-id (:project-id file)
+ :file-id (:id file)}
+ qparams {:page-id (first (get-in file [:data :pages]))}]
+ (st/emit! (rt/nav-new-window :workspace pparams qparams))))
on-duplicate
- (mf/use-callback
- (mf/deps files)
- (fn [event]
- (apply st/emit! (map dd/duplicate-file files))
- (st/emit! (dm/success (tr "dashboard.success-duplicate-file")))))
+ (fn [event]
+ (apply st/emit! (map dd/duplicate-file files))
+ (st/emit! (dm/success (tr "dashboard.success-duplicate-file"))))
delete-fn
- (mf/use-callback
- (mf/deps files)
- (fn [event]
- (apply st/emit! (map dd/delete-file files))
- (st/emit! (dm/success (tr "dashboard.success-delete-file")))))
+ (fn [event]
+ (apply st/emit! (map dd/delete-file files))
+ (st/emit! (dm/success (tr "dashboard.success-delete-file"))))
on-delete
- (mf/use-callback
- (mf/deps files)
- (fn [event]
- (dom/stop-propagation event)
- (if multi?
- (st/emit! (modal/show
- {:type :confirm
- :title (tr "modals.delete-file-multi-confirm.title" file-count)
- :message (tr "modals.delete-file-multi-confirm.message" file-count)
- :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count)
+ (fn [event]
+ (dom/stop-propagation event)
+ (if multi?
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-file-multi-confirm.title" file-count)
+ :message (tr "modals.delete-file-multi-confirm.message" file-count)
+ :accept-label (tr "modals.delete-file-multi-confirm.accept" file-count)
:on-accept delete-fn}))
- (st/emit! (modal/show
- {:type :confirm
- :title (tr "modals.delete-file-confirm.title")
- :message (tr "modals.delete-file-confirm.message")
- :accept-label (tr "modals.delete-file-confirm.accept")
- :on-accept delete-fn})))))
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-file-confirm.title")
+ :message (tr "modals.delete-file-confirm.message")
+ :accept-label (tr "modals.delete-file-confirm.accept")
+ :on-accept delete-fn}))))
+
+ on-move-success
+ (fn [team-id project-id]
+ (if multi?
+ (st/emit! (dm/success (tr "dashboard.success-move-files")))
+ (st/emit! (dm/success (tr "dashboard.success-move-file"))))
+ (if (or navigate? (not= team-id current-team-id))
+ (st/emit! (dd/go-to-files project-id))
+ (st/emit! (dd/fetch-recent-files)
+ (dd/clear-selected-files))))
on-move
- (mf/use-callback
- (mf/deps file)
- (fn [team-id project-id]
- (let [data {:ids (set (map :id files))
- :project-id project-id}
-
- mdata {:on-success
- #(do
- (if multi?
- (st/emit! (dm/success (tr "dashboard.success-move-files")))
- (st/emit! (dm/success (tr "dashboard.success-move-file"))))
- (if (or navigate? (not= team-id current-team-id))
- (st/emit! (rt/nav :dashboard-files
- {:team-id team-id
- :project-id project-id}))
- (st/emit! (dd/fetch-recent-files {:team-id team-id})
- (dd/clear-selected-files))))}]
-
- (st/emitf (dd/move-files (with-meta data mdata))))))
+ (fn [team-id project-id]
+ (let [data {:ids (set (map :id files))
+ :project-id project-id}
+ mdata {:on-success #(on-move-success team-id project-id)}]
+ (st/emitf (dd/move-files (with-meta data mdata)))))
add-shared
- (mf/use-callback
- (mf/deps file)
- (st/emitf (dd/set-file-shared (assoc file :is-shared true))))
+ (st/emitf (dd/set-file-shared (assoc file :is-shared true)))
del-shared
- (mf/use-callback
- (mf/deps file)
- (st/emitf (dd/set-file-shared (assoc file :is-shared false))))
+ (st/emitf (dd/set-file-shared (assoc file :is-shared false)))
on-add-shared
- (mf/use-callback
- (mf/deps file)
- (fn [event]
- (dom/stop-propagation event)
- (st/emit! (modal/show
- {:type :confirm
- :message ""
- :title (tr "modals.add-shared-confirm.message" (:name file))
- :hint (tr "modals.add-shared-confirm.hint")
- :cancel-label :omit
- :accept-label (tr "modals.add-shared-confirm.accept")
- :accept-style :primary
- :on-accept add-shared}))))
+ (fn [event]
+ (dom/stop-propagation event)
+ (st/emit! (modal/show
+ {:type :confirm
+ :message ""
+ :title (tr "modals.add-shared-confirm.message" (:name file))
+ :hint (tr "modals.add-shared-confirm.hint")
+ :cancel-label :omit
+ :accept-label (tr "modals.add-shared-confirm.accept")
+ :accept-style :primary
+ :on-accept add-shared})))
on-del-shared
- (mf/use-callback
- (mf/deps file)
- (fn [event]
- (dom/prevent-default event)
- (dom/stop-propagation event)
- (st/emit! (modal/show
- {:type :confirm
- :message ""
- :title (tr "modals.remove-shared-confirm.message" (:name file))
- :hint (tr "modals.remove-shared-confirm.hint")
- :cancel-label :omit
- :accept-label (tr "modals.remove-shared-confirm.accept")
- :on-accept del-shared}))))]
+ (fn [event]
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (st/emit! (modal/show
+ {:type :confirm
+ :message ""
+ :title (tr "modals.remove-shared-confirm.message" (:name file))
+ :hint (tr "modals.remove-shared-confirm.hint")
+ :cancel-label :omit
+ :accept-label (tr "modals.remove-shared-confirm.accept")
+ :on-accept del-shared})))]
- (mf/use-layout-effect
- (mf/deps show?)
- (fn []
- (let [group-by-team (fn [projects]
- (reduce
- (fn [teams project]
- (update teams (:team-id project)
- #(if (nil? %)
- {:id (:team-id project)
- :name (:team-name project)
- :is-default (:is-default-team project)
- :projects [project]}
- (update % :projects conj project))))
- {}
- projects))]
- (if show?
- (->> (rp/query! :all-projects)
- (rx/map group-by-team)
- (rx/subs #(reset! teams %)))
- (reset! teams [])))))
+ (mf/use-effect
+ (fn []
+ (->> (rp/query! :all-projects)
+ (rx/map group-by-team)
+ (rx/subs #(reset! teams %)))))
(when current-team
(let [sub-options (conj (vec (for [project current-projects]
- [(project-name project)
+ [(get-project-name project)
(on-move (:id current-team)
(:id project))]))
(when (seq other-teams)
[(tr "dashboard.move-to-other-team") nil
(for [team other-teams]
- [(team-name team) nil
+ [(get-team-name team) nil
(for [sub-project (:projects team)]
- [(project-name sub-project)
+ [(get-project-name sub-project)
(on-move (:id team)
(:id sub-project))])])]))
@@ -214,4 +197,3 @@
:top top
:left left
:options options}]))))
-
diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs
index 4a5aca8d6..a43026964 100644
--- a/frontend/src/app/main/ui/dashboard/files.cljs
+++ b/frontend/src/app/main/ui/dashboard/files.cljs
@@ -8,6 +8,7 @@
(:require
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
+ [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
@@ -77,15 +78,11 @@
[:a.btn-secondary.btn-small {:on-click on-create-clicked}
(tr "dashboard.new-file")]]))
-(defn files-ref
- [project-id]
- (l/derived (l/in [:files project-id]) st/state))
-
(mf/defc files-section
[{:keys [project team] :as props}]
- (let [files-ref (mf/use-memo (mf/deps (:id project)) #(files-ref (:id project)))
- files-map (mf/deref files-ref)
+ (let [files-map (mf/deref refs/dashboard-files)
files (->> (vals files-map)
+ (filter #(= (:id project) (:project-id %)))
(sort-by :modified-at)
(reverse))]
diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs
new file mode 100644
index 000000000..241f7c2b2
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/fonts.cljs
@@ -0,0 +1,335 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.dashboard.fonts
+ (:require
+ [app.common.data :as d]
+ [app.common.media :as cm]
+ [app.common.uuid :as uuid]
+ [app.main.data.dashboard :as dd]
+ [app.main.data.fonts :as df]
+ [app.main.data.modal :as modal]
+ [app.main.ui.components.file-uploader :refer [file-uploader]]
+ [app.main.ui.components.context-menu :refer [context-menu]]
+ [app.main.store :as st]
+ [app.main.repo :as rp]
+ [app.main.refs :as refs]
+ [app.main.ui.icons :as i]
+ [app.util.dom :as dom]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.logging :as log]
+ [app.util.keyboard :as kbd]
+ [app.util.router :as rt]
+ [app.util.webapi :as wa]
+ [cuerdas.core :as str]
+ [beicon.core :as rx]
+ [okulary.core :as l]
+ [rumext.alpha :as mf]))
+
+(log/set-level! :trace)
+
+(defn- use-set-page-title
+ [team section]
+ (mf/use-effect
+ (mf/deps team)
+ (fn []
+ (when team
+ (let [tname (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))]
+ (case section
+ :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
+ :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))))
+
+(mf/defc header
+ {::mf/wrap [mf/memo]}
+ [{:keys [section team] :as props}]
+ (let [go-fonts
+ (mf/use-callback
+ (mf/deps team)
+ (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
+
+ go-providers
+ (mf/use-callback
+ (mf/deps team)
+ (st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))]
+
+ (use-set-page-title team section)
+
+ [:header.dashboard-header
+ [:div.dashboard-title
+ [:h1 (tr "labels.fonts")]]
+ [:nav
+ #_[:ul
+ [:li {:class (when (= section :fonts) "active")}
+ [:a {:on-click go-fonts} (tr "labels.custom-fonts")]]
+ [:li {:class (when (= section :providers) "active")}
+ [:a {:on-click go-providers} (tr "labels.font-providers")]]]]
+
+ [:div]]))
+
+(mf/defc font-variant-display-name
+ [{:keys [variant]}]
+ [:*
+ [:span (cm/font-weight->name (:font-weight variant))]
+ (when (not= "normal" (:font-style variant))
+ [:span " " (str/capital (:font-style variant))])])
+
+(mf/defc fonts-upload
+ [{:keys [team installed-fonts] :as props}]
+ (let [fonts (mf/use-state {})
+ input-ref (mf/use-ref)
+
+ uploading (mf/use-state #{})
+
+ on-click
+ (mf/use-callback #(dom/click (mf/ref-val input-ref)))
+
+ font-key-fn
+ (mf/use-callback (juxt :font-family :font-weight :font-style))
+
+ on-selected
+ (mf/use-callback
+ (mf/deps team installed-fonts)
+ (fn [blobs]
+ (->> (df/process-upload blobs (:id team))
+ (rx/subs (fn [result]
+ (swap! fonts df/merge-and-group-fonts installed-fonts result))
+ (fn [error]
+ (js/console.error "error" error))))))
+
+ on-upload
+ (mf/use-callback
+ (mf/deps team)
+ (fn [item]
+ (swap! uploading conj (:id item))
+ (->> (rp/mutation! :create-font-variant item)
+ (rx/delay-at-least 2000)
+ (rx/subs (fn [font]
+ (swap! fonts dissoc (:id item))
+ (swap! uploading disj (:id item))
+ (st/emit! (df/add-font font)))
+ (fn [error]
+ (js/console.log "error" error))))))
+
+ on-blur-name
+ (fn [id event]
+ (let [name (dom/get-target-val event)]
+ (swap! fonts df/rename-and-regroup id name installed-fonts)))
+
+ on-delete
+ (mf/use-callback
+ (mf/deps team)
+ (fn [{:keys [id] :as item}]
+ (swap! fonts dissoc id)))]
+
+ [:div.dashboard-fonts-upload
+ [:div.dashboard-fonts-hero
+ [:div.desc
+ [:h2 (tr "labels.upload-custom-fonts")]
+ [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
+
+ [:div.banner
+ [:div.icon i/msg-info]
+ [:div.content
+ [:& i18n/tr-html {:tag-name "span"
+ :label "dashboard.fonts.hero-text2"}]]]]
+
+ [:div.btn-primary
+ {:on-click on-click}
+ [:span (tr "labels.add-custom-font")]
+ [:& file-uploader {:input-id "font-upload"
+ :accept cm/str-font-types
+ :multi true
+ :input-ref input-ref
+ :on-selected on-selected}]]]
+
+ [:*
+ (for [item (sort-by :font-family (vals @fonts))]
+ (let [uploading? (contains? @uploading (:id item))]
+ [:div.font-item.table-row {:key (:id item)}
+ [:div.table-field.family
+ [:input {:type "text"
+ :on-blur #(on-blur-name (:id item) %)
+ :default-value (:font-family item)}]]
+ [:div.table-field.variants
+ [:span.label
+ [:& font-variant-display-name {:variant item}]]]
+ [:div.table-field.filenames
+ (for [item (:names item)]
+ [:span item])]
+
+ [:div.table-field.options
+ [:button.btn-primary.upload-button
+ {:on-click #(on-upload item)
+ :class (dom/classnames :disabled uploading?)
+ :disabled uploading?}
+ (if uploading?
+ (tr "labels.uploading")
+ (tr "labels.upload"))]
+ [:span.icon.close {:on-click #(on-delete item)} i/close]]]))]]))
+
+(mf/defc installed-font
+ [{:keys [font-id variants] :as props}]
+ (let [font (first variants)
+
+ variants (sort-by (fn [item]
+ [(:font-weight item)
+ (if (= "normal" (:font-style item)) 1 2)])
+ variants)
+
+ open-menu? (mf/use-state false)
+ edit? (mf/use-state false)
+ state (mf/use-var (:font-family font))
+
+ on-change
+ (fn [event]
+ (reset! state (dom/get-target-val event)))
+
+ on-save
+ (fn [event]
+ (let [font-family @state]
+ (st/emit! (df/update-font
+ {:id font-id
+ :name font-family}))
+ (reset! edit? false)))
+
+ on-key-down
+ (fn [event]
+ (when (kbd/enter? event)
+ (on-save event)))
+
+ on-cancel
+ (fn [event]
+ (reset! edit? false)
+ (reset! state (:font-family font)))
+
+ delete-font-fn
+ (fn [] (st/emit! (df/delete-font font-id)))
+
+ delete-variant-fn
+ (fn [id] (st/emit! (df/delete-font-variant id)))
+
+ on-delete
+ (fn []
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-font.title")
+ :message (tr "modals.delete-font.message")
+ :accept-label (tr "labels.delete")
+ :on-accept (fn [props]
+ (delete-font-fn))})))
+
+ on-delete-variant
+ (fn [id]
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-font-variant.title")
+ :message (tr "modals.delete-font-variant.message")
+ :accept-label (tr "labels.delete")
+ :on-accept (fn [props]
+ (delete-variant-fn id))})))]
+
+ [:div.font-item.table-row
+ [:div.table-field.family
+ (if @edit?
+ [:input {:type "text"
+ :default-value @state
+ :on-key-down on-key-down
+ :on-change on-change}]
+ [:span (:font-family font)])]
+
+ [:div.table-field.variants
+ (for [item variants]
+ [:div.variant
+ [:span.label
+ [:& font-variant-display-name {:variant item}]]
+ [:span.icon.close
+ {:on-click #(on-delete-variant (:id item))}
+ i/plus]])]
+
+ [:div]
+
+ (if @edit?
+ [:div.table-field.options
+ [:button.btn-primary
+ {:disabled (str/blank? @state)
+ :on-click on-save
+ :class (dom/classnames :btn-disabled (str/blank? @state))}
+ (tr "labels.save")]
+ [:span.icon.close {:on-click on-cancel} i/close]]
+
+ [:div.table-field.options
+ [:span.icon {:on-click #(reset! open-menu? true)} i/actions]
+ [:& context-menu
+ {:on-close #(reset! open-menu? false)
+ :show @open-menu?
+ :fixed? false
+ :top -15
+ :left -115
+ :options [[(tr "labels.edit") #(reset! edit? true)]
+ [(tr "labels.delete") on-delete]]}]])]))
+
+
+(mf/defc installed-fonts
+ [{:keys [team fonts] :as props}]
+ (let [sterm (mf/use-state "")
+
+ matches?
+ #(str/includes? (str/lower (:font-family %)) @sterm)
+
+ on-change
+ (mf/use-callback
+ (fn [event]
+ (let [val (dom/get-target-val event)]
+ (reset! sterm val))))]
+
+ [:div.dashboard-installed-fonts
+ [:h3 (tr "labels.installed-fonts")]
+ [:div.installed-fonts-header
+ [:div.table-field.family (tr "labels.font-family")]
+ [:div.table-field.variants (tr "labels.font-variants")]
+ [:div]
+ [:div.table-field.search-input
+ [:input {:placeholder (tr "labels.search-font")
+ :default-value ""
+ :on-change on-change
+ }]]]
+
+ (cond
+ (seq fonts)
+ (for [[font-id variants] (->> (vals fonts)
+ (filter matches?)
+ (group-by :font-id))]
+ [:& installed-font {:key (str font-id)
+ :font-id font-id
+ :variants variants}])
+
+ (nil? fonts)
+ [:div.fonts-placeholder
+ [:div.icon i/loader]
+ [:div.label (tr "dashboard.loading-fonts")]]
+
+ :else
+ [:div.fonts-placeholder
+ [:div.icon i/text]
+ [:div.label (tr "dashboard.fonts.empty-placeholder")]])]))
+
+(mf/defc fonts-page
+ [{:keys [team] :as props}]
+ (let [fonts (mf/deref refs/dashboard-fonts)]
+ [:*
+ [:& header {:team team :section :fonts}]
+ [:section.dashboard-container.dashboard-fonts
+ [:& fonts-upload {:team team :installed-fonts fonts}]
+ [:& installed-fonts {:team team :fonts fonts}]]]))
+
+(mf/defc font-providers-page
+ [{:keys [team] :as props}]
+ [:*
+ [:& header {:team team :section :providers}]
+ [:section.dashboard-container
+ [:span "font providers"]]])
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index d5822551c..b6a8b10d2 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -21,11 +21,12 @@
[app.main.worker :as wrk]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
- [app.util.i18n :as i18n :refer [t tr]]
+ [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.timers :as ts]
+ [app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@@ -55,76 +56,66 @@
[{:keys [modified-at]}]
(let [locale (mf/deref i18n/locale)
time (dt/timeago modified-at {:locale locale})]
- (str (t locale "ds.updated-at" time))))
+ (str (tr "ds.updated-at" time))))
+
+(defn create-counter-element
+ [element file-count]
+ (let [counter-el (dom/create-element "div")]
+ (dom/set-property! counter-el "class" "drag-counter")
+ (dom/set-text! counter-el (str file-count))
+ counter-el))
(mf/defc grid-item
{:wrap [mf/memo]}
- [{:keys [id file selected-files navigate?] :as props}]
- (let [local (mf/use-state {:menu-open false
- :menu-pos nil
- :edition false})
- locale (mf/deref i18n/locale)
- item-ref (mf/use-ref)
- menu-ref (mf/use-ref)
- selected? (contains? selected-files id)
-
- selected-file-objs
- (deref refs/dashboard-selected-file-objs)
- ;; not needed to subscribe and repaint if changed
+ [{:keys [file navigate?] :as props}]
+ (let [file-id (:id file)
+ local (mf/use-state {:menu-open false
+ :menu-pos nil
+ :edition false})
+ selected-files (mf/deref refs/dashboard-selected-files)
+ item-ref (mf/use-ref)
+ menu-ref (mf/use-ref)
+ selected? (contains? selected-files file-id)
on-menu-close
(mf/use-callback
#(swap! local assoc :menu-open false))
on-select
- (mf/use-callback
- (mf/deps id selected? selected-files @local)
- (fn [event]
- (when (and (or (not selected?) (> (count selected-files) 1))
- (not (:menu-open @local)))
- (dom/stop-propagation event)
- (let [shift? (kbd/shift? event)]
- (when-not shift?
- (st/emit! (dd/clear-selected-files)))
- (st/emit! (dd/toggle-file-select {:file file}))))))
+ (fn [event]
+ (when (and (or (not selected?) (> (count selected-files) 1))
+ (not (:menu-open @local)))
+ (dom/stop-propagation event)
+ (let [shift? (kbd/shift? event)]
+ (when-not shift?
+ (st/emit! (dd/clear-selected-files)))
+ (st/emit! (dd/toggle-file-select file)))))
on-navigate
(mf/use-callback
- (mf/deps id)
+ (mf/deps file)
(fn [event]
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
- (let [pparams {:project-id (:project-id file)
- :file-id (:id file)}
- qparams {:page-id (first (get-in file [:data :pages]))}]
- (st/emit! (rt/nav :workspace pparams qparams)))))))
-
- create-counter
- (mf/use-callback
- (fn [element file-count]
- (let [counter-el (dom/create-element "div")]
- (dom/set-property! counter-el "class" "drag-counter")
- (dom/set-text! counter-el (str file-count))
- counter-el)))
+ (st/emit! (dd/go-to-workspace file))))))
on-drag-start
(mf/use-callback
(mf/deps selected-files)
(fn [event]
- (let [offset (dom/get-offset-position (.-nativeEvent event))
+ (let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file)))
- item-el (mf/ref-val item-ref)
- counter-el (create-counter item-el
- (if select-current?
- 1
- (count selected-files)))]
-
+ item-el (mf/ref-val item-ref)
+ counter-el (create-counter-element item-el
+ (if select-current?
+ 1
+ (count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files))
- (st/emit! (dd/toggle-file-select {:file file})))
+ (st/emit! (dd/toggle-file-select file)))
(dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move")
@@ -135,7 +126,7 @@
;; afterwards, in the next render cycle.
(dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
- (ts/raf #(.removeChild item-el counter-el)))))
+ (ts/raf #(.removeChild ^js item-el counter-el)))))
on-menu-click
(mf/use-callback
@@ -146,10 +137,11 @@
(let [shift? (kbd/shift? event)]
(when-not shift?
(st/emit! (dd/clear-selected-files)))
- (st/emit! (dd/toggle-file-select {:file file}))))
+ (st/emit! (dd/toggle-file-select file))))
(let [position (dom/get-client-position event)]
- (swap! local assoc :menu-open true
- :menu-pos position))))
+ (swap! local assoc
+ :menu-open true
+ :menu-pos position))))
edit
(mf/use-callback
@@ -168,19 +160,20 @@
:menu-open false)))]
(mf/use-effect
- (mf/deps selected? local)
- (fn []
- (when (and (not selected?) (:menu-open @local))
- (swap! local assoc :menu-open false))))
+ (mf/deps selected? local)
+ (fn []
+ (when (and (not selected?) (:menu-open @local))
+ (swap! local assoc :menu-open false))))
+
+ [:div.grid-item.project-th
+ {:class (dom/classnames :selected selected?)
+ :ref item-ref
+ :draggable true
+ :on-click on-select
+ :on-double-click on-navigate
+ :on-drag-start on-drag-start
+ :on-context-menu on-menu-click}
- [:div.grid-item.project-th {:class (dom/classnames
- :selected selected?)
- :ref item-ref
- :draggable true
- :on-click on-select
- :on-double-click on-navigate
- :on-drag-start on-drag-start
- :on-context-menu on-menu-click}
[:div.overlay]
[:& grid-item-thumbnail {:file file}]
(when (:is-shared file)
@@ -198,7 +191,7 @@
:on-click on-menu-click}
i/actions
(when selected?
- [:& file-menu {:files selected-file-objs
+ [:& file-menu {:files (vals selected-files)
:show? (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
@@ -223,28 +216,24 @@
(mf/defc grid
[{:keys [id opts files] :as props}]
- (let [locale (mf/deref i18n/locale)
- selected-files (mf/deref refs/dashboard-selected-files)]
- [:section.dashboard-grid
- (cond
- (nil? files)
- [:& loading-placeholder]
+ [:section.dashboard-grid
+ (cond
+ (nil? files)
+ [:& loading-placeholder]
- (seq files)
- [:div.grid-row
- (for [item files]
- [:& grid-item
- {:id (:id item)
- :file item
- :selected-files selected-files
- :key (:id item)
- :navigate? true}])]
+ (seq files)
+ [:div.grid-row
+ (for [item files]
+ [:& grid-item
+ {:file item
+ :key (:id item)
+ :navigate? true}])]
- :else
- [:& empty-placeholder])]))
+ :else
+ [:& empty-placeholder])])
(mf/defc line-grid-row
- [{:keys [locale files team-id selected-files on-load-more dragging?] :as props}]
+ [{:keys [files team-id selected-files on-load-more dragging?] :as props}]
(let [rowref (mf/use-ref)
width (mf/use-state nil)
@@ -267,17 +256,19 @@
(mf/use-effect
(fn []
- (let [node (mf/ref-val rowref)
- obs (new js/ResizeObserver
- (fn [entries x]
- (ts/raf #(let [row (first entries)
- row-rect (.-contentRect ^js row)
- row-width (.-width ^js row-rect)]
- (reset! width row-width)))))]
-
- (.observe ^js obs node)
+ (let [node (mf/ref-val rowref)
+ mnt? (volatile! true)
+ sub (->> (wapi/observe-resize node)
+ (rx/observe-on :af)
+ (rx/subs (fn [entries]
+ (let [row (first entries)
+ row-rect (.-contentRect ^js row)
+ row-width (.-width ^js row-rect)]
+ (when @mnt?
+ (reset! width row-width))))))]
(fn []
- (.disconnect ^js obs)))))
+ (vreset! mnt? false)
+ (rx/dispose! sub)))))
[:div.grid-row.no-wrap {:ref rowref}
(when dragging?
@@ -294,12 +285,11 @@
[:div.grid-item.placeholder {:on-click on-load-more}
[:div.placeholder-icon i/arrow-down]
[:div.placeholder-label
- (t locale "dashboard.show-all-files")]])]))
+ (tr "dashboard.show-all-files")]])]))
(mf/defc line-grid
[{:keys [project-id team-id opts files on-load-more] :as props}]
- (let [locale (mf/deref i18n/locale)
- dragging? (mf/use-state false)
+ (let [dragging? (mf/use-state false)
selected-files (mf/deref refs/dashboard-selected-files)
selected-project (mf/deref refs/dashboard-selected-project)
@@ -327,6 +317,12 @@
(when-not (dnd/from-child? e)
(reset! dragging? false))))
+ on-drop-success
+ (fn []
+ (st/emit! (dm/success (tr "dashboard.success-move-file"))
+ (dd/fetch-recent-files)
+ (dd/clear-selected-files)))
+
on-drop
(mf/use-callback
(mf/deps files selected-files)
@@ -335,11 +331,7 @@
(when (not= selected-project project-id)
(let [data {:ids selected-files
:project-id project-id}
-
- mdata {:on-success
- (st/emitf (dm/success (tr "dashboard.success-move-file"))
- (dd/fetch-recent-files {:team-id team-id})
- (dd/clear-selected-files))}]
+ mdata {:on-success on-drop-success}]
(st/emit! (dd/move-files (with-meta data mdata)))))))]
[:section.dashboard-grid {:on-drag-enter on-drag-enter
@@ -355,8 +347,7 @@
:team-id team-id
:selected-files selected-files
:on-load-more on-load-more
- :dragging? @dragging?
- :locale locale}]
+ :dragging? @dragging?}]
:else
[:& empty-placeholder {:dragging? @dragging?}])]))
diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs
index 77db054de..e61d4bcd4 100644
--- a/frontend/src/app/main/ui/dashboard/libraries.cljs
+++ b/frontend/src/app/main/ui/dashboard/libraries.cljs
@@ -10,20 +10,16 @@
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.icons :as i]
+ [app.main.refs :as refs]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[okulary.core :as l]
[rumext.alpha :as mf]))
-(defn files-ref
- [team-id]
- (l/derived (l/in [:shared-files team-id]) st/state))
-
(mf/defc libraries-page
[{:keys [team] :as props}]
- (let [files-ref (mf/use-memo (mf/deps (:id team)) #(files-ref (:id team)))
- files-map (mf/deref files-ref)
+ (let [files-map (mf/deref refs/dashboard-shared-files)
files (->> (vals files-map)
(sort-by :modified-at)
(reverse))]
@@ -33,9 +29,11 @@
(dom/set-html-title (tr "title.dashboard.shared-libraries"
(if (:is-default team)
(tr "dashboard.your-penpot")
- (:name team))))
- (st/emit! (dd/fetch-shared-files {:team-id (:id team)})
- (dd/clear-selected-files))))
+ (:name team))))))
+
+ (mf/use-effect
+ (st/emitf (dd/fetch-shared-files)
+ (dd/clear-selected-files)))
[:*
[:header.dashboard-header
diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs
index fc2ace6f0..946478a09 100644
--- a/frontend/src/app/main/ui/dashboard/project_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs
@@ -9,10 +9,11 @@
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
+ [app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
- [app.main.ui.context :as ctx]
[app.main.ui.components.context-menu :refer [context-menu]]
+ [app.main.ui.context :as ctx]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx]
@@ -28,81 +29,63 @@
left (or left 0)
current-team-id (mf/use-ctx ctx/current-team-id)
- teams (mf/use-state nil)
+ teams (mf/deref refs/teams)
+ teams (-> teams (dissoc current-team-id) vals vec)
+
+ on-duplicate-success
+ (fn [new-project]
+ (st/emit! (dm/success (tr "dashboard.success-duplicate-project"))
+ (rt/nav :dashboard-files
+ {:team-id (:team-id new-project)
+ :project-id (:id new-project)})))
on-duplicate
- (mf/use-callback
- (mf/deps project)
- #(let [on-success
- (fn [new-project]
- (st/emit! (dm/success (tr "dashboard.success-duplicate-project"))
- (rt/nav :dashboard-files
- {:team-id (:team-id new-project)
- :project-id (:id new-project)})))]
- (st/emit! (dd/duplicate-project
- (with-meta project {:on-success on-success})))))
+ (fn []
+ (st/emit! (dd/duplicate-project
+ (with-meta project {:on-success on-duplicate-success}))))
toggle-pin
- (mf/use-callback
- (mf/deps project)
- (st/emitf (dd/toggle-project-pin project)))
+ (st/emitf (dd/toggle-project-pin project))
+
+ on-move-success
+ (fn [team-id]
+ (st/emit! (dd/go-to-projects team-id)))
on-move
- (mf/use-callback
- (mf/deps project)
- (fn [team-id]
- (let [data {:id (:id project)
- :team-id team-id}
-
- mdata {:on-success
- (st/emitf (rt/nav :dashboard-projects
- {:team-id team-id}))}]
-
+ (fn [team-id]
+ (let [data {:id (:id project) :team-id team-id}
+ mdata {:on-success #(on-move-success team-id)}]
(st/emitf (dm/success (tr "dashboard.success-move-project"))
- (dd/move-project (with-meta data mdata))))))
+ (dd/move-project (with-meta data mdata)))))
delete-fn
- (mf/use-callback
- (mf/deps project)
- (fn [event]
- (st/emit! (dm/success (tr "dashboard.success-delete-project"))
- (dd/delete-project project)
- (rt/nav :dashboard-projects {:team-id (:team-id project)}))))
+ (fn [event]
+ (st/emit! (dm/success (tr "dashboard.success-delete-project"))
+ (dd/delete-project project)
+ (dd/go-to-projects (:team-id project))))
on-delete
- (mf/use-callback
- (mf/deps project)
- (st/emitf (modal/show
- {:type :confirm
- :title (tr "modals.delete-project-confirm.title")
- :message (tr "modals.delete-project-confirm.message")
- :accept-label (tr "modals.delete-project-confirm.accept")
- :on-accept delete-fn})))]
+ (st/emitf
+ (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-project-confirm.title")
+ :message (tr "modals.delete-project-confirm.message")
+ :accept-label (tr "modals.delete-project-confirm.accept")
+ :on-accept delete-fn}))]
- (mf/use-layout-effect
- (mf/deps show?)
- (fn []
- (if show?
- (->> (rp/query! :teams)
- (rx/map (fn [teams]
- (remove #(= (:id %) current-team-id) teams)))
- (rx/subs #(reset! teams %)))
- (reset! teams []))))
-
- (when @teams
- [:& context-menu {:on-close on-menu-close
- :show show?
- :fixed? (or (not= top 0) (not= left 0))
- :min-width? true
- :top top
- :left left
- :options [[(tr "labels.rename") on-edit]
- [(tr "dashboard.duplicate") on-duplicate]
- [(tr "dashboard.pin-unpin") toggle-pin]
- (when (seq @teams)
- [(tr "dashboard.move-to") nil
- (for [team @teams]
- [(:name team) (on-move (:id team))])])
- [:separator]
- [(tr "labels.delete") on-delete]]}])))
+ [:& context-menu {:on-close on-menu-close
+ :show show?
+ :fixed? (or (not= top 0) (not= left 0))
+ :min-width? true
+ :top top
+ :left left
+ :options [[(tr "labels.rename") on-edit]
+ [(tr "dashboard.duplicate") on-duplicate]
+ [(tr "dashboard.pin-unpin") toggle-pin]
+ (when (seq teams)
+ [(tr "dashboard.move-to") nil
+ (for [team teams]
+ [(:name team) (on-move (:id team))])])
+ [:separator]
+ [(tr "labels.delete") on-delete]]}]))
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index b75d0545c..911f6afc4 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -25,45 +25,29 @@
(mf/defc header
{::mf/wrap [mf/memo]}
- [{:keys [locale team] :as props}]
- (let [create #(st/emit! (dd/create-project {:team-id (:id team)}))]
+ []
+ (let [create (st/emitf (dd/create-project))]
[:header.dashboard-header
[:div.dashboard-title
- [:h1 (t locale "dashboard.projects-title")]]
+ [:h1 (tr "dashboard.projects-title")]]
[:a.btn-secondary.btn-small {:on-click create}
- (t locale "dashboard.new-project")]]))
-
-(defn files-ref
- [project-id]
- (l/derived (l/in [:files project-id]) st/state))
-
-(defn recent-ref
- [project-id]
- (l/derived (l/in [:recent-files project-id]) st/state))
+ (tr "dashboard.new-project")]]))
(mf/defc project-item
- [{:keys [project first? locale] :as props}]
- (let [files-ref (mf/use-memo (mf/deps project) #(files-ref (:id project)))
- recent-ref (mf/use-memo (mf/deps project) #(recent-ref (:id project)))
-
- files-map (mf/deref files-ref)
- recent-ids (mf/deref recent-ref)
-
- files (some->> recent-ids
- (map #(get files-map %))
- (sort-by :modified-at)
- (filter some?)
- (reverse))
+ [{:keys [project first? files] :as props}]
+ (let [locale (mf/deref i18n/locale)
project-id (:id project)
team-id (:team-id project)
file-count (or (:count project) 0)
- dstate (mf/deref refs/dashboard-local)
- edit-id (:project-for-edit dstate)
- local (mf/use-state {:menu-open false
- :menu-pos nil
- :edition? (= (:id project) edit-id)})
+ dstate (mf/deref refs/dashboard-local)
+ edit-id (:project-for-edit dstate)
+
+ local
+ (mf/use-state {:menu-open false
+ :menu-pos nil
+ :edition? (= (:id project) edit-id)})
on-nav
(mf/use-callback
@@ -131,12 +115,15 @@
(if (:is-default project)
(tr "labels.drafts")
(:name project))])
- [:& project-menu {:project project
- :show? (:menu-open @local)
- :left (:x (:menu-pos @local))
- :top (:y (:menu-pos @local))
- :on-edit on-edit-open
- :on-menu-close on-menu-close}]
+
+ (when (:menu-open @local)
+ [:& project-menu {:project project
+ :show? (:menu-open @local)
+ :left (:x (:menu-pos @local))
+ :top (:y (:menu-pos @local))
+ :on-edit on-edit-open
+ :on-menu-close on-menu-close}])
+
[:span.info (str file-count " files")]
(when (> file-count 0)
(let [time (-> (:modified-at project)
@@ -145,20 +132,25 @@
[:a.btn-secondary.btn-small
{:on-click create-file}
- (t locale "dashboard.new-file")]]
+ (tr "dashboard.new-file")]]
[:& line-grid
{:project-id (:id project)
+ :project project
:team-id team-id
:on-load-more on-nav
:files files}]]))
+
+(def recent-files-ref
+ (l/derived :dashboard-recent-files st/state))
+
(mf/defc projects-section
[{:keys [team projects] :as props}]
- (let [projects (->> (vals projects)
- (sort-by :modified-at)
- (reverse))
- locale (mf/deref i18n/locale)]
+ (let [projects (->> (vals projects)
+ (sort-by :modified-at)
+ (reverse))
+ recent-map (mf/deref recent-files-ref)]
(mf/use-effect
(mf/deps team)
@@ -166,18 +158,22 @@
(dom/set-html-title (tr "title.dashboard.projects"
(if (:is-default team)
(tr "dashboard.your-penpot")
- (:name team))))
- (st/emit! (dd/fetch-recent-files {:team-id (:id team)})
- (dd/clear-selected-files))))
+ (:name team))))))
+
+ (mf/use-effect
+ (st/emitf (dd/fetch-recent-files)
+ (dd/clear-selected-files)))
(when (seq projects)
[:*
- [:& header {:locale locale
- :team team}]
+ [:& header]
[:section.dashboard-container
- (for [project projects]
- [:& project-item {:project project
- :locale locale
- :first? (= project (first projects))
- :key (:id project)}])]])))
+ (for [{:keys [id] :as project} projects]
+ (let [files (when recent-map
+ (->> (vals recent-map)
+ (filterv #(= id (:project-id %)))))]
+ [:& project-item {:project project
+ :files files
+ :first? (= project (first projects))
+ :key (:id project)}]))]])))
diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs
index 2f9568e14..d3c6ed9ed 100644
--- a/frontend/src/app/main/ui/dashboard/search.cljs
+++ b/frontend/src/app/main/ui/dashboard/search.cljs
@@ -7,56 +7,52 @@
(ns app.main.ui.dashboard.search
(:require
[app.main.data.dashboard :as dd]
+ [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
- [app.util.i18n :as i18n :refer [t]]
+ [app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.alpha :as mf]))
-
-(def result-ref
- (l/derived (l/in [:dashboard-local :search-result]) st/state))
-
(mf/defc search-page
[{:keys [team search-term] :as props}]
- (let [result (mf/deref result-ref)
- locale (mf/deref i18n/locale)]
-
+ (let [result (mf/deref refs/dashboard-search-result)]
(mf/use-effect
- (mf/deps team search-term)
+ (mf/deps team)
(fn []
- (dom/set-html-title (t locale "title.dashboard.search"
+ (dom/set-html-title (tr "title.dashboard.search"
(if (:is-default team)
- (t locale "dashboard.your-penpot")
- (:name team))))
- (when search-term
- (st/emit! (dd/search-files {:team-id (:id team)
- :search-term search-term})
- (dd/clear-selected-files)))))
+ (tr "dashboard.your-penpot")
+ (:name team))))))
+ (mf/use-effect
+ (mf/deps search-term)
+ (fn []
+ (st/emit! (dd/search {:search-term search-term})
+ (dd/clear-selected-files))))
[:*
[:header.dashboard-header
[:div.dashboard-title
- [:h1 (t locale "dashboard.title-search")]]]
+ [:h1 (tr "dashboard.title-search")]]]
[:section.dashboard-container.search
(cond
(empty? search-term)
[:div.grid-empty-placeholder
[:div.icon i/search]
- [:div.text (t locale "dashboard.type-something")]]
+ [:div.text (tr "dashboard.type-something")]]
(nil? result)
[:div.grid-empty-placeholder
[:div.icon i/search]
- [:div.text (t locale "dashboard.searching-for" search-term)]]
+ [:div.text (tr "dashboard.searching-for" search-term)]]
(empty? result)
[:div.grid-empty-placeholder
[:div.icon i/search]
- [:div.text (t locale "dashboard.no-matches-for" search-term)]]
+ [:div.text (tr "dashboard.no-matches-for" search-term)]]
:else
[:& grid {:files result
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index f2b331a0e..b35edc7e0 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -9,7 +9,6 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.config :as cfg]
- [app.main.data.auth :as da]
[app.main.data.comments :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
@@ -28,7 +27,7 @@
[app.util.avatars :as avatars]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
- [app.util.i18n :as i18n :refer [t tr]]
+ [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.router :as rt]
@@ -47,24 +46,26 @@
selected-project (:selected-project dstate)
edit-id (:project-for-edit dstate)
- local (mf/use-state {:menu-open false
- :menu-pos nil
- :edition? (= (:id item) edit-id)
- :dragging? false})
+ local (mf/use-state
+ {:menu-open false
+ :menu-pos nil
+ :edition? (= (:id item) edit-id)
+ :dragging? false})
on-click
(mf/use-callback
(mf/deps item)
(fn []
- (st/emit! (rt/nav :dashboard-files {:team-id (:team-id item)
- :project-id (:id item)}))))
+ (st/emit! (dd/go-to-files (:id item)))))
on-menu-click
- (mf/use-callback (fn [event]
- (let [position (dom/get-client-position event)]
- (dom/prevent-default event)
- (swap! local assoc :menu-open true
- :menu-pos position))))
+ (mf/use-callback
+ (fn [event]
+ (let [position (dom/get-client-position event)]
+ (dom/prevent-default event)
+ (swap! local assoc
+ :menu-open true
+ :menu-pos position))))
on-menu-close
(mf/use-callback #(swap! local assoc :menu-open false))
@@ -101,21 +102,22 @@
(when-not (dnd/from-child? e)
(swap! local assoc :dragging? false))))
+ on-drop-success
+ (mf/use-callback
+ (mf/deps (:id item))
+ (st/emitf (dm/success (tr "dashboard.success-move-file"))
+ (dd/go-to-files (:id item))))
+
on-drop
(mf/use-callback
- (mf/deps item selected-files)
- (fn [e]
- (swap! local assoc :dragging? false)
- (when (not= selected-project (:id item))
- (let [data {:ids selected-files
- :project-id (:id item)}
-
- mdata {:on-success
- (st/emitf (dm/success (tr "dashboard.success-move-file"))
- (rt/nav :dashboard-files
- {:team-id team-id
- :project-id (:id item)}))}]
- (st/emit! (dd/move-files (with-meta data mdata)))))))]
+ (mf/deps item selected-files)
+ (fn [e]
+ (swap! local assoc :dragging? false)
+ (when (not= selected-project (:id item))
+ (let [data {:ids selected-files
+ :project-id (:id item)}
+ mdata {:on-success on-drop-success}]
+ (st/emit! (dd/move-files (with-meta data mdata)))))))]
[:*
[:li {:class (if selected? "current"
@@ -139,7 +141,7 @@
:on-menu-close on-menu-close}]]))
(mf/defc sidebar-search
- [{:keys [search-term team-id locale] :as props}]
+ [{:keys [search-term team-id] :as props}]
(let [search-term (or search-term "")
focused? (mf/use-state false)
emit! (mf/use-memo #(f/debounce st/emit! 500))
@@ -149,12 +151,9 @@
(mf/deps team-id)
(fn [event]
(reset! focused? true)
- (let [target (dom/get-target event)
- value (dom/get-value target)]
- (dom/select-text! target)
- (if (empty? value)
- (emit! (rt/nav :dashboard-search {:team-id team-id} {}))
- (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value}))))))
+ (let [value (dom/get-target-val event)]
+ (dom/select-text! (dom/get-target event))
+ (emit! (dd/go-to-search value)))))
on-search-blur
(mf/use-callback
@@ -165,9 +164,8 @@
(mf/use-callback
(mf/deps team-id)
(fn [event]
- (let [value (-> (dom/get-target event)
- (dom/get-value))]
- (emit! (rt/nav :dashboard-search {:team-id team-id} {:search-term value})))))
+ (let [value (dom/get-target-val event)]
+ (emit! (dd/go-to-search value)))))
on-clear-click
(mf/use-callback
@@ -176,14 +174,14 @@
(let [search-input (dom/get-element "search-input")]
(dom/clean-value! search-input)
(dom/focus! search-input)
- (emit! (rt/nav :dashboard-search {:team-id team-id} {})))))]
+ (emit! (dd/go-to-search)))))]
[:form.sidebar-search
[:input.input-text
{:key :images-search-box
:id "search-input"
:type "text"
- :placeholder (t locale "dashboard.search-placeholder")
+ :placeholder (tr "dashboard.search-placeholder")
:default-value search-term
:auto-complete "off"
:on-focus on-search-focus
@@ -201,7 +199,7 @@
i/search])]))
(mf/defc teams-selector-dropdown
- [{:keys [team profile locale] :as props}]
+ [{:keys [team profile] :as props}]
(let [show-dropdown? (mf/use-state false)
teams (mf/deref refs/teams)
@@ -211,16 +209,15 @@
team-selected
(mf/use-callback
- (fn [team-id]
- (da/set-current-team! team-id)
- (st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
+ (fn [team-id]
+ (st/emit! (dd/go-to-projects team-id))))]
[:ul.dropdown.teams-dropdown
- [:li.title (t locale "dashboard.switch-team")]
+ [:li.title (tr "dashboard.switch-team")]
[:hr]
[:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
[:span.team-icon i/logo-icon]
- [:span.team-text (t locale "dashboard.your-penpot")]]
+ [:span.team-text (tr "dashboard.your-penpot")]]
(for [team (remove :is-default (vals teams))]
[:* {:key (:id team)}
@@ -231,7 +228,7 @@
[:hr]
[:li.action {:on-click on-create-clicked}
- (t locale "dashboard.create-new-team")]]))
+ (tr "dashboard.create-new-team")]]))
(s/def ::member-id ::us/uuid)
(s/def ::leave-modal-form
@@ -241,21 +238,17 @@
{::mf/register modal/components
::mf/register-as ::leave-and-reassign}
[{:keys [members profile team accept]}]
- (let [form (fm/use-form :spec ::leave-modal-form :initial {})
- not-current-user? (fn [{:keys [id]}] (not= id (:id profile)))
- members (->> members (filterv not-current-user?))
- options (into [{:value "" :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}]
- (map #(hash-map :label (:name %) :value (str (:id %))) members))
-
- on-cancel
- (mf/use-callback (st/emitf (modal/hide)))
+ (let [form (fm/use-form :spec ::leave-modal-form :initial {})
+ members (some->> members (filterv #(not= (:id %) (:id profile))))
+ options (into [{:value ""
+ :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}]
+ (map #(hash-map :label (:name %) :value (str (:id %))) members))
+ on-cancel (st/emitf (modal/hide))
on-accept
- (mf/use-callback
- (mf/deps form)
- (fn [event]
- (let [member-id (get-in @form [:clean-data :member-id])]
- (accept member-id))))]
+ (fn [event]
+ (let [member-id (get-in @form [:clean-data :member-id])]
+ (accept member-id)))]
[:div.modal-overlay
[:div.modal-container.confirm-dialog
@@ -290,113 +283,81 @@
:value (tr "modals.leave-and-reassign.promote-and-leave")
:on-click on-accept}]]]]]))
-
(mf/defc team-options-dropdown
- [{:keys [team locale profile] :as props}]
- (let [members (mf/use-state [])
+ [{:keys [team profile] :as props}]
+ (let [go-members (st/emitf (dd/go-to-team-members))
+ go-settings (st/emitf (dd/go-to-team-settings))
- go-members
- (mf/use-callback
- (mf/deps team)
- (st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
-
- go-settings
- (mf/use-callback
- (mf/deps team)
- (st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
+ members-map (mf/deref refs/dashboard-team-members)
+ members (vals members-map)
on-create-clicked
- (mf/use-callback
- (st/emitf (modal/show :team-form {})))
+ (st/emitf (modal/show :team-form {}))
on-rename-clicked
- (mf/use-callback
- (mf/deps team)
- (st/emitf (modal/show :team-form {:team team})))
+ (st/emitf (modal/show :team-form {:team team}))
on-leaved-success
- (mf/use-callback
- (mf/deps team profile)
- (fn []
- (let [team-id (:default-team-id profile)]
- (da/set-current-team! team-id)
- (st/emit! (modal/hide)
- (du/fetch-teams)
- (rt/nav :dashboard-projects {:team-id team-id})))))
+ (fn []
+ (st/emit! (modal/hide)
+ (dd/go-to-projects (:default-team-id profile))))
leave-fn
- (mf/use-callback
- (mf/deps team)
- (st/emitf (dd/leave-team (with-meta team {:on-success on-leaved-success}))))
+ (st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
leave-and-reassign-fn
- (mf/use-callback
- (mf/deps team)
- (fn [member-id]
- (let [team (assoc team :reassign-to member-id)]
- (st/emit! (dd/leave-team (with-meta team {:on-success on-leaved-success}))))))
+ (fn [member-id]
+ (let [params {:reassign-to member-id}]
+ (st/emit! (dd/leave-team (with-meta params {:on-success on-leaved-success})))))
on-leave-clicked
- (mf/use-callback
- (mf/deps team)
- (st/emitf (modal/show
- {:type :confirm
- :title (t locale "modals.leave-confirm.title")
- :message (t locale "modals.leave-confirm.message")
- :accept-label (t locale "modals.leave-confirm.accept")
- :on-accept leave-fn})))
+ (st/emitf (modal/show
+ {:type :confirm
+ :title (tr "modals.leave-confirm.title")
+ :message (tr "modals.leave-confirm.message")
+ :accept-label (tr "modals.leave-confirm.accept")
+ :on-accept leave-fn}))
on-leave-as-owner-clicked
- (mf/use-callback
- (mf/deps team @members)
- (st/emitf (modal/show
- {:type ::leave-and-reassign
- :profile profile
- :team team
- :accept leave-and-reassign-fn
- :members @members})))
+ (st/emitf (modal/show
+ {:type ::leave-and-reassign
+ :profile profile
+ :team team
+ :members members
+ :accept leave-and-reassign-fn}))
delete-fn
- (mf/use-callback
- (mf/deps team)
- (st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success}))))
+ (st/emitf (dd/delete-team (with-meta team {:on-success on-leaved-success})))
on-delete-clicked
- (mf/use-callback
- (mf/deps team)
- (st/emitf (modal/show
- {:type :confirm
- :title (t locale "modals.delete-team-confirm.title")
- :message (t locale "modals.delete-team-confirm.message")
- :accept-label (t locale "modals.delete-team-confirm.accept")
- :on-accept delete-fn})))]
-
- (mf/use-layout-effect
- (mf/deps (:id team))
- (fn []
- (->> (rp/query! :team-members {:team-id (:id team)})
- (rx/subs #(reset! members %)))))
+ (st/emitf
+ (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-team-confirm.title")
+ :message (tr "modals.delete-team-confirm.message")
+ :accept-label (tr "modals.delete-team-confirm.accept")
+ :on-accept delete-fn}))]
[:ul.dropdown.options-dropdown
- [:li {:on-click go-members} (t locale "labels.members")]
- [:li {:on-click go-settings} (t locale "labels.settings")]
+ [:li {:on-click go-members} (tr "labels.members")]
+ [:li {:on-click go-settings} (tr "labels.settings")]
[:hr]
- [:li {:on-click on-rename-clicked} (t locale "labels.rename")]
+ [:li {:on-click on-rename-clicked} (tr "labels.rename")]
(cond
(:is-owner team)
- [:li {:on-click on-leave-as-owner-clicked} (t locale "dashboard.leave-team")]
+ [:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
- (> (count @members) 1)
- [:li {:on-click on-leave-clicked} (t locale "dashboard.leave-team")])
+ (> (count members) 1)
+ [:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])
(when (:is-owner team)
- [:li {:on-click on-delete-clicked} (t locale "dashboard.delete-team")])]))
+ [:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch
- [{:keys [team profile locale] :as props}]
+ [{:keys [team profile] :as props}]
(let [show-dropdown? (mf/use-state false)
show-team-opts-ddwn? (mf/use-state false)
@@ -408,7 +369,7 @@
(if (:is-default team)
[:div.team-name
[:span.team-icon i/logo-icon]
- [:span.team-text (t locale "dashboard.default-team-name")]]
+ [:span.team-text (tr "dashboard.default-team-name")]]
[:div.team-name
[:span.team-icon
[:img {:src (cfg/resolve-team-photo-url team)}]]
@@ -425,23 +386,22 @@
[:& dropdown {:show @show-teams-ddwn?
:on-close #(reset! show-teams-ddwn? false)}
[:& teams-selector-dropdown {:team team
- :profile profile
- :locale locale}]]
+ :profile profile}]]
[:& dropdown {:show @show-team-opts-ddwn?
:on-close #(reset! show-team-opts-ddwn? false)}
[:& team-options-dropdown {:team team
- :profile profile
- :locale locale}]]]))
+ :profile profile}]]]))
(mf/defc sidebar-content
- [{:keys [locale projects profile section team project search-term] :as props}]
+ [{:keys [projects profile section team project search-term] :as props}]
(let [default-project-id
(->> (vals projects)
(d/seek :is-default)
(:id))
projects? (= section :dashboard-projects)
+ fonts? (= section :dashboard-fonts)
libs? (= section :dashboard-libraries)
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
@@ -451,6 +411,11 @@
(mf/deps team)
(st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
+ go-fonts
+ (mf/use-callback
+ (mf/deps team)
+ (st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
+
go-drafts
(mf/use-callback
(mf/deps team default-project-id)
@@ -469,29 +434,36 @@
(filter :is-pinned))]
[:div.sidebar-content
- [:& sidebar-team-switch {:team team :profile profile :locale locale}]
+ [:& sidebar-team-switch {:team team :profile profile}]
[:hr]
[:& sidebar-search {:search-term search-term
- :team-id (:id team)
- :locale locale}]
+ :team-id (:id team)}]
[:div.sidebar-content-section
[:ul.sidebar-nav.no-overflow
[:li.recent-projects
{:on-click go-projects
:class-name (when projects? "current")}
- [:span.element-title (t locale "labels.projects")]]
+ [:span.element-title (tr "labels.projects")]]
[:li {:on-click go-drafts
:class-name (when drafts? "current")}
- [:span.element-title (t locale "labels.drafts")]]
+ [:span.element-title (tr "labels.drafts")]]
[:li {:on-click go-libs
:class-name (when libs? "current")}
- [:span.element-title (t locale "labels.shared-libraries")]]]]
+ [:span.element-title (tr "labels.shared-libraries")]]]]
[:hr]
+ [:div.sidebar-content-section
+ [:ul.sidebar-nav.no-overflow
+ [:li.recent-projects
+ {:on-click go-fonts
+ :class-name (when fonts? "current")}
+ [:span.element-title (tr "labels.fonts")]]]]
+
+ [:hr]
[:div.sidebar-content-section
(if (seq pinned-projects)
[:ul.sidebar-nav
@@ -504,11 +476,11 @@
:selected? (= (:id item) (:id project))}])]
[:div.sidebar-empty-placeholder
[:span.icon i/pin]
- [:span.text (t locale "dashboard.no-projects-placeholder")]])]]))
+ [:span.text (tr "dashboard.no-projects-placeholder")]])]]))
(mf/defc profile-section
- [{:keys [profile locale team] :as props}]
+ [{:keys [profile team] :as props}]
(let [show (mf/use-state false)
photo (cfg/resolve-profile-photo-url profile)
@@ -530,18 +502,18 @@
[:ul.dropdown
[:li {:on-click (partial on-click :settings-profile)}
[:span.icon i/user]
- [:span.text (t locale "labels.profile")]]
+ [:span.text (tr "labels.profile")]]
[:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock]
- [:span.text (t locale "labels.password")]]
- [:li {:on-click (partial on-click (da/logout))}
+ [:span.text (tr "labels.password")]]
+ [:li {:on-click (partial on-click (du/logout))}
[:span.icon i/exit]
- [:span.text (t locale "labels.logout")]]
+ [:span.text (tr "labels.logout")]]
(when cfg/feedback-enabled
[:li.feedback {:on-click (partial on-click :settings-feedback)}
[:span.icon i/msg-info]
- [:span.text (t locale "labels.give-feedback")]
+ [:span.text (tr "labels.give-feedback")]
[:span.primary-badge "ALPHA"]])]]]
(when (and team profile)
@@ -552,15 +524,11 @@
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
- (let [locale (mf/deref i18n/locale)
- team (obj/get props "team")
- profile (obj/get props "profile")
- props (-> (obj/clone props)
- (obj/set! "locale" locale))]
+ (let [team (obj/get props "team")
+ profile (obj/get props "profile")]
[:div.dashboard-sidebar
[:div.sidebar-inside
[:> sidebar-content props]
[:& profile-section
{:profile profile
- :team team
- :locale locale}]]]))
+ :team team}]]]))
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index 64eed4bee..168a944c2 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -32,22 +32,9 @@
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section team] :as props}]
- (let [go-members
- (mf/use-callback
- (mf/deps team)
- (st/emitf (rt/nav :dashboard-team-members {:team-id (:id team)})))
-
- go-settings
- (mf/use-callback
- (mf/deps team)
- (st/emitf (rt/nav :dashboard-team-settings {:team-id (:id team)})))
-
- invite-member
- (mf/use-callback
- (mf/deps team)
- (st/emitf (modal/show {:type ::invite-member
- :team team})))
-
+ (let [go-members (st/emitf (dd/go-to-team-members))
+ go-settings (st/emitf (dd/go-to-team-settings))
+ invite-member (st/emitf (modal/show {:type ::invite-member}))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)]
@@ -69,6 +56,16 @@
(tr "dashboard.invite-profile")]
[:div])]))
+(defn get-available-roles
+ []
+ [{:value "" :label (tr "labels.role")}
+ {:value "admin" :label (tr "labels.admin")}
+ {:value "editor" :label (tr "labels.editor")}
+ ;; Temporarily disabled viewer role
+ ;; https://tree.taiga.io/project/uxboxproject/issue/1083
+ ;; {:value "viewer" :label (tr "labels.viewer")}
+ ])
+
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-member-form
@@ -77,53 +74,40 @@
(mf/defc invite-member-modal
{::mf/register modal/components
::mf/register-as ::invite-member}
- [{:keys [team] :as props}]
- (let [roles [{:value "" :label (tr "labels.role")}
- {:value "admin" :label (tr "labels.admin")}
- {:value "editor" :label (tr "labels.editor")}]
- ;; Temporarily disabled viewer role
- ;; https://tree.taiga.io/project/uxboxproject/issue/1083
- ;; {:value "viewer" :label (tr "labels.viewer")}]
-
- initial (mf/use-memo (mf/deps team) (constantly {:team-id (:id team)
- :role "editor"}))
+ []
+ (let [roles (mf/use-memo get-available-roles)
+ initial (mf/use-memo (constantly {:role "editor"}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
on-success
- (mf/use-callback
- (mf/deps team)
- (st/emitf (dm/success (tr "notifications.invitation-email-sent"))
- (modal/hide)))
+ (st/emitf (dm/success (tr "notifications.invitation-email-sent"))
+ (modal/hide))
on-error
- (mf/use-callback
- (mf/deps team)
- (fn [form {:keys [type code] :as error}]
- (let [email (get @form [:data :email])]
- (cond
- (and (= :validation type)
- (= :profile-is-muted code))
- (dm/error (tr "errors.profile-is-muted"))
+ (fn [form {:keys [type code] :as error}]
+ (let [email (get @form [:data :email])]
+ (cond
+ (and (= :validation type)
+ (= :profile-is-muted code))
+ (dm/error (tr "errors.profile-is-muted"))
- (and (= :validation type)
- (= :member-is-muted code))
- (dm/error (tr "errors.member-is-muted"))
+ (and (= :validation type)
+ (= :member-is-muted code))
+ (dm/error (tr "errors.member-is-muted"))
- (and (= :validation type)
- (= :email-has-permanent-bounces))
- (dm/error (tr "errors.email-has-permanent-bounces" email))
+ (and (= :validation type)
+ (= :email-has-permanent-bounces))
+ (dm/error (tr "errors.email-has-permanent-bounces" email))
- :else
- (dm/error (tr "errors.generic"))))))
+ :else
+ (dm/error (tr "errors.generic")))))
on-submit
- (mf/use-callback
- (mf/deps team)
- (fn [form]
- (let [params (:clean-data @form)
- mdata {:on-success (partial on-success form)
- :on-error (partial on-error form)}]
- (st/emit! (dd/invite-team-member (with-meta params mdata))))))]
+ (fn [form]
+ (let [params (:clean-data @form)
+ mdata {:on-success (partial on-success form)
+ :on-error (partial on-error form)}]
+ (st/emit! (dd/invite-team-member (with-meta params mdata)))))]
[:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form}
@@ -139,50 +123,39 @@
[:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
-
(mf/defc team-member
+ {::mf/wrap [mf/memo]}
[{:keys [team member profile] :as props}]
(let [show? (mf/use-state false)
set-role
- #(st/emit! (dd/update-team-member-role {:team-id (:id team)
- :member-id (:id member)
- :role %}))
- set-owner-fn
- (partial set-role :owner)
+ (fn [role]
+ (let [params {:member-id (:id member) :role role}]
+ (st/emit! (dd/update-team-member-role params))))
- set-admin
- (mf/use-callback (mf/deps team member) (partial set-role :admin))
-
- set-editor
- (mf/use-callback (mf/deps team member) (partial set-role :editor))
-
- set-viewer
- (mf/use-callback (mf/deps team member) (partial set-role :viewer))
+ set-owner-fn (partial set-role :owner)
+ set-admin (partial set-role :admin)
+ set-editor (partial set-role :editor)
+ set-viewer (partial set-role :viewer)
set-owner
- (mf/use-callback
- (mf/deps team member)
- (st/emitf (modal/show
- {:type :confirm
- :title (tr "modals.promote-owner-confirm.title")
- :message (tr "modals.promote-owner-confirm.message")
- :accept-label (tr "modals.promote-owner-confirm.accept")
- :on-accept set-owner-fn})))
+ (st/emitf (modal/show
+ {:type :confirm
+ :title (tr "modals.promote-owner-confirm.title")
+ :message (tr "modals.promote-owner-confirm.message")
+ :accept-label (tr "modals.promote-owner-confirm.accept")
+ :on-accept set-owner-fn}))
delete-fn
- (st/emitf (dd/delete-team-member {:team-id (:id team) :member-id (:id member)}))
+ (st/emitf (dd/delete-team-member {:member-id (:id member)}))
delete
- (mf/use-callback
- (mf/deps team member)
- (st/emitf (modal/show
- {:type :confirm
- :title (tr "modals.delete-team-member-confirm.title")
- :message (tr "modals.delete-team-member-confirm.message")
- :accept-label (tr "modals.delete-team-member-confirm.accept")
- :on-accept delete-fn})))]
-
+ (st/emitf (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-team-member-confirm.title")
+ :message (tr "modals.delete-team-member-confirm.message")
+ :accept-label (tr "modals.delete-team-member-confirm.accept")
+ :on-accept delete-fn}))]
[:div.table-row
[:div.table-field.name (:name member)]
@@ -244,23 +217,21 @@
(for [item members]
[:& team-member {:member item :team team :profile profile :key (:id item)}])]]))
-(defn- members-ref
- [{:keys [id] :as team}]
- (l/derived (l/in [:team-members id]) st/state))
-
(mf/defc team-members-page
[{:keys [team profile] :as props}]
- (let [members-ref (mf/use-memo (mf/deps team) #(members-ref team))
- members-map (mf/deref members-ref)]
+ (let [members-map (mf/deref refs/dashboard-team-members)]
(mf/use-effect
(mf/deps team)
(fn []
- (dom/set-html-title (tr "title.team-members"
- (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team))))
- (st/emit! (dd/fetch-team-members team))))
+ (dom/set-html-title
+ (tr "title.team-members"
+ (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))))))
+
+ (mf/use-effect
+ (st/emitf (dd/fetch-team-members)))
[:*
[:& header {:section :dashboard-team-members
@@ -270,42 +241,35 @@
:team team
:members-map members-map}]]]))
-(defn- stats-ref
- [{:keys [id] :as team}]
- (l/derived (l/in [:team-stats id]) st/state))
-
(mf/defc team-settings-page
[{:keys [team profile] :as props}]
(let [finput (mf/use-ref)
- members-ref (mf/use-memo (mf/deps team) #(members-ref team))
- members-map (mf/deref members-ref)
-
+ members-map (mf/deref refs/dashboard-team-members)
owner (->> (vals members-map)
(d/seek :is-owner))
- stats-ref (mf/use-memo (mf/deps team) #(stats-ref team))
- stats (mf/deref stats-ref)
+ stats (mf/deref refs/dashboard-team-stats)
on-image-click
(mf/use-callback #(dom/click (mf/ref-val finput)))
on-file-selected
- (mf/use-callback
- (mf/deps team)
- (fn [file]
- (st/emit! (dd/update-team-photo {:file file
- :team-id (:id team)}))))]
+ (fn [file]
+ (st/emit! (dd/update-team-photo {:file file})))]
+
(mf/use-effect
- (mf/deps team)
- (fn []
- (dom/set-html-title (tr "title.team-settings"
- (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team))))
- (st/emit! (dd/fetch-team-members team)
- (dd/fetch-team-stats team))))
+ (mf/deps team)
+ (fn []
+ (dom/set-html-title (tr "title.team-settings"
+ (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))))))
+
+ (mf/use-effect
+ (st/emitf (dd/fetch-team-members)
+ (dd/fetch-team-stats)))
[:*
[:& header {:section :dashboard-team-settings
diff --git a/frontend/src/app/main/ui/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs
index 0fd481771..ee82788f0 100644
--- a/frontend/src/app/main/ui/handoff.cljs
+++ b/frontend/src/app/main/ui/handoff.cljs
@@ -91,7 +91,7 @@
(events/unlistenByKey key1))))]
(mf/use-effect on-mount)
- (hooks/use-shortcuts sc/shortcuts)
+ (hooks/use-shortcuts ::handoff sc/shortcuts)
[:div.handoff-layout {:class (dom/classnames :force-visible
(:show-thumbnails state))}
diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs
index bc2a8fd2d..5b0b0645e 100644
--- a/frontend/src/app/main/ui/hooks.cljs
+++ b/frontend/src/app/main/ui/hooks.cljs
@@ -9,10 +9,11 @@
(:require
[app.common.spec :as us]
[app.main.data.shortcuts :as dsc]
+ [app.main.store :as st]
[app.util.dom :as dom]
- [app.util.object :as obj]
[app.util.dom.dnd :as dnd]
[app.util.logging :as log]
+ [app.util.object :as obj]
[app.util.timers :as ts]
[app.util.transit :as t]
[app.util.webapi :as wapi]
@@ -35,11 +36,13 @@
state))
(defn use-shortcuts
- [shortcuts]
+ [key shortcuts]
(mf/use-effect
+ #js [(str key) shortcuts]
(fn []
- (dsc/bind-shortcuts shortcuts)
- (fn [] (dsc/remove-shortcuts)))))
+ (st/emit! (dsc/push-shortcuts key shortcuts))
+ (fn []
+ (st/emit! (dsc/pop-shortcuts key))))))
(defn invisible-image
[]
diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs
index 0d6fc0d8b..75dce1ac1 100644
--- a/frontend/src/app/main/ui/measurements.cljs
+++ b/frontend/src/app/main/ui/measurements.cljs
@@ -235,7 +235,7 @@
[{:keys [bounds frame selected-shapes hover-shape zoom]}]
(let [selected-ids (into #{} (map :id) selected-shapes)
selected-selrect (gsh/selection-rect selected-shapes)
- hover-selrect (:selrect hover-shape)
+ hover-selrect (-> hover-shape :points gsh/points->selrect)
bounds-selrect (bound->selrect bounds)
hover-selected-shape? (not (contains? selected-ids (:id hover-shape)))]
diff --git a/frontend/src/app/main/ui/messages.cljs b/frontend/src/app/main/ui/messages.cljs
index e1848c969..70aecb9b2 100644
--- a/frontend/src/app/main/ui/messages.cljs
+++ b/frontend/src/app/main/ui/messages.cljs
@@ -18,33 +18,33 @@
(mf/defc banner
[{:keys [type position status controls content actions on-close] :as props}]
[:div.banner {:class (dom/classnames
- :warning (= type :warning)
- :error (= type :error)
- :success (= type :success)
- :info (= type :info)
- :fixed (= position :fixed)
- :floating (= position :floating)
- :inline (= position :inline)
- :hide (= status :hide))}
+ :warning (= type :warning)
+ :error (= type :error)
+ :success (= type :success)
+ :info (= type :info)
+ :fixed (= position :fixed)
+ :floating (= position :floating)
+ :inline (= position :inline)
+ :hide (= status :hide))}
[:div.wrapper
- [:div.icon (case type
- :warning i/msg-warning
- :error i/msg-error
- :success i/msg-success
- :info i/msg-info
- i/msg-error)]
- [:div.content {:class (dom/classnames
- :inline-actions (= controls :inline-actions)
- :bottom-actions (= controls :bottom-actions))}
- content
- (when (or (= controls :bottom-actions) (= controls :inline-actions))
- [:div.actions
- (for [action actions]
- [:div.btn-secondary.btn-small {:key (uuid/next)
- :on-click (:callback action)}
- (:label action)])])]
- (when (= controls :close)
- [:div.btn-close {:on-click on-close} i/close])]])
+ [:div.icon (case type
+ :warning i/msg-warning
+ :error i/msg-error
+ :success i/msg-success
+ :info i/msg-info
+ i/msg-error)]
+ [:div.content {:class (dom/classnames
+ :inline-actions (= controls :inline-actions)
+ :bottom-actions (= controls :bottom-actions))}
+ content
+ (when (or (= controls :bottom-actions) (= controls :inline-actions))
+ [:div.actions
+ (for [action actions]
+ [:div.btn-secondary.btn-small {:key (uuid/next)
+ :on-click (:callback action)}
+ (:label action)])])]
+ (when (= controls :close)
+ [:div.btn-close {:on-click on-close} i/close])]])
(mf/defc notifications
[]
diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs
index a14f22a9f..307915145 100644
--- a/frontend/src/app/main/ui/onboarding.cljs
+++ b/frontend/src/app/main/ui/onboarding.cljs
@@ -34,7 +34,7 @@
(mf/defc onboarding-start
[{:keys [next] :as props}]
[:div.modal-container.onboarding
- [:div.modal-left
+ [:div.modal-left.welcome
[:img {:src "images/login-on.jpg" :border "0" :alt "Penpot"}]]
[:div.modal-right
[:div.modal-title
@@ -296,7 +296,7 @@
(defmethod render-release-notes "0.0"
[params]
- (render-release-notes (assoc params :version "1.5")))
+ (render-release-notes (assoc params :version "1.6")))
(defmethod render-release-notes "1.4"
[{:keys [slide klass next finish navigate version]}]
@@ -474,3 +474,101 @@
{:slide @slide
:navigate navigate
:total 3}]]]]]])))
+
+(defmethod render-release-notes "1.6"
+ [{:keys [slide klass next finish navigate version]}]
+ (mf/html
+ (case @slide
+ :start
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/login-on.jpg" :border "0" :alt "What's new Alpha release 1.6.0"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "What's new?"]]
+ [:span.release "Alpha version " version]
+ [:div.modal-content
+ [:p "Penpot continues growing with new features that improve performance, user experience and visual design."]
+ [:p "We are happy to show you a sneak peak of the most important stuff that the Alpha 1.6.0 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/custom-fonts.gif" :border "0" :alt "Upload/use custom fonts"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Upload/use custom fonts"]]
+ [:div.modal-content
+ [:p "From now on you can upload fonts to a Penpot team and use them across its files. This is one of the most requested features since our first release (we listen!)"]
+ [:p "We hope you enjoy having more typography options and our brand new font selector."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 1
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/scale-text.gif" :border "0" :alt "Interactively scale text"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Scale text layers at resizing"]]
+ [:div.modal-content
+ [:p "New main menu option “Scale text (K)” to enable scale text mode."]
+ [:p "Disabled by default, this tool is disabled back after being used."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 2
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/performance.gif" :border "0" :alt "Performance improvements"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Performance improvements"]]
+ [:div.modal-content
+ [:p "Penpot brings important improvements handling large files. The performance in managing files in the dashboard has also been improved."]
+ [:p "You should have the feeling that files and layers show up a bit faster :)"]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click next} "Continue"]
+ [:& navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]]
+
+ 3
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/features/shapes-to-path.gif" :border "0" :alt "Shapes to path"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Shapes to path"]]
+ [:div.modal-content
+ [:p "Now you can edit basic shapes like rectangles, circles and image containers by double clicking."]
+ [:p "An easy way to increase speed by working with vectors!"]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click finish} "Start!"]
+ [:& navigation-bullets
+ {:slide @slide
+ :navigate navigate
+ :total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs
index 36402814e..46726c87f 100644
--- a/frontend/src/app/main/ui/settings/change_email.cljs
+++ b/frontend/src/app/main/ui/settings/change_email.cljs
@@ -7,7 +7,6 @@
(ns app.main.ui.settings.change-email
(:require
[app.common.spec :as us]
- [app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
diff --git a/frontend/src/app/main/ui/settings/delete_account.cljs b/frontend/src/app/main/ui/settings/delete_account.cljs
index abf29ccc5..f26881ddb 100644
--- a/frontend/src/app/main/ui/settings/delete_account.cljs
+++ b/frontend/src/app/main/ui/settings/delete_account.cljs
@@ -6,14 +6,13 @@
(ns app.main.ui.settings.delete-account
(:require
- [app.main.data.auth :as da]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs]
- [app.util.i18n :as i18n :refer [tr t]]
+ [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
@@ -26,42 +25,36 @@
(rx/of (dm/error msg)))
(rx/throw error)))
-(defn on-success
- [x]
- (st/emit! (rt/nav :auth-login)))
-
(mf/defc delete-account-modal
{::mf/register modal/components
::mf/register-as :delete-account}
[props]
- (let [locale (mf/deref i18n/locale)
- on-close
+ (let [on-close
(mf/use-callback (st/emitf (modal/hide)))
on-accept
(mf/use-callback
(st/emitf (modal/hide)
- (da/request-account-deletion
- (with-meta {} {:on-error on-error
- :on-success on-success}))))]
+ (du/request-account-deletion
+ (with-meta {} {:on-error on-error}))))]
[:div.modal-overlay
[:div.modal-container.change-email-modal
[:div.modal-header
[:div.modal-header-title
- [:h2 (t locale "modals.delete-account.title")]]
+ [:h2 (tr "modals.delete-account.title")]]
[:div.modal-close-button
{:on-click on-close} i/close]]
[:div.modal-content
[:& msgs/inline-banner
{:type :warning
- :content (t locale "modals.delete-account.info")}]]
+ :content (tr "modals.delete-account.info")}]]
[:div.modal-footer
[:div.action-buttons
[:button.btn-warning.btn-large {:on-click on-accept}
- (t locale "modals.delete-account.confirm")]
+ (tr "modals.delete-account.confirm")]
[:button.btn-secondary.btn-large {:on-click on-close}
- (t locale "modals.delete-account.cancel")]]]]]))
+ (tr "modals.delete-account.cancel")]]]]]))
diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs
index 83d4993d5..ed5347091 100644
--- a/frontend/src/app/main/ui/settings/sidebar.cljs
+++ b/frontend/src/app/main/ui/settings/sidebar.cljs
@@ -7,8 +7,8 @@
(ns app.main.ui.settings.sidebar
(:require
[app.config :as cf]
- [app.main.data.auth :as da]
[app.main.data.modal :as modal]
+ [app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.dashboard.sidebar :refer [profile-section]]
[app.main.ui.icons :as i]
@@ -26,7 +26,7 @@
go-dashboard
(mf/use-callback
(mf/deps profile)
- (st/emitf (rt/nav :dashboard-projects {:team-id (da/current-team-id profile)})))
+ (st/emitf (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})))
go-settings-profile
(mf/use-callback
diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs
index 8573682fd..bbd09a262 100644
--- a/frontend/src/app/main/ui/shapes/attrs.cljs
+++ b/frontend/src/app/main/ui/shapes/attrs.cljs
@@ -81,6 +81,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))
diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs
new file mode 100644
index 000000000..76acb5b07
--- /dev/null
+++ b/frontend/src/app/main/ui/shapes/fill_image.cljs
@@ -0,0 +1,39 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.shapes.fill-image
+ (:require
+ [app.common.geom.shapes :as gsh]
+ [app.config :as cfg]
+ [app.util.object :as obj]
+ [rumext.alpha :as mf]
+ [app.common.geom.point :as gpt]
+ [app.main.ui.shapes.image :as image]))
+
+(mf/defc fill-image-pattern
+ {::mf/wrap-props false}
+ [props]
+
+ (let [shape (obj/get props "shape")
+ render-id (obj/get props "render-id")]
+ (when (contains? shape :fill-image)
+ (let [{:keys [x y width height]} (:selrect shape)
+ fill-image-id (str "fill-image-" render-id)
+ media (:fill-image shape)
+ {:keys [uri loading]} (image/use-image-uri media)
+ transform (gsh/transform-matrix shape)]
+
+ [:pattern {:id fill-image-id
+ :patternUnits "userSpaceOnUse"
+ :x x
+ :y y
+ :height height
+ :width width
+ :patternTransform transform
+ :data-loading (str loading)}
+ [:image {:xlinkHref uri
+ :width width
+ :height height}]]))))
diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs
index 22aa27c30..966619c67 100644
--- a/frontend/src/app/main/ui/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/shapes/frame.cljs
@@ -21,7 +21,7 @@
[props]
(let [childs (unchecked-get props "childs")
shape (unchecked-get props "shape")
- {:keys [id x y width height]} shape
+ {:keys [id width height]} shape
props (-> (merge frame-default-props shape)
(attrs/extract-style-attrs)
diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs
index 4ad49835b..124ce7052 100644
--- a/frontend/src/app/main/ui/shapes/image.cljs
+++ b/frontend/src/app/main/ui/shapes/image.cljs
@@ -17,13 +17,10 @@
[beicon.core :as rx]
[rumext.alpha :as mf]))
-(mf/defc image-shape
- {::mf/wrap-props false}
- [props]
-
- (let [shape (unchecked-get props "shape")
- {:keys [id x y width height rotation metadata]} shape
- uri (cfg/resolve-file-media metadata)
+(defn use-image-uri
+ [media]
+ (let [uri (mf/use-memo (mf/deps (:id media))
+ #(cfg/resolve-file-media media))
embed-resources? (mf/use-ctx muc/embed-ctx)
data-uri (mf/use-state (when (not embed-resources?) uri))]
@@ -38,6 +35,17 @@
(rx/mapcat wapi/read-file-as-data-url)
(rx/subs #(reset! data-uri %))))))
+ {:uri (or @data-uri uri)
+ :loading (not (some? @data-uri))}))
+
+(mf/defc image-shape
+ {::mf/wrap-props false}
+ [props]
+
+ (let [shape (unchecked-get props "shape")
+ {:keys [id x y width height rotation metadata]} shape
+ {:keys [uri loading]} (use-image-uri metadata)]
+
(let [transform (geom/transform-matrix shape)
props (-> (attrs/extract-style-attrs shape)
(obj/merge!
@@ -46,17 +54,15 @@
:transform transform
:width width
:height height
- :preserveAspectRatio "none"}))
+ :preserveAspectRatio "none"})
+ (cond-> loading
+ (obj/set! "data-loading" "true")))
+
on-drag-start (fn [event]
;; Prevent browser dragging of the image
(dom/prevent-default event))]
- (if (nil? @data-uri)
- [:> "rect" (obj/merge!
- props
- #js {:fill "#E8E9EA"
- :stroke "#000000"})]
- [:> "image" (obj/merge!
- props
- #js {:xlinkHref @data-uri
- :onDragStart on-drag-start})]))))
+ [:> "image" (obj/merge!
+ props
+ #js {:xlinkHref uri
+ :onDragStart on-drag-start})])))
diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs
index 88df3cff8..82dddd238 100644
--- a/frontend/src/app/main/ui/shapes/shape.cljs
+++ b/frontend/src/app/main/ui/shapes/shape.cljs
@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.main.ui.context :as muc]
+ [app.main.ui.shapes.fill-image :as fim]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.gradients :as grad]
[app.main.ui.shapes.svg-defs :as defs]
@@ -16,8 +17,9 @@
[rumext.alpha :as mf]))
(mf/defc shape-container
- {::mf/wrap-props false}
- [props]
+ {::mf/forward-ref true
+ ::mf/wrap-props false}
+ [props ref]
(let [shape (obj/get props "shape")
children (obj/get props "children")
pointer-events (obj/get props "pointer-events")
@@ -33,6 +35,7 @@
frame? (= :frame type)
group-props (-> (obj/clone props)
(obj/without ["shape" "children"])
+ (obj/set! "ref" ref)
(obj/set! "id" (str "shape-" (:id shape)))
(obj/set! "filter" (filters/filter-str filter-id shape))
(obj/set! "style" styles)
@@ -49,8 +52,9 @@
[:& (mf/provider muc/render-ctx) {:value render-id}
[:> wrapper-tag group-props
[:defs
- [:& defs/svg-defs {:shape shape :render-id render-id}]
- [:& filters/filters {:shape shape :filter-id filter-id}]
- [:& grad/gradient {:shape shape :attr :fill-color-gradient}]
- [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
+ [:& defs/svg-defs {:shape shape :render-id render-id}]
+ [:& filters/filters {:shape shape :filter-id filter-id}]
+ [:& grad/gradient {:shape shape :attr :fill-color-gradient}]
+ [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
+ [:& fim/fill-image-pattern {:shape shape :render-id render-id}]]
children]]))
diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs
index f26386dae..48a516240 100644
--- a/frontend/src/app/main/ui/shapes/text.cljs
+++ b/frontend/src/app/main/ui/shapes/text.cljs
@@ -22,7 +22,7 @@
(let [node (obj/get props "node")
text (:text node)
style (sts/generate-text-styles node)]
- [:span {:style style}
+ [:span.text-node {:style style}
(if (= text "") "\u00A0" text)]))
(mf/defc render-root
@@ -102,6 +102,10 @@
:height (if (#{:auto-height :auto-width} grow-type) 100000 height)
:style (-> (obj/new) (attrs/add-layer-props shape))
:ref ref}
+ ;; We use a class here because react has a bug that won't use the appropiate selector for
+ ;; `background-clip`
+ [:style ".text-node { background-clip: text;
+ -webkit-background-clip: text;" ]
[:& render-node {:index 0
:shape shape
:node content}]]))
diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs
index 8c5fc6284..61ef3fd4e 100644
--- a/frontend/src/app/main/ui/shapes/text/embed.cljs
+++ b/frontend/src/app/main/ui/shapes/text/embed.cljs
@@ -62,7 +62,7 @@
"Given a font and the variant-id, retrieves the style CSS for it."
[{:keys [id backend family variants] :as font} font-variant-id]
(if (= :google backend)
- (let [uri (fonts/gfont-url family [{:id font-variant-id}])]
+ (let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})]
(->> (http/send! {:method :get
:mode :cors
:omit-default-headers true
diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs
index 21825299e..8c15ccb43 100644
--- a/frontend/src/app/main/ui/shapes/text/styles.cljs
+++ b/frontend/src/app/main/ui/shapes/text/styles.cljs
@@ -65,9 +65,7 @@
font-id (:font-id data (:font-id txt/default-text-attrs))
font-variant-id (:font-variant-id data)
- font-family (:font-family data)
font-size (:font-size data)
-
fill-color (:fill-color data)
fill-opacity (:fill-opacity data)
@@ -77,7 +75,8 @@
;; (uc/hex->rgba fill-color fill-opacity))
[r g b a] (uc/hex->rgba fill-color fill-opacity)
- text-color (str/format "rgba(%s, %s, %s, %s)" r g b a)
+ text-color (when (and (some? fill-color) (some? fill-opacity))
+ (str/format "rgba(%s, %s, %s, %s)" r g b a))
fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration
@@ -89,10 +88,10 @@
(let [text-color (-> (update gradient :type keyword)
(uc/gradient->css))]
(-> base
- (obj/set! "background" "var(--text-color)")
+ (obj/set! "--text-color" text-color)
+ (obj/set! "backgroundImage" "var(--text-color)")
(obj/set! "WebkitTextFillColor" "transparent")
- (obj/set! "WebkitBackgroundClip" "text")
- (obj/set! "--text-color" text-color))))
+ (obj/set! "WebkitBackgroundClip" "text"))))
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))
@@ -106,14 +105,15 @@
(pos? (alength font-id)))
(fonts/ensure-loaded! font-id)
(let [font (get fontsdb font-id)]
- (let [font-family (or (:family font)
- (obj/get data "fontFamily"))
+ (let [font-family (str/quote
+ (or (:family font)
+ (:font-family data)))
font-variant (d/seek #(= font-variant-id (:id %))
(:variants font))
- font-style (or (:style font-variant)
- (obj/get data "fontStyle"))
- font-weight (or (:weight font-variant)
- (obj/get data "fontWeight"))]
+ font-style (or (:style font-variant)
+ (:font-style data))
+ font-weight (or (:weight font-variant)
+ (:font-weight data))]
(obj/set! base "fontFamily" font-family)
(obj/set! base "fontStyle" font-style)
(obj/set! base "fontWeight" font-weight))))
diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs
index 86f002d21..0cd9c0b5a 100644
--- a/frontend/src/app/main/ui/static.cljs
+++ b/frontend/src/app/main/ui/static.cljs
@@ -6,21 +6,21 @@
(ns app.main.ui.static
(:require
- [cljs.spec.alpha :as s]
- [rumext.alpha :as mf]
- [app.main.ui.context :as ctx]
- [app.main.data.auth :as da]
[app.main.data.messages :as dm]
- [app.main.store :as st]
+ [app.main.data.users :as du]
[app.main.refs :as refs]
- [cuerdas.core :as str]
+ [app.main.store :as st]
+ [app.main.ui.context :as ctx]
+ [app.main.ui.icons :as i]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
- [app.main.ui.icons :as i]))
+ [cljs.spec.alpha :as s]
+ [cuerdas.core :as str]
+ [rumext.alpha :as mf]))
(defn- go-to-dashboard
[profile]
- (let [team-id (da/current-team-id profile)]
+ (let [team-id (du/get-current-team-id profile)]
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))
(mf/defc not-found
@@ -38,7 +38,7 @@
[:div.sign-info
[:span (tr "labels.not-found.auth-info") " " [:b (:email profile)]]
[:a.btn-primary.btn-small
- {:on-click (st/emitf (da/logout))}
+ {:on-click (st/emitf (du/logout))}
(tr "labels.sign-out")]]]]]))
(mf/defc bad-gateway
diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs
index bbb940413..930968526 100644
--- a/frontend/src/app/main/ui/viewer.cljs
+++ b/frontend/src/app/main/ui/viewer.cljs
@@ -237,7 +237,7 @@
(events/unlistenByKey key3))))]
(mf/use-effect on-mount)
- (hooks/use-shortcuts sc/shortcuts)
+ (hooks/use-shortcuts ::viewer sc/shortcuts)
[:div.viewer-layout {:class (dom/classnames :force-visible
(:show-thumbnails state))}
diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs
index 2644e620c..46a29b984 100644
--- a/frontend/src/app/main/ui/viewer/header.cljs
+++ b/frontend/src/app/main/ui/viewer/header.cljs
@@ -10,6 +10,7 @@
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.comments :as dcm]
+ [app.main.data.events :as ev]
[app.main.data.messages :as dm]
[app.main.data.viewer :as dv]
[app.main.data.viewer.shortcuts :as sc]
@@ -23,6 +24,7 @@
[app.util.router :as rt]
[app.util.webapi :as wapi]
[cuerdas.core :as str]
+ [potok.core :as ptk]
[rumext.alpha :as mf]))
(mf/defc zoom-widget
diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs
index 6833c69d0..6fc58ebd2 100644
--- a/frontend/src/app/main/ui/workspace.cljs
+++ b/frontend/src/app/main/ui/workspace.cljs
@@ -100,8 +100,13 @@
(mf/use-layout-effect
(mf/deps page-id)
(fn []
- (st/emit! (dw/initialize-page page-id))
- (st/emitf (dw/finalize-page page-id))))
+ (if (nil? page-id)
+ (st/emit! (dw/go-to-page))
+ (st/emit! (dw/initialize-page page-id)))
+
+ (fn []
+ (when page-id
+ (st/emit! (dw/finalize-page page-id))))))
(when page
[:& workspace-content {:key page-id
@@ -116,7 +121,6 @@
(mf/defc workspace
{::mf/wrap [mf/memo]}
[{:keys [project-id file-id page-id layout-name] :as props}]
-
(let [file (mf/deref refs/workspace-file)
project (mf/deref refs/workspace-project)
layout (mf/deref refs/workspace-layout)]
@@ -134,7 +138,7 @@
(mf/use-effect
(fn []
;; Close any non-modal dialog that may be still open
- (st/emitf dm/hide)))
+ (st/emit! dm/hide)))
(mf/use-effect
(mf/deps file)
@@ -154,6 +158,9 @@
(if (and (and file project)
(:initialized file))
- [:& workspace-page {:page-id page-id :file file :layout layout}]
+ [:& workspace-page {:key (str "page-" page-id)
+ :page-id page-id
+ :file file
+ :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 176cb0d46..d9d4df230 100644
--- a/frontend/src/app/main/ui/workspace/colorpalette.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs
@@ -209,7 +209,7 @@
(into []
(cond
(= selected :recent) (reverse recent-colors)
- (= selected :file) (vals file-colors)
+ (= selected :file) (->> (vals file-colors) (sort-by :name))
:else (library->colors shared-libs selected))))))
(mf/use-effect
@@ -222,7 +222,8 @@
(mf/deps file-colors)
(fn []
(when (= selected :file)
- (reset! current-library-colors (into [] (vals file-colors))))))
+ (reset! current-library-colors (into [] (->> (vals file-colors)
+ (sort-by :name)))))))
[:& palette {:left-sidebar? left-sidebar?
:current-colors @current-library-colors
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index 77cd0f51a..309803e9e 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -218,6 +218,13 @@
(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")]]
diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
index f948e8237..1692a392f 100644
--- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs
+++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs
@@ -45,12 +45,12 @@
(st/emit! (dw/upload-media-workspace params)))))]
[:li.tooltip.tooltip-right
- {:alt (tr "workspace.toolbar.image")
+ {:alt (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))
:on-click on-click}
[:*
i/image
[:& file-uploader {:input-id "image-upload"
- :accept cm/str-media-types
+ :accept cm/str-image-types
:multi true
:input-ref ref
:on-selected on-files-selected}]]]))
diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs
index 9faf50f85..48b6f00e8 100644
--- a/frontend/src/app/main/ui/workspace/shapes.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes.cljs
@@ -51,7 +51,8 @@
"Draws the root shape of the viewport and recursively all the shapes"
{::mf/wrap-props false}
[props]
- (let [objects (obj/get props "objects")
+ (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 %)))]
@@ -59,7 +60,8 @@
(if (= (:type item) :frame)
[:& frame-wrapper {:shape item
:key (:id item)
- :objects objects}]
+ :objects objects
+ :thumbnail? (not (get active-frames (:id item) false))}]
[:& shape-wrapper {:shape item
:key (:id item)}]))))
@@ -70,7 +72,7 @@
[props]
(let [shape (obj/get props "shape")
frame (obj/get props "frame")
- shape (-> (geom/transform-shape shape)
+ shape (-> (geom/transform-shape shape {:round-coords? false})
(geom/translate-to-frame frame))
opts #js {:shape shape
:frame frame}
diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
index 91e31c6ed..8875ba6e5 100644
--- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
@@ -9,6 +9,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.data.workspace :as dw]
+ [app.main.data.workspace.changes :as dch]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as muc]
@@ -17,11 +18,18 @@
[app.main.ui.shapes.text.embed :as ste]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
+ [app.util.object :as obj]
[app.util.timers :as ts]
[beicon.core :as rx]
[okulary.core :as l]
[rumext.alpha :as mf]))
+(def obs-config
+ #js {:attributes true
+ :childList true
+ :subtree true
+ :characterData true})
+
(defn make-is-moving-ref
[id]
(let [check-moving (fn [local]
@@ -43,14 +51,34 @@
(let [new-shape (unchecked-get new-props "shape")
old-shape (unchecked-get old-props "shape")
+ new-thumbnail? (unchecked-get new-props "thumbnail?")
+ old-thumbnail? (unchecked-get old-props "thumbnail?")
+
new-objects (unchecked-get new-props "objects")
old-objects (unchecked-get old-props "objects")
new-children (->> new-shape :shapes (mapv #(get new-objects %)))
old-children (->> old-shape :shapes (mapv #(get old-objects %)))]
(and (= new-shape old-shape)
+ (= new-thumbnail? old-thumbnail?)
(= new-children old-children))))
+(mf/defc thumbnail
+ {::mf/wrap-props false}
+ [props]
+ (let [shape (obj/get props "shape")]
+ (when (:thumbnail shape)
+ [:image.frame-thumbnail
+ {:id (str "thumbnail-" (:id shape))
+ :xlinkHref (:thumbnail shape)
+ :x (:x shape)
+ :y (:y shape)
+ :width (:width shape)
+ :height (:height shape)
+ ;; DEBUG
+ ;; :style {:filter "sepia(1)"}
+ }])))
+
;; This custom deffered don't deffer rendering when ghost rendering is
;; used.
(defn custom-deferred
@@ -76,6 +104,8 @@
[props]
(let [shape (unchecked-get props "shape")
objects (unchecked-get props "objects")
+ thumbnail? (unchecked-get props "thumbnail?")
+
edition (mf/deref refs/selected-edition)
embed-fonts? (mf/use-ctx muc/embed-ctx)
@@ -86,14 +116,30 @@
(filterv #(and (= :text (:type %))
(= (:id shape) (:frame-id %)))))
- ds-modifier (get-in shape [:modifiers :displacement])]
+ ds-modifier (get-in shape [:modifiers :displacement])
+
+ rendered? (mf/use-state false)
+
+ show-thumbnail? (and thumbnail? (some? (:thumbnail shape)))
+
+ on-dom
+ (mf/use-callback
+ (fn [node]
+ (ts/schedule-on-idle #(reset! rendered? (some? node)))))]
(when (and shape (not (:hidden shape)))
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
- [:> shape-container {:shape shape}
- (when embed-fonts?
- [:& ste/embed-fontfaces-style {:shapes text-childs}])
- [:& frame-shape
- {:shape shape
- :childs children}]]])))))
+
+ (when-not show-thumbnail?
+ [:> shape-container {:shape shape
+ :ref on-dom}
+
+ (when embed-fonts?
+ [:& ste/embed-fontfaces-style {:shapes text-childs}])
+
+ [:& frame-shape {:shape shape
+ :childs children}]])
+
+ (when (or (not @rendered?) show-thumbnail?)
+ [:& thumbnail {:shape shape}])])))))
diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs
index d7ce59151..db7ece44a 100644
--- a/frontend/src/app/main/ui/workspace/shapes/group.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs
@@ -38,7 +38,7 @@
{:keys [id x y width height]} shape
- childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
+ childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape) {:with-modifiers? true}))
childs (mf/deref childs-ref)]
[:> shape-container {:shape shape}
diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs
index 16101feca..88cc10114 100644
--- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs
@@ -30,12 +30,14 @@
(let [{:keys [x y]} position
on-enter
- (fn [event]
- (st/emit! (drp/path-pointer-enter position)))
+ (mf/use-callback
+ (fn [event]
+ (st/emit! (drp/path-pointer-enter position))))
on-leave
- (fn [event]
- (st/emit! (drp/path-pointer-leave position)))
+ (mf/use-callback
+ (fn [event]
+ (st/emit! (drp/path-pointer-leave position))))
on-mouse-down
(fn [event]
@@ -48,6 +50,9 @@
(let [shift? (kbd/shift? event)
ctrl? (kbd/ctrl? event)]
(cond
+ last-p?
+ (st/emit! (drp/reset-last-handler))
+
(and (= edit-mode :move) ctrl? (not curve?))
(st/emit! (drp/make-curve position))
@@ -81,8 +86,7 @@
:on-mouse-down on-mouse-down
:on-mouse-enter on-enter
:on-mouse-leave on-leave
- :style {:pointer-events (when last-p? "none")
- :cursor (cond
+ :style {:cursor (cond
(= edit-mode :draw) cur/pen-node
(= edit-mode :move) cur/pointer-node)
:fill "transparent"}}]]))
diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs
index 084da1e56..9e4a3efbe 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs
@@ -21,6 +21,7 @@
[app.util.logging :as log]
[app.util.object :as obj]
[app.util.timers :as timers]
+ [app.util.webapi :as wapi]
[app.util.text-editor :as ted]
[okulary.core :as l]
[beicon.core :as rx]
@@ -62,6 +63,7 @@
(true? (obj/get props "edition?"))
(update-with-current-editor-state))
+ mnt (mf/use-ref true)
paragraph-ref (mf/use-state nil)
handle-resize-text
@@ -83,20 +85,24 @@
(mf/deps handle-resize-text)
(fn [node]
(when node
- (let [obs-ref (atom nil)]
- (timers/schedule
- (fn []
- (when-let [ps-node (dom/query node ".paragraph-set")]
- (reset! paragraph-ref ps-node))))))))]
+ (timers/schedule
+ #(when (mf/ref-val mnt)
+ (when-let [ps-node (dom/query node ".paragraph-set")]
+ (reset! paragraph-ref ps-node)))))))]
(mf/use-effect
(mf/deps @paragraph-ref handle-resize-text grow-type)
(fn []
(when-let [paragraph-node @paragraph-ref]
- (let [observer (js/ResizeObserver. handle-resize-text)]
+ (let [sub (->> (wapi/observe-resize paragraph-node)
+ (rx/observe-on :af)
+ (rx/subs handle-resize-text))]
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
- (.observe observer paragraph-node)
- #(.disconnect observer)))))
+ (fn []
+ (rx/dispose! sub))))))
+
+ (mf/use-effect
+ (fn [] #(mf/set-ref-val! mnt false)))
[:& text/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
index fb13896df..de210b422 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
@@ -213,7 +213,7 @@
(fn [path]
(fn [event]
(dom/stop-propagation event)
- (swap! state update :folded-groups
+ (swap! state update :folded-groups
toggle-folded-group path))))
on-group
@@ -401,7 +401,7 @@
(fn [path]
(fn [event]
(dom/stop-propagation event)
- (swap! state update :folded-groups
+ (swap! state update :folded-groups
toggle-folded-group path))))
on-group
@@ -427,7 +427,7 @@
(when local?
[:div.assets-button {:on-click add-graphic}
i/plus
- [:& file-uploader {:accept cm/str-media-types
+ [:& file-uploader {:accept cm/str-image-types
:multi true
:input-ref input-ref
:on-selected on-file-selected}]])]
@@ -499,7 +499,8 @@
(mf/defc color-item
[{:keys [color local? file-id selected-colors multi-colors? multi-assets?
- on-asset-click on-assets-delete on-clear-selection colors locale] :as props}]
+ on-asset-click on-assets-delete on-clear-selection on-group
+ colors locale] :as props}]
(let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
id (:id color)
input-ref (mf/use-ref)
@@ -623,17 +624,24 @@
[(t locale "workspace.assets.rename") rename-color-clicked])
(when-not (or multi-colors? multi-assets?)
[(t locale "workspace.assets.edit") edit-color-clicked])
- [(t locale "workspace.assets.delete") delete-color]]}])]))
+ [(t locale "workspace.assets.delete") delete-color]
+ (when-not multi-assets?
+ [(tr "workspace.assets.group") on-group])]}])]))
(mf/defc colors-box
[{:keys [file-id local? colors locale open? selected-assets
on-asset-click on-assets-delete on-clear-selection] :as props}]
- (let [selected-colors (:colors selected-assets)
+ (let [state (mf/use-state {:folded-groups empty-folded-groups})
+
+ selected-colors (:colors selected-assets)
multi-colors? (> (count selected-colors) 1)
multi-assets? (or (not (empty? (:components selected-assets)))
(not (empty? (:graphics selected-assets)))
(not (empty? (:typographies selected-assets))))
+ groups (group-assets colors)
+ folded-groups (:folded-groups @state)
+
add-color
(mf/use-callback
(mf/deps file-id)
@@ -651,7 +659,39 @@
:on-accept add-color
:data {:color "#406280"
:opacity 1}
- :position :right})))]
+ :position :right})))
+
+ create-group
+ (mf/use-callback
+ (mf/deps colors selected-colors on-clear-selection file-id)
+ (fn [name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> colors
+ (filter #(contains? selected-colors (:id %)))
+ (map #(dwl/update-color
+ (assoc % :name
+ (str name " / "
+ (cp/merge-path-item (:path %) (:name %))))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
+
+ on-fold-group
+ (mf/use-callback
+ (mf/deps groups folded-groups)
+ (fn [path]
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! state update :folded-groups
+ toggle-folded-group path))))
+
+ on-group
+ (mf/use-callback
+ (mf/deps colors selected-colors)
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :create-group-dialog {:create create-group})))]
[:div.asset-section
[:div.asset-title {:class (when (not open?) "closed")}
@@ -661,24 +701,41 @@
(when local?
[:div.assets-button {:on-click add-color-clicked} i/plus])]
(when open?
- [:div.asset-list
- (for [color colors]
- (let [color (cond-> color
- (:value color) (assoc :color (:value color) :opacity 1)
- (:value color) (dissoc :value)
- true (assoc :file-id file-id))]
- [:& color-item {:key (:id color)
- :color color
- :file-id file-id
- :local? local?
- :selected-colors selected-colors
- :multi-colors? multi-colors?
- :multi-assets? multi-assets?
- :on-asset-click on-asset-click
- :on-assets-delete on-assets-delete
- :on-clear-selection on-clear-selection
- :colors colors
- :locale locale}]))])]))
+ (for [group groups]
+ (let [path (first group)
+ colors (second group)
+ group-open? (not (contains? folded-groups path))]
+ [:*
+ (when-not (empty? path)
+ (let [[other-path last-path truncated] (cp/compact-path path 35)]
+ [:div.group-title {:class (when-not group-open? "closed")
+ :on-click (on-fold-group path)}
+ [:span i/arrow-slide]
+ (when-not (empty? other-path)
+ [:span.dim {:title (when truncated path)}
+ other-path "\u00A0/\u00A0"])
+ [:span {:title (when truncated path)}
+ last-path]]))
+ (when group-open?
+ [:div.asset-list
+ (for [color colors]
+ (let [color (cond-> color
+ (:value color) (assoc :color (:value color) :opacity 1)
+ (:value color) (dissoc :value)
+ true (assoc :file-id file-id))]
+ [:& color-item {:key (:id color)
+ :color color
+ :file-id file-id
+ :local? local?
+ :selected-colors selected-colors
+ :multi-colors? multi-colors?
+ :multi-assets? multi-assets?
+ :on-asset-click on-asset-click
+ :on-assets-delete on-assets-delete
+ :on-clear-selection on-clear-selection
+ :on-group on-group
+ :colors colors
+ :locale locale}]))])])))]))
;; ---- Typography box ----
@@ -690,15 +747,19 @@
:menu-open? false
:top nil
:left nil
- :id nil})
+ :id nil
+ :folded-groups empty-folded-groups})
local (deref refs/workspace-local)
+ groups (group-assets typographies)
+ folded-groups (:folded-groups @state)
+
selected-typographies (:typographies selected-assets)
multi-typographies? (> (count selected-typographies) 1)
- multi-assets? (or (not (empty? (:graphics selected-assets)))
- (not (empty? (:colors selected-assets)))
- (not (empty? (:typographies selected-assets))))
+ multi-assets? (or (not (empty? (:components selected-assets)))
+ (not (empty? (:graphics selected-assets)))
+ (not (empty? (:colors selected-assets))))
add-typography
(mf/use-callback
@@ -722,6 +783,38 @@
(run! #(st/emit! (dwt/update-text-attrs {:id % :editor (get-in local [:editors %]) :attrs attrs}))
ids)))
+ create-group
+ (mf/use-callback
+ (mf/deps typographies selected-typographies on-clear-selection file-id)
+ (fn [name]
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction))
+ (apply st/emit!
+ (->> typographies
+ (filter #(contains? selected-typographies (:id %)))
+ (map #(dwl/update-typography
+ (assoc % :name
+ (str name " / "
+ (cp/merge-path-item (:path %) (:name %))))
+ file-id))))
+ (st/emit! (dwu/commit-undo-transaction))))
+
+ on-fold-group
+ (mf/use-callback
+ (mf/deps groups folded-groups)
+ (fn [path]
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! state update :folded-groups
+ toggle-folded-group path))))
+
+ on-group
+ (mf/use-callback
+ (mf/deps typographies selected-typographies)
+ (fn [event]
+ (dom/stop-propagation event)
+ (modal/show! :create-group-dialog {:create create-group})))
+
on-context-menu
(mf/use-callback
(mf/deps selected-typographies on-clear-selection)
@@ -788,22 +881,40 @@
[(t locale "workspace.assets.rename") handle-rename-typography-clicked])
(when-not (or multi-typographies? multi-assets?)
[(t locale "workspace.assets.edit") handle-edit-typography-clicked])
- [(t locale "workspace.assets.delete") handle-delete-typography]]}]
+ [(t locale "workspace.assets.delete") handle-delete-typography]
+ (when-not multi-assets?
+ [(tr "workspace.assets.group") on-group])]}]
(when open?
- [:div.asset-list
- (for [typography typographies]
- [:& typography-entry
- {:key (:id typography)
- :typography typography
- :file file
- :read-only? (not local?)
- :on-context-menu #(on-context-menu (:id typography) %)
- :on-change #(handle-change typography %)
- :selected? (contains? selected-typographies (:id typography))
- :on-click #(on-asset-click % (:id typography) {"" typographies}
- (partial apply-typography typography))
- :editting? (= editting-id (:id typography))
- :focus-name? (= (:rename-typography local) (:id typography))}])])]))
+ (for [group groups]
+ (let [path (first group)
+ typographies (second group)
+ group-open? (not (contains? folded-groups path))]
+ [:*
+ (when-not (empty? path)
+ (let [[other-path last-path truncated] (cp/compact-path path 35)]
+ [:div.group-title {:class (when-not group-open? "closed")
+ :on-click (on-fold-group path)}
+ [:span i/arrow-slide]
+ (when-not (empty? other-path)
+ [:span.dim {:title (when truncated path)}
+ other-path "\u00A0/\u00A0"])
+ [:span {:title (when truncated path)}
+ last-path]]))
+ (when group-open?
+ [:div.asset-list
+ (for [typography typographies]
+ [:& typography-entry
+ {:key (:id typography)
+ :typography typography
+ :file file
+ :read-only? (not local?)
+ :on-context-menu #(on-context-menu (:id typography) %)
+ :on-change #(handle-change typography %)
+ :selected? (contains? selected-typographies (:id typography))
+ :on-click #(on-asset-click % (:id typography) {"" typographies}
+ (partial apply-typography typography))
+ :editting? (= editting-id (:id typography))
+ :focus-name? (= (:rename-typography local) (:id typography))}])])])))]))
;; --- Assets toolbox ----
@@ -811,37 +922,36 @@
(defn file-colors-ref
[id]
(l/derived (fn [state]
- (let [wfile (:workspace-file state)]
+ (let [wfile (:workspace-data state)]
(if (= (:id wfile) id)
- (vals (get-in wfile [:data :colors]))
+ (vals (get-in wfile [:colors]))
(vals (get-in state [:workspace-libraries id :data :colors])))))
st/state =))
-
(defn file-media-ref
[id]
(l/derived (fn [state]
- (let [wfile (:workspace-file state)]
+ (let [wfile (:workspace-data state)]
(if (= (:id wfile) id)
- (vals (get-in wfile [:data :media]))
+ (vals (get-in wfile [:media]))
(vals (get-in state [:workspace-libraries id :data :media])))))
st/state =))
(defn file-components-ref
[id]
(l/derived (fn [state]
- (let [wfile (:workspace-file state)]
+ (let [wfile (:workspace-data state)]
(if (= (:id wfile) id)
- (vals (get-in wfile [:data :components]))
+ (vals (get-in wfile [:components]))
(vals (get-in state [:workspace-libraries id :data :components])))))
st/state =))
(defn file-typography-ref
[id]
(l/derived (fn [state]
- (let [wfile (:workspace-file state)]
+ (let [wfile (:workspace-data state)]
(if (= (:id wfile) id)
- (vals (get-in wfile [:data :typographies]))
+ (vals (get-in wfile [:typographies]))
(vals (get-in state [:workspace-libraries id :data :typographies])))))
st/state =))
@@ -860,7 +970,7 @@
(sort-by #(str/lower (:name %)) comp-fn))))
(mf/defc file-library
- [{:keys [file local? default-open? filters locale] :as props}]
+ [{:keys [file local? default-open? filters locale] :as props}]
(let [open-file (mf/deref (open-file-ref (:id file)))
open? (-> open-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 fa842aa4d..310ece276 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
@@ -150,10 +150,6 @@
(dom/prevent-default event)
(let [id (:id item)]
(cond
- (or (:blocked item)
- (:hidden item))
- nil
-
(kbd/shift? event)
(st/emit! (dw/shift-select-shapes id))
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 5c83e42cc..0d6ab2546 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
@@ -128,7 +128,6 @@
:on-click #(handle-change % "rtl")}
i/text-direction-rtl]]))
-
(mf/defc vertical-align
[{:keys [shapes ids values on-change] :as props}]
(let [{:keys [vertical-align]} values
@@ -225,68 +224,80 @@
(tr "workspace.options.text-options.title"))
emit-update!
- (fn [id attrs]
- (let [attrs (select-keys attrs root-attrs)]
- (when-not (empty? attrs)
- (st/emit! (dwt/update-root-attrs {:id id :attrs attrs}))))
+ (mf/use-callback
+ (fn [id attrs]
+ (let [attrs (select-keys attrs root-attrs)]
+ (when-not (empty? attrs)
+ (st/emit! (dwt/update-root-attrs {:id id :attrs attrs}))))
- (let [attrs (select-keys attrs paragraph-attrs)]
- (when-not (empty? attrs)
- (st/emit! (dwt/update-paragraph-attrs {:id id :attrs attrs}))))
+ (let [attrs (select-keys attrs paragraph-attrs)]
+ (when-not (empty? attrs)
+ (st/emit! (dwt/update-paragraph-attrs {:id id :attrs attrs}))))
- (let [attrs (select-keys attrs text-attrs)]
- (when-not (empty? attrs)
- (st/emit! (dwt/update-text-attrs {:id id :attrs attrs})))))
+ (let [attrs (select-keys attrs text-attrs)]
+ (when-not (empty? attrs)
+ (st/emit! (dwt/update-text-attrs {:id id :attrs attrs}))))))
+
+ on-change
+ (mf/use-callback
+ (mf/deps ids)
+ (fn [attrs]
+ (run! #(emit-update! % attrs) ids)))
typography
- (cond
- (and (:typography-ref-id values)
- (not= (:typography-ref-id values) :multiple)
- (not= (:typography-ref-file values) file-id))
- (-> shared-libs
- (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)])
- (assoc :file-id (:typography-ref-file values)))
+ (mf/use-memo
+ (mf/deps values file-id shared-libs)
+ (fn []
+ (cond
+ (and (:typography-ref-id values)
+ (not= (:typography-ref-id values) :multiple)
+ (not= (:typography-ref-file values) file-id))
+ (-> shared-libs
+ (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)])
+ (assoc :file-id (:typography-ref-file values)))
- (and (:typography-ref-id values)
- (not= (:typography-ref-id values) :multiple)
- (= (:typography-ref-file values) file-id))
- (get typographies (:typography-ref-id values)))
+ (and (:typography-ref-id values)
+ (not= (:typography-ref-id values) :multiple)
+ (= (:typography-ref-file values) file-id))
+ (get typographies (:typography-ref-id values)))))
on-convert-to-typography
- (mf/use-callback
- (mf/deps values)
- (fn [event]
- (let [setted-values (-> (d/without-nils values)
- (select-keys
- (d/concat text-font-attrs
- text-spacing-attrs
- text-transform-attrs)))
- typography (merge txt/default-typography setted-values)
- typography (generate-typography-name typography)]
- (let [id (uuid/next)]
- (st/emit! (dwl/add-typography (assoc typography :id id) false))
- (run! #(emit-update! % {:typography-ref-id id
- :typography-ref-file file-id}) ids)))))
+ (fn [event]
+ (let [setted-values (-> (d/without-nils values)
+ (select-keys
+ (d/concat text-font-attrs
+ text-spacing-attrs
+ text-transform-attrs)))
+ typography (merge txt/default-typography setted-values)
+ typography (generate-typography-name typography)]
+ (let [id (uuid/next)]
+ (st/emit! (dwl/add-typography (assoc typography :id id) false))
+ (run! #(emit-update! % {:typography-ref-id id
+ :typography-ref-file file-id}) ids))))
handle-detach-typography
- (fn []
- (run! #(emit-update! % {:typography-ref-file nil
- :typography-ref-id nil})
- ids))
+ (mf/use-callback
+ (mf/deps on-change)
+ (fn []
+ (on-change {:typography-ref-file nil
+ :typography-ref-id nil})))
handle-change-typography
- (fn [changes]
- (st/emit! (dwl/update-typography (merge typography changes) file-id)))
+ (mf/use-callback
+ (mf/deps typography file-id)
+ (fn [changes]
+ (st/emit! (dwl/update-typography (merge typography changes) file-id))))
+
+ multiple? (->> values vals (d/seek #(= % :multiple)))
opts #js {:ids ids
:values values
- :on-change (fn [attrs]
- (run! #(emit-update! % attrs) ids))}]
+ :on-change on-change}]
[:div.element-set
[:div.element-set-title
[:span label]
- (when (not typography)
+ (when (and (not typography) (not multiple?))
[:div.add-page {:on-click on-convert-to-typography} i/close])]
(cond
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 14e715e87..e0649d7b6 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
@@ -6,19 +6,30 @@
(ns app.main.ui.workspace.sidebar.options.menus.typography
(:require
+ ["react-virtualized" :as rvt]
+ [app.common.exceptions :as ex]
[app.common.data :as d]
+ [app.common.pages :as cp]
[app.common.text :as txt]
[app.main.data.workspace.texts :as dwt]
+ [app.main.data.shortcuts :as dsc]
+ [app.main.data.fonts :as df]
+ [app.main.data.workspace :as dw]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
+ [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.editable-select :refer [editable-select]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
[app.util.dom :as dom]
- [app.util.i18n :as i18n :refer [t]]
+ [app.util.object :as obj]
+ [app.util.timers :as tm]
+ [app.util.keyboard :as kbd]
+ [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
+ [goog.events :as events]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@@ -27,74 +38,288 @@
""
(str value)))
-(mf/defc font-select-optgroups
+(defn- get-next-font
+ [{:keys [id] :as current} fonts]
+ (if (seq fonts)
+ (let [index (d/index-of-pred fonts #(= (:id %) id))
+ index (or index -1)
+ next (ex/ignoring (nth fonts (inc index)))]
+ (or next (first fonts)))
+ current))
+
+(defn- get-prev-font
+ [{:keys [id] :as current} fonts]
+ (if (seq fonts)
+ (let [index (d/index-of-pred fonts #(= (:id %) id))
+ next (ex/ignoring (nth fonts (dec index)))]
+ (or next (peek fonts)))
+ current))
+
+(mf/defc font-item
{::mf/wrap [mf/memo]}
- [{:keys [locale] :as props}]
- [:*
- [:optgroup {:label (t locale "workspace.options.text-options.preset")}
- (for [font fonts/local-fonts]
- [:option {:value (:id font)
- :key (:id font)}
- (:name font)])]
- [:optgroup {:label (t locale "workspace.options.text-options.google")}
- (for [font (fonts/resolve-fonts :google)]
- [:option {:value (:id font)
- :key (:id font)}
- (:name font)])]])
+ [{:keys [font current? on-click style]}]
+ (let [item-ref (mf/use-ref)
+ on-click (mf/use-callback (mf/deps font) #(on-click font))]
+
+ (mf/use-effect
+ (mf/deps current?)
+ (fn []
+ (when current?
+ (let [element (mf/ref-val item-ref)]
+ (when-not (dom/is-in-viewport? element)
+ (dom/scroll-into-view! element))))))
+
+ [:div.font-item {:ref item-ref
+ :style style
+ :class (when current? "selected")
+ :on-click on-click}
+ [:span.icon (when current? i/tick)]
+ [:span.label (:name font)]]))
+
+(declare row-renderer)
+
+(defn filter-fonts
+ [{:keys [term backends]} fonts]
+ (let [xform (cond-> (map identity)
+ (seq term)
+ (comp (filter #(str/includes? (str/lower (:name %)) term)))
+
+ (seq backends)
+ (comp (filter #(contains? backends (:backend %)))))]
+ (into [] xform fonts)))
+
+(defn- toggle-backend
+ [backends id]
+ (if (contains? backends id)
+ (disj backends id)
+ (conj backends id)))
+
+(mf/defc font-selector
+ [{:keys [on-select on-close current-font] :as props}]
+ (let [selected (mf/use-state current-font)
+ state (mf/use-state {:term "" :backends #{}})
+
+ flist (mf/use-ref)
+ input (mf/use-ref)
+ ddown (mf/use-ref)
+
+ fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts))
+
+ select-next
+ (mf/use-callback
+ (mf/deps fonts)
+ (fn [event]
+ (dom/stop-propagation event)
+ (dom/prevent-default event)
+ (swap! selected get-next-font fonts)))
+
+ select-prev
+ (mf/use-callback
+ (mf/deps fonts)
+ (fn [event]
+ (dom/stop-propagation event)
+ (dom/prevent-default event)
+ (swap! selected get-prev-font fonts)))
+
+ on-key-down
+ (mf/use-callback
+ (mf/deps fonts)
+ (fn [event]
+ (cond
+ (kbd/up-arrow? event) (select-prev event)
+ (kbd/down-arrow? event) (select-next event)
+ (kbd/esc? event) (on-close)
+ (kbd/enter? event) (on-close)
+ :else (dom/focus! (mf/ref-val input)))))
+
+ on-filter-change
+ (mf/use-callback
+ (mf/deps)
+ (fn [event]
+ (let [value (dom/get-target-val event)]
+ (swap! state assoc :term value))))
+
+ on-select-and-close
+ (mf/use-callback
+ (mf/deps on-select on-close)
+ (fn [font]
+ (on-select font)
+ (on-close)))
+ ]
+
+ (mf/use-effect
+ (mf/deps fonts)
+ (fn []
+ (let [key (events/listen js/document "keydown" on-key-down)]
+ #(events/unlistenByKey key))))
+
+ (mf/use-effect
+ (mf/deps @selected)
+ (fn []
+ (when-let [inst (mf/ref-val flist)]
+ (when-let [index (:index @selected)]
+ (.scrollToRow ^js inst index)))))
+
+ (mf/use-effect
+ (mf/deps @selected)
+ (fn []
+ (on-select @selected)))
+
+ (mf/use-effect
+ (fn []
+ (st/emit! (dsc/push-shortcuts :typography {}))
+ (fn []
+ (st/emit! (dsc/pop-shortcuts :typography)))))
+
+ (mf/use-effect
+ (fn []
+ (let [index (d/index-of-pred fonts #(= (:id %) (:id current-font)))
+ inst (mf/ref-val flist)]
+ (tm/schedule
+ #(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
+ (.scrollToPosition ^js inst offset))))))
+
+ [:div.font-selector
+ [:div.font-selector-dropdown
+ [:header
+ [:input {:placeholder "Search font"
+ :value (:term @state)
+ :ref input
+ :spell-check false
+ :on-change on-filter-change}]
+
+ #_[:div.options
+ {:on-click #(swap! state assoc :show-options true)
+ :class (when (seq (:backends @state)) "active")}
+ i/picker-hsv]
+
+ #_[:& dropdown {:show (:show-options @state false)
+ :on-close #(swap! state dissoc :show-options)}
+ (let [backends (:backends @state)]
+ [:div.backend-filters.dropdown {:ref ddown}
+ [:div.backend-filter
+ {:class (when (backends :custom) "selected")
+ :on-click #(swap! state update :backends toggle-backend :custom)}
+ [:div.checkbox-icon i/tick]
+ [:div.backend-name (tr "labels.custom-fonts")]]
+ [:div.backend-filter
+ {:class (when (backends :google) "selected")
+ :on-click #(swap! state update :backends toggle-backend :google)}
+ [:div.checkbox-icon i/tick]
+ [:div.backend-name "Google Fonts"]]])]]
+
+ [:hr]
+
+ [:div.fonts-list
+ [:> rvt/AutoSizer {}
+ (fn [props]
+ (let [width (obj/get props "width")
+ height (obj/get props "height")
+ render #(row-renderer fonts @selected on-select-and-close %)]
+ (mf/html
+ [:> rvt/List #js {:height height
+ :ref flist
+ :width width
+ :rowCount (count fonts)
+ :rowHeight 32
+ :rowRenderer render}])))]]]]))
+(defn row-renderer
+ [fonts selected on-select props]
+ (let [index (obj/get props "index")
+ key (obj/get props "key")
+ style (obj/get props "style")
+ font (nth fonts index)]
+ (mf/html
+ [:& font-item {:key key
+ :font font
+ :style style
+ :on-click on-select
+ :current? (= (:id font) (:id selected))}])))
(mf/defc font-options
- [{:keys [editor ids values locale on-change] :as props}]
- (let [{:keys [font-id
- font-size
- font-variant-id]} values
+ [{:keys [editor ids values on-change] :as props}]
+ (let [{:keys [font-id font-size font-variant-id]} values
- font-id (or font-id (:font-id txt/default-text-attrs))
- font-size (or font-size (:font-size txt/default-text-attrs))
+ font-id (or font-id (:font-id txt/default-text-attrs))
+ font-size (or font-size (:font-size txt/default-text-attrs))
font-variant-id (or font-variant-id (:font-variant-id txt/default-text-attrs))
- fonts (mf/deref fonts/fontsdb)
- font (get fonts font-id)
+ fonts (mf/deref fonts/fontsdb)
+ font (get fonts font-id)
+
+ open-selector? (mf/use-state false)
change-font
- (fn [new-font-id]
- (let [{:keys [family] :as font} (get fonts new-font-id)
- {:keys [id name weight style]} (fonts/get-default-variant font)]
- (on-change {:font-id new-font-id
- :font-family family
- :font-variant-id (or id name)
- :font-weight weight
- :font-style style})))
+ (mf/use-callback
+ (mf/deps on-change 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)]
+ (on-change {:font-id new-font-id
+ :font-family family
+ :font-variant-id (or id name)
+ :font-weight weight
+ :font-style style}))))
on-font-family-change
- (fn [event]
- (let [new-font-id (dom/get-target-val event)]
- (when-not (str/empty? new-font-id)
- (let [font (get fonts new-font-id)]
- (fonts/ensure-loaded! new-font-id (partial change-font new-font-id))))))
+ (mf/use-callback
+ (mf/deps fonts change-font)
+ (fn [event]
+ (let [new-font-id (dom/get-target-val event)]
+ (when-not (str/empty? new-font-id)
+ (let [font (get fonts new-font-id)]
+ (fonts/ensure-loaded! new-font-id (partial change-font new-font-id)))))))
on-font-size-change
- (fn [new-font-size]
- (when-not (str/empty? new-font-size)
- (on-change {:font-size (str new-font-size)})))
+ (mf/use-callback
+ (mf/deps on-change)
+ (fn [new-font-size]
+ (when-not (str/empty? new-font-size)
+ (on-change {:font-size (str new-font-size)}))))
on-font-variant-change
- (fn [event]
- (let [new-variant-id (dom/get-target-val event)
- variant (d/seek #(= new-variant-id (:id %)) (:variants font))]
- (on-change {:font-id (:id font)
- :font-family (:family font)
- :font-variant-id new-variant-id
- :font-weight (:weight variant)
- :font-style (:style variant)})))]
+ (mf/use-callback
+ (mf/deps font on-change)
+ (fn [event]
+ (let [new-variant-id (dom/get-target-val event)
+ variant (d/seek #(= new-variant-id (:id %)) (:variants font))]
+ (on-change {:font-id (:id font)
+ :font-family (:family font)
+ :font-variant-id new-variant-id
+ :font-weight (:weight variant)
+ :font-style (:style variant)}))))
+
+ on-font-select
+ (mf/use-callback
+ (mf/deps change-font)
+ (fn [font*]
+ (when (not= font font*)
+ (change-font (:id font*)))))
+
+ on-font-selector-close
+ (mf/use-callback
+ #(reset! open-selector? false))]
[:*
+ (when @open-selector?
+ [:& font-selector
+ {:current-font font
+ :on-close on-font-selector-close
+ :on-select on-font-select}])
+
[:div.row-flex
- [:select.input-select.font-option
- {:value (attr->string font-id)
- :on-change on-font-family-change}
- (when (= font-id :multiple)
- [:option {:value ""} (t locale "settings.multiple")])
- [:& font-select-optgroups {:locale locale}]]]
+ [:div.input-select.font-option
+ {:on-click #(reset! open-selector? true)}
+ (cond
+ (= :multiple font-id)
+ "--"
+
+ (some? font)
+ (:name font)
+
+ :else
+ (tr "dashboard.fonts.deleted-placeholder"))]]
+
[:div.row-flex
(let [size-options [8 9 10 11 12 14 18 24 36 48 72]
@@ -120,7 +345,7 @@
(mf/defc spacing-options
- [{:keys [editor ids values locale on-change] :as props}]
+ [{:keys [editor ids values on-change] :as props}]
(let [{:keys [line-height
letter-spacing]} values
@@ -135,7 +360,7 @@
[:div.spacing-options
[:div.input-icon
[:span.icon-before.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.line-height")}
+ {:alt (tr "workspace.options.text-options.line-height")}
i/line-height]
[:input.input-text
{:type "number"
@@ -143,12 +368,12 @@
:min "0"
:max "200"
:value (attr->string line-height)
- :placeholder (t locale "settings.multiple")
+ :placeholder (tr "settings.multiple")
:on-change #(handle-change % :line-height)}]]
[:div.input-icon
[:span.icon-before.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.letter-spacing")}
+ {:alt (tr "workspace.options.text-options.letter-spacing")}
i/letter-spacing]
[:input.input-text
{:type "number"
@@ -156,11 +381,11 @@
:min "0"
:max "200"
:value (attr->string letter-spacing)
- :placeholder (t locale "settings.multiple")
+ :placeholder (tr "settings.multiple")
:on-change #(handle-change % :letter-spacing)}]]]))
(mf/defc text-transform-options
- [{:keys [editor ids values locale on-change] :as props}]
+ [{:keys [editor ids values on-change] :as props}]
(let [{:keys [text-transform]} values
text-transform (or text-transform "none")
@@ -170,35 +395,32 @@
(on-change {:text-transform type}))]
[:div.align-icons
[:span.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.none")
+ {:alt (tr "workspace.options.text-options.none")
:class (dom/classnames :current (= "none" text-transform))
:on-click #(handle-change % "none")}
i/minus]
[:span.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.uppercase")
+ {:alt (tr "workspace.options.text-options.uppercase")
:class (dom/classnames :current (= "uppercase" text-transform))
:on-click #(handle-change % "uppercase")}
i/uppercase]
[:span.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.lowercase")
+ {:alt (tr "workspace.options.text-options.lowercase")
:class (dom/classnames :current (= "lowercase" text-transform))
:on-click #(handle-change % "lowercase")}
i/lowercase]
[:span.tooltip.tooltip-bottom
- {:alt (t locale "workspace.options.text-options.titlecase")
+ {:alt (tr "workspace.options.text-options.titlecase")
:class (dom/classnames :current (= "capitalize" text-transform))
:on-click #(handle-change % "capitalize")}
i/titlecase]]))
(mf/defc typography-options
[{:keys [ids editor values on-change]}]
- (let [locale (mf/deref i18n/locale)
- opts #js {:editor editor
+ (let [opts #js {:editor editor
:ids ids
:values values
- :locale locale
:on-change on-change}]
-
[:div.element-set-content
[:> font-options opts]
[:div.row-flex
@@ -208,15 +430,20 @@
(mf/defc typography-entry
[{:keys [typography read-only? selected? on-click on-change on-detach on-context-menu editting? focus-name? file]}]
- (let [locale (mf/deref i18n/locale)
- open? (mf/use-state editting?)
+ (let [open? (mf/use-state editting?)
hover-detach (mf/use-state false)
name-input-ref (mf/use-ref nil)
+ value (mf/use-state (cp/merge-path-item (:path typography) (:name typography)))
#_(rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
+
+ handle-change
+ (fn [event]
+ (reset! value (dom/get-target-val event)))
+
handle-go-to-edit
(fn [] (st/emit! (rt/nav :workspace {:project-id (:project-id file)
:file-id (:id file)}
@@ -248,7 +475,7 @@
{:style {:font-family (:font-family typography)
:font-weight (:font-weight typography)
:font-style (:font-style typography)}}
- (t locale "workspace.assets.typography.sample")]
+ (tr "workspace.assets.typography.sample")]
[:div.typography-name (:name typography)]]
[:div.element-set-actions
(when on-detach
@@ -270,32 +497,32 @@
[:span (:name typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.font-id")]
+ [:span.label (tr "workspace.assets.typography.font-id")]
[:span (:font-id typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.font-variant-id")]
+ [:span.label (tr "workspace.assets.typography.font-variant-id")]
[:span (:font-variant-id typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.font-size")]
+ [:span.label (tr "workspace.assets.typography.font-size")]
[:span (:font-size typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.line-height")]
+ [:span.label (tr "workspace.assets.typography.line-height")]
[:span (:line-height typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.letter-spacing")]
+ [:span.label (tr "workspace.assets.typography.letter-spacing")]
[:span (:letter-spacing typography)]]
[:div.row-flex
- [:span.label (t locale "workspace.assets.typography.text-transform")]
+ [:span.label (tr "workspace.assets.typography.text-transform")]
[:span (:text-transform typography)]]
[:div.go-to-lib-button
{:on-click handle-go-to-edit}
- (t locale "workspace.assets.typography.go-to-edit")]]
+ (tr "workspace.assets.typography.go-to-edit")]]
[:*
[:div.element-set-content
@@ -303,7 +530,8 @@
[:input.element-name.adv-typography-name
{:type "text"
:ref name-input-ref
- :value (:name typography)
- :on-change #(on-change {:name (dom/get-target-val %)})}]]]
+ :value @value
+ :on-change handle-change
+ :on-blur #(on-change {:name @value})}]]]
[:& typography-options {:values typography
:on-change on-change}]])]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
index 96e5f4021..be8a067ef 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
@@ -27,7 +27,19 @@
(defn color-picker-callback
[color disable-gradient disable-opacity handle-change-color handle-open handle-close]
(fn [event]
- (let [x (.-clientX event)
+ (let [color
+ (cond
+ (uc/multiple? color)
+ {:color cp/default-color
+ :opacity 1}
+
+ (= :multiple (:opacity color))
+ (assoc color :opacity 1)
+
+ :else
+ color)
+
+ x (.-clientX event)
y (.-clientY event)
props {:x x
:y y
@@ -98,16 +110,12 @@
handle-click-color (mf/use-callback
(mf/deps color)
- (let [;; If multiple, we change to default color
- color (if (uc/multiple? color)
- {:color cp/default-color :opacity 1}
- color)]
- (color-picker-callback color
- disable-gradient
- disable-opacity
- handle-pick-color
- handle-open
- handle-close)))
+ (color-picker-callback color
+ disable-gradient
+ disable-opacity
+ handle-pick-color
+ handle-open
+ handle-close))
prev-color (h/use-previous color)]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
index 259ff36a6..86a049053 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
@@ -148,22 +148,23 @@
extract-attrs
(fn [[ids values] {:keys [id type shapes content] :as shape}]
- (let [props (get-in type->props [type attr-type])
- result (case props
- :ignore [ids values]
- :shape [(conj ids id)
- (merge-attrs values (merge
- (empty-map attrs)
- (select-keys shape attrs)))]
- :text [(conj ids id)
- (-> values
- (merge-attrs (select-keys shape attrs))
- (merge-attrs (attrs/get-attrs-multi (txt/node-seq content) attrs)))]
- :children (let [children (->> (:shapes shape []) (map #(get objects %)))
- [new-ids new-values] (get-attrs children objects attr-type)]
- [(d/concat ids new-ids) (merge-attrs values new-values)])
- [])]
- result))]
+ (let [props (get-in type->props [type attr-type])]
+ (case props
+ :ignore [ids values]
+ :shape [(conj ids id)
+ (merge-attrs values (merge
+ (empty-map attrs)
+ (select-keys shape attrs)))]
+ :text [(conj ids id)
+ (-> values
+ (merge-attrs (select-keys shape attrs))
+ (merge-attrs (merge
+ (select-keys txt/default-text-attrs attrs)
+ (attrs/get-attrs-multi (txt/node-seq content) attrs))))]
+ :children (let [children (->> (:shapes shape []) (map #(get objects %)))
+ [new-ids new-values] (get-attrs children objects attr-type)]
+ [(d/concat ids new-ids) (merge-attrs values new-values)])
+ [])))]
(reduce extract-attrs [[] []] shapes)))
(mf/defc options
diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
index eb430cbf2..ae5ac0ed8 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
@@ -164,7 +164,7 @@
(defn- make-page-ref
[page-id]
(l/derived (fn [state]
- (let [page (get-in state [:workspace-file :data :pages-index page-id])]
+ (let [page (get-in state [:workspace-data :pages-index page-id])]
(select-keys page [:id :name])))
st/state =))
@@ -198,11 +198,13 @@
(mf/defc sitemap
[{:keys [layout] :as props}]
- (let [create (mf/use-callback #(st/emit! dw/create-empty-page))
+ (let [file (mf/deref refs/workspace-file)
+ create (mf/use-callback
+ (mf/deps file)
+ (st/emitf (dw/create-page {:file-id (:id file)
+ :project-id (:project-id file)})))
show-pages? (mf/use-state true)
- file (mf/deref refs/workspace-file)
-
toggle-pages
(mf/use-callback #(reset! show-pages? not))]
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 6e2363cb6..a210dad73 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.viewport
(:require
[app.common.data :as d]
+ [app.common.pages :as cp]
[app.common.geom.shapes :as gsh]
[app.main.refs :as refs]
[app.main.ui.context :as ctx]
@@ -27,6 +28,7 @@
[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]
+ [app.main.ui.workspace.viewport.thumbnail-renderer :as wtr]
[app.main.ui.workspace.viewport.utils :as utils]
[app.main.ui.workspace.viewport.widgets :as widgets]
[beicon.core :as rx]
@@ -59,6 +61,10 @@
drawing (mf/deref refs/workspace-drawing)
options (mf/deref refs/workspace-page-options)
objects (mf/deref refs/workspace-page-objects)
+ object-modifiers (mf/deref refs/workspace-modifiers)
+ objects (mf/use-memo
+ (mf/deps objects object-modifiers)
+ #(cp/merge-modifiers objects object-modifiers))
;; STATE
alt? (mf/use-state false)
@@ -67,6 +73,7 @@
hover-ids (mf/use-state nil)
hover (mf/use-state nil)
frame-hover (mf/use-state nil)
+ active-frames (mf/use-state {})
;; REFS
viewport-ref (mf/use-ref nil)
@@ -131,22 +138,26 @@
show-prototypes? (= options-mode :prototype)
show-selection-handlers? (seq selected)
show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (not (empty? selected)))
- show-snap-points? (and (contains? layout :dynamic-alignment) (or drawing-obj transform))
+ show-snap-points? (and (or (contains? layout :dynamic-alignment)
+ (contains? layout :snap-grid))
+ (or drawing-obj transform))
show-selrect? (and selrect (empty? drawing))
- show-measures? (and (not transform) (not path-editing?) show-distances?)
- ]
+ show-measures? (and (not transform) (not path-editing?) show-distances?)]
(hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?)
(hooks/setup-viewport-size viewport-ref)
(hooks/setup-cursor cursor alt? panning drawing-tool drawing-path? path-editing?)
(hooks/setup-resize layout viewport-ref)
(hooks/setup-keyboard alt? ctrl?)
- (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids)
+ (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom)
(hooks/setup-viewport-modifiers modifiers selected objects render-ref)
(hooks/setup-shortcuts path-editing? drawing-path?)
+ (hooks/setup-active-frames objects vbox hover active-frames)
[:div.viewport
[:div.viewport-overlays
+ [:& wtr/frame-renderer {:objects objects}]
+
(when show-comments?
[:& comments/comments-layer {:vbox vbox
:vport vport
@@ -179,7 +190,8 @@
[:& (mf/provider muc/embed-ctx) {:value true}
;; Render root shape
[:& shapes/root-shape {:key page-id
- :objects objects}]]]
+ :objects objects
+ :active-frames @active-frames}]]]
[:svg.viewport-controls
{:xmlns "http://www.w3.org/2000/svg"
@@ -205,8 +217,7 @@
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-pointer-move on-pointer-move
- :on-pointer-up on-pointer-up
- }
+ :on-pointer-up on-pointer-up}
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
@@ -222,6 +233,7 @@
(when show-selection-handlers?
[:& selection/selection-handlers
{:selected selected
+ :shapes selected-shapes
:zoom zoom
:edition edition
:disable-handlers (or drawing-tool edition)
@@ -276,6 +288,7 @@
:zoom zoom
:page-id page-id
:selected selected
+ :objects objects
:modifiers modifiers}])
(when show-snap-distance?
@@ -302,5 +315,6 @@
{:selected selected}])
(when show-selrect?
- [:& widgets/selection-rect {:data selrect}])]]]))
+ [:& widgets/selection-rect {:data selrect
+ :zoom zoom}])]]]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
index f7550428f..a32ae106e 100644
--- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
@@ -142,7 +142,7 @@
(not selected?)
(not edition)
(not drawing-path?)
- (not (#{:comments :path} drawing-tool)))
+ (not drawing-tool))
(st/emit! (dw/select-shape (:id @hover)))))))))
(defn on-double-click
@@ -179,8 +179,8 @@
(dw/start-editing-selected))
:else
- ;; Do nothing
- nil))))))
+ (st/emit! (dw/selected-to-path)
+ (dw/start-editing-selected))))))))
(defn on-context-menu
[hover]
diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
index 1fe1c1685..bbda478b0 100644
--- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
@@ -90,12 +90,12 @@
(hooks/use-stream ms/keyboard-alt #(reset! alt? %))
(hooks/use-stream ms/keyboard-ctrl #(reset! ctrl? %)))
-(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids]
+(defn setup-hover-shapes [page-id move-stream selected objects transform selected ctrl? hover hover-ids zoom]
(let [query-point
(mf/use-callback
(mf/deps page-id)
(fn [point]
- (let [rect (gsh/center->rect point 8 8)]
+ (let [rect (gsh/center->rect point (/ 5 zoom) (/ 5 zoom))]
(uw/ask-buffered!
{:cmd :selection/query
:page-id page-id
@@ -153,14 +153,46 @@
(utils/update-transform render-node roots modifiers)
(utils/remove-transform render-node roots))))))
-(defn setup-shortcuts [path-editing? drawing-path?]
+(defn inside-vbox [vbox objects frame-id]
+ (let [frame (get objects frame-id)]
+
+ (and (some? frame)
+ (gsh/overlaps? frame vbox))))
+
+(defn setup-active-frames
+ [objects vbox hover active-frames]
+
+ (mf/use-effect
+ (mf/deps vbox)
+
+ (fn []
+ (swap! active-frames
+ (fn [active-frames]
+ (let [set-active-frames
+ (fn [active-frames id active?]
+ (cond-> active-frames
+ (and active? (inside-vbox vbox objects id))
+ (assoc id true)))]
+ (reduce-kv set-active-frames {} active-frames))))))
+
+ (mf/use-effect
+ (mf/deps @hover @active-frames)
+ (fn []
+ (let [frame-id (if (= :frame (:type @hover))
+ (:id @hover)
+ (:frame-id @hover))]
+ (when (not (contains? @active-frames frame-id))
+ (swap! active-frames assoc frame-id true))))))
+
+;; NOTE: this is executed on each page change, maybe we need to move
+;; this shortcuts outside the viewport?
+
+(defn setup-shortcuts
+ [path-editing? drawing-path?]
+ (hooks/use-shortcuts ::workspace wsc/shortcuts)
(mf/use-effect
(mf/deps path-editing? drawing-path?)
(fn []
- (cond
- (or drawing-path? path-editing?)
- (dsc/bind-shortcuts psc/shortcuts)
-
- :else
- (dsc/bind-shortcuts wsc/shortcuts))
- dsc/remove-shortcuts)))
+ (when (or drawing-path? path-editing?)
+ (st/emit! (dsc/push-shortcuts ::path psc/shortcuts))
+ (st/emitf (dsc/pop-shortcuts ::path))))))
diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs
index e292b16dc..6c8733776 100644
--- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs
@@ -29,7 +29,7 @@
(mf/deps shape)
#(when path? (upf/format-path (:content shape))))
- {:keys [id x y width height]} shape
+ {:keys [id x y width height selrect]} shape
outline-type (case (:type shape)
:circle "ellipse"
@@ -53,10 +53,10 @@
{:d path-data
:transform nil}
- {:x x
- :y y
- :width width
- :height height})]
+ {:x (:x selrect)
+ :y (:y selrect)
+ :width (:width selrect)
+ :height (:height selrect)})]
[:> outline-type (map->obj (merge common props))]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs
index 8cad2d8fc..85b91a8ff 100644
--- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs
@@ -241,8 +241,7 @@
disable-handlers (obj/get props "disable-handlers")
current-transform (mf/deref refs/current-transform)
- selrect (-> (:selrect shape)
- minimum-selrect)
+ selrect (:selrect shape)
transform (geom/transform-matrix shape {:no-flip true})]
(when (not (#{:move :rotate} current-transform))
@@ -327,7 +326,7 @@
(mf/defc single-selection-handlers
[{:keys [shape zoom color disable-handlers on-move-selected] :as props}]
(let [shape-id (:id shape)
- shape (geom/transform-shape shape)
+ shape (geom/transform-shape shape {:round-coords? false})
frame (mf/deref (refs/object-by-id (:frame-id shape)))
frame (when-not (= (:id frame) uuid/zero) frame)
@@ -355,13 +354,8 @@
(mf/defc selection-handlers
{::mf/wrap [mf/memo]}
- [{:keys [selected edition zoom disable-handlers on-move-selected] :as props}]
- (let [;; We need remove posible nil values because on shape
- ;; deletion many shape will reamin selected and deleted
- ;; in the same time for small instant of time
- shapes (->> (mf/deref (refs/objects-by-id selected))
- (remove nil?))
- num (count shapes)
+ [{:keys [shapes selected edition zoom disable-handlers on-move-selected] :as props}]
+ (let [num (count shapes)
{:keys [id type] :as shape} (first shapes)
color (if (or (> num 1) (nil? (:shape-ref shape)))
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 9f7e24b27..f9cc2707c 100644
--- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui.workspace.viewport.snap-points
(:require
+ [app.common.pages :as cp]
[app.common.math :as mth]
[app.common.data :as d]
[app.common.geom.point :as gpt]
@@ -116,7 +117,6 @@
snap-lines (->> (into (process-snap-lines @state :x)
(process-snap-lines @state :y))
(into #{}))]
-
(mf/use-effect
(fn []
(let [sub (->> subject
@@ -131,7 +131,7 @@
#(rx/dispose! sub))))
(mf/use-effect
- (mf/deps shapes modifiers)
+ (mf/deps shapes filter-shapes modifiers)
(fn []
(rx/push! subject props)))
@@ -152,15 +152,26 @@
(mf/defc snap-points
{::mf/wrap [mf/memo]}
- [{:keys [layout zoom selected page-id drawing transform modifiers] :as props}]
- (let [shapes (mf/deref (refs/objects-by-id selected))
- filter-shapes (mf/deref refs/selected-shapes-with-children)
+ [{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}]
+
+ (let [;; shapes (mf/deref (refs/objects-by-id selected))
+ ;; filter-shapes (mf/deref refs/selected-shapes-with-children)
+
+ 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)))))
+
shapes (if drawing [drawing] shapes)]
(when (or drawing transform)
[:& snap-feedback {:shapes shapes
diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs
new file mode 100644
index 000000000..dc6d5eb69
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs
@@ -0,0 +1,135 @@
+;; 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.thumbnail-renderer
+ (:require
+ [app.main.data.workspace.changes :as dwc]
+ [app.main.data.workspace.persistence :as dwp]
+ [app.main.store :as st]
+ [app.util.dom :as dom]
+ [app.util.object :as obj]
+ [app.util.timers :as timers]
+ [beicon.core :as rx]
+ [rumext.alpha :as mf]))
+
+(mf/defc frame-thumbnail
+ "Renders the canvas and image for a frame thumbnail and stores its value into the shape"
+ [{:keys [shape on-thumbnail-data on-frame-not-found]}]
+
+ (let [thumbnail-img (mf/use-ref nil)
+ thumbnail-canvas (mf/use-ref nil)
+
+ on-dom-rendered
+ (mf/use-callback
+ (mf/deps (:id shape))
+ (fn [node]
+ (when node
+ (let [img-node (mf/ref-val thumbnail-img)]
+ (timers/schedule-on-idle
+ #(let [frame-node (dom/get-element (str "shape-" (:id shape)))
+ loading-node (when frame-node
+ (dom/query frame-node "[data-loading=\"true\"]"))]
+ (if (and (some? frame-node) (not (some? loading-node)))
+ (let [xml (-> (js/XMLSerializer.)
+ (.serializeToString frame-node)
+ js/encodeURIComponent
+ js/unescape
+ js/btoa)
+ img-src (str "data:image/svg+xml;base64," xml)]
+ (obj/set! img-node "src" img-src))
+
+ (on-frame-not-found (:id shape)))))))))
+
+ on-image-load
+ (mf/use-callback
+ (mf/deps on-thumbnail-data)
+ (fn []
+ (let [canvas-node (mf/ref-val thumbnail-canvas)
+ img-node (mf/ref-val thumbnail-img)
+ canvas-context (.getContext canvas-node "2d")
+ _ (.drawImage canvas-context img-node 0 0)
+ data (.toDataURL canvas-node "image/jpeg" 0.8)]
+ (on-thumbnail-data data))))]
+
+ [:div.frame-renderer {:ref on-dom-rendered
+ :style {:display "none"}}
+ [:img.thumbnail-img
+ {:ref thumbnail-img
+ :width (:width shape)
+ :height (:height shape)
+ :on-load on-image-load}]
+
+ [:canvas.thumbnail-canvas
+ {:ref thumbnail-canvas
+ :width (:width shape)
+ :height (:height shape)}]]))
+
+(mf/defc frame-renderer
+ "Component in charge of creating thumbnails and storing them"
+ {::mf/wrap-props false}
+ [props]
+ (let [objects (obj/get props "objects")
+
+ ;; Id of the current frame being rendered
+ shape-id (mf/use-state nil)
+
+ ;; This subject will emit a value every time there is a free "slot" to render
+ ;; a thumbnail
+ next (mf/use-memo #(rx/behavior-subject :next))
+
+ render-frame
+ (mf/use-callback
+ (fn [frame-id]
+ (reset! shape-id frame-id)))
+
+ updates-stream
+ (mf/use-memo
+ (fn []
+ (let [update-events
+ (->> st/stream
+ (rx/filter dwp/update-frame-thumbnail?))]
+ (->> (rx/zip update-events next)
+ (rx/map first)))))
+
+ on-thumbnail-data
+ (mf/use-callback
+ (mf/deps @shape-id)
+ (fn [data]
+ (reset! shape-id nil)
+ (timers/schedule
+ (fn []
+ (st/emit! (dwc/update-shapes [@shape-id]
+ #(assoc % :thumbnail data)
+ {:save-undo? false}))
+ (rx/push! next :next)))))
+
+ on-frame-not-found
+ (mf/use-callback
+ (fn [frame-id]
+ ;; If we couldn't find the frame maybe is still rendering. We push the event again
+ ;; after a time
+ (reset! shape-id nil)
+ (rx/push! next :next)
+ (timers/schedule-on-idle (st/emitf (dwp/update-frame-thumbnail frame-id)))))]
+
+ (mf/use-effect
+ (mf/deps render-frame)
+ (fn []
+ (let [sub (->> updates-stream
+ (rx/subs #(render-frame (-> (deref %) :frame-id))))]
+
+ #(rx/dispose! sub))))
+
+ (mf/use-layout-effect
+ (fn []
+ (timers/schedule-on-idle
+ #(st/emit! (dwp/watch-state-changes)))))
+
+ (when (and (some? @shape-id) (contains? objects @shape-id))
+ [:& frame-thumbnail {:key (str "thumbnail-" @shape-id)
+ :shape (get objects @shape-id)
+ :on-thumbnail-data on-thumbnail-data
+ :on-frame-not-found on-frame-not-found}])))
diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs
index e3df76657..092da6bbc 100644
--- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs
@@ -15,8 +15,19 @@
(defn update-transform [node shapes modifiers]
(doseq [{:keys [id type]} shapes]
- (when-let [node (dom/get-element (str "shape-" id))]
- (let [node (if (= :frame type) (.-parentNode node) node)]
+ (let [shape-node (dom/get-element (str "shape-" id))
+
+ ;; When the shape is a frame we maybe need to move its thumbnail
+ thumb-node (dom/get-element (str "thumbnail-" id))]
+ (when-let [node (cond
+ (and (some? shape-node) (= :frame type))
+ (.-parentNode shape-node)
+
+ (and (some? thumb-node) (= :frame type))
+ (.-parentNode thumb-node)
+
+ :else
+ shape-node)]
(dom/set-attribute node "transform" (str (:displacement modifiers)))))))
(defn remove-transform [node shapes]
diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
index 6bd73feb8..3faa46555 100644
--- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
@@ -64,13 +64,19 @@
(mf/defc selection-rect
{:wrap [mf/memo]}
- [{:keys [data] :as props}]
+ [{:keys [data zoom] :as props}]
(when data
[:rect.selection-rect
{:x (:x data)
:y (:y data)
:width (:width data)
- :height (:height data)}]))
+ :height (:height data)
+ :style {;; Primary with 0.1 opacity
+ :fill "rgb(49, 239, 184, 0.1)"
+
+ ;; Primary color
+ :stroke "rgb(49, 239, 184)"
+ :stroke-width (/ 1 zoom)}}]))
;; Ensure that the label has always the same font
;; size, regardless of zoom
diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs
index 05183e2eb..8899e6f23 100644
--- a/frontend/src/app/util/color.cljs
+++ b/frontend/src/app/util/color.cljs
@@ -135,7 +135,7 @@
:else "transparent")))
-(defn multiple? [{:keys [id file-id value color gradient]}]
+(defn multiple? [{:keys [id file-id value color gradient opacity]}]
(or (= value :multiple)
(= color :multiple)
(= gradient :multiple)
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 95679b7c8..7601435ca 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -293,3 +293,21 @@
(defn remove-attribute [^js node ^string attr]
(.removeAttribute node attr))
+
+(defn scroll-into-view!
+ ([element]
+ (.scrollIntoView ^js element false))
+ ([element scroll-top]
+ (.scrollIntoView ^js element scroll-top)))
+
+(defn is-in-viewport?
+ [element]
+ (let [rect (.getBoundingClientRect element)
+ height (or (.-innerHeight js/window)
+ (.. js/document -documentElement -clientHeight))
+ width (or (.-innerWidth js/window)
+ (.. js/document -documentElement -clientWidth))]
+ (and (>= (.-top rect) 0)
+ (>= (.-left rect) 0)
+ (<= (.-bottom rect) height)
+ (<= (.-right rect) width))))
diff --git a/frontend/src/app/util/geom/grid.cljs b/frontend/src/app/util/geom/grid.cljs
index c1f882204..bd8f58bd1 100644
--- a/frontend/src/app/util/geom/grid.cljs
+++ b/frontend/src/app/util/geom/grid.cljs
@@ -88,7 +88,9 @@
(defn grid-snap-points
"Returns the snap points for a given grid"
- ([shape coord] (mapcat #(grid-snap-points shape % coord) (:grids shape)))
+ ([shape coord]
+ (mapcat #(grid-snap-points shape % coord) (:grids shape)))
+
([shape {:keys [type display params] :as grid} coord]
(when (:display grid)
(case type
diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs
index ac66402b0..6bc479f80 100644
--- a/frontend/src/app/util/i18n.cljs
+++ b/frontend/src/app/util/i18n.cljs
@@ -10,6 +10,7 @@
[app.config :as cfg]
[app.util.globals :as globals]
[app.util.storage :refer [storage]]
+ [app.util.object :as obj]
[app.util.transit :as t]
[beicon.core :as rx]
[cuerdas.core :as str]
@@ -24,6 +25,8 @@
{:label "Deutsch (community)" :value "de"}
{:label "Русский (community)" :value "ru"}
{:label "Türkçe (community)" :value "tr"}
+ {:label "Rumanian (communit)" :value "ro"}
+ {:label "Portuguese (Brazil, community)" :value "pt_br"}
{:label "Ελληνική γλώσσα (community)" :value "el"}
{:label "简体中文 (community)" :value "zh_cn"}])
@@ -51,7 +54,7 @@
cfg/default-language))))
(defonce translations #js {})
-(defonce locale (l/atom (or (get storage ::locale)
+(defonce locale (l/atom (or (get @storage ::locale)
(autodetect))))
;; The traslations `data` is a javascript object and should be treated
@@ -136,6 +139,13 @@
([code] (t @locale code))
([code & args] (apply t @locale code args)))
+(mf/defc tr-html
+ {::mf/wrap-props false}
+ [props]
+ (let [label (obj/get props "label")
+ tag-name (obj/get props "tag-name" "p")]
+ [:> tag-name {:dangerouslySetInnerHTML #js {:__html (tr label)}}]))
+
;; DEPRECATED
(defn use-locale
[]
diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/frontend/src/app/util/path/shapes_to_path.cljs
new file mode 100644
index 000000000..a1d42ba47
--- /dev/null
+++ b/frontend/src/app/util/path/shapes_to_path.cljs
@@ -0,0 +1,147 @@
+;; 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.path.shapes-to-path
+ (:require
+ [app.common.data :as d]
+ [app.common.geom.matrix :as gmt]
+ [app.common.geom.point :as gpt]
+ [app.common.geom.shapes :as gsh]
+ [app.common.geom.shapes.path :as gsp]
+ [app.util.path.commands :as pc]))
+
+(def bezier-circle-c 0.551915024494)
+(def dissoc-attrs [:x :y :width :height
+ :rx :ry :r1 :r2 :r3 :r4
+ :medata])
+(def allowed-transform-types #{:rect
+ :circle
+ :image})
+
+(defn make-corner-arc
+ "Creates a curvle corner for border radius"
+ [from to corner radius]
+ (let [x (case corner
+ :top-left (:x from)
+ :top-right (- (:x from) radius)
+ :bottom-right (- (:x to) radius)
+ :bottom-left (:x to))
+
+ y (case corner
+ :top-left (- (:y from) radius)
+ :top-right (:y from)
+ :bottom-right (- (:y to) (* 2 radius))
+ :bottom-left (- (:y to) radius))
+
+ width (* radius 2)
+ height (* radius 2)
+
+ c bezier-circle-c
+ c1x (+ x (* (/ width 2) (- 1 c)))
+ c2x (+ x (* (/ width 2) (+ 1 c)))
+ c1y (+ y (* (/ height 2) (- 1 c)))
+ c2y (+ y (* (/ height 2) (+ 1 c)))
+
+ h1 (case corner
+ :top-left (assoc from :y c1y)
+ :top-right (assoc from :x c2x)
+ :bottom-right (assoc from :y c2y)
+ :bottom-left (assoc from :x c1x))
+
+ h2 (case corner
+ :top-left (assoc to :x c1x)
+ :top-right (assoc to :y c1y)
+ :bottom-right (assoc to :x c2x)
+ :bottom-left (assoc to :y c2y))]
+
+ (pc/make-curve-to to h1 h2)))
+
+(defn circle->path
+ "Creates the bezier curves to approximate a circle shape"
+ [x y width height]
+ (let [mx (+ x (/ width 2))
+ my (+ y (/ height 2))
+ ex (+ x width)
+ ey (+ y height)
+
+ pc (gpt/point mx my)
+ p1 (gpt/point mx y)
+ p2 (gpt/point ex my)
+ p3 (gpt/point mx ey)
+ p4 (gpt/point x my)
+
+ c bezier-circle-c
+ c1x (+ x (* (/ width 2) (- 1 c)))
+ c2x (+ x (* (/ width 2) (+ 1 c)))
+ c1y (+ y (* (/ height 2) (- 1 c)))
+ c2y (+ y (* (/ height 2) (+ 1 c)))]
+
+ [(pc/make-move-to p1)
+ (pc/make-curve-to p2 (assoc p1 :x c2x) (assoc p2 :y c1y))
+ (pc/make-curve-to p3 (assoc p2 :y c2y) (assoc p3 :x c2x))
+ (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y))
+ (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))]))
+
+(defn rect->path
+ "Creates a bezier curve that approximates a rounded corner rectangle"
+ [x y width height r1 r2 r3 r4]
+ (let [p1 (gpt/point x (+ y r1))
+ p2 (gpt/point (+ x r1) y)
+
+ p3 (gpt/point (+ width x (- r2)) y)
+ p4 (gpt/point (+ width x) (+ y r2))
+
+ p5 (gpt/point (+ width x) (+ height y (- r3)))
+ p6 (gpt/point (+ width x (- r3)) (+ height y))
+
+ p7 (gpt/point (+ x r4) (+ height y))
+ p8 (gpt/point x (+ height y (- r4)))]
+ (-> []
+ (conj (pc/make-move-to p1))
+ (cond-> (not= p1 p2)
+ (conj (make-corner-arc p1 p2 :top-left r1)))
+ (conj (pc/make-line-to p3))
+ (cond-> (not= p3 p4)
+ (conj (make-corner-arc p3 p4 :top-right r2)))
+ (conj (pc/make-line-to p5))
+ (cond-> (not= p5 p6)
+ (conj (make-corner-arc p5 p6 :bottom-right r3)))
+ (conj (pc/make-line-to p7))
+ (cond-> (not= p7 p8)
+ (conj (make-corner-arc p7 p8 :bottom-left r4)))
+ (conj (pc/make-line-to p1)))))
+
+(defn convert-to-path
+ "Transforms the given shape to a path"
+ [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape}]
+
+ (if (contains? allowed-transform-types type)
+ (let [r1 (or r1 rx 0)
+ r2 (or r2 rx 0)
+ r3 (or r3 rx 0)
+ r4 (or r4 rx 0)
+
+ new-content
+ (case type
+ :circle
+ (circle->path x y width height)
+ (rect->path x y width height r1 r2 r3 r4))
+
+ ;; Apply the transforms that had the shape
+ transform (:transform shape)
+ new-content (cond-> new-content
+ (some? transform)
+ (gsp/transform-content (gmt/transform-in (gsh/center-shape shape) transform)))]
+
+ (-> shape
+ (d/without-keys dissoc-attrs)
+ (assoc :type :path)
+ (assoc :content new-content)
+ (cond-> (= :image type)
+ (assoc :fill-image metadata))))
+ ;; Do nothing if the shape is not of a correct type
+ shape))
+
diff --git a/frontend/src/app/util/path/subpaths.cljs b/frontend/src/app/util/path/subpaths.cljs
index 3337b6c88..010f1343a 100644
--- a/frontend/src/app/util/path/subpaths.cljs
+++ b/frontend/src/app/util/path/subpaths.cljs
@@ -22,7 +22,10 @@
(defn add-subpath-command
"Adds a command to the subpath"
[subpath command]
- (let [p (upc/command->point command)]
+ (let [command (if (= :close-path (:command command))
+ (upc/make-line-to (:from subpath))
+ command)
+ p (upc/command->point command)]
(-> subpath
(assoc :to p)
(update :data conj command))))
diff --git a/frontend/src/app/util/quadtree.js b/frontend/src/app/util/quadtree.js
index a6f904c10..000ae7c42 100644
--- a/frontend/src/app/util/quadtree.js
+++ b/frontend/src/app/util/quadtree.js
@@ -32,12 +32,16 @@
"use strict";
goog.provide("app.util.quadtree");
+goog.require("cljs.core");
goog.scope(function() {
const self = app.util.quadtree;
+ const eq = cljs.core._EQ_;
+ const contains = cljs.core.contains_QMARK_;
class Node {
- constructor(bounds, data) {
+ constructor(id, bounds, data) {
+ this.id = id;
this.bounds = bounds;
this.data = data;
}
@@ -51,8 +55,8 @@ goog.scope(function() {
this.level = level || 0;
this.bounds = bounds;
- this.objects = [];
- this.indexes = [];
+ this.objects = [];
+ this.indexes = [];
}
split() {
@@ -183,14 +187,18 @@ goog.scope(function() {
this.objects = [];
this.indexes = [];
}
+
+ getObjects() {
+ return this.objects;
+ }
}
self.create = function(rect) {
return new Quadtree(rect, 10, 4, 0);
};
- self.insert = function(index, bounds, data) {
- const node = new Node(bounds, data);
+ self.insert = function(index, id, bounds, data) {
+ const node = new Node(id, bounds, data);
index.insert(node);
return index;
};
@@ -210,4 +218,29 @@ goog.scope(function() {
}
};
+ self.remove = function(index, id) {
+ const result = self.create(index.bounds);
+
+ for (let node of index.objects) {
+ if (!eq(id, node.id)) {
+ self.insert(result, node.id, node.bounds, node.data);
+ }
+ }
+
+ return result;
+ }
+
+ // FIXME: Inefficient to recreate the index. Needs to be improved
+ self.remove_all = function(index, ids) {
+ const result = self.create(index.bounds);
+
+ for (let node of self.search(index, index.bounds)) {
+ if (!contains(ids, node.id)) {
+ self.insert(result, node.id, node.bounds, node.data);
+ }
+ }
+
+ return result;
+ }
+
});
diff --git a/frontend/src/app/util/range_tree.js b/frontend/src/app/util/range_tree.js
index ed62a0200..0b7532b8a 100644
--- a/frontend/src/app/util/range_tree.js
+++ b/frontend/src/app/util/range_tree.js
@@ -13,7 +13,7 @@
"use strict";
goog.provide("app.util.range_tree");
-goog.require("cljs.core")
+goog.require("cljs.core");
goog.scope(function() {
const eq = cljs.core._EQ_;
@@ -92,7 +92,7 @@ goog.scope(function() {
}
isEmpty() {
- return this.root === null;
+ return !this.root;
}
toString() {
@@ -111,12 +111,12 @@ goog.scope(function() {
// Tree implementation functions
function isRed(branch) {
- return branch !== null && branch.color === Color.RED;
+ return branch && branch.color === Color.RED;
}
// Insert recursively in the tree
function recInsert (branch, value, data) {
- if (branch === null) {
+ if (!branch) {
const ret = new Node(value, data);
ret.color = Color.RED;
return ret;
@@ -144,7 +144,7 @@ goog.scope(function() {
// Search for the min node
function searchMin(branch) {
- if (branch.left === null) {
+ if (!branch.left) {
return branch;
} else {
return searchMin(branch.left);
@@ -153,7 +153,7 @@ goog.scope(function() {
// Remove the lefmost node of the current branch
function recRemoveMin(branch) {
- if (branch.left === null) {
+ if (!branch.left) {
return null;
}
@@ -167,7 +167,7 @@ goog.scope(function() {
// Remove the data element for the value given
// this will not remove the node, we have to remove the empty node afterwards
function recRemoveData(branch, value, data) {
- if (branch === null) {
+ if (!branch) {
// Not found
return branch;
} else if (branch.value === value) {
@@ -193,7 +193,7 @@ goog.scope(function() {
if (isRed(branch.left)) {
branch = rotateRight(branch);
}
- if (value === branch.value && branch.right === null) {
+ if (value === branch.value && !branch.right) {
return null;
}
if (!isRed(branch.right) && !isRed(branch.right.left)) {
@@ -214,7 +214,7 @@ goog.scope(function() {
// Retrieve all the data related to value
function recGet(branch, value) {
- if (branch === null) {
+ if (!branch) {
return null;
} else if (branch.value === value) {
return branch.data;
@@ -226,7 +226,7 @@ goog.scope(function() {
}
function recUpdate(branch, value, oldData, newData) {
- if (branch === null) {
+ if (!branch) {
return branch;
} else if (branch.value === value) {
branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it);
@@ -239,7 +239,7 @@ goog.scope(function() {
}
function recRangeQuery(branch, fromValue, toValue, result) {
- if (branch === null) {
+ if (!branch) {
return result;
}
if (fromValue < branch.value) {
@@ -287,7 +287,7 @@ goog.scope(function() {
function moveRedLeft(branch) {
flipColors(branch);
- if (isRed(branch.right.left)) {
+ if (branch.right && isRed(branch.right.left)) {
branch.right = rotateRight(branch.right);
branch = rotateLeft(branch);
flipColors(branch);
@@ -329,7 +329,7 @@ goog.scope(function() {
// This will return the string representation. We don't care about internal structure
// only the data
function recToString(branch, result) {
- if (branch === null) {
+ if (!branch) {
return;
}
diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs
index ab20b3d67..0fc55dc8f 100644
--- a/frontend/src/app/util/router.cljs
+++ b/frontend/src/app/util/router.cljs
@@ -58,29 +58,48 @@
;; --- Navigate (Event)
-(deftype Navigate [id params qparams replace]
- ptk/UpdateEvent
- (update [_ state]
- (dissoc state :exception))
+(defn navigated
+ [match]
+ (ptk/reify ::navigated
+ IDeref
+ (-deref [_] match)
- ptk/EffectEvent
- (effect [_ state stream]
- (let [router (:router state)
- history (:history state)
- path (resolve router id params qparams)]
- (if ^boolean replace
- (bhistory/replace-token! history path)
- (bhistory/set-token! history path)))))
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc state :route match))))
+
+(defn navigate*
+ [id params qparams replace]
+ (ptk/reify ::navigate
+ IDeref
+ (-deref [_]
+ {:id id
+ :path-params params
+ :query-params qparams
+ :replace replace})
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (dissoc state :exception))
+
+ ptk/EffectEvent
+ (effect [_ state stream]
+ (let [router (:router state)
+ history (:history state)
+ path (resolve router id params qparams)]
+ (if ^boolean replace
+ (bhistory/replace-token! history path)
+ (bhistory/set-token! history path))))))
(defn nav
([id] (nav id nil nil))
([id params] (nav id params nil))
- ([id params qparams] (Navigate. id params qparams false)))
+ ([id params qparams] (navigate* id params qparams false)))
(defn nav'
([id] (nav id nil nil))
([id params] (nav id params nil))
- ([id params qparams] (Navigate. id params qparams true)))
+ ([id params qparams] (navigate* id params qparams true)))
(def navigate nav)
diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs
index e88dc656f..aecd1f03a 100644
--- a/frontend/src/app/util/storage.cljs
+++ b/frontend/src/app/util/storage.cljs
@@ -8,6 +8,7 @@
(:require
[app.util.transit :as t]
[app.util.timers :as tm]
+ [app.util.globals :as g]
[app.common.exceptions :as ex]))
(defn- ^boolean is-worker?
@@ -19,65 +20,35 @@
[v]
(ex/ignoring (t/decode v)))
-(def local
- {:get #(decode (.getItem ^js js/localStorage (name %)))
- :set #(.setItem ^js js/localStorage (name %1) (t/encode %2))})
-
-(def session
- {:get #(decode (.getItem ^js js/sessionStorage (name %)))
- :set #(.setItem ^js js/sessionStorage (name %1) (t/encode %2))})
(defn- persist
- [alias storage value]
- (when-not (is-worker?)
- (tm/schedule-on-idle
- (fn [] ((:set storage) alias value)))))
+ [storage prev curr]
+ (run! (fn [key]
+ (let [prev* (get prev key)
+ curr* (get curr key)]
+ (when (not= curr* prev*)
+ (tm/schedule-on-idle
+ #(if (some? curr*)
+ (.setItem ^js storage (t/encode key) (t/encode curr*))
+ (.removeItem ^js storage (t/encode key)))))))
+
+ (into #{} (concat (keys curr)
+ (keys prev)))))
(defn- load
- [alias storage]
- (when-not (is-worker?)
- ((:get storage) alias)))
-
-(defn- make-storage
- [alias storage]
- (let [data (atom (load alias storage))]
- (add-watch data :sub #(persist alias storage %4))
- (reify
- Object
- (toString [_]
- (str "Storage" (pr-str @data)))
-
- ICounted
- (-count [_]
- (count @data))
-
- ISeqable
- (-seq [_]
- (seq @data))
-
- IReset
- (-reset! [self newval]
- (reset! data newval))
-
- ISwap
- (-swap! [self f]
- (swap! data f))
- (-swap! [self f x]
- (swap! data f x))
- (-swap! [self f x y]
- (swap! data f x y))
- (-swap! [self f x y more]
- (apply swap! data f x y more))
-
- ILookup
- (-lookup [_ key]
- (get @data key nil))
- (-lookup [_ key not-found]
- (get @data key not-found)))))
+ [storage]
+ (when storage
+ (let [len (.-length ^js storage)]
+ (reduce (fn [res index]
+ (let [key (.key ^js storage index)
+ val (.getItem ^js storage key)]
+ (try
+ (assoc res (t/decode key) (t/decode val))
+ (catch :default e
+ res))))
+ {}
+ (range len)))))
-(defonce storage
- (make-storage "app" local))
-
-(defonce cache
- (make-storage "cache" session))
+(defonce storage (atom (load (unchecked-get g/global "localStorage"))))
+(add-watch storage :persistence #(persist js/localStorage %3 %4))
diff --git a/frontend/src/app/util/theme.cljs b/frontend/src/app/util/theme.cljs
index 93e76f8d5..deed7644a 100644
--- a/frontend/src/app/util/theme.cljs
+++ b/frontend/src/app/util/theme.cljs
@@ -17,7 +17,7 @@
[app.util.transit :as t]
[app.util.storage :refer [storage]]))
-(defonce theme (get storage ::theme cfg/default-theme))
+(defonce theme (get @storage ::theme cfg/default-theme))
(defonce theme-sub (rx/subject))
(defonce themes #js {})
diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs
index 67b75291c..317ff866e 100644
--- a/frontend/src/app/util/time.cljs
+++ b/frontend/src/app/util/time.cljs
@@ -12,6 +12,9 @@
["date-fns/locale/el" :default dateFnsLocalesEl]
["date-fns/locale/fr" :default dateFnsLocalesFr]
["date-fns/locale/ca" :default dateFnsLocalesCa]
+ ["date-fns/locale/de" :default dateFnsLocalesDe]
+ ["date-fns/locale/ro" :default dateFnsLocalesRo]
+ ["date-fns/locale/pt-BR" :default dateFnsLocalesPtBr]
["date-fns/locale/en-US" :default dateFnsLocalesEnUs]
["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn]
["date-fns/locale/es" :default dateFnsLocalesEs]
@@ -205,6 +208,9 @@
:ca dateFnsLocalesCa
:el dateFnsLocalesEl
:ru dateFnsLocalesRu
+ :ro dateFnsLocalesRo
+ :de dateFnsLocalesDe
+ :pt_br dateFnsLocalesPtBr
:zh_cn dateFnsLocalesZhCn})
(defn timeago
diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs
index 4675f0262..be8cfdbc8 100644
--- a/frontend/src/app/util/timers.cljs
+++ b/frontend/src/app/util/timers.cljs
@@ -34,12 +34,13 @@
(-dispose [_]
(js/clearInterval sem)))))
-(if (and (exists? js/window) (.-requestIdleCallback js/window))
+(if (and (exists? js/window)
+ (.-requestIdleCallback js/window))
(do
(def ^:private request-idle-callback #(js/requestIdleCallback %))
(def ^:private cancel-idle-callback #(js/cancelIdleCallback %)))
(do
- (def ^:private request-idle-callback #(js/setTimeout % 100))
+ (def ^:private request-idle-callback #(js/setTimeout % 250))
(def ^:private cancel-idle-callback #(js/clearTimeout %))))
(defn schedule-on-idle
diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs
index 9e289c83e..0da4d753a 100644
--- a/frontend/src/app/util/webapi.cljs
+++ b/frontend/src/app/util/webapi.cljs
@@ -29,6 +29,10 @@
[file]
(file-reader #(.readAsText %1 file)))
+(defn read-file-as-array-buffer
+ [file]
+ (file-reader #(.readAsArrayBuffer %1 file)))
+
(defn read-file-as-data-url
[file]
(file-reader #(.readAsDataURL ^js %1 file)))
@@ -127,3 +131,14 @@
:else
(ex/raise :type :not-supported
:hint "seems like the current browset does not support fullscreen api.")))
+
+(defn observe-resize
+ [node]
+ (rx/create
+ (fn [subs]
+ (let [obs (js/ResizeObserver.
+ (fn [entries x]
+ (rx/push! subs entries)))]
+ (.observe ^js obs node)
+ (fn []
+ (.disconnect ^js obs))))))
diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs
index 107270387..77032133a 100644
--- a/frontend/src/app/worker/impl.cljs
+++ b/frontend/src/app/worker/impl.cljs
@@ -39,11 +39,15 @@
(defmethod handler :update-page-indices
[{:keys [page-id changes] :as message}]
- (swap! state ch/process-changes changes false)
+ (let [old-objects (get-in @state [:pages-index page-id :objects])]
+ (swap! state ch/process-changes changes false)
- (let [objects (get-in @state [:pages-index page-id :objects])
- message (assoc message :objects objects)]
- (handler (-> message
- (assoc :cmd :selection/update-index)))
- (handler (-> message
- (assoc :cmd :snaps/update-index)))))
+ (let [new-objects (get-in @state [:pages-index page-id :objects])
+ message (assoc message
+ :objects new-objects
+ :new-objects new-objects
+ :old-objects old-objects)]
+ (handler (-> message
+ (assoc :cmd :selection/update-index)))
+ (handler (-> message
+ (assoc :cmd :snaps/update-index))))))
diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs
index 2d0ac3a5d..09cc4041a 100644
--- a/frontend/src/app/worker/selection.cljs
+++ b/frontend/src/app/worker/selection.cljs
@@ -15,12 +15,126 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.util.quadtree :as qdt]
- [app.worker.impl :as impl]))
+ [app.worker.impl :as impl]
+ [clojure.set :as set]))
(defonce state (l/atom {}))
-(declare index-object)
-(declare create-index)
+(defn index-shape
+ [objects parents-index masks-index]
+ (fn [index shape]
+ (let [{:keys [x y width height]} (gsh/points->selrect (:points shape))
+ shape-bound #js {:x x :y y :width width :height height}
+
+ parents (get parents-index (:id shape))
+ masks (get masks-index (:id shape))
+
+ frame (when (and (not= :frame (:type shape))
+ (not= (:frame-id shape) uuid/zero))
+ (get objects (:frame-id shape)))]
+ (qdt/insert index
+ (:id shape)
+ shape-bound
+ (assoc shape :frame frame :masks masks :parents parents)))))
+
+(defn- create-index
+ [objects]
+ (let [shapes (-> objects (dissoc uuid/zero) (vals))
+ parents-index (cp/generate-child-all-parents-index objects)
+ masks-index (cp/create-mask-index objects parents-index)
+ bounds #js {:x (int -0.5e7)
+ :y (int -0.5e7)
+ :width (int 1e7)
+ :height (int 1e7)}
+
+ index (reduce (index-shape objects parents-index masks-index)
+ (qdt/create bounds)
+ shapes)
+
+ z-index (cp/calculate-z-index objects)]
+
+ {:index index :z-index z-index}))
+
+(defn- update-index
+ [{index :index z-index :z-index :as data} old-objects new-objects]
+
+ (if (some? data)
+ (let [changes? (fn [id]
+ (not= (get old-objects id)
+ (get new-objects id)))
+
+ changed-ids (into #{}
+ (comp (filter changes?)
+ (filter #(not= % uuid/zero)))
+ (set/union (keys old-objects)
+ (keys new-objects)))
+
+ shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?)))
+ parents-index (cp/generate-child-all-parents-index new-objects shapes)
+ masks-index (cp/create-mask-index new-objects parents-index)
+
+ new-index (qdt/remove-all index changed-ids)
+
+ index (reduce (index-shape new-objects parents-index masks-index)
+ new-index
+ shapes)
+
+ z-index (cp/update-z-index z-index changed-ids old-objects new-objects)]
+
+ {:index index :z-index z-index})
+
+ ;; If not previous data. We need to create from scratch
+ (create-index new-objects)))
+
+(defn- query-index
+ [{index :index z-index :z-index} rect frame-id include-frames? include-groups? disabled-masks reverse?]
+ (let [result (-> (qdt/search index (clj->js rect))
+ (es6-iterator-seq))
+
+ ;; Check if the shape matches the filter criteria
+ match-criteria?
+ (fn [shape]
+ (and (not (:hidden shape))
+ (not (:blocked shape))
+ (or (not frame-id) (= frame-id (:frame-id shape)))
+ (case (:type shape)
+ :frame include-frames?
+ :group include-groups?
+ true)))
+
+ overlaps?
+ (fn [shape]
+ (gsh/overlaps? shape rect))
+
+ overlaps-masks?
+ (fn [masks]
+ (->> masks
+ (some (comp not overlaps?))
+ not))
+
+ add-z-index
+ (fn [{:keys [id frame-id] :as shape}]
+ (assoc shape :z (+ (get z-index id)
+ (get z-index frame-id 0))))
+
+ ;; Shapes after filters of overlapping and criteria
+ matching-shapes
+ (into []
+ (comp (map #(unchecked-get % "data"))
+ (filter match-criteria?)
+ (filter overlaps?)
+ (filter (comp overlaps? :frame))
+ (filter (comp overlaps-masks? :masks))
+ (map add-z-index))
+ result)
+
+ keyfn (if reverse? (comp - :z) :z)]
+
+ (into (d/ordered-set)
+ (->> matching-shapes
+ (sort-by keyfn)
+ (map :id)))))
+
(defmethod impl/handler :selection/initialize-index
[{:keys [file-id data] :as message}]
@@ -35,96 +149,18 @@
nil))
(defmethod impl/handler :selection/update-index
- [{:keys [page-id objects] :as message}]
- (let [index (create-index objects)]
- (swap! state update page-id (constantly index))
- nil))
+ [{:keys [page-id old-objects new-objects] :as message}]
+ (swap! state update page-id update-index old-objects new-objects)
+ nil)
(defmethod impl/handler :selection/query
[{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?]
:or {include-groups? true disabled-masks #{} reverse? false} :as message}]
(when-let [index (get @state page-id)]
- (let [result (-> (qdt/search index (clj->js rect))
- (es6-iterator-seq))
-
- ;; Check if the shape matches the filter criteria
- match-criteria?
- (fn [shape]
- (and (not (:hidden shape))
- (not (:blocked shape))
- (or (not frame-id) (= frame-id (:frame-id shape)))
- (case (:type shape)
- :frame include-frames?
- :group include-groups?
- true)))
-
- overlaps?
- (fn [shape]
- (gsh/overlaps? shape rect))
-
- overlaps-masks?
- (fn [masks]
- (->> masks
- (some (comp not overlaps?))
- not))
-
- ;; Shapes after filters of overlapping and criteria
- matching-shapes
- (into []
- (comp (map #(unchecked-get % "data"))
- (filter match-criteria?)
- (filter (comp overlaps? :frame))
- (filter (comp overlaps-masks? :masks))
- (filter overlaps?))
- result)
-
- keyfn (if reverse? (comp - :z) :z)]
-
- (into (d/ordered-set)
- (->> matching-shapes
- (sort-by keyfn)
- (map :id))))))
-
-(defn create-mask-index
- "Retrieves the mask information for an object"
- [objects parents-index]
- (let [retrieve-masks
- (fn [id parents]
- (->> parents
- (map #(get objects %))
- (filter #(:masked-group? %))
- ;; Retrieve the masking element
- (mapv #(get objects (->> % :shapes first)))))]
- (->> parents-index
- (d/mapm retrieve-masks))))
-
-(defn- create-index
- [objects]
- (let [shapes (-> objects (dissoc uuid/zero) (vals))
- z-index (cp/calculate-z-index objects)
- parents-index (cp/generate-child-all-parents-index objects)
- masks-index (create-mask-index objects parents-index)
- bounds (gsh/selection-rect shapes)
- bounds #js {:x (:x bounds)
- :y (:y bounds)
- :width (:width bounds)
- :height (:height bounds)}]
-
- (reduce (partial index-object objects z-index parents-index masks-index)
- (qdt/create bounds)
- shapes)))
-
-(defn- index-object
- [objects z-index parents-index masks-index index obj]
- (let [{:keys [x y width height]} (:selrect obj)
- shape-bound #js {:x x :y y :width width :height height}
- parents (get parents-index (:id obj))
- masks (get masks-index (:id obj))
- z (get z-index (:id obj))
- frame (when (and (not= :frame (:type obj))
- (not= (:frame-id obj) uuid/zero))
- (get objects (:frame-id obj)))]
- (qdt/insert index
- shape-bound
- (assoc obj :frame frame :masks masks :parents parents :z z))))
+ (query-index index rect frame-id include-frames? include-groups? disabled-masks reverse?)))
+(defmethod impl/handler :selection/query-z-index
+ [{:keys [page-id objects ids]}]
+ (when-let [{z-index :z-index} (get @state page-id)]
+ (->> ids (map #(+ (get z-index %)
+ (get z-index (get-in objects [% :frame-id])))))))
diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs
index 9da49438b..a2c395448 100644
--- a/frontend/src/app/worker/snaps.cljs
+++ b/frontend/src/app/worker/snaps.cljs
@@ -6,48 +6,133 @@
(ns app.worker.snaps
(:require
- [okulary.core :as l]
- [app.common.uuid :as uuid]
- [app.common.pages :as cp]
[app.common.data :as d]
- [app.worker.impl :as impl]
- [app.util.range-tree :as rt]
+ [app.common.pages :as cp]
+ [app.common.uuid :as uuid]
+ [app.util.geom.grid :as gg]
[app.util.geom.snap-points :as snap]
- [app.util.geom.grid :as gg]))
+ [app.util.range-tree :as rt]
+ [app.worker.impl :as impl]
+ [clojure.set :as set]
+ [okulary.core :as l]))
(defonce state (l/atom {}))
-(defn- create-coord-data
- "Initializes the range tree given the shapes"
- [frame-id shapes coord]
- (let [process-shape (fn [coord]
- (fn [shape]
- (concat
- (let [points (snap/shape-snap-points shape)]
- (map #(vector % (:id shape)) points))
+(defn process-shape [frame-id coord]
+ (fn [shape]
+ (let [points (snap/shape-snap-points shape)
+ shape-data (->> points (mapv #(vector % (:id shape))))]
+ (if (= (:id shape) frame-id)
+ (d/concat
+ shape-data
- ;; The grid points are only added by the "root" of the coord-dat
- (when (= (:id shape) frame-id)
- (let [points (gg/grid-snap-points shape coord)]
- (map #(vector % :layout) points))))))
- into-tree (fn [tree [point _ :as data]]
- (rt/insert tree (coord point) 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 coord))
- (reduce into-tree (rt/make-tree)))))
+ (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 [frame-shapes (->> (vals objects)
- (filter :frame-id)
- (group-by :frame-id))
- frame-shapes (->> (cp/select-frames objects)
- (reduce #(update %1 (:id %2) conj %2) frame-shapes))]
+ (let [shapes-data (aggregate-data objects)
- (d/mapm (fn [frame-id shapes] {:x (create-coord-data frame-id shapes :x)
- :y (create-coord-data frame-id shapes :y)})
- frame-shapes)))
+ 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 :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 (keys old-objects) (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"
@@ -60,6 +145,16 @@
(let [snap-data (initialize-snap-data objects)]
(assoc state page-id snap-data)))
+(defn- update-page [state page-id old-objects new-objects]
+ (let [changed? #(not= (get old-objects %) (get new-objects %))
+ changed-ids (into #{}
+ (filter changed?)
+ (set/union (keys old-objects) (keys new-objects)))
+
+ snap-data (get state page-id)
+ snap-data (update-snap-data snap-data old-objects new-objects)]
+ (assoc state page-id snap-data)))
+
;; Public API
(defmethod impl/handler :snaps/initialize-index
[{:keys [file-id data] :as message}]
@@ -74,9 +169,11 @@
nil))
(defmethod impl/handler :snaps/update-index
- [{:keys [page-id objects] :as message}]
- ;; TODO: Check the difference and update the index acordingly
- (swap! state index-page page-id objects)
+ [{: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)
nil)
diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs
index f8db65343..ddf65c6b1 100644
--- a/frontend/src/app/worker/thumbnails.cljs
+++ b/frontend/src/app/worker/thumbnails.cljs
@@ -34,7 +34,7 @@
(p/create
(fn [resolve reject]
(->> (http/send! {:uri uri
- :query {:file-id file-id :id page-id}
+ :query {:file-id file-id :id page-id :strip-thumbnails true}
:method :get})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)
@@ -50,7 +50,7 @@
(let [prev (get @cache ckey)]
(if (= (:data prev) data)
(:result prev)
- (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150"})
+ (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150" :thumbnails? true})
result (rds/renderToStaticMarkup elem)]
(swap! cache assoc ckey {:data data :result result})
result))))
diff --git a/frontend/tests/app/test_components_sync.cljs b/frontend/tests/app/test_components_sync.cljs
index 5fa6bcd12..e93ab55ed 100644
--- a/frontend/tests/app/test_components_sync.cljs
+++ b/frontend/tests/app/test_components_sync.cljs
@@ -1,18 +1,18 @@
(ns app.test-components-sync
- (:require [cljs.test :as t :include-macros true]
- [cljs.pprint :refer [pprint]]
- [clojure.stacktrace :as stk]
- [beicon.core :as rx]
- [linked.core :as lks]
- [app.test-helpers.events :as the]
- [app.test-helpers.pages :as thp]
- [app.test-helpers.libraries :as thl]
- [app.common.geom.point :as gpt]
- [app.common.data :as d]
- [app.common.pages.helpers :as cph]
- [app.main.data.workspace.common :as dwc]
- [app.main.data.workspace.libraries :as dwl]
- [app.main.data.workspace.libraries-helpers :as dwlh]))
+ (:require
+ [cljs.test :as t :include-macros true]
+ [cljs.pprint :refer [pprint]]
+ [beicon.core :as rx]
+ [linked.core :as lks]
+ [app.test-helpers.events :as the]
+ [app.test-helpers.pages :as thp]
+ [app.test-helpers.libraries :as thl]
+ [app.common.geom.point :as gpt]
+ [app.common.data :as d]
+ [app.common.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]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
diff --git a/frontend/tests/app/test_shapes.cljs b/frontend/tests/app/test_shapes.cljs
index 157780071..2b473c02a 100644
--- a/frontend/tests/app/test_shapes.cljs
+++ b/frontend/tests/app/test_shapes.cljs
@@ -50,10 +50,6 @@
(dwl/add-recent-color color))
(rx/map
(fn [new-state]
- (t/is (= (get-in new-state [:workspace-file
- :data
- :recent-colors])
- [color]))
(t/is (= (get-in new-state [:workspace-data
:recent-colors])
[color]))))
diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po
new file mode 100644
index 000000000..86bac1358
--- /dev/null
+++ b/frontend/translations/ar.po
@@ -0,0 +1,232 @@
+msgid ""
+msgstr ""
+"PO-Revision-Date: 2021-05-17 21:32+0000\n"
+"Last-Translator: Amine Gdoura \n"
+"Language-Team: Arabic "
+"\n"
+"Language: ar\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
+"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
+"X-Generator: Weblate 4.7-dev\n"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.already-have-account"
+msgstr "هل لديك حساب؟"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.check-your-email"
+msgstr "تحقق من بريدك الإلكتروني وانقر على الرابط للتحقق والبدء في استخدام Penpot."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.confirm-password"
+msgstr "تأكيد كلمة المرور"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-account"
+msgstr "إنشاء حساب تجريبي"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-profile"
+msgstr "ترغب في التجربة فحسب؟"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.demo-warning"
+msgstr "هذه خدمة تجريبية ، لا تستخدمها للعمل الحقيقي ، سيتم مسح المشاريع بشكل دوري."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.email"
+msgstr "البريد الالكتروني"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.forgot-password"
+msgstr "هل نسيت كلمة السر؟"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.fullname"
+msgstr "الاسم بالكامل"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.go-back-to-login"
+msgstr "الرجوع للخلف!"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.login-here"
+msgstr "تسجيل الدخول هنا"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-submit"
+msgstr "تسجيل الدخول"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-subtitle"
+msgstr "أدخل التفاصيل أدناه"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-title"
+msgstr "سعيد برؤيتك مجددا!"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-github-submit"
+msgstr "تسجيل الدخول عبر Github"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-gitlab-submit"
+msgstr "تسجيل الدخول عبر Gitlab"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-google-submit"
+msgstr "تسجيل الدخول عبر جوجل"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-ldap-submit"
+msgstr "تسجيل الدخول باستخدام LDAP"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-oidc-submit"
+msgstr "تسجيل الدخول باستخدام OpenID (SSO)"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.new-password"
+msgstr "اكتب كلمة مرور جديدة"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.invalid-token-error"
+msgstr "رمز الاسترداد غير صالح."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.password-changed-succesfully"
+msgstr "تم تغيير كلمة المرور بنجاح"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "auth.notifications.team-invitation-accepted"
+msgstr "تم الانضمام إلى الفريق بنجاح"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.password"
+msgstr "كلمه السر"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.password-length-hint"
+msgstr "8 أحرف على الأقل"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-submit"
+msgstr "إستعادة كلمة المرور"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-subtitle"
+msgstr "سنرسل لك رسالة بريد إلكتروني تحتوي على التعليمات"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-title"
+msgstr "نسيت كلمة المرور؟"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.recovery-submit"
+msgstr "تغيير كلمة المرور"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.register"
+msgstr "لا تملك حساب بعد؟"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.register-submit"
+msgstr "إنشاء حساب"
+
+#: src/app/main/ui/auth/register.cljs
+#, fuzzy
+msgid "auth.register-subtitle"
+msgstr "إنه مجاني ، مفتوح المصدر"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-title"
+msgstr "إنشاء حساب"
+
+#: src/app/main/ui/auth.cljs
+msgid "auth.sidebar-tagline"
+msgstr "الحل (مفتوح المصدر) للتصميم والنمذجة."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.terms-privacy-agreement"
+msgstr "عند إنشاء حساب جديد ، فإنك توافق على شروط الخدمة وسياسة الخصوصية الخاصة بنا."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.verification-email-sent"
+msgstr "لقد أرسلنا رسالة تحقق إلى بريدك الالكتروني"
+
+#, fuzzy, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"ستتم إضافة أي خط ويب تقوم بتحميله هنا إلى قائمة عائلة الخطوط المتوفرة في "
+"خصائص النص الخاصة بملفات هذا الفريق. سيتم تجميع الخطوط التي لها نفس اسم "
+"عائلة الخطوط على أنها ** عائلة خط واحدة **. يمكنك تحميل الخطوط بالتنسيقات "
+"التالية: ** TTF و OTF و WOFF ** (ستحتاج إلى تنسيق واحد فقط)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"يجب عليك فقط تحميل الخطوط التي تمتلكها أو لديك ترخيص لاستخدامها في Penpot. "
+"اكتشف المزيد في قسم حقوق المحتوى في [شروط خدمة Penpot] "
+"(https://penpot.app/terms.html). قد ترغب أيضًا في القراءة عن [ترخيص الخطوط] "
+"(2)."
+
+msgid "labels.custom-fonts"
+msgstr "خطوط مخصصة"
+
+msgid "labels.font-family"
+msgstr "عائلة الخط"
+
+#, fuzzy
+msgid "labels.font-providers"
+msgstr "موفرو الخطوط"
+
+#, fuzzy
+msgid "labels.font-variant"
+msgstr "نمط"
+
+msgid "labels.fonts"
+msgstr "الخطوط"
+
+msgid "labels.images"
+msgstr "الصور"
+
+msgid "labels.installed-fonts"
+msgstr "الخطوط المتوفرة"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.members"
+msgstr "الأعضاء"
+
+msgid "labels.search-font"
+msgstr "البحث عن الخط"
+
+#, fuzzy
+msgid "labels.upload"
+msgstr "تحميل"
+
+msgid "labels.upload-custom-fonts"
+msgstr "تحميل الخطوط المخصصة"
+
+msgid "labels.uploading"
+msgstr "جارٍ التحميل ..."
+
+msgid "modals.delete-font.message"
+msgstr "هل أنت متأكد أنك تريد حذف هذا الخط؟ لن يتم تحميله إذا تم استخدامه في ملف."
+
+msgid "modals.delete-font.title"
+msgstr "حذف الخط"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+#, fuzzy
+msgid "title.dashboard.font-providers"
+msgstr "موفرو الخطوط - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "الخطوط -٪ s - Penpot"
+
+msgid "workspace.viewport.click-to-close-path"
+msgstr "انقر لإغلاق المسار"
\ No newline at end of file
diff --git a/frontend/translations/da.po b/frontend/translations/da.po
new file mode 100644
index 000000000..2847eee0e
--- /dev/null
+++ b/frontend/translations/da.po
@@ -0,0 +1,484 @@
+msgid ""
+msgstr ""
+"PO-Revision-Date: 2021-05-17 21:32+0000\n"
+"Last-Translator: Simon Bechmann \n"
+"Language-Team: Danish "
+"\n"
+"Language: da\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.7-dev\n"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.already-have-account"
+msgstr "Har du allerede en konto?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.check-your-email"
+msgstr ""
+"Tjek din mail og klik på linket for at bekræfte og starte med at bruge "
+"Penpot."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.confirm-password"
+msgstr "Bekræft adgangskode"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-account"
+msgstr "Lav demokonto"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-profile"
+msgstr "Vil du bare prøve det?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.demo-warning"
+msgstr ""
+"Det her er en DEMO service, BRUG IKKE for rigtigt arbejde, projekterne vil "
+"blive slettet periodevis."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.email"
+msgstr "Email"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.forgot-password"
+msgstr "Glemt adgangskode?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.fullname"
+msgstr "Fulde Navn"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.go-back-to-login"
+msgstr "Gå tilbage!"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.login-here"
+msgstr "Log på her"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-submit"
+msgstr "Log på"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-subtitle"
+msgstr "Indtast dine oplysninger nedenunder"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-title"
+msgstr "Fedt at se dig igen!"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-github-submit"
+msgstr "Log på med Github"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-gitlab-submit"
+msgstr "Log på med Gitlab"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-google-submit"
+msgstr "Log på med Google"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-ldap-submit"
+msgstr "Log på med LDAP"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-oidc-submit"
+msgstr "Log på med OpenID (SSO)"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.new-password"
+msgstr "Indtast et nyt kodeord"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.invalid-token-error"
+msgstr "Genopretningspoletten er ugyldig."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.password-changed-succesfully"
+msgstr "Adgangskoden er blevet ændret"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.profile-not-verified"
+msgstr "Profilen er ikke bekræftet, venligt verificer profilen før du går videre."
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.recovery-token-sent"
+msgstr "Gendannelseslink for adgangskoden er sendt til din indbakke."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "auth.notifications.team-invitation-accepted"
+msgstr "Tilsluttet teamet med succes"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.password"
+msgstr "Adgangskode"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.password-length-hint"
+msgstr "Mindst 8 karakterer"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-submit"
+msgstr "Gendan Adgangskode"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-subtitle"
+msgstr "Vi sender dig en mail med instruktioner"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-title"
+msgstr "Glemt adgangskode?"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.recovery-submit"
+msgstr "Skift din adgangskode"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.register"
+msgstr "Ingen konto?"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.register-submit"
+msgstr "Opret en konto"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-subtitle"
+msgstr "Det er gratis, det er Open Source"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-title"
+msgstr "Opret en konto"
+
+#: src/app/main/ui/auth.cljs
+msgid "auth.sidebar-tagline"
+msgstr "Open-source løsningen for design og prototyping."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.terms-privacy-agreement"
+msgstr ""
+"Når du opretter en ny konto, accepterer du vores servicevilkår og "
+"fortrolighedspolitik."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.verification-email-sent"
+msgstr "Vi har sendt en bekræftelsesmail til"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.add-shared"
+msgstr "Tilføj som Delt Bibliotek"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.change-email"
+msgstr "Skift email"
+
+#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
+msgid "dashboard.copy-suffix"
+msgstr "(kopi)"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.create-new-team"
+msgstr "+ Opret nyt team"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.default-team-name"
+msgstr "Dit Penpot"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.delete-team"
+msgstr "Slet team"
+
+msgid "dashboard.draft-title"
+msgstr "Udkast"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate"
+msgstr "Dublikér"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate-multi"
+msgstr "Dublikér %s filer"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.empty-files"
+msgstr "Du har stadig ingen filer her"
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"Alle web skrifttyper, som du uploader her, vil blive tilføjet til listen af "
+"skrifttypefamilier tilgængelig ved tekstindstillingerne i filerne for dette "
+"team. Skrifttyper med det samme skrifttypefamilienavn vil blive grupperet "
+"som en **enkelt skrifttypefamilie**. Du kan uploade skrifttyper med "
+"følgende formater: **TTF, OTF og WOFF** (kun én er nødvendig)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"Du bør kun uploade skrifttyper som du ejer eller har licens til at bruge i "
+"Penpot. Find ud af mere i sektionen om indholdsrettigheder i [Penpot's "
+"Terms of Service] (https://penpot.app/terms.html). Du kan også læse om "
+"[font licensing](2)."
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.invite-profile"
+msgstr "Invitér til team"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.leave-team"
+msgstr "Forlad team"
+
+#: src/app/main/ui/dashboard/libraries.cljs
+msgid "dashboard.libraries-title"
+msgstr "Delte Biblioteker"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.loading-files"
+msgstr "indlæser dine filer…"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to"
+msgstr "Flyt til"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-multi"
+msgstr "Flyt %s filer til"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-other-team"
+msgstr "Flyt til andet team"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs
+msgid "dashboard.new-file"
+msgstr "+ Ny Fil"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "Ny Fil"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.new-project"
+msgstr "+ Nyt projekt"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "Nyt Projekt"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.no-matches-for"
+msgstr "Intet match fundet for “%s“"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.no-projects-placeholder"
+msgstr "Fastgjorte projekter bliver vist her"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-changed-successfully"
+msgstr "Din email-adresse er blevet opdateret med succes"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-verified-successfully"
+msgstr "Din email-adresse er blevet bekræftet med succes"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.notifications.password-saved"
+msgstr "Adgangskode gemt med succes!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.num-of-members"
+msgstr "%s medlemmer"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.open-in-new-tab"
+msgstr "Åben fil i en ny fane"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.password-change"
+msgstr "Skift adgangskode"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.pin-unpin"
+msgstr "Fastgør/Løsne"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.projects-title"
+msgstr "Projekter"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.promote-to-owner"
+msgstr "Forfrem til ejer"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.remove-account"
+msgstr "Vil du slette din konto?"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.remove-shared"
+msgstr "Fjern som Delt Bibliotek"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.search-placeholder"
+msgstr "Søg…"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.searching-for"
+msgstr "Søger efter “%s“…"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-language"
+msgstr "Vælg UI sprog"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-theme"
+msgstr "Vælg tema"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.show-all-files"
+msgstr "Vis alle filer"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-delete-file"
+msgstr "Din fil er blevet slettet med succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-delete-project"
+msgstr "Dit projekt er blevet slettet med succes"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-duplicate-file"
+msgstr "Din fil er blevet dublikeret med succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-duplicate-project"
+msgstr "Dit projekt er blevet dublikeret med succes"
+
+#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-file"
+msgstr "Din fil er blevet flyttet med succes"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-files"
+msgstr "Dine filer er blevet flyttet med succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-move-project"
+msgstr "Dit projekt er blevet flyttet med succes"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.switch-team"
+msgstr "Skift team"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-info"
+msgstr "Team info"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-members"
+msgstr "Medlemmer"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-projects"
+msgstr "Team projekter"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.theme-change"
+msgstr "UI tema"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.title-search"
+msgstr "Søgeresultater"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.type-something"
+msgstr "Skriv for at søge i resultater"
+
+#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
+msgid "dashboard.update-settings"
+msgstr "Opdater indstillinger"
+
+#: src/app/main/ui/settings.cljs
+msgid "dashboard.your-account-title"
+msgstr "Din konto"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-email"
+msgstr "Email"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-name"
+msgstr "Dit navn"
+
+#: 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 "Dit Penpot"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-cancel"
+msgstr "Fortryd"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-ok"
+msgstr "Ok"
+
+#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs
+msgid "ds.confirm-title"
+msgstr "Er du sikker?"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "ds.updated-at"
+msgstr "Opdateret: %s"
+
+#: src/app/main/data/workspace.cljs
+msgid "errors.clipboard-not-implemented"
+msgstr "Din browser kan ikke gøre denne operation"
+
+msgid "labels.custom-fonts"
+msgstr "Brugerdefinerede skrifttyper"
+
+msgid "labels.font-family"
+msgstr "Skrifttypefamilie"
+
+msgid "labels.font-providers"
+msgstr "Skrifttype udbydere"
+
+msgid "labels.font-variant"
+msgstr "Stil"
+
+msgid "labels.fonts"
+msgstr "Skrifttyper"
+
+msgid "labels.installed-fonts"
+msgstr "Installeret skrifttyper"
+
+msgid "labels.search-font"
+msgstr "Søg efter skrifttype"
+
+msgid "labels.upload"
+msgstr "Upload"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Upload brugerdefinerede skrifttyper"
+
+msgid "labels.uploading"
+msgstr "Uploader..."
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Er du sikker på, at du vil slette denne skrifttype? Den vil ikke indlæse, "
+"hvis den bliver brugt i en fil."
+
+msgid "modals.delete-font.title"
+msgstr "Sletter skrifttype"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Skrifttype Udbydere - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Skrifttyper - %s - Penpot"
\ No newline at end of file
diff --git a/frontend/translations/de.po b/frontend/translations/de.po
index ee2c448b3..c5179aefa 100644
--- a/frontend/translations/de.po
+++ b/frontend/translations/de.po
@@ -1,10 +1,15 @@
msgid ""
msgstr ""
+"PO-Revision-Date: 2021-05-13 08:44+0000\n"
+"Last-Translator: Andrey Antukh \n"
+"Language-Team: German "
+"\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.7-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -2250,15 +2255,15 @@ msgstr "Stift (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "Ellipse (E)"
+msgstr "Ellipse (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Zeichenfläche (A)"
+msgstr "Zeichenfläche (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Bild (K)"
+msgstr "Bild (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2266,15 +2271,15 @@ msgstr "Verschieben"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Pfad (P)"
+msgstr "Pfad (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Rechteck (R)"
+msgstr "Rechteck (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Text (T)"
+msgstr "Text (%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/translations/el.po b/frontend/translations/el.po
index 2aa22d3f1..d5b782250 100644
--- a/frontend/translations/el.po
+++ b/frontend/translations/el.po
@@ -2204,15 +2204,15 @@ msgstr "Στροφή (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "Έλλειψη (Ε)"
+msgstr "Έλλειψη (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Artboard (Α)"
+msgstr "Artboard (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Εικόνα (Κ)"
+msgstr "Εικόνα (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2220,15 +2220,15 @@ msgstr ""
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Path (Ρ)"
+msgstr "Path (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Ορθογώνιο (R)"
+msgstr "Ορθογώνιο (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Κείμενο (Τ)"
+msgstr "Κείμενο (%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 6fb62455c..a0cd82b37 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
-"PO-Revision-Date: 2021-04-14 13:44+0000\n"
-"Last-Translator: Andrés Moya \n"
+"PO-Revision-Date: 2021-05-20 14:12+0000\n"
+"Last-Translator: Jan C. Borchardt \n"
"Language-Team: English "
"\n"
"Language: en\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.6-dev\n"
+"X-Generator: Weblate 4.7-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -206,6 +206,29 @@ msgstr "Duplicate %s files"
msgid "dashboard.empty-files"
msgstr "You still have no files here"
+msgid "dashboard.fonts.deleted-placeholder"
+msgstr "Font deleted"
+
+msgid "dashboard.fonts.empty-placeholder"
+msgstr "You still have no custom fonts installed."
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"Any web font you upload here will be added to the font family list "
+"available at the text properties of the files of this team. Fonts with the "
+"same font family name will be grouped as a **single font family**. You can "
+"upload fonts with the following formats: **TTF, OTF and WOFF** (only one "
+"will be needed)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"You should only upload fonts you own or have license to use in Penpot. Find "
+"out more in the Content rights section of [Penpot's Terms of "
+"Service](https://penpot.app/terms.html). You also might want to read about "
+"[font licensing](https://www.typography.com/faq)."
+
#: src/app/main/ui/dashboard/team.cljs
msgid "dashboard.invite-profile"
msgstr "Invite to team"
@@ -222,6 +245,9 @@ msgstr "Shared Libraries"
msgid "dashboard.loading-files"
msgstr "loading your files …"
+msgid "dashboard.loading-fonts"
+msgstr "loading your fonts …"
+
#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
msgid "dashboard.move-to"
msgstr "Move to"
@@ -238,10 +264,18 @@ msgstr "Move to other team"
msgid "dashboard.new-file"
msgstr "+ New File"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "New File"
+
#: src/app/main/ui/dashboard/projects.cljs
msgid "dashboard.new-project"
msgstr "+ New project"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "New Project"
+
#: src/app/main/ui/dashboard/search.cljs
msgid "dashboard.no-matches-for"
msgstr "No matches found for “%s“"
@@ -336,7 +370,7 @@ msgstr "Your file has been moved successfully"
#: src/app/main/ui/dashboard/file_menu.cljs
msgid "dashboard.success-move-files"
-msgstr "Your files has been moved successfully"
+msgstr "Your files have been moved successfully"
#: src/app/main/ui/dashboard/project_menu.cljs
msgid "dashboard.success-move-project"
@@ -765,6 +799,9 @@ msgstr "You are seeing version %s"
msgid "labels.accept"
msgstr "Accept"
+msgid "labels.add-custom-font"
+msgstr "Add custom font"
+
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
msgid "labels.admin"
msgstr "Admin"
@@ -813,6 +850,9 @@ msgstr "Create new team"
msgid "labels.create-team.placeholder"
msgstr "Enter new team name"
+msgid "labels.custom-fonts"
+msgstr "Custom fonts"
+
#: src/app/main/ui/settings/sidebar.cljs
msgid "labels.dashboard"
msgstr "Dashboard"
@@ -857,6 +897,18 @@ msgstr "Feedback disabled"
msgid "labels.feedback-sent"
msgstr "Feedback sent"
+msgid "labels.font-family"
+msgstr "Font Family"
+
+msgid "labels.font-providers"
+msgstr "Font providers"
+
+msgid "labels.font-variants"
+msgstr "Styles"
+
+msgid "labels.fonts"
+msgstr "Fonts"
+
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.give-feedback"
msgstr "Give feedback"
@@ -871,6 +923,9 @@ msgstr "Icons"
msgid "labels.images"
msgstr "Images"
+msgid "labels.installed-fonts"
+msgstr "Installed fonts"
+
#: src/app/main/ui/static.cljs
msgid "labels.internal-error.desc-message"
msgstr ""
@@ -889,6 +944,9 @@ msgstr "Language"
msgid "labels.logout"
msgstr "Logout"
+msgid "labels.manage-fonts"
+msgstr "Manage fonts"
+
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.members"
msgstr "Members"
@@ -987,13 +1045,16 @@ msgstr "Role"
msgid "labels.save"
msgstr "Save"
+msgid "labels.search-font"
+msgstr "Search font"
+
#: src/app/main/ui/settings/feedback.cljs
msgid "labels.send"
msgstr "Send"
#: src/app/main/ui/settings/feedback.cljs
msgid "labels.sending"
-msgstr "Sending..."
+msgstr "Sending…"
#: src/app/main/ui/static.cljs
msgid "labels.service-unavailable.desc-message"
@@ -1031,6 +1092,15 @@ msgstr "Update"
msgid "labels.update-team"
msgstr "Update team"
+msgid "labels.upload"
+msgstr "Upload"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Upload custom fonts"
+
+msgid "labels.uploading"
+msgstr "Uploading…"
+
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.viewer"
msgstr "Viewer"
@@ -1131,6 +1201,22 @@ msgstr "Are you sure you want to delete %s files?"
msgid "modals.delete-file-multi-confirm.title"
msgstr "Deleting %s files"
+msgid "modals.delete-font-variant.message"
+msgstr ""
+"Are you sure you want to delete this font style? It will not load if is "
+"used in a file."
+
+msgid "modals.delete-font-variant.title"
+msgstr "Deleting font style"
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Are you sure you want to delete this font? It will not load if is used in a "
+"file."
+
+msgid "modals.delete-font.title"
+msgstr "Deleting font"
+
#: src/app/main/ui/workspace/sidebar/sitemap.cljs
msgid "modals.delete-page.body"
msgstr "Are you sure you want to delete this page?"
@@ -1294,6 +1380,14 @@ msgstr "Mixed"
msgid "title.dashboard.files"
msgstr "%s - Penpot"
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Font Providers - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fonts - %s - Penpot"
+
#: src/app/main/ui/dashboard/projects.cljs
msgid "title.dashboard.projects"
msgstr "Projects - %s - Penpot"
@@ -1504,7 +1598,7 @@ msgstr "Search assets"
#: src/app/main/ui/workspace/sidebar/assets.cljs
msgid "workspace.assets.selected-count"
msgid_plural "workspace.assets.selected-count"
-msgstr[0] "%s items selected"
+msgstr[0] "%s item selected"
msgstr[1] "%s items selected"
#: src/app/main/ui/workspace/sidebar/assets.cljs
@@ -1559,6 +1653,10 @@ msgstr "Radial gradient"
msgid "workspace.header.menu.disable-dynamic-alignment"
msgstr "Disable dynamic alignment"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-scale-text"
+msgstr "Disable scale text"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.disable-snap-grid"
msgstr "Disable snap to grid"
@@ -1567,6 +1665,10 @@ msgstr "Disable snap to grid"
msgid "workspace.header.menu.enable-dynamic-alignment"
msgstr "Enable dynamic aligment"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-scale-text"
+msgstr "Enable scale text"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.enable-snap-grid"
msgstr "Snap to grid"
@@ -2365,15 +2467,15 @@ msgstr "Curve (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "Ellipse (E)"
+msgstr "Ellipse (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Artboard (A)"
+msgstr "Artboard (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Image (K)"
+msgstr "Image (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2381,15 +2483,15 @@ msgstr "Move"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Path (P)"
+msgstr "Path (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Rectangle (R)"
+msgstr "Rectangle (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Text (T)"
+msgstr "Text (%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 44078530f..7063b5649 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -9,14 +9,13 @@ 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.6-dev\n"
+"X-Generator: Weblate 4.7-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
msgstr "¿Tienes ya una cuenta?"
#: src/app/main/ui/auth/register.cljs
-#, fuzzy
msgid "auth.check-your-email"
msgstr ""
"Comprueba tu email y haz click en el link de verificación para comenzar a "
@@ -211,6 +210,29 @@ msgstr "Duplicar %s archivos"
msgid "dashboard.empty-files"
msgstr "Todavía no hay ningún archivo aquí"
+msgid "dashboard.fonts.deleted-placeholder"
+msgstr "Fuente eliminada."
+
+msgid "dashboard.fonts.empty-placeholder"
+msgstr "Aun no tienes fuentes personalizadas."
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"Any web font you upload here will be added to the font family list "
+"available at the text properties of the files of this team. Fonts with the "
+"same font family name will be grouped as a **single font family**. You can "
+"upload fonts with the following formats: **TTF, OTF and WOFF** (only one "
+"will be needed)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"You should only upload fonts you own or have license to use in Penpot. Find "
+"out more in the Content rights section of [Penpot's Terms of "
+"Service](https://penpot.app/terms.html). You also might want to read about "
+"[font licensing](2)."
+
#: src/app/main/ui/dashboard/team.cljs
msgid "dashboard.invite-profile"
msgstr "Invitar al equipo"
@@ -227,6 +249,9 @@ msgstr "Bibliotecas Compartidas"
msgid "dashboard.loading-files"
msgstr "cargando tus ficheros …"
+msgid "dashboard.loading-fonts"
+msgstr "cargando tus fuentes …"
+
#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
msgid "dashboard.move-to"
msgstr "Mover a"
@@ -243,10 +268,18 @@ msgstr "Mover a otro equipo"
msgid "dashboard.new-file"
msgstr "+ Nuevo Archivo"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "Nuevo Archivo"
+
#: src/app/main/ui/dashboard/projects.cljs
msgid "dashboard.new-project"
msgstr "+ Nuevo proyecto"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "Nuevo Proyecto"
+
#: src/app/main/ui/dashboard/search.cljs
msgid "dashboard.no-matches-for"
msgstr "No se encuentra “%s“"
@@ -772,6 +805,9 @@ msgstr "Estás viendo la versión %s"
msgid "labels.accept"
msgstr "Aceptar"
+msgid "labels.add-custom-font"
+msgstr "Añadir fuentes personalizada"
+
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
msgid "labels.admin"
msgstr "Administración"
@@ -816,6 +852,9 @@ msgstr "Crear"
msgid "labels.create-team"
msgstr "Crea un nuevo equipo"
+msgid "labels.custom-fonts"
+msgstr "Fuentes personalizadas"
+
#: src/app/main/ui/settings/sidebar.cljs
msgid "labels.dashboard"
msgstr "Panel"
@@ -860,6 +899,18 @@ msgstr "El modulo de recepción de opiniones esta deshabilitado."
msgid "labels.feedback-sent"
msgstr "Opinión enviada"
+msgid "labels.font-family"
+msgstr "Familia de fuente"
+
+msgid "labels.font-providers"
+msgstr "Proveedores de fuentes"
+
+msgid "labels.font-variants"
+msgstr "Estilos"
+
+msgid "labels.fonts"
+msgstr "Fuentes"
+
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.give-feedback"
msgstr "Danos tu opinión"
@@ -874,6 +925,9 @@ msgstr "Iconos"
msgid "labels.images"
msgstr "Imágenes"
+msgid "labels.installed-fonts"
+msgstr "Fuentes instaladas"
+
#: src/app/main/ui/static.cljs
msgid "labels.internal-error.desc-message"
msgstr ""
@@ -892,6 +946,9 @@ msgstr "Idioma"
msgid "labels.logout"
msgstr "Salir"
+msgid "labels.manage-fonts"
+msgstr "Administrar fuentes"
+
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.members"
msgstr "Integrantes"
@@ -990,6 +1047,9 @@ msgstr "Cargo"
msgid "labels.save"
msgstr "Guardar"
+msgid "labels.search-font"
+msgstr "Buscar fuente"
+
#: src/app/main/ui/settings/feedback.cljs
msgid "labels.send"
msgstr "Enviar"
@@ -1034,6 +1094,15 @@ msgstr "Actualizar"
msgid "labels.update-team"
msgstr "Actualiza el equipo"
+msgid "labels.upload"
+msgstr "Subir"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Subir fuente"
+
+msgid "labels.uploading"
+msgstr "Subiendo..."
+
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.viewer"
msgstr "Visualizador"
@@ -1134,6 +1203,22 @@ msgstr "¿Seguro que quieres eliminar %s archivos?"
msgid "modals.delete-file-multi-confirm.title"
msgstr "Eliminando %s archivos"
+msgid "modals.delete-font-variant.message"
+msgstr ""
+"Estas seguro de querer eliminar esta estilo de fuente? Dejara de cargar si "
+"es usada en algun fichero."
+
+msgid "modals.delete-font-variant.title"
+msgstr "Eliminando estilo de fuente"
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Estas seguro de querer eliminar esta fuente? Dejara de cargar si es usada "
+"en algun fichero."
+
+msgid "modals.delete-font.title"
+msgstr "Eliminando fuente"
+
#: src/app/main/ui/workspace/sidebar/sitemap.cljs
msgid "modals.delete-page.body"
msgstr "¿Seguro que quieres borrar esta página?"
@@ -1289,6 +1374,14 @@ msgstr "Verificación de email enviada a %s. Comprueba tu correo."
msgid "settings.multiple"
msgstr "Varios"
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Proveedores de fuentes - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fuentes - %s - Penpot"
+
#: src/app/main/ui/dashboard/projects.cljs
msgid "title.dashboard.projects"
msgstr "Proyectos - %s - Penpot"
@@ -1552,6 +1645,10 @@ msgstr "Degradado radial"
msgid "workspace.header.menu.disable-dynamic-alignment"
msgstr "Desactivar alineamiento dinámico"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-scale-text"
+msgstr "Desactivar escalar texto"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.disable-snap-grid"
msgstr "Desactivar alinear a la rejilla"
@@ -1560,6 +1657,10 @@ msgstr "Desactivar alinear a la rejilla"
msgid "workspace.header.menu.enable-dynamic-alignment"
msgstr "Activar alineamiento dinámico"
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-scale-text"
+msgstr "Activar escalar texto"
+
#: src/app/main/ui/workspace/header.cljs
msgid "workspace.header.menu.enable-snap-grid"
msgstr "Alinear a la rejilla"
@@ -2360,15 +2461,15 @@ msgstr "Curva (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "Elipse (E)"
+msgstr "Elipse (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Tablero (A)"
+msgstr "Tablero (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Imagen (K)"
+msgstr "Imagen (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2376,15 +2477,15 @@ msgstr "Mover"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Ruta (P)"
+msgstr "Ruta (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Rectángulo (R)"
+msgstr "Rectángulo (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Texto (T)"
+msgstr "Texto (%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po
index 4f7151394..34e70bab4 100644
--- a/frontend/translations/fr.po
+++ b/frontend/translations/fr.po
@@ -1,10 +1,15 @@
msgid ""
msgstr ""
+"PO-Revision-Date: 2021-05-13 08:47+0000\n"
+"Last-Translator: Andrey Antukh \n"
+"Language-Team: French "
+"\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"X-Generator: Weblate 4.7-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -2036,15 +2041,15 @@ msgstr "Courbe (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "Ellipse (E)"
+msgstr "Ellipse (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Plan de travail (A)"
+msgstr "Plan de travail (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Image (K)"
+msgstr "Image (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2052,15 +2057,15 @@ msgstr "Déplacer"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Chemin (P)"
+msgstr "Chemin (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Rectangle (R)"
+msgstr "Rectangle (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Texte (T)"
+msgstr "Texte (%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/translations/id.po b/frontend/translations/id.po
new file mode 100644
index 000000000..b1fc23594
--- /dev/null
+++ b/frontend/translations/id.po
@@ -0,0 +1,6 @@
+msgid ""
+msgstr ""
+"X-Generator: Weblate\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
\ No newline at end of file
diff --git a/frontend/translations/nb_NO.po b/frontend/translations/nb_NO.po
new file mode 100644
index 000000000..833e3b94d
--- /dev/null
+++ b/frontend/translations/nb_NO.po
@@ -0,0 +1,796 @@
+msgid ""
+msgstr ""
+"PO-Revision-Date: 2021-05-09 00:37+0000\n"
+"Last-Translator: Allan Nordhøy \n"
+"Language-Team: Norwegian Bokmål "
+"\n"
+"Language: nb_NO\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.7-dev\n"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.confirm-password"
+msgstr "Bekreft passord"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs
+#, fuzzy
+msgid "auth.email"
+msgstr "E-post"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.forgot-password"
+msgstr "Glemt passordet?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.fullname"
+msgstr "Fullt navn"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.new-password"
+msgstr "Skriv inn et nytt passord"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.invalid-token-error"
+msgstr "Gjenopprettelsessymbolet er ugyldig."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.password"
+msgstr "Passord"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-submit"
+msgstr "Gjenopprett passord"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.recovery-submit"
+msgstr "Endre passordet ditt"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.register"
+msgstr "Ingen konto enda?"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+#, fuzzy
+msgid "auth.register-submit"
+msgstr "Opprett konto"
+
+#: src/app/main/ui/auth/register.cljs
+#, fuzzy
+msgid "auth.register-title"
+msgstr "Opprett konto"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.add-shared"
+msgstr "Legg til som delt bibliotek"
+
+#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
+msgid "dashboard.copy-suffix"
+msgstr "(kopi)"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.default-team-name"
+msgstr "Din Penpot"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.delete-team"
+msgstr "Slett lag"
+
+msgid "dashboard.draft-title"
+msgstr "Kladd"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.invite-profile"
+msgstr "Inviter til lag"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.leave-team"
+msgstr "Forlat lag"
+
+#: src/app/main/ui/dashboard/libraries.cljs
+msgid "dashboard.libraries-title"
+msgstr "Delte biblioteker"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to"
+msgstr "Flytt til"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-multi"
+msgstr "Flytt %s filer til"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+#, fuzzy
+msgid "dashboard.move-to-other-team"
+msgstr "Flytt til annet lag"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs
+#, fuzzy
+msgid "dashboard.new-file"
+msgstr "+ Ny fil"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.new-project"
+msgstr "+ Nytt prosjekt"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.num-of-members"
+msgstr "%s medlemmer"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.open-in-new-tab"
+msgstr "Åpne fil i ny fane"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.password-change"
+msgstr "Endre passord"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.pin-unpin"
+msgstr "Fest/løsne"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.projects-title"
+msgstr "Prosjekter"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.promote-to-owner"
+msgstr "Promoter til eier"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.remove-account"
+msgstr "Ønsker du å fjerne kontoen din?"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+#, fuzzy
+msgid "dashboard.remove-shared"
+msgstr "Fjern som delt bibliotek"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.search-placeholder"
+msgstr "Søk …"
+
+#: src/app/main/ui/dashboard/search.cljs
+#, fuzzy
+msgid "dashboard.searching-for"
+msgstr "Şøker etter «%s» …"
+
+#: src/app/main/ui/settings/options.cljs
+#, fuzzy
+msgid "dashboard.select-ui-language"
+msgstr "Velg grensesnittsspråk"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-theme"
+msgstr "Velg drakt"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.show-all-files"
+msgstr "Vis alle filer"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.switch-team"
+msgstr "Bytt lag"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-info"
+msgstr "Laginfo"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-members"
+msgstr "Lagmedlemmer"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-projects"
+msgstr "Lagprosjekter"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.theme-change"
+msgstr "Grensesnittsdrakt"
+
+#: src/app/main/ui/settings.cljs
+msgid "dashboard.your-account-title"
+msgstr "Din konto"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-name"
+msgstr "Ditt navn"
+
+#: 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 "Din Penpot"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-cancel"
+msgstr "Avbryt"
+
+#: src/app/main/ui/confirm.cljs
+#, fuzzy
+msgid "ds.confirm-ok"
+msgstr "OK"
+
+#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs
+msgid "ds.confirm-title"
+msgstr "Er du sikker?"
+
+#: src/app/main/ui/components/color_input.cljs
+msgid "errors.invalid-color"
+msgstr "Ugyldig farge"
+
+#, fuzzy
+msgid "errors.media-format-unsupported"
+msgstr "Bildeformatet støttes ikke (må være SVG, JPG, eller PNG)."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.unexpected-token"
+msgstr "Ukjent symbol"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-start"
+msgstr "Ta del i sludringen"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.description"
+msgstr "Beskrivelse"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-go-to"
+msgstr "Gå til diskusjoner"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-title"
+msgstr "Lagdiskusjoner"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.subject"
+msgstr "Emne"
+
+#: src/app/main/ui/handoff/attributes/blur.cljs
+msgid "handoff.attributes.blur.value"
+msgstr "Verdi"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.download"
+msgstr "Last ned kildebilde"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.height"
+msgstr "Høyde"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.width"
+msgstr "Bredde"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.height"
+msgstr "Høyde"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.left"
+msgstr "Venstre"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.width"
+msgstr "Bredde"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow"
+msgstr "Skygge"
+
+#: src/app/main/ui/handoff/attributes/stroke.cljs
+msgid "handoff.attributes.stroke.width"
+msgstr "Bredde"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography"
+msgstr "Typografi"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-family"
+msgstr "Skriftfamilie"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-size"
+msgstr "Skriftstørrelse"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-style"
+msgstr "Skriftstil"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code"
+msgstr "Kode"
+
+msgid "handoff.tabs.code.selected.circle"
+msgstr "Sirkel"
+
+msgid "handoff.tabs.code.selected.group"
+msgstr "Gruppe"
+
+msgid "handoff.tabs.code.selected.image"
+msgstr "Bilde"
+
+msgid "handoff.tabs.code.selected.path"
+msgstr "Sti"
+
+msgid "handoff.tabs.code.selected.svg-raw"
+msgstr "SVG"
+
+msgid "handoff.tabs.code.selected.text"
+msgstr "Tekst"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.info"
+msgstr "Info"
+
+msgid "labels.accept"
+msgstr "Godta"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.cancel"
+msgstr "Avbryt"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.confirm-password"
+msgstr "Bekreft passord"
+
+msgid "labels.content"
+msgstr "Innhold"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "labels.create"
+msgstr "Opprett"
+
+#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team"
+msgstr "Opprett nytt lag"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team.placeholder"
+msgstr "Skriv inn nytt lagnavn"
+
+msgid "labels.custom-fonts"
+msgstr "Egendefinerte skrifter"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.dashboard"
+msgstr "Oversikt"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete"
+msgstr "Slett"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment"
+msgstr "Slett kommentar"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment-thread"
+msgstr "Slett tråd"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete-multi-files"
+msgstr "Slett %s filer"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.drafts"
+msgstr "Kladder"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.edit"
+msgstr "Rediger"
+
+msgid "labels.font-family"
+msgstr "Skriftfamilie"
+
+msgid "labels.font-providers"
+msgstr "Skrifttilbydere"
+
+msgid "labels.font-variant"
+msgstr "Stil"
+
+msgid "labels.fonts"
+msgstr "Skrifter"
+
+msgid "labels.icons"
+msgstr "Ikoner"
+
+msgid "labels.images"
+msgstr "Bilder"
+
+msgid "labels.installed-fonts"
+msgstr "Installerte skrifter"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "labels.language"
+msgstr "Språk"
+
+#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs
+#, fuzzy
+msgid "labels.logout"
+msgstr "Logg ut"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.members"
+msgstr "Medlemmer"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.name"
+msgstr "Navn"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.new-password"
+msgstr "Nytt passord"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.auth-info"
+msgstr "Du er innlogget som"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.old-password"
+msgstr "Gammelt passord"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.owner"
+msgstr "Eier"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.password"
+msgstr "Passord"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.permissions"
+msgstr "Tilganger"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.profile"
+msgstr "Profil"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.projects"
+msgstr "Prosjekter"
+
+#, fuzzy
+msgid "labels.recent"
+msgstr "Nylige"
+
+#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.remove"
+msgstr "Fjern"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.rename"
+msgstr "Gi nytt navn"
+
+#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
+msgid "labels.retry"
+msgstr "Prøv igjen"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.role"
+msgstr "Rolle"
+
+msgid "labels.save"
+msgstr "Lagre"
+
+#, fuzzy
+msgid "labels.search-font"
+msgstr "Søk etter skrift"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.send"
+msgstr "Send"
+
+#: src/app/main/ui/settings/feedback.cljs
+#, fuzzy
+msgid "labels.sending"
+msgstr "Sender …"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.settings"
+msgstr "Innstillinger"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.shared-libraries"
+msgstr "Delte bibliotek"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.show-all-comments"
+msgstr "Vis alle kommentarer"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.sign-out"
+msgstr "Logg ut"
+
+msgid "labels.upload"
+msgstr "Last opp"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Last opp egendefinerte skrifter"
+
+msgid "labels.uploading"
+msgstr "Laster opp …"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.write-new-comment"
+msgstr "Skriv ny kommentar"
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "media.loading"
+msgstr "Laster inn bilde …"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+#, fuzzy
+msgid "modals.add-shared-confirm.accept"
+msgstr "Legg til som delt bibliotek"
+
+#: src/app/main/ui/settings/change_email.cljs
+#, fuzzy
+msgid "modals.change-email.confirm-email"
+msgstr "Bekreft ny e-postadresse"
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "modals.delete-account.confirm"
+msgstr "Ja, slett kontoen min"
+
+#: src/app/main/ui/comments.cljs
+msgid "modals.delete-comment-thread.accept"
+msgstr "Slett samtale"
+
+#: src/app/main/ui/comments.cljs
+msgid "modals.delete-comment-thread.title"
+msgstr "Slett samtale"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-confirm.accept"
+msgstr "Slett fil"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-multi-confirm.accept"
+msgstr "Slett filer"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs
+msgid "modals.delete-page.title"
+msgstr "Slett side"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "modals.delete-project-confirm.accept"
+msgstr "Slett prosjekt"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.delete-team-member-confirm.accept"
+msgstr "Slett medlem"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.delete-team-member-confirm.title"
+msgstr "Slett lagmedlem"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.accept"
+msgstr "Forlat lag"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.cancel"
+msgstr "Avbryt"
+
+#: src/app/main/ui/dashboard/team.cljs
+#, fuzzy
+msgid "notifications.invitation-email-sent"
+msgstr "Invitasjon sendt"
+
+#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs
+#, fuzzy
+msgid "notifications.profile-saved"
+msgstr "Profil lagret"
+
+#: src/app/main/ui/settings/options.cljs
+#, fuzzy
+msgid "title.settings.options"
+msgstr "Innstillinger - Penpot"
+
+#: src/app/main/ui/settings/password.cljs
+#, fuzzy
+msgid "title.settings.password"
+msgstr "Passord - Penpot"
+
+#: src/app/main/ui/settings/profile.cljs
+#, fuzzy
+msgid "title.settings.profile"
+msgstr "Profil - Penpot"
+
+#: src/app/main/ui/dashboard/team.cljs
+#, fuzzy
+msgid "title.team-settings"
+msgstr "Innstillinger - %s - Penpot"
+
+#: src/app/main/ui/workspace.cljs
+#, fuzzy
+msgid "title.workspace"
+msgstr "%s - Penpot"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.edit-page"
+msgstr "Rediger side"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.create-link"
+msgstr "Opprett lenke"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.remove-link"
+msgstr "Fjern lenke"
+
+msgid "workspace.assets.box-filter-graphics"
+msgstr "Grafikk"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.colors"
+msgstr "Farger"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.components"
+msgstr "Komponenter"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.create-group"
+msgstr "Opprett en gruppe"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.delete"
+msgstr "Slett"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.edit"
+msgstr "Rediger"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.file-library"
+msgstr "Filbibliotek"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.graphics"
+msgstr "Grafikk"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.group"
+msgstr "Gruppe"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.group-name"
+msgstr "Gruppenavn"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.libraries"
+msgstr "Bibliotek"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.rename"
+msgstr "Gi nytt navn"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-id"
+msgstr "Skrift"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-size"
+msgstr "Størrelse"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-variant-id"
+msgstr "Variant"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-snap-grid"
+msgstr "Fest til rutenett"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-grid"
+msgstr "Vis rutenett"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-layers"
+msgstr "Vis lag"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-rules"
+msgstr "Vis regler"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.add"
+msgstr "Legg til"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.colors"
+msgstr "%s farger"
+
+#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.file-library"
+msgstr "Filbibliotek"
+
+#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.recent-colors"
+msgstr "Nylige farger"
+
+#: src/app/main/ui/workspace/colorpicker.cljs
+msgid "workspace.libraries.colors.save-color"
+msgstr "Lagre fargestil"
+
+#: src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.small-thumbnails"
+msgstr "Små miniatyrbilder"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.components"
+msgstr "%s komponenter"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.file-library"
+msgstr "Filbibliotek"
+
+msgid "workspace.options.blur-options.background-blur"
+msgstr "Bakgrunn"
+
+msgid "workspace.options.blur-options.layer-blur"
+msgstr "Lag"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs
+msgid "workspace.options.component"
+msgstr "Komponent"
+
+#: src/app/main/ui/workspace/sidebar/options.cljs
+msgid "workspace.options.design"
+msgstr "Design"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.exporting-object"
+msgstr "Eksporterer …"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.columns"
+msgstr "Kolonner"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.height"
+msgstr "Høyde"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.rows"
+msgstr "Rader"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.set-default"
+msgstr "Sett som forvalg"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.size"
+msgstr "Størrelse"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type"
+msgstr "Type"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.use-default"
+msgstr "Bruk forvalg"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.width"
+msgstr "Bredde"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.row"
+msgstr "Rader"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.lighten"
+msgstr "Lysne"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.screen"
+msgstr "Skjerm"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.radius.all-corners"
+msgstr "Alle hjørner"
\ No newline at end of file
diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po
new file mode 100644
index 000000000..a00125a3d
--- /dev/null
+++ b/frontend/translations/pt_BR.po
@@ -0,0 +1,928 @@
+msgid ""
+msgstr ""
+"PO-Revision-Date: 2021-05-17 21:32+0000\n"
+"Last-Translator: Eranot \n"
+"Language-Team: Portuguese (Brazil) "
+"\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 4.7-dev\n"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.already-have-account"
+msgstr "Já tem uma conta?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.check-your-email"
+msgstr ""
+"Verifique seu e-mail e clique no link de verificação para começar a usar o "
+"Penpot."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.confirm-password"
+msgstr "Confirmar senha"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-account"
+msgstr "Criar conta de demonstração"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-profile"
+msgstr "Só quer experimentar?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.demo-warning"
+msgstr ""
+"Este é um serviço DEMONSTRATIVO, NÃO USE para trabalho real, os projetos "
+"serão apagados periodicamente."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.email"
+msgstr "E-mail"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.forgot-password"
+msgstr "Esqueceu a senha?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.fullname"
+msgstr "Nome completo"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.go-back-to-login"
+msgstr "Voltar!"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.login-here"
+msgstr "Entrar aqui"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-submit"
+msgstr "Entrar"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-subtitle"
+msgstr "Insira seus dados abaixo"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-title"
+msgstr "Bom te ver de novo!"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-github-submit"
+msgstr "Entrar com o Github"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-gitlab-submit"
+msgstr "Entrar com o Gitlab"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-google-submit"
+msgstr "Entrar com o Google"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-ldap-submit"
+msgstr "Entrar com LDAP"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-oidc-submit"
+msgstr "Entrar com OpenID (SSO)"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.new-password"
+msgstr "Digite uma nova senha"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.invalid-token-error"
+msgstr "O token de recuperação é inválido."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.password-changed-succesfully"
+msgstr "Senha alterada com sucesso"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.profile-not-verified"
+msgstr "Perfil não verificado. Por favor, verifique o perfil antes de continuar."
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.recovery-token-sent"
+msgstr "Link de recuperação de senha foi enviado para sua caixa de entrada."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "auth.notifications.team-invitation-accepted"
+msgstr "Entrou para a equipe com sucesso"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.password"
+msgstr "Senha"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.password-length-hint"
+msgstr "Pelo menos 8 caracteres"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-submit"
+msgstr "Recuperar senha"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-subtitle"
+msgstr "Enviaremos para você um e-mail com instruções"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-title"
+msgstr "Esqueceu a senha?"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.recovery-submit"
+msgstr "Mudar sua senha"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.register"
+msgstr "Ainda não tem uma conta?"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.register-submit"
+msgstr "Criar uma conta"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-subtitle"
+msgstr "É de graça, é código aberto"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-title"
+msgstr "Criar uma conta"
+
+#: src/app/main/ui/auth.cljs
+msgid "auth.sidebar-tagline"
+msgstr "A solução de código aberto para design e prototipagem."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.terms-privacy-agreement"
+msgstr ""
+"Ao criar uma nova conta, você concorda com nossos termos de serviço e "
+"política de privacidade."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.verification-email-sent"
+msgstr "Enviamos um e-mail de verificação para"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.add-shared"
+msgstr "Adicionar como Biblioteca Compartilhada"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.change-email"
+msgstr "Alterar e-mail"
+
+#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
+msgid "dashboard.copy-suffix"
+msgstr "(copiar)"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.create-new-team"
+msgstr "+ Criar nova equipe"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.default-team-name"
+msgstr "Sua Penpot"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.delete-team"
+msgstr "Deletar equipe"
+
+msgid "dashboard.draft-title"
+msgstr "Rascunho"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate"
+msgstr "Duplicar"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate-multi"
+msgstr "Duplicar %s arquivos"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.empty-files"
+msgstr "Você ainda não tem arquivos aqui"
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"Qualquer fonte da web que você carregar aqui será adicionada à lista de "
+"família de fontes disponível nas propriedades de texto dos arquivos desta "
+"equipe. As fontes com o mesmo nome de família de fontes serão agrupadas "
+"como uma **única família de fontes**. Você pode fazer upload de fontes com "
+"os seguintes formatos: **TTF, OTF e WOFF** (apenas uma será necessária)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"Você deve carregar apenas fontes que possui ou tem licença para usar na "
+"Penpot. Descubra mais na seção de Direitos de conteúdo nos [Termos de "
+"Serviço da Penpot](https://penpot.app/terms.html). Você pode também querer "
+"ler sobre [licenciamento de fontes](2)."
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.invite-profile"
+msgstr "Convidar para a equipe"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.leave-team"
+msgstr "Sair da equipe"
+
+#: src/app/main/ui/dashboard/libraries.cljs
+msgid "dashboard.libraries-title"
+msgstr "Bibliotecas Compartilhadas"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.loading-files"
+msgstr "carregando seus arquivos…"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to"
+msgstr "Mover para"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-multi"
+msgstr "Mover %s arquivos para"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-other-team"
+msgstr "Mover para outra equipe"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs
+msgid "dashboard.new-file"
+msgstr "+ Novo arquivo"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "Novo arquivo"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.new-project"
+msgstr "+ Novo projeto"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "Novo projeto"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.no-matches-for"
+msgstr "Nenhuma correspondência encontrada para “%s“"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.no-projects-placeholder"
+msgstr "Projetos fixados aparecerão aqui"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-changed-successfully"
+msgstr "Seu endereço de e-mail foi atualizado com sucesso"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-verified-successfully"
+msgstr "Seu endereço de e-mail foi verificado com sucesso"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.notifications.password-saved"
+msgstr "Senha salva com sucesso!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.num-of-members"
+msgstr "%s membros"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.open-in-new-tab"
+msgstr "Abrir arquivo em uma nova guia"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.password-change"
+msgstr "Alterar senha"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.pin-unpin"
+msgstr "Fixar/Desafixar"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.projects-title"
+msgstr "Projetos"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.promote-to-owner"
+msgstr "Promover a proprietário"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.remove-account"
+msgstr "Quer remover sua conta?"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.remove-shared"
+msgstr "Remover como Biblioteca Compartilhada"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.search-placeholder"
+msgstr "Pesquisar…"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.searching-for"
+msgstr "Pesquisando por “%s“…"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-language"
+msgstr "Selecionar idioma da UI"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-theme"
+msgstr "Selecionar tema"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.show-all-files"
+msgstr "Mostrar todos os arquivos"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-delete-file"
+msgstr "Seu arquivo foi excluído com sucesso"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-delete-project"
+msgstr "Seu projeto foi excluído com sucesso"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-duplicate-file"
+msgstr "Seu arquivo foi duplicado com sucesso"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-duplicate-project"
+msgstr "Seu projeto foi duplicado com sucesso"
+
+#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-file"
+msgstr "Seu arquivo foi movido com sucesso"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-files"
+msgstr "Seus arquivos foram movidos com sucesso"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-move-project"
+msgstr "Seu projeto foi movido com sucesso"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.switch-team"
+msgstr "Trocar de equipe"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-info"
+msgstr "Informação da equipe"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-members"
+msgstr "Membros da equipe"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-projects"
+msgstr "Projetos da equipe"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.theme-change"
+msgstr "Tema da UI"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.title-search"
+msgstr "Resultados da pesquisa"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.type-something"
+msgstr "Digite para pesquisar resultados"
+
+#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
+msgid "dashboard.update-settings"
+msgstr "Atualizar configurações"
+
+#: src/app/main/ui/settings.cljs
+msgid "dashboard.your-account-title"
+msgstr "Sua conta"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-email"
+msgstr "E-mail"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-name"
+msgstr "Seu nome"
+
+#: 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 "Sua Penpot"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-cancel"
+msgstr "Cancelar"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-ok"
+msgstr "Ok"
+
+#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs
+msgid "ds.confirm-title"
+msgstr "Tem certeza?"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "ds.updated-at"
+msgstr "Atualizado: %s"
+
+#: src/app/main/data/workspace.cljs
+msgid "errors.clipboard-not-implemented"
+msgstr "Seu navegador não pode fazer esta operação"
+
+#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs
+msgid "errors.email-already-exists"
+msgstr "E-mail já utilizado"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.email-already-validated"
+msgstr "E-mail já validado."
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "errors.email-invalid-confirmation"
+msgstr "E-mail de confirmação deve ser o mesmo"
+
+#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "errors.generic"
+msgstr "Algo errado aconteceu."
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.google-auth-not-enabled"
+msgstr "Autenticação com google desativada no backend"
+
+#: src/app/main/ui/components/color_input.cljs
+msgid "errors.invalid-color"
+msgstr "Cor inválida"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.ldap-disabled"
+msgstr "Autenticação por LDAP está desativada."
+
+msgid "errors.media-format-unsupported"
+msgstr "O formato da imagem não é compatível (deve ser svg, jpg ou png)."
+
+#: src/app/main/data/workspace/persistence.cljs
+msgid "errors.media-too-large"
+msgstr "A imagem é muito grande para ser inserida (deve ter menos de 5mb)."
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "errors.media-type-mismatch"
+msgstr "Parece que o conteúdo da imagem não corresponde à extensão do arquivo."
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "errors.media-type-not-allowed"
+msgstr "Parece que esta não é uma imagem válida."
+
+msgid "errors.network"
+msgstr "Não foi possível conectar ao servidor backend."
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.password-invalid-confirmation"
+msgstr "A senha de confirmação deve ser a mesma"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.password-too-short"
+msgstr "A senha deve ter pelo menos 8 caracteres"
+
+msgid "errors.terms-privacy-agreement-invalid"
+msgstr "Você deve aceitar nossos termos de serviço e política de privacidade."
+
+#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "errors.unexpected-error"
+msgstr "Um erro inesperado ocorreu."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.unexpected-token"
+msgstr "Token desconhecido"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.wrong-credentials"
+msgstr "O nome de usuário ou a senha parecem estar errados."
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.wrong-old-password"
+msgstr "A senha antiga está incorreta"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-start"
+msgstr "Junte-se ao chat"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-subtitle"
+msgstr "Com vontade de falar? Converse conosco no Gitter"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.description"
+msgstr "Descrição"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-go-to"
+msgstr "Ir para discussões"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.line-height"
+msgstr "Altura da linha"
+
+msgid "handoff.attributes.typography.text-decoration.none"
+msgstr "Nenhum"
+
+msgid "handoff.attributes.typography.text-decoration.strikethrough"
+msgstr "Riscado"
+
+msgid "handoff.attributes.typography.text-decoration.underline"
+msgstr "Sublinhado"
+
+msgid "handoff.attributes.typography.text-transform.lowercase"
+msgstr "Minúsculo"
+
+msgid "handoff.attributes.typography.text-transform.none"
+msgstr "Nenhum"
+
+msgid "handoff.attributes.typography.text-transform.uppercase"
+msgstr "Maiúsculo"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code"
+msgstr "Código"
+
+msgid "handoff.tabs.code.selected.circle"
+msgstr "Círculo"
+
+msgid "handoff.tabs.code.selected.curve"
+msgstr "Curva"
+
+msgid "handoff.tabs.code.selected.frame"
+msgstr "Prancheta"
+
+msgid "handoff.tabs.code.selected.group"
+msgstr "Grupo"
+
+msgid "handoff.tabs.code.selected.image"
+msgstr "Imagem"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code.selected.multiple"
+msgstr "%s selecionados"
+
+msgid "handoff.tabs.code.selected.rect"
+msgstr "Retângulo"
+
+msgid "handoff.tabs.code.selected.svg-raw"
+msgstr "SVG"
+
+msgid "handoff.tabs.code.selected.text"
+msgstr "Texto"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.info"
+msgstr "Informação"
+
+msgid "history.alert-message"
+msgstr "Você está vendo a versão %s"
+
+msgid "labels.accept"
+msgstr "Aceitar"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.admin"
+msgstr "Administrador"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.bad-gateway.desc-message"
+msgstr ""
+"Parece que você precisa esperar um pouco e tentar novamente; estamos "
+"realizando pequenas manutenções em nossos servidores."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.bad-gateway.main-message"
+msgstr "Bad Gateway"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.cancel"
+msgstr "Cancelar"
+
+msgid "labels.centered"
+msgstr "Centro"
+
+#: src/app/main/ui/dashboard/comments.cljs
+msgid "labels.comments"
+msgstr "Comentários"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.confirm-password"
+msgstr "Confirmar senha"
+
+msgid "labels.content"
+msgstr "Conteúdo"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "labels.create"
+msgstr "Criar"
+
+#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team"
+msgstr "Criar nova equipe"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team.placeholder"
+msgstr "Insira o nome da nova equipe"
+
+msgid "labels.custom-fonts"
+msgstr "Fontes personalizadas"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.dashboard"
+msgstr "Painel"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete"
+msgstr "Excluir"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment"
+msgstr "Excluir comentário"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment-thread"
+msgstr "Excluir tópico"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete-multi-files"
+msgstr "Excluir %s arquivos"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.drafts"
+msgstr "Rascunhos"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.edit"
+msgstr "Editar"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.editor"
+msgstr "Editor"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.email"
+msgstr "E-mail"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-disabled"
+msgstr "Feedback desativado"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-sent"
+msgstr "Feedback enviado"
+
+msgid "labels.font-family"
+msgstr "Família da fonte"
+
+msgid "labels.font-providers"
+msgstr "Provedores de fonte"
+
+msgid "labels.font-variant"
+msgstr "Estilo"
+
+msgid "labels.fonts"
+msgstr "Fontes"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.give-feedback"
+msgstr "Enviar feedback"
+
+msgid "labels.icons"
+msgstr "Ícones"
+
+msgid "labels.images"
+msgstr "Imagens"
+
+msgid "labels.installed-fonts"
+msgstr "Fontes instaladas"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.internal-error.desc-message"
+msgstr ""
+"Algo errado aconteceu. Repita a operação e se o problema persistir, entre "
+"em contato com o suporte."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.internal-error.main-message"
+msgstr "Erro interno"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "labels.language"
+msgstr "Linguagem"
+
+#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.logout"
+msgstr "Sair"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.members"
+msgstr "Membros"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.name"
+msgstr "Nome"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.new-password"
+msgstr "Nova senha"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs
+msgid "labels.no-comments-available"
+msgstr "Você não tem notificações de comentários pendentes"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.auth-info"
+msgstr "Você está conectado como"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.desc-message"
+msgstr "Esta página não existe ou você não tem permissão para acessá-la."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.main-message"
+msgstr "Oops!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-files"
+msgid_plural "labels.num-of-files"
+msgstr[0] "1 arquivo"
+msgstr[1] "%s arquivos"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-projects"
+msgid_plural "labels.num-of-projects"
+msgstr[0] "1 projeto"
+msgstr[1] "% projetos"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.old-password"
+msgstr "Senha antiga"
+
+#: src/app/main/ui/workspace/comments.cljs
+msgid "labels.only-yours"
+msgstr "Apenas seu"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.owner"
+msgstr "Proprietário"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.password"
+msgstr "Senha"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.permissions"
+msgstr "Permissões"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.profile"
+msgstr "Perfil"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.projects"
+msgstr "Projetos"
+
+msgid "labels.recent"
+msgstr "Recente"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.release-notes"
+msgstr "Notas de lançamento"
+
+#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.remove"
+msgstr "Excluir"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.rename"
+msgstr "Renomear"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.rename-team"
+msgstr "Renomear equipe"
+
+#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
+msgid "labels.retry"
+msgstr "Tentar novamente"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.role"
+msgstr "Cargo"
+
+msgid "labels.save"
+msgstr "Salvar"
+
+msgid "labels.search-font"
+msgstr "Buscar fonte"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.send"
+msgstr "Enviar"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.sending"
+msgstr "Enviando..."
+
+msgid "labels.upload"
+msgstr "Carregar"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Carregar fontes personalizadas"
+
+msgid "labels.uploading"
+msgstr "Carregando..."
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Tem certeza que deseja excluir essa fonte? Ela não será carregada se for "
+"utilizada em um arquivo."
+
+msgid "modals.delete-font.title"
+msgstr "Excluindo fonte"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.invite-member-confirm.accept"
+msgstr "Enviar convite"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.invite-member.title"
+msgstr "Convidar para se juntar à equipe"
+
+msgid "modals.leave-and-reassign.forbiden"
+msgstr ""
+"Você não pode deixar a equipe se não houver outro membro para promover a "
+"proprietário. Você pode excluir a equipe."
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.hint2"
+msgstr "Selecione outro membro para promover antes de sair"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.promote-and-leave"
+msgstr "Promover e sair"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.select-memeber-to-promote"
+msgstr "Selecione um membro para promover"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.title"
+msgstr "Selecione um membro para promover"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.accept"
+msgstr "Sair da equipe"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.message"
+msgstr "Tem certeza de que deseja sair deste equipe?"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.title"
+msgstr "Saindo da equipe"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.accept"
+msgstr "Promover"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.message"
+msgstr "Tem certeza de que deseja promover este usuário a proprietário?"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.title"
+msgstr "Promover a proprietário"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.remove-shared-confirm.accept"
+msgstr "Remover como Biblioteca Compartilhada"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.remove-shared-confirm.message"
+msgstr "Remover “%s” como Biblioteca Compartilhada"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.accept"
+msgstr "Atualizar componente"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.cancel"
+msgstr "Cancelar"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Provedores de fonte - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fontes - %s - Penpot"
\ No newline at end of file
diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po
new file mode 100644
index 000000000..6823ed273
--- /dev/null
+++ b/frontend/translations/ro.po
@@ -0,0 +1,2610 @@
+msgid ""
+msgstr ""
+"PO-Revision-Date: 2021-05-17 21:32+0000\n"
+"Last-Translator: George Lemon \n"
+"Language-Team: Romanian "
+"\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
+"20)) ? 1 : 2;\n"
+"X-Generator: Weblate 4.7-dev\n"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.already-have-account"
+msgstr "Ai deja un cont?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.check-your-email"
+msgstr ""
+"Verificați adresa de e-mail, faceți click pe link-ul de verificare și "
+"începeți să utilizați Penpot."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.confirm-password"
+msgstr "Confirmați parola"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-account"
+msgstr "Creează un cont demo"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-profile"
+msgstr "Vrei doar să încerci?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.demo-warning"
+msgstr ""
+"Acesta este un DEMO, NU UTILIZAȚI pentru lucrări reale, întrucât proiectele "
+"vor fi șterse periodic."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.email"
+msgstr "Adresă E-mail"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.forgot-password"
+msgstr "Ai uitat parola?"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.fullname"
+msgstr "Numele complet"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.go-back-to-login"
+msgstr "Întoarce-te!"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.login-here"
+msgstr "Conectează-te"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-submit"
+msgstr "Intră în cont"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-subtitle"
+msgstr "Introduceți detaliile dvs. mai jos"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-title"
+msgstr "Mă bucur să te văd din nou!"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-github-submit"
+msgstr "Conectează-te cu Github"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-gitlab-submit"
+msgstr "Conectează-te cu Gitlab"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-google-submit"
+msgstr "Conectează-te cu Google"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-ldap-submit"
+msgstr "Conectează-te cu LDAP"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-oidc-submit"
+msgstr "Conectează-te cu OpenID (SSO)"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.new-password"
+msgstr "Introduceți o parolă nouă"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.invalid-token-error"
+msgstr "Codul de recuperare nu este valid."
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.notifications.password-changed-succesfully"
+msgstr "Parola a fost schimbată cu success"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.profile-not-verified"
+msgstr ""
+"Profilul nu este verificat, vă rugăm să verificați profilul înainte de a "
+"continua."
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.notifications.recovery-token-sent"
+msgstr "Un link de recuperare a parolei s-a trimis pe e-mail."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "auth.notifications.team-invitation-accepted"
+msgstr "Te-ai alăturat echipei cu success"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.password"
+msgstr "Parola"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.password-length-hint"
+msgstr "Cel puțin 8 caractere"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-submit"
+msgstr "Recuperare Parolă"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-subtitle"
+msgstr "Îți vom trimite un email cu instrucțiunile"
+
+#: src/app/main/ui/auth/recovery_request.cljs
+msgid "auth.recovery-request-title"
+msgstr "Ai uitat parola?"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "auth.recovery-submit"
+msgstr "Schimbă parola"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.register"
+msgstr "Nu aveți încă un cont?"
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.register-submit"
+msgstr "Creează un cont"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-subtitle"
+msgstr "Este gratuit, este Open Source"
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.register-title"
+msgstr "Creează un cont"
+
+#: src/app/main/ui/auth.cljs
+msgid "auth.sidebar-tagline"
+msgstr "Soluția open-source pentru proiectare design și prototipare."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.terms-privacy-agreement"
+msgstr ""
+"Atunci când creați un cont nou, sunteți de acord cu termenii noștri de "
+"servicii și politica de confidențialitate."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "auth.verification-email-sent"
+msgstr "Am trimis un email de verificare la"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.add-shared"
+msgstr "Adăugați ca bibliotecă partajată"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.change-email"
+msgstr "Schimbă adresa de e-mail"
+
+#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
+msgid "dashboard.copy-suffix"
+msgstr "(copiază)"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.create-new-team"
+msgstr "+ Creează o nouă echipă"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.default-team-name"
+msgstr "Contul Penpot"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.delete-team"
+msgstr "Șterge echipa"
+
+msgid "dashboard.draft-title"
+msgstr "Draft"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate"
+msgstr "Duplicat"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.duplicate-multi"
+msgstr "Duplicați %s fișiere"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.empty-files"
+msgstr "Încă nu aveți fișiere aici"
+
+#, markdown
+msgid "dashboard.fonts.hero-text1"
+msgstr ""
+"Fonturile încărcate vor fi adăugate la familia de fonturi disponibilă "
+"acestei echipe. Font-urile cu același nume vor fi grupate ca **o singură "
+"familie de font-uri**. Tipurile de fişiere acceptate: **TTF, OTF și WOFF** "
+"(se poate urca doar un singur tip)."
+
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"Ar trebui să urcați doar fonturi la care aveți drept de folosință sau "
+"fonturi personale. Află mai multe despre Dreptul de conținut la secțiunea "
+"[Termenii și Condițiile Penpot](https://penpot.app/terms.html). De "
+"asemenea, vă recomandăm să citiți și despre [licențierea fonturilor](2)."
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.invite-profile"
+msgstr "Invită o echipă"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.leave-team"
+msgstr "Părăsește echipa"
+
+#: src/app/main/ui/dashboard/libraries.cljs
+msgid "dashboard.libraries-title"
+msgstr "Librării Partajate"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.loading-files"
+msgstr "încărcarea fișierelor …"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to"
+msgstr "Mută la"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-multi"
+msgstr "Mutați %s fișiere la"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.move-to-other-team"
+msgstr "Mutați la altă echipă"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/files.cljs
+msgid "dashboard.new-file"
+msgstr "+ Fișier nou"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "Fișer nou"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.new-project"
+msgstr "+ Proiect nou"
+
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "Proiect nou"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.no-matches-for"
+msgstr "Nu există rezultate pentru “%s“"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.no-projects-placeholder"
+msgstr "Proiectele fixate vor apărea aici"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-changed-successfully"
+msgstr "Adresa ta de email a fost actualizată cu success"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "dashboard.notifications.email-verified-successfully"
+msgstr "Adresa ta de email este confirmată"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.notifications.password-saved"
+msgstr "Parolă actualizată cu success!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.num-of-members"
+msgstr "%s membrii"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.open-in-new-tab"
+msgstr "Deschide fișier într-o pagină nouă"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "dashboard.password-change"
+msgstr "Schimbă parola"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.pin-unpin"
+msgstr "Pin/Unpin"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "dashboard.projects-title"
+msgstr "Proiecte"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.promote-to-owner"
+msgstr "Promovează la administrator"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.remove-account"
+msgstr "Doriți să vă ștergeți contul?"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.remove-shared"
+msgstr "Elimină ca şi Colecţie Distribuită"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.search-placeholder"
+msgstr "Căutare…"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.searching-for"
+msgstr "Căutare pentru “%s“…"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-language"
+msgstr "Selectați limbajul interfeței"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.select-ui-theme"
+msgstr "Selectați o temă"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "dashboard.show-all-files"
+msgstr "Afișați toate fișierele"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-delete-file"
+msgstr "Fișierele s-au șters cu succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-delete-project"
+msgstr "Proiectul s-a șters cu succes"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-duplicate-file"
+msgstr "Fișierele s-au duplicat cu succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-duplicate-project"
+msgstr "Proiectul s-a duplicat cu succes"
+
+#: src/app/main/ui/dashboard/grid.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-file"
+msgstr "Fișierul a fost mutat cu succes"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "dashboard.success-move-files"
+msgstr "Fișerele au fost mutate cu succes"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "dashboard.success-move-project"
+msgstr "Proiectul a fost mutat cu succes"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "dashboard.switch-team"
+msgstr "Schimbă echipa"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-info"
+msgstr "Informațiile echipei"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-members"
+msgstr "Membrii echipei"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "dashboard.team-projects"
+msgstr "Proiectele echipei"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "dashboard.theme-change"
+msgstr "Interfață temă"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.title-search"
+msgstr "Rezultatele căutării"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "dashboard.type-something"
+msgstr "Scrie pentru a începe căutarea"
+
+#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
+msgid "dashboard.update-settings"
+msgstr "Actualizare setări"
+
+#: src/app/main/ui/settings.cljs
+msgid "dashboard.your-account-title"
+msgstr "Contul tău"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-email"
+msgstr "Email"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "dashboard.your-name"
+msgstr "Numele tău"
+
+#: 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 "Contul Penpot"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-cancel"
+msgstr "Anulează"
+
+#: src/app/main/ui/confirm.cljs
+msgid "ds.confirm-ok"
+msgstr "Ok"
+
+#: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs
+msgid "ds.confirm-title"
+msgstr "Ești sigur?"
+
+#: src/app/main/ui/dashboard/grid.cljs
+msgid "ds.updated-at"
+msgstr "Actualizat: %s"
+
+#: src/app/main/data/workspace.cljs
+msgid "errors.clipboard-not-implemented"
+msgstr "Bowser-ul tău nu permite clipboard"
+
+#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/change_email.cljs
+msgid "errors.email-already-exists"
+msgstr "Email deja trimis"
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.email-already-validated"
+msgstr "Adresa de email este deja validată."
+
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "errors.email-has-permanent-bounces"
+msgstr "Adresa de email «%s» are multe rapoarte permanente de respingere."
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "errors.email-invalid-confirmation"
+msgstr "E-mailul de confirmare trebuie să se potrivească"
+
+#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "errors.generic"
+msgstr "S-a întâmplat ceva în neregulă."
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.google-auth-not-enabled"
+msgstr "Autentificarea cu Google nu este permisă"
+
+#: src/app/main/ui/components/color_input.cljs
+msgid "errors.invalid-color"
+msgstr "Culoare invalidă"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.ldap-disabled"
+msgstr "Autentificarea cu LDAP este dezactivată."
+
+msgid "errors.media-format-unsupported"
+msgstr "Formatul imaginii nu este acceptat (poate fi svg, jpg sau png)."
+
+#: src/app/main/data/workspace/persistence.cljs
+msgid "errors.media-too-large"
+msgstr "Imaginea este prea mare pentru a fi inserată (trebuie să fie sub 5mb)."
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "errors.media-type-mismatch"
+msgstr "Se pare că conținutul imaginii nu se potrivește cu extensia de fișier."
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "errors.media-type-not-allowed"
+msgstr "Se pare că aceasta nu este o imagine validă."
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "errors.member-is-muted"
+msgstr ""
+"Profilul pe care încercați să îl invitați este dezactivat (din cauza spam "
+"sau inactivitate)."
+
+msgid "errors.network"
+msgstr "Nu s-a reușit conectarea la server."
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.password-invalid-confirmation"
+msgstr "Parola de confirmare trebuie să se potrivească"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.password-too-short"
+msgstr "Parola trebuie să conțină cel puțin 8 caractere"
+
+#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "errors.profile-is-muted"
+msgstr ""
+"Profilul tău conține adrese de email dezactivate (rapoarte spam sau "
+"inactive)."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "errors.registration-disabled"
+msgstr "Înregistrarea este dezactivată în prezent."
+
+msgid "errors.terms-privacy-agreement-invalid"
+msgstr "Trebuie să acceptați termenii serviciului și politica de confidențialitate."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.token-expired"
+msgstr "Codul este expirat"
+
+#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "errors.unexpected-error"
+msgstr "A apărut o eroare neașteptată."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.unexpected-token"
+msgstr "Cod necunoscut"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.wrong-credentials"
+msgstr "Numele de utilizator sau parola par a fi greșite."
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.wrong-old-password"
+msgstr "Parola veche este incorectă"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-start"
+msgstr "Alătură-te chatului"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-subtitle"
+msgstr "Te simți sociabil? Hai să vorbim pe Gitter"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.description"
+msgstr "Descriere"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-go-to"
+msgstr "Du-te la discuții"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-subtitle1"
+msgstr "Alătură-te forumului de comunicare al echipei Penpot."
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-subtitle2"
+msgstr ""
+"Poți pune întrebări, iei parte la discuții deschise și poți contribui la "
+"dezvoltarea proiectului."
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-title"
+msgstr "Discuțiile echipei"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.subject"
+msgstr "Subiect"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.subtitle"
+msgstr ""
+"Descrie motivul pentru care ne scrii, specificând eventuale probleme, idei "
+"sau nelămuriri. Un membru al echipei noastre îți va răspunde în scurt timp."
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.title"
+msgstr "Adresă de Email"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "generic.error"
+msgstr "Am întâmpinat o eroare"
+
+#: src/app/main/ui/handoff/attributes/blur.cljs
+msgid "handoff.attributes.blur"
+msgstr "Blur"
+
+#: src/app/main/ui/handoff/attributes/blur.cljs
+msgid "handoff.attributes.blur.value"
+msgstr "Valoare"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.hex"
+msgstr "HEX"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.hsla"
+msgstr "HSLA"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.rgba"
+msgstr "RGBA"
+
+#: src/app/main/ui/handoff/attributes/fill.cljs
+msgid "handoff.attributes.fill"
+msgstr "Fill"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.download"
+msgstr "Descarcă imaginea sursă"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.height"
+msgstr "Înălțime"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.width"
+msgstr "Lățime"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout"
+msgstr "Layout"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.height"
+msgstr "Înălțime"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.left"
+msgstr "Stânga"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs, src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.radius"
+msgstr "Rază"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.rotation"
+msgstr "Rotație"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.top"
+msgstr "Top"
+
+#: src/app/main/ui/handoff/attributes/layout.cljs
+msgid "handoff.attributes.layout.width"
+msgstr "Lățime"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow"
+msgstr "Umbră"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow.shorthand.blur"
+msgstr "B"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow.shorthand.offset-x"
+msgstr "X"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow.shorthand.offset-y"
+msgstr "Y"
+
+#: src/app/main/ui/handoff/attributes/shadow.cljs
+msgid "handoff.attributes.shadow.shorthand.spread"
+msgstr "S"
+
+#: src/app/main/ui/handoff/attributes/stroke.cljs
+msgid "handoff.attributes.stroke"
+msgstr "Linie"
+
+#, permanent
+msgid "handoff.attributes.stroke.alignment.center"
+msgstr "Centru"
+
+#, permanent
+msgid "handoff.attributes.stroke.alignment.inner"
+msgstr "Interior"
+
+#, permanent
+msgid "handoff.attributes.stroke.alignment.outer"
+msgstr "Exterior"
+
+msgid "handoff.attributes.stroke.style.dotted"
+msgstr "Punctat"
+
+msgid "handoff.attributes.stroke.style.mixed"
+msgstr "Mixat"
+
+msgid "handoff.attributes.stroke.style.none"
+msgstr "Niciunul"
+
+msgid "handoff.attributes.stroke.style.solid"
+msgstr "Solid"
+
+#: src/app/main/ui/handoff/attributes/stroke.cljs
+msgid "handoff.attributes.stroke.width"
+msgstr "Lățime"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography"
+msgstr "Tipografie"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-family"
+msgstr "Familie de Fonturi"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-size"
+msgstr "Dimensiune Font"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.font-style"
+msgstr "Stil Font"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.letter-spacing"
+msgstr "Spațiere"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.line-height"
+msgstr "Înălțimea rândului"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.text-decoration"
+msgstr "Decorare Text"
+
+msgid "handoff.attributes.typography.text-decoration.none"
+msgstr "Niciunul"
+
+msgid "handoff.attributes.typography.text-decoration.strikethrough"
+msgstr "Strikethrough"
+
+msgid "handoff.attributes.typography.text-decoration.underline"
+msgstr "Subliniat"
+
+#: src/app/main/ui/handoff/attributes/text.cljs
+msgid "handoff.attributes.typography.text-transform"
+msgstr "Transformare Text"
+
+msgid "handoff.attributes.typography.text-transform.lowercase"
+msgstr "Minuscule"
+
+msgid "handoff.attributes.typography.text-transform.none"
+msgstr "Niciunul"
+
+msgid "handoff.attributes.typography.text-transform.titlecase"
+msgstr "Title Case"
+
+msgid "handoff.attributes.typography.text-transform.uppercase"
+msgstr "Majuscule"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code"
+msgstr "Cod"
+
+msgid "handoff.tabs.code.selected.circle"
+msgstr "Cerc"
+
+msgid "handoff.tabs.code.selected.curve"
+msgstr "Curbat"
+
+msgid "handoff.tabs.code.selected.frame"
+msgstr "Planșă de lucru"
+
+msgid "handoff.tabs.code.selected.group"
+msgstr "Grup"
+
+msgid "handoff.tabs.code.selected.image"
+msgstr "Imagine"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code.selected.multiple"
+msgstr "%s Selectate"
+
+msgid "handoff.tabs.code.selected.path"
+msgstr "Traiectorie"
+
+msgid "handoff.tabs.code.selected.rect"
+msgstr "Dreptunghi"
+
+msgid "handoff.tabs.code.selected.svg-raw"
+msgstr "SVG"
+
+msgid "handoff.tabs.code.selected.text"
+msgstr "Text"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.info"
+msgstr "Info"
+
+msgid "history.alert-message"
+msgstr "Utilizezi versiunea %s"
+
+msgid "labels.accept"
+msgstr "Accept"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.admin"
+msgstr "Administrator"
+
+#: src/app/main/ui/workspace/comments.cljs
+msgid "labels.all"
+msgstr "Toate"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.bad-gateway.desc-message"
+msgstr "Momentan serverele noastre sunt în mentenanță. Revino în scurt timp."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.bad-gateway.main-message"
+msgstr "Eroare de Server"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.cancel"
+msgstr "Anulează"
+
+msgid "labels.centered"
+msgstr "Centru"
+
+#: src/app/main/ui/dashboard/comments.cljs
+msgid "labels.comments"
+msgstr "Comentarii"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.confirm-password"
+msgstr "Confirmă parola"
+
+msgid "labels.content"
+msgstr "Conținut"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "labels.create"
+msgstr "Creează"
+
+#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team"
+msgstr "Creează o echipă"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team.placeholder"
+msgstr "Introduceți noul nume al echipei"
+
+msgid "labels.custom-fonts"
+msgstr "Fonturi personalizate"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.dashboard"
+msgstr "Administrare"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete"
+msgstr "Șterge"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment"
+msgstr "Șterge comentariu"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment-thread"
+msgstr "Șterge discuție"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete-multi-files"
+msgstr "Șterge %s fișiere"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.drafts"
+msgstr "Drafturi"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.edit"
+msgstr "Editează"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.editor"
+msgstr "Editor"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.email"
+msgstr "Adresă de Email"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-disabled"
+msgstr "Feedback dezactivat"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-sent"
+msgstr "Feedback trimis"
+
+msgid "labels.font-family"
+msgstr "Familie de Fonturi"
+
+msgid "labels.font-providers"
+msgstr "Provideri de Fonturi"
+
+msgid "labels.font-variant"
+msgstr "Stil"
+
+msgid "labels.fonts"
+msgstr "Fonturi"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.give-feedback"
+msgstr "Lasă un feedback"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.hide-resolved-comments"
+msgstr "Ascunde comentariile rezolvate"
+
+msgid "labels.icons"
+msgstr "Iconițe"
+
+msgid "labels.images"
+msgstr "Imagini"
+
+msgid "labels.installed-fonts"
+msgstr "Fonturi instalate"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.internal-error.desc-message"
+msgstr ""
+"Am întâmpinat o eroare. Te rugăm, mai încearcă o dată. Dacă problema "
+"persistă poți contacta echipa de suport."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.internal-error.main-message"
+msgstr "Eroare internă"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "labels.language"
+msgstr "Limbă"
+
+#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.logout"
+msgstr "Deconectare"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.members"
+msgstr "Membri"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.name"
+msgstr "Nume"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.new-password"
+msgstr "Parolă nouă"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs
+msgid "labels.no-comments-available"
+msgstr "Nu există notificări de comentarii în aștepare"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.auth-info"
+msgstr "Ești conectat ca fiind"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.desc-message"
+msgstr "Această pagină nu există sau nu ai permisiunea să o accesezi."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.main-message"
+msgstr "Opa!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-files"
+msgid_plural "labels.num-of-files"
+msgstr[0] "1 fişier"
+msgstr[1] "%s fişiere"
+msgstr[2] "%s fişiere"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-projects"
+msgid_plural "labels.num-of-projects"
+msgstr[0] "1 proiecte"
+msgstr[1] "%s proiecte"
+msgstr[2] "%s proiecte"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.old-password"
+msgstr "Parola veche"
+
+#: src/app/main/ui/workspace/comments.cljs
+msgid "labels.only-yours"
+msgstr "Personale"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.owner"
+msgstr "Autor"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.password"
+msgstr "Parola"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.permissions"
+msgstr "Permisiuni"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.profile"
+msgstr "Profil"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.projects"
+msgstr "Proiecte"
+
+msgid "labels.recent"
+msgstr "Recente"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.release-notes"
+msgstr "Mențiuni"
+
+#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.remove"
+msgstr "Elimină"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.rename"
+msgstr "Redenumire"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.rename-team"
+msgstr "Modifică numele echipei"
+
+#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
+msgid "labels.retry"
+msgstr "Încearcă din nou"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.role"
+msgstr "Rol"
+
+msgid "labels.save"
+msgstr "Salvează"
+
+msgid "labels.search-font"
+msgstr "Caută font"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.send"
+msgstr "Trimitere"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.sending"
+msgstr "Se trimite..."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.service-unavailable.desc-message"
+msgstr "Momentan suntem în mentenanță."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.service-unavailable.main-message"
+msgstr "Serviciul nu este disponibil"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.settings"
+msgstr "Setări"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.shared-libraries"
+msgstr "Colecții distribuite"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.show-all-comments"
+msgstr "Afișează toate comentariile"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.show-your-comments"
+msgstr "Afișează doar comentariile mele"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.sign-out"
+msgstr "Deconectare"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "labels.update"
+msgstr "Actualizare"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.update-team"
+msgstr "Actualizare echipă"
+
+msgid "labels.upload"
+msgstr "Încărcare"
+
+msgid "labels.upload-custom-fonts"
+msgstr "Încarcă fonturi personalizate"
+
+msgid "labels.uploading"
+msgstr "Se încarcă..."
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.viewer"
+msgstr "Vizitator"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.write-new-comment"
+msgstr "Scrie un comentariu"
+
+#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
+msgid "media.loading"
+msgstr "Încarcă imaginea…"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.add-shared-confirm.accept"
+msgstr "Adaugă la Colecții distribuite"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.add-shared-confirm.hint"
+msgstr ""
+"O dată adăugat la Colecții distribuite, toate fișierele acestei colecții "
+"vor deveni disponibile altora."
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.add-shared-confirm.message"
+msgstr "Adaugă “%s” la Colecții Distribuite"
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "modals.change-email.confirm-email"
+msgstr "Verifică-ți adresa de e-mail"
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "modals.change-email.info"
+msgstr "Îți vom trimite un email pe adresa “%s” pentru identificare."
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "modals.change-email.new-email"
+msgstr "Mail nou"
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "modals.change-email.submit"
+msgstr "Schimbă adresa de e-mail"
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "modals.change-email.title"
+msgstr "Schimbă-ți adresa de E-mail"
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "modals.delete-account.cancel"
+msgstr "Anulează ștergerea contului"
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "modals.delete-account.confirm"
+msgstr "Confirm ștergerea contului"
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "modals.delete-account.info"
+msgstr "Prin ștergerea contului, se vor șterge toate proiectele și arhivele tale."
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "modals.delete-account.title"
+msgstr "Ești sigur că dorești ștergerea contului?"
+
+#: src/app/main/ui/comments.cljs
+msgid "modals.delete-comment-thread.accept"
+msgstr "Șterge conversație"
+
+#: src/app/main/ui/comments.cljs
+msgid "modals.delete-comment-thread.message"
+msgstr ""
+"Ești sigur că dorești să ștergi această conversație? Toate discuțiile din "
+"cadrul subiect vor fi șterse."
+
+#: src/app/main/ui/comments.cljs
+msgid "modals.delete-comment-thread.title"
+msgstr "Șterge conversație"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-confirm.accept"
+msgstr "Șterge fișier"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-confirm.message"
+msgstr "Ești sigur că dorești să ștergi acest fișier?"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-confirm.title"
+msgstr "Ștergere fișier"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-multi-confirm.accept"
+msgstr "Șterge fișiere"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-multi-confirm.message"
+msgstr "Ești sigur că dorești să ștergi aceste %s fișiere?"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.delete-file-multi-confirm.title"
+msgstr "Ștergere %s fișeiere în curs"
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Ești sigur că dorești să ștergi acest font? O dată șters acesta nu se va "
+"mai încărca în proiectele tale."
+
+msgid "modals.delete-font.title"
+msgstr "Ștergere font"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs
+msgid "modals.delete-page.body"
+msgstr "Ești sigur că dorești să ștergi această pagină?"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs
+msgid "modals.delete-page.title"
+msgstr "Șterge pagină"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "modals.delete-project-confirm.accept"
+msgstr "Șterge proiect"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "modals.delete-project-confirm.message"
+msgstr "Ești sigur că dorești să ștergi acest proiect?"
+
+#: src/app/main/ui/dashboard/project_menu.cljs
+msgid "modals.delete-project-confirm.title"
+msgstr "Șterge proiect"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.delete-team-confirm.accept"
+msgstr "Șterge echipă"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.delete-team-confirm.message"
+msgstr ""
+"Ești sigur că dorești să ștergi această echipă? Toate proiectele și "
+"fișierele asociate acesteia vor fi permanent șterse."
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.delete-team-confirm.title"
+msgstr "Ștergere echipă în curs"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.delete-team-member-confirm.accept"
+msgstr "Elimină membru"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.delete-team-member-confirm.message"
+msgstr "Ești sigur că dorești să elimini acest membru din echipă?"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.delete-team-member-confirm.title"
+msgstr "Elimină un membru al echipei"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.invite-member-confirm.accept"
+msgstr "Trimite invitație"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.invite-member.title"
+msgstr "Invită o persoană în echipă"
+
+msgid "modals.leave-and-reassign.forbiden"
+msgstr ""
+"Nu puteţi părăsi echipa dacă nu există un alt membru care să devină "
+"administrator. Aţi putea şterge echipa."
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.hint1"
+msgstr "Ești administratorul echipei %s."
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.hint2"
+msgstr "Selectează un membru pentru a-l promova, înainte de a părăsi echipa"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.promote-and-leave"
+msgstr "Promovează şi părăseşte echipa"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.select-memeber-to-promote"
+msgstr "Selectează un membru pentru promovare"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-and-reassign.title"
+msgstr "Selectează un membru pentru promovare"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.accept"
+msgstr "Părăsește echipa"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.message"
+msgstr "Ești sigur că dorești să părăsești această echipă?"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "modals.leave-confirm.title"
+msgstr "Părăsire echipă în curs"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.accept"
+msgstr "Promovează"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.message"
+msgstr "Ești sigur că dorești să promovezi acest utilizator ca deținător al echipei?"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "modals.promote-owner-confirm.title"
+msgstr "Confirmare promovare"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.remove-shared-confirm.accept"
+msgstr "Elimină din Colecțiile Distribuite"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.remove-shared-confirm.hint"
+msgstr ""
+"O dată șters din Colecțiile Distribuite, toate fișierele acestei colecții "
+"nu vor mai fi disponibile altora."
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "modals.remove-shared-confirm.message"
+msgstr "Șterge “%s” din Colecții Distribuite"
+
+#: 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 "Actualizare componentă"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.cancel"
+msgstr "Anulează"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.hint"
+msgstr ""
+"Actualizezi o componentă dintr-o colecţie distribuită. Pot fi afectate alte "
+"fişiere ce o folosesc."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "modals.update-remote-component.message"
+msgstr "Actualizaţi o componentă dintr-o colecţie distribuită"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "notifications.invitation-email-sent"
+msgstr "Invitaţie trimisă cu succes"
+
+#: src/app/main/ui/settings/delete_account.cljs
+msgid "notifications.profile-deletion-not-allowed"
+msgstr "Nu poţi şterge profilul. Reatribuie echipele înainte de a continua."
+
+#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/options.cljs
+msgid "notifications.profile-saved"
+msgstr "Profil salvat cu succes!"
+
+#: src/app/main/ui/settings/change_email.cljs
+msgid "notifications.validation-email-sent"
+msgstr "Un e-mail de verificare a fost trimis la %s. Verifică-ţi adresa de e-mail!"
+
+#: src/app/main/ui/auth/recovery.cljs
+msgid "profile.recovery.go-to-login"
+msgstr "Mergi la autentificare"
+
+#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs, src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs, src/app/main/ui/workspace/sidebar/options/menus/layer.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs, src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
+msgid "settings.multiple"
+msgstr "Multiple"
+
+#: src/app/main/ui/dashboard/files.cljs
+msgid "title.dashboard.files"
+msgstr "%s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.font-providers"
+msgstr "Furnizori de Fonturi - %s - Penpot"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fonturi - %s - Penpot"
+
+#: src/app/main/ui/dashboard/projects.cljs
+msgid "title.dashboard.projects"
+msgstr "Proiecte - %s - Penpot"
+
+#: src/app/main/ui/dashboard/search.cljs
+msgid "title.dashboard.search"
+msgstr "Caută - %s - Penpot"
+
+#: src/app/main/ui/dashboard/libraries.cljs
+msgid "title.dashboard.shared-libraries"
+msgstr "Colecţii Distribuite - %s - Penpot"
+
+#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/auth.cljs
+msgid "title.default"
+msgstr "Penpot - Libertate în Design pentru Echipe"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "title.settings.feedback"
+msgstr "Oferă feedback - Penpot"
+
+#: src/app/main/ui/settings/options.cljs
+msgid "title.settings.options"
+msgstr "Setări - Penpot"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "title.settings.password"
+msgstr "Parolă - Penpot"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "title.settings.profile"
+msgstr "Profil - Penpot"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "title.team-members"
+msgstr "Membri - %s - Penpot"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "title.team-settings"
+msgstr "Setări - %s - Penpot"
+
+#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs
+msgid "title.viewer"
+msgstr "%s - Vizualizare - Penpot"
+
+#: src/app/main/ui/workspace.cljs
+msgid "title.workspace"
+msgstr "%s - Penpot"
+
+#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs
+msgid "viewer.empty-state"
+msgstr "Nu există ferestre disponibile pe această pagină."
+
+#: src/app/main/ui/handoff.cljs, src/app/main/ui/viewer.cljs
+msgid "viewer.frame-not-found"
+msgstr "Fereastra nu există."
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.dont-show-interactions"
+msgstr "Nu afişa interacţiunile"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.edit-page"
+msgstr "Editează pagina"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.fullscreen"
+msgstr "Ecran complet"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.copy-link"
+msgstr "Copiază link"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.create-link"
+msgstr "Creează link"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.placeholder"
+msgstr "Link-ul distribuit va apărea aici"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.remove-link"
+msgstr "Elimină link"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.subtitle"
+msgstr "Prin acest link se permite accesul public"
+
+#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.share.title"
+msgstr "Distribuie link"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.show-interactions"
+msgstr "Afişează interacţiunile"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.show-interactions-on-click"
+msgstr "Afişează interacţiunile la click"
+
+#: src/app/main/ui/viewer/header.cljs
+msgid "viewer.header.sitemap"
+msgstr "Harta site-ului"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.hcenter"
+msgstr "Aliniază orizontal"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.hdistribute"
+msgstr "Introdu spaţierea orizontală"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.hleft"
+msgstr "Aliniază la stânga"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.hright"
+msgstr "Aliniază la dreapta"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.vbottom"
+msgstr "Aliniază jos"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.vcenter"
+msgstr "Aliniază vertical"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.vdistribute"
+msgstr "Introdu spaţierea pe verticală"
+
+#: src/app/main/ui/workspace/sidebar/align.cljs
+msgid "workspace.align.vtop"
+msgstr "Aliniază sus"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.assets"
+msgstr "Obiecte"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.box-filter-all"
+msgstr "Toate obiectele"
+
+msgid "workspace.assets.box-filter-graphics"
+msgstr "Obiecte grafice"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.colors"
+msgstr "Culori"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.components"
+msgstr "Componente"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.create-group"
+msgstr "Creează grup"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.create-group-hint"
+msgstr "Obiectele vor fi numite automat ca \"nume grup / nume obiect\""
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.delete"
+msgstr "Şterge"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.duplicate"
+msgstr "Duplică"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.edit"
+msgstr "Editează"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.file-library"
+msgstr "Colecţii"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.graphics"
+msgstr "Obiecte grafice"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.group"
+msgstr "Grup"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.group-name"
+msgstr "Nume grup"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.libraries"
+msgstr "Colecţii"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.not-found"
+msgstr "Nu au fost găsite obiecte"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.rename"
+msgstr "Redenumeşte"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.search"
+msgstr "Caută obiecte"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.selected-count"
+msgid_plural "workspace.assets.selected-count"
+msgstr[0] "%s obiect selectat"
+msgstr[1] "%s obiecte selectate"
+msgstr[2] "%s obiecte selectate"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.shared"
+msgstr "DISTRIBUITE"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs, src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "workspace.assets.typography"
+msgstr "Tipografii"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-id"
+msgstr "Font"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-size"
+msgstr "Dimensiune"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.font-variant-id"
+msgstr "Variante"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.go-to-edit"
+msgstr "Editează fişierul în Colecţia de stiluri"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.letter-spacing"
+msgstr "Spaţiere Litere"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.line-height"
+msgstr "Înălţime linie"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs, src/app/main/ui/handoff/attributes/text.cljs, src/app/main/ui/handoff/attributes/text.cljs
+msgid "workspace.assets.typography.sample"
+msgstr "Ag"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.assets.typography.text-transform"
+msgstr "Transformare Text"
+
+#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs
+msgid "workspace.gradients.linear"
+msgstr "Gradient liniar"
+
+#: src/app/main/data/workspace/libraries.cljs, src/app/main/ui/components/color_bullet.cljs
+msgid "workspace.gradients.radial"
+msgstr "Gradient radial"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-dynamic-alignment"
+msgstr "Dezactivează alinierea dinamică"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-scale-text"
+msgstr "Dezactivează dimensionarea textului"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.disable-snap-grid"
+msgstr "Dezactivați snap-ul la grilă"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-dynamic-alignment"
+msgstr "Aliniere dinamică"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-scale-text"
+msgstr "Activează scalarea textului"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.enable-snap-grid"
+msgstr "Aliniază per grilă"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-assets"
+msgstr "Ascunde obiectele"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-grid"
+msgstr "Ascunde grila de ghidaj"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-layers"
+msgstr "Ascunde layere"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-palette"
+msgstr "Ascunde paleta de culori"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.hide-rules"
+msgstr "Ascunde ghidul liniar"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.select-all"
+msgstr "Selectează tot"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-assets"
+msgstr "Afişează asset-uri"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-grid"
+msgstr "Afişează sistemul grid"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-layers"
+msgstr "Afişează layere"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-palette"
+msgstr "Afişează paleta de culori"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.menu.show-rules"
+msgstr "Afişează Liniarul"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.save-error"
+msgstr "Eroare în timpul salvării"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.saved"
+msgstr "Salvat"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.saving"
+msgstr "Salvare în curs"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.unsaved"
+msgstr "Modificări nesalvate"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.header.viewer"
+msgstr "Vizualizare (%s)"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.add"
+msgstr "Adaugă"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.colors"
+msgstr "%s culori"
+
+#: src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.big-thumbnails"
+msgstr "Thumbnail mare"
+
+#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.file-library"
+msgstr "Colecţie"
+
+#: src/app/main/ui/workspace/colorpicker/libraries.cljs, src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.recent-colors"
+msgstr "Culori recente"
+
+#: src/app/main/ui/workspace/colorpicker.cljs
+msgid "workspace.libraries.colors.save-color"
+msgstr "Salvează stilul culorii"
+
+#: src/app/main/ui/workspace/colorpalette.cljs
+msgid "workspace.libraries.colors.small-thumbnails"
+msgstr "Thumbnail mic"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.components"
+msgstr "%s componente"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.file-library"
+msgstr "Colecţie"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.graphics"
+msgstr "%s obiecte grafice"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.in-this-file"
+msgstr "COLECŢIILE FIŞIERULUI"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.libraries"
+msgstr "COLECŢII"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.library"
+msgstr "COLECŢIE"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.no-libraries-need-sync"
+msgstr "Nu există Colecţii Distribuite ce necesită update"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.no-matches-for"
+msgstr "Nu au fost găsite asemănări pentru “%s“"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.no-shared-libraries-available"
+msgstr "Nu există Colecţii Distribuite"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.search-shared-libraries"
+msgstr "Caută în colecţiile distribuite"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.shared-libraries"
+msgstr "COLECŢII DISTRIBUITE"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.libraries.text.multiple-typography"
+msgstr "Tipografii"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.libraries.text.multiple-typography-tooltip"
+msgstr "Decupleză tipograficele"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.typography"
+msgstr "%s tipografice"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.update"
+msgstr "Actualizaţi"
+
+#: src/app/main/ui/workspace/libraries.cljs
+msgid "workspace.libraries.updates"
+msgstr "ACTUALIZĂRI"
+
+msgid "workspace.library.all"
+msgstr "Toate colecţiile"
+
+msgid "workspace.library.libraries"
+msgstr "Colecţii"
+
+msgid "workspace.library.own"
+msgstr "Colecţiile mele"
+
+msgid "workspace.library.store"
+msgstr "Stocaţi colecţiile"
+
+msgid "workspace.options.blur-options.background-blur"
+msgstr "Fundal"
+
+msgid "workspace.options.blur-options.layer-blur"
+msgstr "Strat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
+msgid "workspace.options.blur-options.title"
+msgstr "Blur"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
+msgid "workspace.options.blur-options.title.group"
+msgstr "Blur pe grup"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
+msgid "workspace.options.blur-options.title.multiple"
+msgstr "Selecţie de blur"
+
+#: src/app/main/ui/workspace/sidebar/options/page.cljs
+msgid "workspace.options.canvas-background"
+msgstr "Fundal canvas"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs
+msgid "workspace.options.component"
+msgstr "Componentă"
+
+#: src/app/main/ui/workspace/sidebar/options.cljs
+msgid "workspace.options.design"
+msgstr "Design"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.export"
+msgstr "Exportă"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.export-object"
+msgstr "Exportă forma obiectului"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs
+msgid "workspace.options.export.suffix"
+msgstr "Sufix"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "workspace.options.exporting-object"
+msgstr "Se exportă…"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
+msgid "workspace.options.fill"
+msgstr "Umple"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.auto"
+msgstr "Auto"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.column"
+msgstr "Coloane"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.columns"
+msgstr "Coloane"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.gutter"
+msgstr "Spaţiere"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.height"
+msgstr "Înălţime"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.margin"
+msgstr "Margine"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.rows"
+msgstr "Rânduri"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.set-default"
+msgstr "Setează ca predefinit"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.size"
+msgstr "Mărime"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type"
+msgstr "Tip"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.bottom"
+msgstr "Jos"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.center"
+msgstr "Centru"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.left"
+msgstr "Stânga"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.right"
+msgstr "Dreapta"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.stretch"
+msgstr "Întinde"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.type.top"
+msgstr "Sus"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.use-default"
+msgstr "Foloseşte default"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.params.width"
+msgstr "Lăţime"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.row"
+msgstr "Rânduri"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.square"
+msgstr "Pătrat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+msgid "workspace.options.grid.title"
+msgstr "Grilă & Layout"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
+msgid "workspace.options.group-fill"
+msgstr "Group fill"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.group-stroke"
+msgstr "Group stroke"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.color"
+msgstr "Culoare"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.color-burn"
+msgstr "Color burn"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.color-dodge"
+msgstr "Color dodge"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.darken"
+msgstr "Întunecat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.difference"
+msgstr "Difference"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.exclusion"
+msgstr "Exclusion"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.hard-light"
+msgstr "Lumină Puternică"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.hue"
+msgstr "Hue"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.lighten"
+msgstr "Luminat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.luminosity"
+msgstr "Luminozitate"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.multiply"
+msgstr "Multiplică"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.normal"
+msgstr "Normal"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.overlay"
+msgstr "Overlay"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.saturation"
+msgstr "Saturaţie"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.screen"
+msgstr "Screen"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.blend-mode.soft-light"
+msgstr "Soft light"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.title"
+msgstr "Layer"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.title.group"
+msgstr "Grupează layere"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+msgid "workspace.options.layer-options.title.multiple"
+msgstr "Layere selectate"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.navigate-to"
+msgstr "Navighează la"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.none"
+msgstr "Nici unul"
+
+#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.position"
+msgstr "Poziţie"
+
+#: src/app/main/ui/workspace/sidebar/options.cljs
+msgid "workspace.options.prototype"
+msgstr "Prototip"
+
+msgid "workspace.options.radius"
+msgstr "Rază"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.radius.all-corners"
+msgstr "Toate colţurile"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.radius.single-corners"
+msgstr "Colţuri unice"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.rotation"
+msgstr "Rotaţie"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.select-a-shape"
+msgstr "Selectează o formă, o planşă sau grupează pentru a conecta o altă planşă."
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.select-artboard"
+msgstr "Selectează planşa"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
+msgid "workspace.options.selection-fill"
+msgstr "Selection fill"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.selection-stroke"
+msgstr "Selection stroke"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.blur"
+msgstr "Blur"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.drop-shadow"
+msgstr "Drop shadow"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.inner-shadow"
+msgstr "Inner shadow"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.offsetx"
+msgstr "X"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.offsety"
+msgstr "Y"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.spread"
+msgstr "Accentuare"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.title"
+msgstr "Umbră"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.title.group"
+msgstr "Group shadow"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs
+msgid "workspace.options.shadow-options.title.multiple"
+msgstr "Selection shadows"
+
+#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+msgid "workspace.options.size"
+msgstr "Mărime"
+
+#: src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs
+msgid "workspace.options.size-presets"
+msgstr "Dimensiuni presetate"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke"
+msgstr "Stroke"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.center"
+msgstr "Centru"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.dashed"
+msgstr "Dashed"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.dotted"
+msgstr "Dotted"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.inner"
+msgstr "Interior"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.mixed"
+msgstr "Mixed"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.outer"
+msgstr "Exterior"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+msgid "workspace.options.stroke.solid"
+msgstr "Solid"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-bottom"
+msgstr "Aliniază jos"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-center"
+msgstr "Aliniază centru"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-justify"
+msgstr "Justify"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-left"
+msgstr "Aliniază la stânga"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-middle"
+msgstr "Aliniază la mijloc"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-right"
+msgstr "Aliniază la dreapta"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.align-top"
+msgstr "Aliniază sus"
+
+msgid "workspace.options.text-options.decoration"
+msgstr "Decorare text"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.direction-ltr"
+msgstr "LTR"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.direction-rtl"
+msgstr "RTL"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.google"
+msgstr "Google"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.grow-auto-height"
+msgstr "Înălţime auto"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.grow-auto-width"
+msgstr "Lăţime auto"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.grow-fixed"
+msgstr "Fix"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.letter-spacing"
+msgstr "Spaţiere Litere"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.line-height"
+msgstr "Înălţime linii"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.lowercase"
+msgstr "Minuscule"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.none"
+msgstr "Nici unul"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.preset"
+msgstr "Presetat"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.strikethrough"
+msgstr "Strikethrough"
+
+msgid "workspace.options.text-options.text-case"
+msgstr "Case"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.title"
+msgstr "Text"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.title-group"
+msgstr "Grupează text"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.title-selection"
+msgstr "Selecţie text"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.titlecase"
+msgstr "Încadrare Titlu"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+msgid "workspace.options.text-options.underline"
+msgstr "Subliniază"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+msgid "workspace.options.text-options.uppercase"
+msgstr "Majuscule"
+
+msgid "workspace.options.text-options.vertical-align"
+msgstr "Aliniere verticală"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+msgid "workspace.options.use-play-button"
+msgstr "Foloseşte butonul play din header pentru a porni vizualizarea prototip."
+
+msgid "workspace.path.actions.add-node"
+msgstr "Adaugă nod (%s)"
+
+msgid "workspace.path.actions.delete-node"
+msgstr "Şterge nod (%s)"
+
+msgid "workspace.path.actions.draw-nodes"
+msgstr "Desenează noduri (%s)"
+
+msgid "workspace.path.actions.join-nodes"
+msgstr "Adaugă noduri (%s)"
+
+msgid "workspace.path.actions.make-corner"
+msgstr "În colţ (%s)"
+
+msgid "workspace.path.actions.make-curve"
+msgstr "În curbă (%s)"
+
+msgid "workspace.path.actions.merge-nodes"
+msgstr "Uneşte noduri (%s)"
+
+msgid "workspace.path.actions.move-nodes"
+msgstr "Mută noduri (%s)"
+
+msgid "workspace.path.actions.separate-nodes"
+msgstr "Separă noduri (%s)"
+
+msgid "workspace.path.actions.snap-nodes"
+msgstr "Trage noduri (%s)"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.back"
+msgstr "Trimite înapoi"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.backward"
+msgstr "Trimite în urmă"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.copy"
+msgstr "Copiază"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.create-component"
+msgstr "Creează componentă"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.cut"
+msgstr "Taie"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.delete"
+msgstr "Şterge"
+
+#: 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-instance"
+msgstr "Detaşează instanţă"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.duplicate"
+msgstr "Duplică"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.edit"
+msgstr "Editează"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.flip-horizontal"
+msgstr "Întoarce pe orizontală"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.flip-vertical"
+msgstr "Întoarce pe verticală"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.forward"
+msgstr "Aduceţi înainte"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.front"
+msgstr "Aduceţi în faţă"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.go-main"
+msgstr "Mergi la componenta principală"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.group"
+msgstr "Grupează"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.hide"
+msgstr "Ascunde"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.lock"
+msgstr "Blochează"
+
+#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.mask"
+msgstr "Maschează"
+
+#: src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.paste"
+msgstr "Lipeşte"
+
+#: 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.reset-overrides"
+msgstr "Resetează suprascrierile"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.show"
+msgstr "Afişează"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.show-main"
+msgstr "Afişează componenta principală"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.ungroup"
+msgstr "Degrupează"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.unlock"
+msgstr "Deblochează"
+
+#: src/app/main/ui/workspace/context_menu.cljs
+msgid "workspace.shape.menu.unmask"
+msgstr "Demaschează"
+
+#: 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 "Actualizaţi principala componentă"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.sidebar.history"
+msgstr "Istoric (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.sidebar.layers"
+msgstr "Layere (%s)"
+
+#: src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs, src/app/main/ui/handoff/attributes/svg.cljs
+msgid "workspace.sidebar.options.svg-attrs.title"
+msgstr "Atribute SVG importate"
+
+#: src/app/main/ui/workspace/sidebar/sitemap.cljs
+msgid "workspace.sidebar.sitemap"
+msgstr "Pagini"
+
+#: src/app/main/ui/workspace/header.cljs
+msgid "workspace.sitemap"
+msgstr "Harta site-ului"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.assets"
+msgstr "Obiecte (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.color-palette"
+msgstr "Paletă de culori (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.comments"
+msgstr "Comentarii (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.curve"
+msgstr "Curbe (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.ellipse"
+msgstr "Elipsă (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.frame"
+msgstr "Planşă de lucru (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.image"
+msgstr "Imagine (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.move"
+msgstr "Poziţionează"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.path"
+msgstr "Cale (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.rect"
+msgstr "Dreptunghi (%s)"
+
+#: src/app/main/ui/workspace/left_toolbar.cljs
+msgid "workspace.toolbar.text"
+msgstr "Text (%s)"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.empty"
+msgstr "Nu sunt modificări în istoric"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.entry.delete"
+msgstr "Şters %s"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.entry.modify"
+msgstr "Modificat %s"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.entry.move"
+msgstr "Obiecte mutate"
+
+msgid "workspace.undo.entry.multiple.circle"
+msgstr "cercuri"
+
+msgid "workspace.undo.entry.multiple.color"
+msgstr "Culori"
+
+msgid "workspace.undo.entry.multiple.component"
+msgstr "componente"
+
+msgid "workspace.undo.entry.multiple.curve"
+msgstr "curbe"
+
+msgid "workspace.undo.entry.multiple.frame"
+msgstr "planşă de lucru"
+
+msgid "workspace.undo.entry.multiple.group"
+msgstr "grupuri"
+
+msgid "workspace.undo.entry.multiple.media"
+msgstr "obiecte grafice"
+
+msgid "workspace.undo.entry.multiple.multiple"
+msgstr "obiecte"
+
+msgid "workspace.undo.entry.multiple.page"
+msgstr "pagini"
+
+msgid "workspace.undo.entry.multiple.path"
+msgstr "căi"
+
+msgid "workspace.undo.entry.multiple.rect"
+msgstr "dreptunghiuri"
+
+msgid "workspace.undo.entry.multiple.shape"
+msgstr "forme"
+
+msgid "workspace.undo.entry.multiple.text"
+msgstr "text"
+
+msgid "workspace.undo.entry.multiple.typography"
+msgstr "obiecte tipografice"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.entry.new"
+msgstr "Nou %s"
+
+msgid "workspace.undo.entry.single.circle"
+msgstr "cerc"
+
+msgid "workspace.undo.entry.single.color"
+msgstr "culoare"
+
+msgid "workspace.undo.entry.single.component"
+msgstr "componentă"
+
+msgid "workspace.undo.entry.single.curve"
+msgstr "curbă"
+
+msgid "workspace.undo.entry.single.frame"
+msgstr "planşă de lucru"
+
+msgid "workspace.undo.entry.single.group"
+msgstr "grup"
+
+msgid "workspace.undo.entry.single.image"
+msgstr "imagine"
+
+msgid "workspace.undo.entry.single.media"
+msgstr "obiect grafic"
+
+msgid "workspace.undo.entry.single.multiple"
+msgstr "obiect"
+
+msgid "workspace.undo.entry.single.page"
+msgstr "pagină"
+
+msgid "workspace.undo.entry.single.path"
+msgstr "cale"
+
+msgid "workspace.undo.entry.single.rect"
+msgstr "dreptunghi"
+
+msgid "workspace.undo.entry.single.shape"
+msgstr "formă"
+
+msgid "workspace.undo.entry.single.text"
+msgstr "text"
+
+msgid "workspace.undo.entry.single.typography"
+msgstr "obiect tipografic"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.entry.unknown"
+msgstr "Operaţiune terminată %s"
+
+#: src/app/main/ui/workspace/sidebar/history.cljs
+msgid "workspace.undo.title"
+msgstr "Istoric"
+
+#: src/app/main/data/workspace/libraries.cljs
+msgid "workspace.updates.dismiss"
+msgstr "Renunţă"
+
+#: src/app/main/data/workspace/libraries.cljs
+msgid "workspace.updates.there-are-updates"
+msgstr "Există actualizări în colecţiile distribuite"
+
+#: src/app/main/data/workspace/libraries.cljs
+msgid "workspace.updates.update"
+msgstr "Actualizează"
+
+msgid "workspace.viewport.click-to-close-path"
+msgstr "Click pentru a închide calea"
\ No newline at end of file
diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po
index 3cb0f1e42..9118ae535 100644
--- a/frontend/translations/ru.po
+++ b/frontend/translations/ru.po
@@ -1146,11 +1146,11 @@ msgstr ""
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "Рабочая область (A)"
+msgstr "Рабочая область (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "Изображение (K)"
+msgstr "Изображение (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -1158,15 +1158,15 @@ msgstr "Вытеснить"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "Линия (P)"
+msgstr "Линия (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "Прямоугольник (R)"
+msgstr "Прямоугольник (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "Текст (T)"
+msgstr "Текст (%s)"
#: src/app/main/data/workspace/libraries.cljs
msgid "workspace.updates.dismiss"
diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po
index 97dfb17d1..a947e2223 100644
--- a/frontend/translations/tr.po
+++ b/frontend/translations/tr.po
@@ -1,10 +1,15 @@
msgid ""
msgstr ""
+"PO-Revision-Date: 2021-05-17 21:32+0000\n"
+"Last-Translator: Gizem Akgüney \n"
+"Language-Team: Turkish "
+"\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.7-dev\n"
#: src/app/main/ui/auth/register.cljs
msgid "auth.already-have-account"
@@ -74,17 +79,25 @@ msgstr "Github ile giriş yap"
msgid "auth.login-with-gitlab-submit"
msgstr "Gitlab ile giriş yap"
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-google-submit"
+msgstr "Google ile Giriş Yap"
+
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-ldap-submit"
msgstr "LDAP ile giriş yap"
+#: src/app/main/ui/auth/login.cljs
+msgid "auth.login-with-oidc-submit"
+msgstr "OpenID (SSO) ile Giriş Yap"
+
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.new-password"
msgstr "Yeni bir parola gir"
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.notifications.invalid-token-error"
-msgstr "Kurtarma bağlantısı geçerli değil"
+msgstr "Kurtarma jetonu geçerli değil"
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.notifications.password-changed-succesfully"
@@ -195,6 +208,15 @@ msgstr "%s dosyanın kopyasını oluştur"
msgid "dashboard.empty-files"
msgstr "Burada hiç dosyan yok"
+#, markdown
+msgid "dashboard.fonts.hero-text2"
+msgstr ""
+"Sadece kendinize ait veya Penpot'ta kullanılabilecek bir lisansa sahip olan "
+"fontları yükleyebilirsiniz. [Penpot's Terms of Service] içindeki İçerik "
+"hakları bölümünden detaylı bilgi alabilirsiniz "
+"(https://penpot.app/terms.html). Ayrıca [font licensing](2) hakkında daha "
+"fazla bilgi almak isteyebilirsiniz."
+
#: src/app/main/ui/dashboard/team.cljs
msgid "dashboard.invite-profile"
msgstr "Takıma davet et"
@@ -227,10 +249,18 @@ msgstr "Başkta takıma taşı"
msgid "dashboard.new-file"
msgstr "Yeni Dosya"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-file-prefix"
+msgstr "Yeni Dosya"
+
#: src/app/main/ui/dashboard/projects.cljs
msgid "dashboard.new-project"
msgstr "Yeni Proje"
+#: src/app/main/data/dashboard.cljs
+msgid "dashboard.new-project-prefix"
+msgstr "Yeni Proje"
+
#: src/app/main/ui/dashboard/search.cljs
msgid "dashboard.no-matches-for"
msgstr "%s için hiç sonuç bulunamadı"
@@ -249,7 +279,7 @@ msgstr "E-posta adresin başarıyla doğrulandı"
#: src/app/main/ui/settings/password.cljs
msgid "dashboard.notifications.password-saved"
-msgstr "Parola kaydedildi"
+msgstr "Parola başarıyla kaydedildi!"
#: src/app/main/ui/dashboard/team.cljs
msgid "dashboard.num-of-members"
@@ -289,7 +319,7 @@ msgstr "Ara…"
#: src/app/main/ui/dashboard/search.cljs
msgid "dashboard.searching-for"
-msgstr "%s aranıyor"
+msgstr "%s aranıyor…"
#: src/app/main/ui/settings/options.cljs
msgid "dashboard.select-ui-language"
@@ -405,7 +435,7 @@ msgstr "E-posta zaten kullanımda"
#: src/app/main/ui/auth/verify_token.cljs
msgid "errors.email-already-validated"
-msgstr "E-posta zaten doğrulandı"
+msgstr "E-posta zaten doğrulandı."
#: 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"
@@ -423,6 +453,10 @@ msgstr "Bir şeyler ters gitti."
msgid "errors.google-auth-not-enabled"
msgstr "Google ile oturum açma devre dışı bırakıldı"
+#: src/app/main/ui/components/color_input.cljs
+msgid "errors.invalid-color"
+msgstr "Geçersiz renk"
+
#: src/app/main/ui/auth/login.cljs
msgid "errors.ldap-disabled"
msgstr "LDAP ile oturum açma devre dışı bırakıldı."
@@ -432,7 +466,7 @@ msgstr "Görsel formatı desteklenmiyor (svg, jpg veya png olmalı)."
#: src/app/main/data/workspace/persistence.cljs
msgid "errors.media-too-large"
-msgstr "Bu görsel eklemek için çok büyük (5MB altında olmalı)"
+msgstr "Bu görsel eklemek için çok büyük (5MB altında olmalı)."
#: src/app/main/data/workspace/persistence.cljs, src/app/main/data/media.cljs
msgid "errors.media-type-mismatch"
@@ -449,7 +483,7 @@ msgstr ""
"var veya spam olarak bildirilmiş."
msgid "errors.network"
-msgstr "Sunucuya bağlanılamıyor"
+msgstr "Sunucuya bağlanılamıyor."
#: src/app/main/ui/settings/password.cljs
msgid "errors.password-invalid-confirmation"
@@ -457,4 +491,433 @@ msgstr "Parolalar eşleşmedi"
#: src/app/main/ui/settings/password.cljs
msgid "errors.password-too-short"
-msgstr "Parola en az 8 karakterden oluşmalı"
\ No newline at end of file
+msgstr "Parola en az 8 karakterden oluşmalı"
+
+#: src/app/main/ui/auth/recovery_request.cljs, src/app/main/ui/settings/change_email.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "errors.profile-is-muted"
+msgstr ""
+"Profilinizde sessize alınmış e-postalar var (spam raporları veya yüksek "
+"geri dönüşler sebebiyle)."
+
+#: src/app/main/ui/auth/register.cljs
+msgid "errors.registration-disabled"
+msgstr "Kayıt olma şu anda devre dışı."
+
+msgid "errors.terms-privacy-agreement-invalid"
+msgstr "Hizmet şartlarımızı ve gizlilik politikamızı kabul etmelisin."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.token-expired"
+msgstr "Jetonun süresi geçti"
+
+#: src/app/main/data/media.cljs, src/app/main/ui/workspace/sidebar/options/menus/exports.cljs, src/app/main/ui/handoff/exports.cljs
+msgid "errors.unexpected-error"
+msgstr "Beklenmedik bir hata oluştu."
+
+#: src/app/main/ui/auth/verify_token.cljs
+msgid "errors.unexpected-token"
+msgstr "Bilinmeyen jeton"
+
+#: src/app/main/ui/auth/login.cljs
+msgid "errors.wrong-credentials"
+msgstr "Kullanıcı adı veya parola yanlış gözüküyor."
+
+#: src/app/main/ui/settings/password.cljs
+msgid "errors.wrong-old-password"
+msgstr "Eski parola yanlış"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-start"
+msgstr "Sohbete katıl"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.chat-subtitle"
+msgstr "Sohbet etmek ister misin? Glitter'da bizimle sohbet edebilirsin"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.description"
+msgstr "Açıklama"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-go-to"
+msgstr "Tartışmalar bölümüne git"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.discussions-title"
+msgstr "Takım tartışmaları"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.subject"
+msgstr "Konu"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "feedback.title"
+msgstr "E-posta"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "generic.error"
+msgstr "Bir hata oluştu"
+
+#: src/app/main/ui/handoff/attributes/blur.cljs
+msgid "handoff.attributes.blur"
+msgstr "Bulanıklaştır"
+
+#: src/app/main/ui/handoff/attributes/blur.cljs
+msgid "handoff.attributes.blur.value"
+msgstr "Değer"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.hex"
+msgstr "HEX"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.hsla"
+msgstr "HSLA"
+
+#: src/app/main/ui/handoff/attributes/common.cljs
+msgid "handoff.attributes.color.rgba"
+msgstr "RGBA"
+
+#: src/app/main/ui/handoff/attributes/fill.cljs
+msgid "handoff.attributes.fill"
+msgstr "Doldur"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.download"
+msgstr "Kaynak görselini indir"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.height"
+msgstr "Yükseklik"
+
+#: src/app/main/ui/handoff/attributes/image.cljs
+msgid "handoff.attributes.image.width"
+msgstr "Genişlik"
+
+msgid "handoff.attributes.typography.text-decoration.none"
+msgstr "Hiçbiri"
+
+msgid "handoff.attributes.typography.text-transform.lowercase"
+msgstr "Küçük Harf"
+
+msgid "handoff.attributes.typography.text-transform.none"
+msgstr "Hiçbiri"
+
+msgid "handoff.attributes.typography.text-transform.uppercase"
+msgstr "Büyük Harf"
+
+msgid "handoff.tabs.code.selected.circle"
+msgstr "Daire"
+
+msgid "handoff.tabs.code.selected.curve"
+msgstr "Eğri"
+
+msgid "handoff.tabs.code.selected.group"
+msgstr "Grup"
+
+msgid "handoff.tabs.code.selected.image"
+msgstr "Görsel"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.code.selected.multiple"
+msgstr "%s Seçildi"
+
+msgid "handoff.tabs.code.selected.path"
+msgstr "Yol"
+
+msgid "handoff.tabs.code.selected.rect"
+msgstr "Dikdörtgen"
+
+msgid "handoff.tabs.code.selected.svg-raw"
+msgstr "SVG"
+
+msgid "handoff.tabs.code.selected.text"
+msgstr "Metin"
+
+#: src/app/main/ui/handoff/right_sidebar.cljs
+msgid "handoff.tabs.info"
+msgstr "Bilgi"
+
+msgid "history.alert-message"
+msgstr "%s sürümünü görüyorsun"
+
+msgid "labels.accept"
+msgstr "Kabul et"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.admin"
+msgstr "Yönetici"
+
+#: src/app/main/ui/workspace/comments.cljs
+msgid "labels.all"
+msgstr "Hepsi"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.cancel"
+msgstr "İptal"
+
+msgid "labels.centered"
+msgstr "Orta"
+
+#: src/app/main/ui/dashboard/comments.cljs
+msgid "labels.comments"
+msgstr "Yorumlar"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.confirm-password"
+msgstr "Parolayı onayla"
+
+msgid "labels.content"
+msgstr "İçerik"
+
+#: src/app/main/ui/workspace/sidebar/assets.cljs
+msgid "labels.create"
+msgstr "Oluştur"
+
+#: src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team"
+msgstr "Yeni takım oluştur"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.create-team.placeholder"
+msgstr "Yeni takım adı gir"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.dashboard"
+msgstr "Kontrol paneli"
+
+#: src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete"
+msgstr "Sil"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment"
+msgstr "Yorumu sil"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.delete-comment-thread"
+msgstr "Mesaj dizisini sil"
+
+#: src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.delete-multi-files"
+msgstr "%s dosyayı sil"
+
+#: src/app/main/ui/dashboard/projects.cljs, src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/files.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.drafts"
+msgstr "Taslak"
+
+#: src/app/main/ui/comments.cljs
+msgid "labels.edit"
+msgstr "Düzenle"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.editor"
+msgstr "Editör"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.email"
+msgstr "E-posta"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-disabled"
+msgstr "Geri bildirim devre dışı bırakıldı"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.feedback-sent"
+msgstr "Geri bildirim gönderildi"
+
+msgid "labels.font-family"
+msgstr "Font Ailesi"
+
+msgid "labels.font-variant"
+msgstr "Stil"
+
+msgid "labels.fonts"
+msgstr "Fontlar"
+
+#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.give-feedback"
+msgstr "Geri bildirimde bulun"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.hide-resolved-comments"
+msgstr "Çözülmüş yorumları gizle"
+
+msgid "labels.images"
+msgstr "Görseller"
+
+msgid "labels.installed-fonts"
+msgstr "Yüklenmiş fontlar"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.internal-error.desc-message"
+msgstr ""
+"Kötü bir şey oldu. Lütfen işlemi yeniden deneyin ve sorun devam ederse "
+"destek ile iletişime geçin."
+
+#: src/app/main/ui/settings/options.cljs
+msgid "labels.language"
+msgstr "Dil"
+
+#: src/app/main/ui/settings.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.logout"
+msgstr "Çıkış Yap"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.members"
+msgstr "Üyeler"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.name"
+msgstr "Ad"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.new-password"
+msgstr "Yeni parola"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs
+msgid "labels.no-comments-available"
+msgstr "Bekleyen yorum bildirimi yok"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.auth-info"
+msgstr "Şu şekilde oturum açtınız:"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.desc-message"
+msgstr "Bu sayfa mevcut olmayabilir veya erişim izniniz olmayabilir."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.not-found.main-message"
+msgstr "Oops!"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-files"
+msgid_plural "labels.num-of-files"
+msgstr[0] "1 dosya"
+msgstr[1] "%s dosya"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-projects"
+msgid_plural "labels.num-of-projects"
+msgstr[0] "1 proje"
+msgstr[1] "%s proje"
+
+#: src/app/main/ui/settings/password.cljs
+msgid "labels.old-password"
+msgstr "Eski parola"
+
+#: src/app/main/ui/workspace/comments.cljs
+msgid "labels.only-yours"
+msgstr "Sadece seninkiler"
+
+#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.owner"
+msgstr "Sahip"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.password"
+msgstr "Parola"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.permissions"
+msgstr "İzinler"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.profile"
+msgstr "Profil"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.projects"
+msgstr "Projeler"
+
+msgid "labels.recent"
+msgstr "Son"
+
+#: src/app/main/ui/settings/sidebar.cljs
+msgid "labels.release-notes"
+msgstr "Sürüm notları"
+
+#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
+msgid "labels.remove"
+msgstr "Kaldır"
+
+#: src/app/main/ui/dashboard/sidebar.cljs, src/app/main/ui/dashboard/project_menu.cljs, src/app/main/ui/dashboard/file_menu.cljs
+msgid "labels.rename"
+msgstr "Yeniden adlandır"
+
+#: src/app/main/ui/dashboard/team_form.cljs
+msgid "labels.rename-team"
+msgstr "Takımı yeniden adlandır"
+
+#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
+msgid "labels.retry"
+msgstr "Yeniden dene"
+
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.role"
+msgstr "Rol"
+
+msgid "labels.save"
+msgstr "Kaydet"
+
+msgid "labels.search-font"
+msgstr "Font ara"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.send"
+msgstr "Gönder"
+
+#: src/app/main/ui/settings/feedback.cljs
+msgid "labels.sending"
+msgstr "Gönderiliyor..."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.service-unavailable.desc-message"
+msgstr "Sistemlerimizin programlı bakımını yapıyoruz."
+
+#: src/app/main/ui/static.cljs
+msgid "labels.service-unavailable.main-message"
+msgstr "Hizmet Kullanılamıyor"
+
+#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.settings"
+msgstr "Ayarlar"
+
+#: src/app/main/ui/dashboard/sidebar.cljs
+msgid "labels.shared-libraries"
+msgstr "Paylaşılan Kitaplıklar"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.show-all-comments"
+msgstr "Tüm yorumları göster"
+
+#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
+msgid "labels.show-your-comments"
+msgstr "Yalnızca kendi yorumlarımı göster"
+
+#: src/app/main/ui/static.cljs
+msgid "labels.sign-out"
+msgstr "Çıkış yap"
+
+#: src/app/main/ui/settings/profile.cljs
+msgid "labels.update"
+msgstr "Güncelle"
+
+msgid "labels.upload"
+msgstr "Yükle"
+
+msgid "labels.uploading"
+msgstr "Yükleniyor..."
+
+msgid "modals.delete-font.message"
+msgstr ""
+"Bu fontu silmek istediğine emin misin? Bir dosyada kullanılıyorsa "
+"yüklenmeyecektir."
+
+msgid "modals.delete-font.title"
+msgstr "Fontu sil"
+
+#: src/app/main/ui/dashboard/fonts.cljs
+msgid "title.dashboard.fonts"
+msgstr "Fontlar - %s - Penpot"
\ No newline at end of file
diff --git a/frontend/translations/zh_cn.po b/frontend/translations/zh_CN.po
similarity index 99%
rename from frontend/translations/zh_cn.po
rename to frontend/translations/zh_CN.po
index 9ade5b982..481883c52 100644
--- a/frontend/translations/zh_cn.po
+++ b/frontend/translations/zh_CN.po
@@ -2066,15 +2066,15 @@ msgstr "曲线(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.ellipse"
-msgstr "椭圆(E)"
+msgstr "椭圆(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.frame"
-msgstr "画板(A)"
+msgstr "画板(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.image"
-msgstr "图片(K)"
+msgstr "图片(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.move"
@@ -2082,15 +2082,15 @@ msgstr "移动"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.path"
-msgstr "路径(P)"
+msgstr "路径(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.rect"
-msgstr "矩形(R)"
+msgstr "矩形(%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.text"
-msgstr "文本(T)"
+msgstr "文本(%s)"
#: src/app/main/ui/workspace/sidebar/history.cljs
msgid "workspace.undo.empty"
diff --git a/frontend/vendor/cuerdas/vendor/xregexp.cljs b/frontend/vendor/cuerdas/vendor/xregexp.cljs
deleted file mode 100644
index 2883be3d7..000000000
--- a/frontend/vendor/cuerdas/vendor/xregexp.cljs
+++ /dev/null
@@ -1,4 +0,0 @@
-(ns cuerdas.vendor.xregexp
- (:require ["xregexp" :as XRegExp]))
-
-(goog/exportSymbol "XRegExp" XRegExp)
diff --git a/frontend/vendor/tubax/saxjs.cljs b/frontend/vendor/tubax/saxjs.cljs
new file mode 100644
index 000000000..3dc98550b
--- /dev/null
+++ b/frontend/vendor/tubax/saxjs.cljs
@@ -0,0 +1,4 @@
+(ns tubax.saxjs
+ (:require ["sax" :as sax]))
+
+(goog/exportSymbol "sax" sax)
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 021d9aff6..caa556a85 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3,13 +3,20 @@
"@babel/runtime-corejs3@^7.12.1":
- version "7.13.10"
- resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86"
- integrity sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg==
+ version "7.14.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66"
+ integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg==
dependencies:
core-js-pure "^3.0.0"
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==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@dabh/diagnostics@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
@@ -548,15 +555,15 @@ browserify-zlib@^0.2.0:
pako "~1.0.5"
browserslist@^4.16.3:
- version "4.16.3"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
- integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
+ version "4.16.6"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
+ integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
dependencies:
- caniuse-lite "^1.0.30001181"
- colorette "^1.2.1"
- electron-to-chromium "^1.3.649"
+ caniuse-lite "^1.0.30001219"
+ colorette "^1.2.2"
+ electron-to-chromium "^1.3.723"
escalade "^3.1.1"
- node-releases "^1.1.70"
+ node-releases "^1.1.71"
buffer-crc32@~0.2.3:
version "0.2.13"
@@ -662,10 +669,10 @@ camelcase@^5.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001196:
- version "1.0.30001205"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001205.tgz#d79bf6a6fb13196b4bb46e5143a22ca0242e0ef8"
- integrity sha512-TL1GrS5V6LElbitPazidkBMD9sa448bQDDLrumDqaggmKFcuU2JW1wTOHJPukAcOMtEmLcmDJEzfRrf+GjM0Og==
+caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219:
+ version "1.0.30001221"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz#b916721ddf59066cfbe96c5c9a77cf7ae5c52e65"
+ integrity sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g==
caseless@~0.12.0:
version "0.12.0"
@@ -802,6 +809,11 @@ cloneable-readable@^1.0.0:
process-nextick-args "^2.0.0"
readable-stream "^2.3.5"
+clsx@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
+ integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
+
coa@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
@@ -871,7 +883,7 @@ color@3.0.x:
color-convert "^1.9.1"
color-string "^1.5.2"
-colorette@^1.2.1, colorette@^1.2.2:
+colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@@ -982,14 +994,14 @@ copy-props@^2.0.1:
is-plain-object "^5.0.0"
core-js-pure@^3.0.0:
- version "3.10.2"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.10.2.tgz#065304f8547bf42008d4528dfff973c38bd6a332"
- integrity sha512-uu18pVHQ21n4mzfuSlCXpucu5VKsck3j2m5fjrBOBqqdgWAxwdCgUuGWj6cDDPN1zLj/qtiqKvBMxWgDeeu49Q==
+ version "3.11.2"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.11.2.tgz#10e3b35788c00f431bc0d601d7551475ec3e792c"
+ integrity sha512-DQxdEKm+zFsnON7ZGOgUAQXBt1UJJ01tOzN/HgQ7cNf0oEHW1tcBLfCQQd1q6otdLu5gAdvKYxKHAoXGwE/kiQ==
core-js@^3.6.4:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.0.tgz#9a020547c8b6879f929306949e31496bbe2ae9b3"
- integrity sha512-MQx/7TLgmmDVamSyfE+O+5BHvG1aUGj/gHhLn1wVtm2B5u1eVIPvh7vkfjwWKNCjrTJB8+He99IntSQ1qP+vYQ==
+ version "3.11.2"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.11.2.tgz#af087a43373fc6e72942917c4a4c3de43ed574d6"
+ integrity sha512-3tfrrO1JpJSYGKnd9LKTBPqgUES/UYiCzMKeqwR1+jF16q4kD1BY2NvqkfuzXwQ6+CIWm55V9cjD7PQd+hijdw==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -1038,9 +1050,9 @@ 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.3"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.3.tgz#b8e7d5f19161c4a0ca916f707978848786043afb"
- integrity sha512-2i6v88DTqVBNODyjD9U6Ycn/uSZNvyHe25cIbo2fFnAACAsaLTJsd23miRWiR5NuiGXR9wpJ9d40/9WAhjDIrw==
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
+ integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
dependencies:
node-fetch "2.6.1"
@@ -1109,9 +1121,9 @@ css-tree@1.0.0-alpha.37:
source-map "^0.6.1"
css-tree@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5"
- integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+ integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
@@ -1147,6 +1159,11 @@ cssom@^0.3.4:
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+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==
+
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -1169,10 +1186,10 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
-date-fns@^2.21.1:
- version "2.21.1"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
- integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==
+date-fns@^2.21.3:
+ version "2.21.3"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b"
+ integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw==
dateformat@^3.0.3:
version "3.0.3"
@@ -1302,6 +1319,14 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
+dom-helpers@^5.1.3:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
+ integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+ dependencies:
+ "@babel/runtime" "^7.8.7"
+ csstype "^3.0.2"
+
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -1321,9 +1346,9 @@ domelementtype@1:
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domelementtype@^2.0.1:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
- integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
+ integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domutils@^1.7.0:
version "1.7.0"
@@ -1378,10 +1403,10 @@ editorconfig@^0.15.3:
semver "^5.6.0"
sigmund "^1.0.1"
-electron-to-chromium@^1.3.649:
- version "1.3.703"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.703.tgz#6d9b9a75c42a40775f5930329e642b22b227317f"
- integrity sha512-SVBVhNB+4zPL+rvtWLw7PZQkw/Eqj1HQZs22xtcqW36+xoifzEOEEDEpkxSMfB6RFeSIOcG00w6z5mSqLr1Y6w==
+electron-to-chromium@^1.3.723:
+ version "1.3.726"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz#6d3c577e5f5a48904ba891464740896c05e3bdb1"
+ integrity sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w==
elliptic@^6.5.3:
version "6.5.4"
@@ -1696,9 +1721,9 @@ fd-slicer@~1.1.0:
pend "~1.2.0"
fecha@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41"
- integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
+ integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==
file-uri-to-path@1.0.0:
version "1.0.0"
@@ -2320,9 +2345,9 @@ homedir-polyfill@^1.0.1:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
- version "2.8.8"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
- integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+ version "2.8.9"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+ integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-signature@~1.2.0:
version "1.2.0"
@@ -2501,9 +2526,9 @@ is-callable@^1.1.4, is-callable@^1.2.3:
integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
is-core-module@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
- integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887"
+ integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==
dependencies:
has "^1.0.3"
@@ -3049,7 +3074,7 @@ logform@^2.2.0:
ms "^2.1.1"
triple-beam "^1.3.0"
-loose-envify@^1.0.0, loose-envify@^1.1.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -3120,6 +3145,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+marked@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
+ integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
+
matchdep@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@@ -3215,17 +3245,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.46.0:
- version "1.46.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
- integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
+mime-db@1.47.0:
+ version "1.47.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
+ integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
mime-types@^2.1.12, mime-types@~2.1.19:
- version "2.1.29"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
- integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
+ version "2.1.30"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d"
+ integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==
dependencies:
- mime-db "1.46.0"
+ mime-db "1.47.0"
mimic-fn@^2.0.0:
version "2.1.0"
@@ -3338,10 +3368,10 @@ nan@^2.12.1, nan@^2.13.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-nanoid@^3.1.22:
- version "3.1.22"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
- integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
+nanoid@^3.1.23:
+ version "3.1.23"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
+ integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
nanomatch@^1.2.9:
version "1.2.13"
@@ -3427,7 +3457,7 @@ node-libs-browser@^2.2.1:
util "^0.11.0"
vm-browserify "^1.0.1"
-node-releases@^1.1.70:
+node-releases@^1.1.71:
version "1.1.71"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
@@ -3552,9 +3582,9 @@ object-copy@^0.1.0:
kind-of "^3.0.3"
object-inspect@^1.9.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
- integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.2.tgz#b6385a3e2b7cae0b5eafcf90cddf85d128767f30"
+ integrity sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
@@ -3644,6 +3674,14 @@ one-time@^1.0.0:
dependencies:
fn.name "1.x.x"
+opentype.js@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6"
+ integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==
+ dependencies:
+ string.prototype.codepointat "^0.2.1"
+ tiny-inflate "^1.0.3"
+
ordered-read-streams@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
@@ -3949,13 +3987,13 @@ postcss@^7.0.16:
source-map "^0.6.1"
supports-color "^6.1.0"
-postcss@^8.2.7:
- version "8.2.10"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b"
- integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==
+postcss@^8.2.15:
+ version "8.2.15"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65"
+ integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==
dependencies:
colorette "^1.2.2"
- nanoid "^3.1.22"
+ nanoid "^3.1.23"
source-map "^0.6.1"
pretty-hrtime@^1.0.0:
@@ -3990,6 +4028,15 @@ promise@^7.1.1:
dependencies:
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==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -4106,6 +4153,28 @@ react-dom@~17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.2"
+react-is@^16.8.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==
+
+react-lifecycles-compat@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+ integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
+react-virtualized@^9.22.3:
+ version "9.22.3"
+ resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
+ integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==
+ dependencies:
+ "@babel/runtime" "^7.7.2"
+ clsx "^1.0.4"
+ dom-helpers "^5.1.3"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-lifecycles-compat "^3.0.4"
+
react@~17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -4225,9 +4294,9 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0:
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
repeat-element@^1.1.2:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
- integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+ integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
repeat-string@^1.6.1:
version "1.6.1"
@@ -4363,10 +4432,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
-rxjs@~7.0.0-beta.12:
- version "7.0.0-rc.1"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654"
- integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw==
+rxjs@~7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.1.tgz#5f41c4f991cea550471fc5d215727390103702c7"
+ integrity sha512-wViQ4Vgps1xJwqWIBooMNN44usCSthL7wCUl4qWqrVjhGfWyVyXcxlYzfDKkJKACQvZMTOft/jJ3RkbwK1j9QQ==
dependencies:
tslib "~2.1.0"
@@ -4403,13 +4472,13 @@ sass-graph@2.2.5:
yargs "^13.3.2"
sass@^1.32.8:
- version "1.32.11"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.11.tgz#b236b3ea55c76602c2ef2bd0445f0db581baa218"
- integrity sha512-O9tRcob/fegUVSIV1ihLLZcftIOh0AF1VpKgusUfLqnb2jQ0GLDwI5ivv1FYWivGv8eZ/AwntTyTzjcHu0c/qw==
+ version "1.32.12"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.12.tgz#a2a47ad0f1c168222db5206444a30c12457abb9f"
+ integrity sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA==
dependencies:
chokidar ">=3.0.0 <4.0.0"
-sax@~1.2.4:
+sax@^1.2.4, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -4480,10 +4549,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.11.20:
- version "2.12.5"
- resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
- integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
+shadow-cljs@2.12.6:
+ version "2.12.6"
+ resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.6.tgz#039539fdc35a19c2f2cd15792ae17e7928f97428"
+ integrity sha512-dNw989EFQki/59kD8Cd8b6HIpBTqPj9ksWIvSg6hI1bgezZHT0oHfJH5UIbXPD+dnVLvbOnDnfOMWYH6ozalcA==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"
@@ -4796,6 +4865,11 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
+string.prototype.codepointat@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"
+ integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
+
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@@ -5021,6 +5095,11 @@ timers-ext@^0.1.7:
es5-ext "~0.10.46"
next-tick "1"
+tiny-inflate@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+ integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
to-absolute-glob@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
@@ -5135,10 +5214,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-ua-parser-js@^0.7.18:
- version "0.7.26"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.26.tgz#b3731860e241419abd5b542b1a0881070d92e0ce"
- integrity sha512-VwIvGlFNmpKbjzRt51jpbbFTrKIEgGHxIwA8Y69K1Bqc6bTIV7TaGGABOkghSFQWsLmcRB4drGvpfv9z2szqoQ==
+ua-parser-js@^0.7.18, ua-parser-js@^0.7.28:
+ 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==
ultron@~1.1.0:
version "1.1.1"
@@ -5482,23 +5561,15 @@ y18n@^3.2.1:
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
- integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
-yargs-parser@5.0.0-security.0:
- version "5.0.0-security.0"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz#4ff7271d25f90ac15643b86076a2ab499ec9ee24"
- integrity sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==
- dependencies:
- camelcase "^3.0.0"
- object.assign "^4.1.0"
-
yargs-parser@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
@@ -5515,6 +5586,14 @@ yargs-parser@^13.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
+yargs-parser@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394"
+ integrity sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==
+ dependencies:
+ 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"
@@ -5550,9 +5629,9 @@ yargs@^13.3.2:
yargs-parser "^13.1.2"
yargs@^7.1.0:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"
- integrity sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db"
+ integrity sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==
dependencies:
camelcase "^3.0.0"
cliui "^3.2.0"
@@ -5566,7 +5645,7 @@ yargs@^7.1.0:
string-width "^1.0.2"
which-module "^1.0.0"
y18n "^3.2.1"
- yargs-parser "5.0.0-security.0"
+ yargs-parser "^5.0.1"
yauzl@^2.10.0:
version "2.10.0"
diff --git a/manage.sh b/manage.sh
index 6ea162e26..80d9c6a85 100755
--- a/manage.sh
+++ b/manage.sh
@@ -98,6 +98,54 @@ Copyright (c) UXBOX Labs SL
EOF
}
+function build-frontend-bundle {
+ echo ">> bundle frontend start";
+
+ local version=$(print-current-version);
+ local bundle_dir="./bundle-frontend";
+
+ build "frontend";
+
+ rm -rf $bundle_dir;
+ mv ./frontend/target/dist $bundle_dir;
+ echo $version > $bundle_dir/version.txt;
+ put-license-file $bundle_dir;
+ echo ">> bundle frontend end";
+}
+
+function build-backend-bundle {
+ echo ">> bundle backend start";
+
+ local version=$(print-current-version);
+ local bundle_dir="./bundle-backend";
+
+ build "backend";
+
+ rm -rf $bundle_dir;
+ mv ./backend/target/dist $bundle_dir;
+ echo $version > $bundle_dir/version.txt;
+ put-license-file $bundle_dir;
+ echo ">> bundle frontend end";
+}
+
+function build-exporter-bundle {
+ echo ">> bundle exporter start";
+ local version=$(print-current-version);
+ local bundle_dir="./bundle-exporter";
+
+ build "exporter";
+
+ rm -rf $bundle_dir;
+ mv ./exporter/target $bundle_dir;
+
+ echo $version > $bundle_dir/version.txt
+ put-license-file $bundle_dir;
+
+ echo ">> bundle exporter end";
+}
+
+# DEPRECATED: temporary mantained for backward compatibilty.
+
function build-app-bundle {
echo ">> bundle app start";
@@ -117,22 +165,6 @@ function build-app-bundle {
echo ">> bundle app end";
}
-function build-exporter-bundle {
- echo ">> bundle exporter start";
- local version=$(print-current-version);
- local bundle_dir="./bundle-exporter";
-
- build "exporter";
-
- rm -rf $bundle_dir;
- mv ./exporter/target $bundle_dir;
-
- echo $version > $bundle_dir/version.txt
- put-license-file $bundle_dir;
-
- echo ">> bundle exporter end";
-}
-
function usage {
echo "PENPOT build & release manager"
echo "USAGE: $0 OPTION"
@@ -182,6 +214,14 @@ case $1 in
build-app-bundle;
;;
+ build-frontend-bundle)
+ build-frontend-bundle;
+ ;;
+
+ build-backend-bundle)
+ build-backend-bundle;
+ ;;
+
build-exporter-bundle)
build-exporter-bundle;
;;
diff --git a/version.txt b/version.txt
index 8874652ad..f721a9b5e 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.5.4-alpha
+1.6.0-alpha