diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index 60606932bf..fe1d14d908 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -5,6 +5,7 @@
promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn
promesa.util/with-open clojure.core/with-open
+ app.common.schema.generators/let clojure.core/let
app.common.data/export clojure.core/def
app.common.data.macros/get-in clojure.core/get-in
app.common.data.macros/with-open clojure.core/with-open
diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml
index 7abb7a1885..ca1ab6739a 100644
--- a/backend/resources/log4j2-devenv.xml
+++ b/backend/resources/log4j2-devenv.xml
@@ -30,7 +30,7 @@
-
+
diff --git a/backend/resources/log4j2-experiments.xml b/backend/resources/log4j2-experiments.xml
new file mode 100644
index 0000000000..88542c2774
--- /dev/null
+++ b/backend/resources/log4j2-experiments.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/backend/scripts/repl-test b/backend/scripts/repl-test
new file mode 100755
index 0000000000..a1333a5317
--- /dev/null
+++ b/backend/scripts/repl-test
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+source /home/penpot/backend/environ
+export PENPOT_FLAGS="$PENPOT_FLAGS disable-backend-worker"
+
+export OPTIONS="
+ -A:jmx-remote -A:dev \
+ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
+ -J-Djdk.attach.allowAttachSelf \
+ -J-Dlog4j2.configurationFile=log4j2-experiments.xml \
+ -J-XX:-OmitStackTraceInFastThrow \
+ -J-XX:+UnlockDiagnosticVMOptions \
+ -J-XX:+DebugNonSafepoints \
+ -J-Djdk.tracePinnedThreads=full \
+ -J-Dpolyglot.engine.WarnInterpreterOnly=false \
+ -J--enable-preview";
+
+# Setup HEAP
+#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch"
+export OPTIONS="$OPTIONS -J-Xms1g -J-Xmx25g"
+#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch"
+
+export PENPOT_HTTP_SERVER_IO_THREADS=2
+export PENPOT_HTTP_SERVER_WORKER_THREADS=2
+
+# Increase virtual thread pool size
+# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
+
+# Disable C2 Compiler
+# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
+
+# Disable all compilers
+# export OPTIONS="$OPTIONS -J-Xint"
+
+# Setup GC
+export OPTIONS="$OPTIONS -J-XX:+UseG1GC -J-Xlog:gc:logs/gc.log"
+
+
+# Setup GC
+#export OPTIONS="$OPTIONS -J-XX:+UseZGC -J-XX:+ZGenerational -J-Xlog:gc:gc.log"
+
+# Enable ImageMagick v7.x support
+# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
+
+export OPTIONS_EVAL="nil"
+# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
+
+set -ex
+exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
\ No newline at end of file
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index b4fe60c652..a9e883b8ff 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -209,7 +209,6 @@
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
-(s/def ::svgo-max-procs ::us/integer)
(s/def ::config
(s/keys :opt-un [::secret-key
@@ -329,9 +328,7 @@
::telemetry-uri
::telemetry-referer
::telemetry-with-taiga
- ::tenant
-
- ::svgo-max-procs]))
+ ::tenant]))
(def default-flags
[:enable-backend-api-doc
diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj
index 942d01db7d..6e7407e17d 100644
--- a/backend/src/app/db.clj
+++ b/backend/src/app/db.clj
@@ -517,9 +517,11 @@
(defn rollback!
([conn]
- (let [^Connection conn (get-connection conn)]
- (l/trc :hint "explicit rollback requested")
- (.rollback conn)))
+ (if (and (map? conn) (::savepoint conn))
+ (rollback! conn (::savepoint conn))
+ (let [^Connection conn (get-connection conn)]
+ (l/trc :hint "explicit rollback requested")
+ (.rollback conn))))
([conn ^Savepoint sp]
(let [^Connection conn (get-connection conn)]
(l/trc :hint "explicit rollback requested (savepoint)")
@@ -538,8 +540,13 @@
(let [conn (::conn system)
sp (savepoint conn)]
(try
- (let [result (apply f system params)]
- (release! conn sp)
+ (let [system' (-> system
+ (assoc ::savepoint sp)
+ (dissoc ::rollback))
+ result (apply f system' params)]
+ (if (::rollback system)
+ (rollback! conn sp)
+ (release! conn sp))
result)
(catch Throwable cause
(.rollback ^Connection conn ^Savepoint sp)
@@ -547,8 +554,10 @@
(::pool system)
(with-atomic [conn (::pool system)]
- (let [system (assoc system ::conn conn)
- result (apply f system params)]
+ (let [system' (-> system
+ (assoc ::conn conn)
+ (dissoc ::rollback))
+ result (apply f system' params)]
(when (::rollback system)
(rollback! conn))
result))
diff --git a/backend/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj
index 8eb6772403..03240a9044 100644
--- a/backend/src/app/features/components_v2.clj
+++ b/backend/src/app/features/components_v2.clj
@@ -16,21 +16,30 @@
[app.common.files.migrations :as fmg]
[app.common.files.shapes-helpers :as cfsh]
[app.common.files.validate :as cfv]
+ [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
+ [app.common.geom.shapes.path :as gshp]
[app.common.logging :as l]
+ [app.common.math :as mth]
+ [app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.svg.shapes-builder :as sbuilder]
+ [app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
+ [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
+ [app.common.types.shape.path :as ctsp]
+ [app.common.types.shape.text :as ctsx]
[app.common.uuid :as uuid]
[app.db :as db]
+ [app.db.sql :as sql]
[app.features.fdata :as fdata]
[app.http.sse :as sse]
[app.media :as media]
@@ -41,29 +50,34 @@
[app.storage.tmp :as tmp]
[app.svgo :as svgo]
[app.util.blob :as blob]
+ [app.util.cache :as cache]
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
[cuerdas.core :as str]
[datoteka.io :as io]
- [promesa.core :as p]))
+ [promesa.exec :as px]
+ [promesa.util :as pu]))
(def ^:dynamic *stats*
"A dynamic var for setting up state for collect stats globally."
nil)
-(def ^:dynamic *skip-on-error*
- "A dynamic var for setting up the default error behavior."
- true)
+(def ^:dynamic *cache*
+ "A dynamic var for setting up a cache instance."
+ nil)
+
+(def ^:dynamic *skip-on-graphic-error*
+ "A dynamic var for setting up the default error behavior for graphics processing."
+ nil)
(def ^:dynamic ^:private *system*
"An internal var for making the current `system` available to all
internal functions without the need to explicitly pass it top down."
nil)
-(def ^:dynamic ^:private *max-procs*
- "A dynamic variable that can optionally indicates the maxumum number
- of concurrent graphics migration processes."
+(def ^:dynamic ^:private *team-id*
+ "A dynamic var that holds the current processing team-id."
nil)
(def ^:dynamic ^:private *file-stats*
@@ -91,21 +105,316 @@
;; FILE PREPARATION BEFORE MIGRATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(def valid-color? (sm/lazy-validator ::ctc/recent-color))
+(def valid-fill? (sm/lazy-validator ::cts/fill))
+(def valid-stroke? (sm/lazy-validator ::cts/stroke))
+(def valid-flow? (sm/lazy-validator ::ctp/flow))
+
+(def valid-text-content?
+ (sm/lazy-validator ::ctsx/content))
+
+(def valid-path-content?
+ (sm/lazy-validator ::ctsp/content))
+
+(def valid-path-segment?
+ (sm/lazy-validator ::ctsp/segment))
+
+(def valid-rgb-color-string?
+ (sm/lazy-validator ::ctc/rgb-color))
+
(defn- prepare-file-data
"Apply some specific migrations or fixes to things that are allowed in v1 but not in v2,
or that are the result of old bugs."
[file-data libraries]
(let [detached-ids (volatile! #{})
-
detach-shape
(fn [container shape]
- ;; Detach a shape. If it's inside a component, add it to detached-ids, for further use.
+ ;; Detach a shape. If it's inside a component, add it to detached-ids. This list
+ ;; is used later to process any other copy that was referencing a detached copy.
(let [is-component? (let [root-shape (ctst/get-shape container (:id container))]
(and (some? root-shape) (nil? (:parent-id root-shape))))]
(when is-component?
(vswap! detached-ids conj (:id shape)))
(ctk/detach-shape shape)))
+ fix-bad-children
+ (fn [file-data]
+ ;; Remove any child that does not exist. And also remove duplicated children.
+ (letfn [(fix-container
+ [container]
+ (d/update-when container :objects update-vals (partial fix-shape container)))
+
+ (fix-shape
+ [container shape]
+ (let [objects (:objects container)]
+ (d/update-when shape :shapes
+ (fn [shapes]
+ (->> shapes
+ (d/removev #(nil? (get objects %)))
+ (into [] (distinct)))))))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ fix-missing-image-metadata
+ (fn [file-data]
+ ;; Delete broken image shapes with no metadata.
+ (letfn [(fix-container
+ [container]
+ (d/update-when container :objects #(reduce-kv fix-shape % %)))
+
+ (fix-shape
+ [objects id shape]
+ (if (and (cfh/image-shape? shape)
+ (nil? (:metadata shape)))
+ (-> objects
+ (dissoc id)
+ (d/update-in-when [(:parent-id shape) :shapes]
+ (fn [shapes] (filterv #(not= id %) shapes))))
+ objects))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ fix-page-invalid-options
+ (fn [file-data]
+ (letfn [(update-page [page]
+ (update page :options fix-options))
+
+ (fix-background [options]
+ (if (and (contains? options :background)
+ (not (valid-rgb-color-string? (:background options))))
+ (dissoc options :background)
+ options))
+
+ (fix-options [options]
+ (-> options
+ ;; Some pages has invalid data on flows, we proceed just to
+ ;; delete them.
+ (d/update-when :flows #(filterv valid-flow? %))
+ (fix-background)))]
+
+ (update file-data :pages-index update-vals update-page)))
+
+ ;; Sometimes we found that the file has issues in the internal
+ ;; data structure of the local library; this function tries to
+ ;; fix that issues.
+ fix-file-data
+ (fn [file-data]
+ (-> file-data
+ (d/update-when :colors dissoc nil)
+ (d/update-when :typographies dissoc nil)))
+
+ delete-big-geometry-shapes
+ (fn [file-data]
+ ;; At some point in time, we had a bug that generated shapes
+ ;; with huge geometries that did not validate the
+ ;; schema. Since we don't have a way to fix those shapes, we
+ ;; simply proceed to delete it. We ignore path type shapes
+ ;; because they have not been affected by the bug.
+ (letfn [(fix-container [container]
+ (d/update-when container :objects #(reduce-kv fix-shape % %)))
+
+ (fix-shape [objects id shape]
+ (cond
+ (or (cfh/path-shape? shape)
+ (cfh/bool-shape? shape))
+ objects
+
+ (or (and (number? (:x shape)) (not (sm/valid-safe-number? (:x shape))))
+ (and (number? (:y shape)) (not (sm/valid-safe-number? (:y shape))))
+ (and (number? (:width shape)) (not (sm/valid-safe-number? (:width shape))))
+ (and (number? (:height shape)) (not (sm/valid-safe-number? (:height shape)))))
+ (-> objects
+ (dissoc id)
+ (d/update-in-when [(:parent-id shape) :shapes]
+ (fn [shapes] (filterv #(not= id %) shapes))))
+
+ :else
+ objects))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ ;; Some files has totally broken shapes, we just remove them
+ fix-completly-broken-shapes
+ (fn [file-data]
+ (letfn [(update-object [objects id shape]
+ (cond
+ (nil? (:type shape))
+ (let [ids (cfh/get-children-ids objects id)]
+ (-> objects
+ (dissoc id)
+ (as-> $ (reduce dissoc $ ids))
+ (d/update-in-when [(:parent-id shape) :shapes]
+ (fn [shapes] (filterv #(not= id %) shapes)))))
+
+ (and (cfh/text-shape? shape)
+ (not (seq (:content shape))))
+ (dissoc objects id)
+
+ :else
+ objects))
+
+ (update-container [container]
+ (d/update-when container :objects #(reduce-kv update-object % %)))]
+
+ (-> file-data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+ fix-misc-shape-issues
+ (fn [file-data]
+ (letfn [(fix-container [container]
+ (d/update-when container :objects update-vals fix-shape))
+
+ (fix-shape [shape]
+ (cond-> shape
+ ;; Some shapes has invalid gap value
+ (contains? shape :layout-gap)
+ (d/update-in-when [:layout-gap :column-gap]
+ (fn [gap]
+ (if (or (= gap ##Inf)
+ (= gap ##-Inf))
+ 0
+ gap)))
+
+ (nil? (:name shape))
+ (assoc :name (d/name (:type shape)))
+
+ ;; Fix broken fills
+ (seq (:fills shape))
+ (update :fills (fn [fills] (filterv valid-fill? fills)))
+
+ ;; Fix broken strokes
+ (seq (:strokes shape))
+ (update :strokes (fn [strokes] (filterv valid-stroke? strokes)))
+
+ ;; Fix some broken layout related attrs, probably
+ ;; of copypaste on flex layout betatest period
+ (true? (:layout shape))
+ (assoc :layout :flex)
+
+ (number? (:layout-gap shape))
+ (as-> shape (let [n (:layout-gap shape)]
+ (assoc shape :layout-gap {:row-gap n :column-gap n})))))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ ;; There are some bugs in the past that allows convert text to
+ ;; path and this fix tries to identify this cases and fix them converting
+ ;; the shape back to text shape
+
+ fix-text-shapes-converted-to-path
+ (fn [file-data]
+ (letfn [(fix-container [container]
+ (d/update-when container :objects update-vals fix-shape))
+
+ (fix-shape [shape]
+ (if (and (cfh/path-shape? shape)
+ (contains? shape :content)
+ (some? (:selrect shape))
+ (valid-text-content? (:content shape)))
+ (let [selrect (:selrect shape)]
+ (-> shape
+ (assoc :x (:x selrect))
+ (assoc :y (:y selrect))
+ (assoc :width (:width selrect))
+ (assoc :height (:height selrect))
+ (assoc :type :text)))
+ shape))]
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ fix-broken-paths
+ (fn [file-data]
+ (letfn [(fix-container [container]
+ (d/update-when container :objects update-vals fix-shape))
+
+ (fix-shape [shape]
+ (cond
+ (and (cfh/path-shape? shape)
+ (seq (:content shape))
+ (not (valid-path-content? (:content shape))))
+ (let [shape (update shape :content fix-path-content)
+ [points selrect] (gshp/content->points+selrect shape (:content shape))]
+ (-> shape
+ (dissoc :bool-content)
+ (dissoc :bool-type)
+ (assoc :points points)
+ (assoc :selrect selrect)))
+
+ ;; When we fount a bool shape with no content,
+ ;; we convert it to a simple rect
+ (and (cfh/bool-shape? shape)
+ (not (seq (:bool-content shape))))
+ (let [selrect (or (:selrect shape)
+ (grc/make-rect))
+ points (grc/rect->points selrect)]
+ (-> shape
+ (assoc :x (:x selrect))
+ (assoc :y (:y selrect))
+ (assoc :width (:height selrect))
+ (assoc :height (:height selrect))
+ (assoc :selrect selrect)
+ (assoc :points points)
+ (assoc :type :rect)
+ (assoc :transform (gmt/matrix))
+ (assoc :transform-inverse (gmt/matrix))
+ (dissoc :bool-content)
+ (dissoc :shapes)
+ (dissoc :content)))
+
+ :else
+ shape))
+
+ (fix-path-content [content]
+ (let [[seg1 :as content] (filterv valid-path-segment? content)]
+ (if (and seg1 (not= :move-to (:command seg1)))
+ (let [params (select-keys (:params seg1) [:x :y])]
+ (into [{:command :move-to :params params}] content))
+ content)))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ fix-recent-colors
+ (fn [file-data]
+ ;; Remove invalid colors in :recent-colors
+ (d/update-when file-data :recent-colors
+ (fn [colors]
+ (filterv valid-color? colors))))
+
+ fix-broken-parents
+ (fn [file-data]
+ ;; Find children shapes whose parent-id is not set to the parent that contains them.
+ ;; Remove them from the parent :shapes list.
+ (letfn [(fix-container
+ [container]
+ (d/update-when container :objects #(reduce-kv fix-shape % %)))
+
+ (fix-shape
+ [objects id shape]
+ (reduce (fn [objects child-id]
+ (let [child (get objects child-id)]
+ (cond-> objects
+ (and (some? child) (not= id (:parent-id child)))
+ (d/update-in-when [id :shapes]
+ (fn [shapes] (filterv #(not= child-id %) shapes))))))
+ objects
+ (:shapes shape)))]
+
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
fix-orphan-shapes
(fn [file-data]
;; Find shapes that are not listed in their parent's children list.
@@ -127,13 +436,13 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
remove-nested-roots
(fn [file-data]
;; Remove :component-root in head shapes that are nested.
(letfn [(fix-container [container]
- (update container :objects update-vals (partial fix-shape container)))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
@@ -144,13 +453,13 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
add-not-nested-roots
(fn [file-data]
;; Add :component-root in head shapes that are not nested.
(letfn [(fix-container [container]
- (update container :objects update-vals (partial fix-shape container)))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
@@ -161,13 +470,13 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
fix-orphan-copies
(fn [file-data]
;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't.
(letfn [(fix-container [container]
- (update container :objects update-vals (partial fix-shape container)))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))]
@@ -179,7 +488,7 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
remap-refs
(fn [file-data]
@@ -223,32 +532,32 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
- fix-copies-of-detached
+ fix-converted-copies
(fn [file-data]
- ;; Find any copy that is referencing a detached shape inside a component, and
- ;; undo the nested copy, converting it into a direct copy.
+ ;; If the user has created a copy and then converted into a path or bool,
+ ;; detach it because the synchronization will no longer work.
(letfn [(fix-container [container]
- (update container :objects update-vals fix-shape))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
+
+ (fix-shape [container shape]
+ (if (and (ctk/instance-head? shape)
+ (or (cfh/path-shape? shape)
+ (cfh/bool-shape? shape)))
+ (detach-shape container shape)
+ shape))]
- (fix-shape [shape]
- (cond-> shape
- (@detached-ids (:shape-ref shape))
- (dissoc shape
- :component-id
- :component-file
- :component-root)))]
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
transform-to-frames
(fn [file-data]
;; Transform component and copy heads to frames, and set the
;; frame-id of its childrens
(letfn [(fix-container [container]
- (update container :objects update-vals fix-shape))
+ (d/update-when container :objects update-vals fix-shape))
(fix-shape [shape]
(if (or (nil? (:parent-id shape)) (ctk/instance-head? shape))
@@ -262,7 +571,7 @@
shape))]
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
remap-frame-ids
(fn [file-data]
@@ -270,7 +579,7 @@
;; to point to the head instance.
(letfn [(fix-container
[container]
- (update container :objects update-vals (partial fix-shape container)))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape
[container shape]
@@ -280,14 +589,14 @@
shape)))]
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
fix-frame-ids
(fn [file-data]
;; Ensure that frame-id of all shapes point to the parent or to the frame-id
;; of the parent, and that the destination is indeed a frame.
(letfn [(fix-container [container]
- (update container :objects #(cfh/reduce-objects % fix-shape %)))
+ (d/update-when container :objects #(cfh/reduce-objects % fix-shape %)))
(fix-shape [objects shape]
(let [parent (when (:parent-id shape)
@@ -304,7 +613,7 @@
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))
+ (d/update-when :components update-vals fix-container))))
fix-component-nil-objects
(fn [file-data]
@@ -316,38 +625,91 @@
(dissoc component :objects))
component))]
(-> file-data
- (update :components update-vals fix-component))))
+ (d/update-when :components update-vals fix-component))))
fix-false-copies
(fn [file-data]
;; Find component heads that are not main-instance but have not :shape-ref.
+ ;; Also shapes that have :shape-ref but are not in a copy.
(letfn [(fix-container
[container]
- (update container :objects update-vals fix-shape))
+ (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape
- [shape]
- (if (and (ctk/instance-head? shape)
- (not (ctk/main-instance? shape))
- (not (ctk/in-component-copy? shape)))
- (ctk/detach-shape shape)
+ [container shape]
+ (if (or (and (ctk/instance-head? shape)
+ (not (ctk/main-instance? shape))
+ (not (ctk/in-component-copy? shape)))
+ (and (ctk/in-component-copy? shape)
+ (nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true}))))
+ (detach-shape container shape)
shape))]
(-> file-data
(update :pages-index update-vals fix-container)
- (update :components update-vals fix-container))))]
+ (d/update-when :components update-vals fix-container))))
+
+ fix-copies-of-detached
+ (fn [file-data]
+ ;; Find any copy that is referencing a shape inside a component that have
+ ;; been detached in a previous fix. If so, undo the nested copy, converting
+ ;; it into a direct copy.
+ ;;
+ ;; WARNING: THIS SHOULD BE CALLED AT THE END OF THE PROCESS.
+ (letfn [(fix-container [container]
+ (d/update-when container :objects update-vals fix-shape))
+
+ (fix-shape [shape]
+ (cond-> shape
+ (@detached-ids (:shape-ref shape))
+ (dissoc shape
+ :component-id
+ :component-file
+ :component-root)))]
+ (-> file-data
+ (update :pages-index update-vals fix-container)
+ (d/update-when :components update-vals fix-container))))
+
+ fix-shape-nil-parent-id
+ (fn [file-data]
+ ;; Ensure that parent-id and frame-id are not nil
+ (letfn [(fix-container [container]
+ (d/update-when container :objects update-vals fix-shape))
+
+ (fix-shape [shape]
+ (let [frame-id (or (:frame-id shape)
+ uuid/zero)
+ parent-id (or (:parent-id shape)
+ frame-id)]
+ (assoc shape :frame-id frame-id
+ :parent-id parent-id)))]
+ (-> file-data
+ (update :pages-index update-vals fix-container))))]
(-> file-data
+ (fix-file-data)
+ (fix-page-invalid-options)
+ (fix-completly-broken-shapes)
+ (fix-bad-children)
+ (fix-misc-shape-issues)
+ (fix-recent-colors)
+ (fix-missing-image-metadata)
+ (fix-text-shapes-converted-to-path)
+ (fix-broken-paths)
+ (delete-big-geometry-shapes)
+ (fix-broken-parents)
(fix-orphan-shapes)
+ (fix-orphan-copies)
(remove-nested-roots)
(add-not-nested-roots)
- (fix-orphan-copies)
(remap-refs)
- (fix-copies-of-detached)
+ (fix-converted-copies)
(transform-to-frames)
(remap-frame-ids)
(fix-frame-ids)
(fix-component-nil-objects)
- (fix-false-copies))))
+ (fix-false-copies)
+ (fix-shape-nil-parent-id)
+ (fix-copies-of-detached)))) ; <- Do not add fixes after this one
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS MIGRATION
@@ -574,7 +936,7 @@
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn- collect-and-persist-images
- [svg-data file-id]
+ [svg-data file-id media-id]
(letfn [(process-image [{:keys [href] :as item}]
(try
(let [item (if (str/starts-with? href "data:")
@@ -601,12 +963,13 @@
;; The media processing adds the data to the
;; input map and returns it.
(media/run {:cmd :info :input item}))
-
- (catch Throwable cause
- (l/warn :hint "unexpected exception on processing internal image shape (skiping)"
- :cause cause)
- (when-not *skip-on-error*
- (throw cause)))))
+ (catch Throwable _
+ (let [team-id *team-id*]
+ (l/wrn :hint "unable to process embedded images on svg file"
+ :team-id (str team-id)
+ :file-id (str file-id)
+ :media-id (str media-id)))
+ nil)))
(persist-image [acc {:keys [path size width height mtype href] :as item}]
(let [storage (::sto/storage *system*)
@@ -642,23 +1005,33 @@
(completing persist-image) {}))]
(assoc svg-data :image-data images))))
-(defn- get-svg-content
+(defn- resolve-sobject-id
+ [id]
+ (let [fmobject (db/get *system* :file-media-object {:id id}
+ {::sql/columns [:media-id]})]
+ (:media-id fmobject)))
+
+(defn- get-sobject-content
[id]
(let [storage (::sto/storage *system*)
- conn (::db/conn *system*)
- fmobject (db/get conn :file-media-object {:id id})
- sobject (sto/get-object storage (:media-id fmobject))]
-
+ sobject (sto/get-object storage id)]
(with-open [stream (sto/get-object-data storage sobject)]
(slurp stream))))
(defn- create-shapes-for-svg
[{:keys [id] :as mobj} file-id objects frame-id position]
- (let [svg-text (get-svg-content id)
- svg-text (svgo/optimize *system* svg-text)
- svg-data (-> (csvg/parse svg-text)
- (assoc :name (:name mobj))
- (collect-and-persist-images file-id))]
+ (let [get-svg (fn [sid]
+ (let [svg-text (get-sobject-content sid)
+ svg-text (svgo/optimize *system* svg-text)]
+ (-> (csvg/parse svg-text)
+ (assoc :name (:name mobj)))))
+
+ sid (resolve-sobject-id id)
+ svg-data (if (cache/cache? *cache*)
+ (cache/get *cache* sid (px/wrap-bindings get-svg))
+ (get-svg sid))
+
+ svg-data (collect-and-persist-images svg-data file-id id)]
(sbuilder/create-svg-shapes svg-data position objects frame-id frame-id #{} false)))
@@ -717,42 +1090,64 @@
(defn- create-media-grid
[fdata page-id frame-id grid media-group]
- (let [process (fn [mobj position]
- (let [position (gpt/add position (gpt/point grid-gap grid-gap))
- tp1 (dt/tpoint)]
- (try
- (process-media-object fdata page-id frame-id mobj position)
- (catch Throwable cause
- (l/wrn :hint "unable to process file media object (skiping)"
- :file-id (str (:id fdata))
- :id (str (:id mobj))
- :cause cause)
- (if-not *skip-on-error*
- (throw cause)
- nil))
- (finally
- (l/trc :hint "graphic processed"
- :file-id (str (:id fdata))
- :media-id (str (:id mobj))
- :elapsed (dt/format-duration (tp1)))))))]
+ (letfn [(process [fdata mobj position]
+ (let [position (gpt/add position (gpt/point grid-gap grid-gap))
+ tp (dt/tpoint)
+ err (volatile! false)]
+ (try
+ (let [changes (process-media-object fdata page-id frame-id mobj position)]
+ (cp/process-changes fdata changes false))
+
+ (catch Throwable cause
+ (vreset! err true)
+ (let [cause (pu/unwrap-exception cause)
+ edata (ex-data cause)
+ team-id *team-id*]
+ (cond
+ (instance? org.xml.sax.SAXParseException cause)
+ (l/inf :hint "skip processing media object: invalid svg found"
+ :team-id (str team-id)
+ :file-id (str (:id fdata))
+ :id (str (:id mobj)))
+
+ (instance? org.graalvm.polyglot.PolyglotException cause)
+ (l/inf :hint "skip processing media object: invalid svg found"
+ :team-id (str team-id)
+ :file-id (str (:id fdata))
+ :id (str (:id mobj)))
+
+ (= (:type edata) :not-found)
+ (l/inf :hint "skip processing media object: underlying object does not exist"
+ :team-id (str team-id)
+ :file-id (str (:id fdata))
+ :id (str (:id mobj)))
+
+ :else
+ (let [skip? *skip-on-graphic-error*]
+ (l/wrn :hint "unable to process file media object"
+ :skiped skip?
+ :team-id (str team-id)
+ :file-id (str (:id fdata))
+ :id (str (:id mobj))
+ :cause cause)
+ (when-not skip?
+ (throw cause))))
+ nil))
+ (finally
+ (let [elapsed (tp)]
+ (l/trc :hint "graphic processed"
+ :file-id (str (:id fdata))
+ :media-id (str (:id mobj))
+ :error @err
+ :elapsed (dt/format-duration elapsed)))))))]
(->> (d/zip media-group grid)
- (partition-all (or *max-procs* 1))
- (mapcat (fn [partition]
- (->> partition
- (map (fn [[mobj position]]
- (sse/tap {:type :migration-progress
- :section :graphics
- :name (:name mobj)})
- (p/vthread (process mobj position))))
- (doall)
- (map deref)
- (doall))))
- (filter some?)
- (reduce (fn [fdata changes]
- (-> (assoc-in fdata [:options :components-v2] true)
- (cp/process-changes changes false)))
- fdata))))
+ (reduce (fn [fdata [mobj position]]
+ (sse/tap {:type :migration-progress
+ :section :graphics
+ :name (:name mobj)})
+ (or (process fdata mobj position) fdata))
+ (assoc-in fdata [:options :components-v2] true)))))
(defn- migrate-graphics
[fdata]
@@ -821,9 +1216,13 @@
(decode-row)
(update :data assoc :id id)
(update :data fdata/process-pointers deref)
+ (update :data fdata/process-objects (partial into {}))
+ (update :data (fn [data]
+ (if (> (:version data) 22)
+ (assoc data :version 22)
+ data)))
(fmg/migrate-file))))
-
(defn- get-team
[system team-id]
(-> (db/get system :team {:id team-id}
@@ -832,17 +1231,12 @@
(decode-row)))
(defn- validate-file!
- [file libs throw-on-validate?]
- (try
- (cfv/validate-file! file libs)
- (cfv/validate-file-schema! file)
- (catch Throwable cause
- (if throw-on-validate?
- (throw cause)
- (l/wrn :hint "migrate:file:validation-error" :cause cause)))))
+ [file libs]
+ (cfv/validate-file! file libs)
+ (cfv/validate-file-schema! file))
(defn- process-file
- [{:keys [::db/conn] :as system} id & {:keys [validate? throw-on-validate?]}]
+ [{:keys [::db/conn] :as system} id & {:keys [validate?]}]
(let [file (get-file system id)
libs (->> (files/get-file-libraries conn id)
@@ -855,7 +1249,7 @@
(update :features conj "components/v2"))
_ (when validate?
- (validate-file! file libs throw-on-validate?))
+ (validate-file! file libs))
file (if (contains? (:features file) "fdata/objects-map")
(fdata/enable-objects-map file)
@@ -876,12 +1270,13 @@
(dissoc file :data)))
-
(def ^:private sql:get-and-lock-team-files
"SELECT f.id
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
FOR UPDATE")
(defn- get-and-lock-files
@@ -901,21 +1296,33 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file!
- [system file-id & {:keys [validate? throw-on-validate? max-procs]}]
- (let [tpoint (dt/tpoint)]
+ [system file-id & {:keys [validate? skip-on-graphic-error? label]}]
+ (let [tpoint (dt/tpoint)]
(binding [*file-stats* (atom {})
- *max-procs* max-procs]
+ *skip-on-graphic-error* skip-on-graphic-error?]
(try
- (l/dbg :hint "migrate:file:start" :file-id (str file-id))
+ (l/dbg :hint "migrate:file:start"
+ :file-id (str file-id)
+ :validate validate?
+ :skip-on-graphic-error skip-on-graphic-error?)
(let [system (update system ::sto/storage media/configure-assets-storage)]
(db/tx-run! system
(fn [system]
- (binding [*system* system]
- (fsnap/take-file-snapshot! system {:file-id file-id :label "migration/components-v2"})
- (process-file system file-id
- :validate? validate?
- :throw-on-validate? throw-on-validate?)))))
+ (try
+ (binding [*system* system]
+ (when (string? label)
+ (fsnap/take-file-snapshot! system {:file-id file-id
+ :label (str "migration/" label)}))
+ (process-file system file-id :validate? validate?))
+
+ (catch Throwable cause
+ (let [team-id *team-id*]
+ (l/wrn :hint "error on processing file"
+ :team-id (str team-id)
+ :file-id (str file-id))
+ (throw cause)))))))
+
(finally
(let [elapsed (tpoint)
components (get @*file-stats* :processed/components 0)
@@ -925,46 +1332,52 @@
:file-id (str file-id)
:graphics graphics
:components components
+ :validate validate?
:elapsed (dt/format-duration elapsed))
(some-> *stats* (swap! update :processed/files (fnil inc 0)))
(some-> *team-stats* (swap! update :processed/files (fnil inc 0)))))))))
(defn migrate-team!
- [system team-id & {:keys [validate? throw-on-validate? max-procs]}]
+ [system team-id & {:keys [validate? skip-on-graphic-error? label]}]
(l/dbg :hint "migrate:team:start"
:team-id (dm/str team-id))
(let [tpoint (dt/tpoint)
+ err (volatile! false)
migrate-file
(fn [system file-id]
(migrate-file! system file-id
- :max-procs max-procs
+ :label label
:validate? validate?
- :throw-on-validate? throw-on-validate?))
+ :skip-on-graphic-error? skip-on-graphic-error?))
migrate-team
- (fn [{:keys [::db/conn] :as system} {:keys [id features] :as team}]
- (let [features (-> features
- (disj "ephimeral/v2-migration")
- (conj "components/v2")
- (conj "layout/grid")
- (conj "styles/v2"))]
+ (fn [{:keys [::db/conn] :as system} team-id]
+ (let [{:keys [id features]} (get-team system team-id)]
+ (if (contains? features "components/v2")
+ (l/inf :hint "team already migrated")
+ (let [features (-> features
+ (disj "ephimeral/v2-migration")
+ (conj "components/v2")
+ (conj "layout/grid")
+ (conj "styles/v2"))]
- (run! (partial migrate-file system)
- (get-and-lock-files conn id))
+ (run! (partial migrate-file system)
+ (get-and-lock-files conn id))
- (update-team-features! conn id features)))]
+ (update-team-features! conn id features)))))]
- (binding [*team-stats* (atom {})]
+ (binding [*team-stats* (atom {})
+ *team-id* team-id]
(try
- (db/tx-run! system (fn [system]
- (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"])
- (let [team (get-team system team-id)]
- (if (contains? (:features team) "components/v2")
- (l/inf :hint "team already migrated")
- (migrate-team system team)))))
+ (db/tx-run! system migrate-team team-id)
+
+ (catch Throwable cause
+ (vreset! err true)
+ (throw cause))
+
(finally
(let [elapsed (tpoint)
components (get @*team-stats* :processed/components 0)
@@ -973,9 +1386,21 @@
(some-> *stats* (swap! update :processed/teams (fnil inc 0)))
- (l/dbg :hint "migrate:team:end"
- :team-id (dm/str team-id)
- :files files
- :components components
- :graphics graphics
- :elapsed (dt/format-duration elapsed))))))))
+ (if (cache/cache? *cache*)
+ (let [cache-stats (cache/stats *cache*)]
+ (l/dbg :hint "migrate:team:end"
+ :team-id (dm/str team-id)
+ :files files
+ :components components
+ :graphics graphics
+ :crt (mth/to-fixed (:hit-rate cache-stats) 2)
+ :crq (str (:req-count cache-stats))
+ :error @err
+ :elapsed (dt/format-duration elapsed)))
+
+ (l/dbg :hint "migrate:team:end"
+ :team-id (dm/str team-id)
+ :files files
+ :components components
+ :graphics graphics
+ :elapsed (dt/format-duration elapsed)))))))))
diff --git a/backend/src/app/features/fdata.clj b/backend/src/app/features/fdata.clj
index 68e58833cd..8a57a1aa17 100644
--- a/backend/src/app/features/fdata.clj
+++ b/backend/src/app/features/fdata.clj
@@ -27,7 +27,7 @@
(update :data (fn [fdata]
(-> fdata
(update :pages-index update-vals update-fn)
- (update :components update-vals update-fn))))
+ (d/update-when :components update-vals update-fn))))
(update :features conj "fdata/objects-map"))))
(defn process-objects
@@ -110,6 +110,6 @@
(update :data (fn [fdata]
(-> fdata
(update :pages-index update-vals pmap/wrap)
- (update :components pmap/wrap))))
+ (d/update-when :components pmap/wrap))))
(update :features conj "fdata/pointer-map")))
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index c80210a06b..7028be8bfe 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -301,7 +301,8 @@
::sto/storage (ig/ref ::sto/storage)}
:app.rpc/climit
- {::mtx/metrics (ig/ref ::mtx/metrics)}
+ {::mtx/metrics (ig/ref ::mtx/metrics)
+ ::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/executor)}
@@ -410,8 +411,7 @@
::migrations (ig/ref :app.migrations/migrations)}
::svgo/optimizer
- {::wrk/executor (ig/ref ::wrk/executor)
- ::svgo/max-procs (cf/get :svgo-max-procs)}
+ {}
::audit.tasks/archive
{::props (ig/ref ::setup/props)
diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj
index b730ab1063..58023fe00e 100644
--- a/backend/src/app/redis.clj
+++ b/backend/src/app/redis.clj
@@ -91,7 +91,7 @@
(s/def ::connect? ::us/boolean)
(s/def ::io-threads ::us/integer)
(s/def ::worker-threads ::us/integer)
-(s/def ::cache some?)
+(s/def ::cache cache/cache?)
(s/def ::redis
(s/keys :req [::resources
@@ -168,7 +168,7 @@
(defn- shutdown-resources
[{:keys [::resources ::cache ::timer]}]
- (cache/invalidate-all! cache)
+ (cache/invalidate! cache)
(when resources
(.shutdown ^ClientResources resources))
@@ -211,7 +211,8 @@
(defn get-or-connect
[{:keys [::cache] :as state} key options]
(us/assert! ::redis state)
- (let [connection (cache/get cache key (fn [_] (connect* state options)))]
+ (let [create (fn [_] (connect* state options))
+ connection (cache/get cache key create)]
(-> state
(dissoc ::cache)
(assoc ::connection connection))))
diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj
index d6e4ccb51b..71c64b596a 100644
--- a/backend/src/app/rpc/climit.clj
+++ b/backend/src/app/rpc/climit.clj
@@ -36,24 +36,14 @@
(-> (str id)
(subs 1)))
-(defn- create-bulkhead-cache
- [config]
- (letfn [(load-fn [[id skey]]
- (when-let [config (get config id)]
- (l/trc :hint "insert into cache" :id (id->str id) :key skey)
- (pbh/create :permits (or (:permits config) (:concurrency config))
- :queue (or (:queue config) (:queue-size config))
- :timeout (:timeout config)
- :type :semaphore)))
-
- (on-remove [key _ cause]
+(defn- create-cache
+ [{:keys [::wrk/executor]}]
+ (letfn [(on-remove [key _ cause]
(let [[id skey] key]
- (l/trc :hint "evict from cache" :id (id->str id) :key skey :reason (str cause))))]
-
- (cache/create :executor :same-thread
+ (l/dbg :hint "destroy limiter" :id (id->str id) :key skey :reason (str cause))))]
+ (cache/create :executor executor
:on-remove on-remove
- :keepalive "5m"
- :load-fn load-fn)))
+ :keepalive "5m")))
(s/def ::config/permits ::us/integer)
(s/def ::config/queue ::us/integer)
@@ -70,7 +60,7 @@
(s/def ::path ::fs/path)
(defmethod ig/pre-init-spec ::rpc/climit [_]
- (s/keys :req [::mtx/metrics ::path]))
+ (s/keys :req [::mtx/metrics ::wrk/executor ::path]))
(defmethod ig/init-key ::rpc/climit
[_ {:keys [::path ::mtx/metrics] :as cfg}]
@@ -78,7 +68,7 @@
(when-let [params (some->> path slurp edn/read-string)]
(l/inf :hint "initializing concurrency limit" :config (str path))
(us/verify! ::config params)
- {::cache (create-bulkhead-cache params)
+ {::cache (create-cache cfg)
::config params
::mtx/metrics metrics})))
@@ -89,13 +79,17 @@
(s/def ::rpc/climit
(s/nilable ::instance))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; PUBLIC API
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defn- create-limiter
+ [config [id skey]]
+ (l/dbg :hint "create limiter" :id (id->str id) :key skey)
+ (pbh/create :permits (or (:permits config) (:concurrency config))
+ :queue (or (:queue config) (:queue-size config))
+ :timeout (:timeout config)
+ :type :semaphore))
-(defn invoke!
- [cache metrics id key f]
- (if-let [limiter (cache/get cache [id key])]
+(defn- invoke!
+ [config cache metrics id key f]
+ (if-let [limiter (cache/get cache [id key] (partial create-limiter config))]
(let [tpoint (dt/tpoint)
labels (into-array String [(id->str id)])
wrapped (fn []
@@ -147,7 +141,7 @@
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats))
- (pbh/invoke! limiter wrapped))
+ (px/invoke! limiter wrapped))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
@@ -160,9 +154,43 @@
(measure! (pbh/get-stats limiter)))))
(do
- (l/wrn :hint "unable to load limiter" :id (id->str id))
+ (l/wrn :hint "no limiter found" :id (id->str id))
(f))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; MIDDLEWARE
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def noop-fn (constantly nil))
+
+(defn wrap
+ [{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
+ (if (and (some? climit) (some? id))
+ (let [cache (::cache climit)
+ config (::config climit)]
+ (if-let [config (get config id)]
+ (do
+ (l/dbg :hint "instrumenting method"
+ :limit (id->str id)
+ :service-name (::sv/name mdata)
+ :timeout (:timeout config)
+ :permits (:permits config)
+ :queue (:queue config)
+ :keyed? (not= key-fn noop-fn))
+
+ (fn [cfg params]
+ (invoke! config cache metrics id (key-fn params) (partial f cfg params))))
+
+ (do
+ (l/wrn :hint "no config found for specified queue" :id (id->str id))
+ f)))
+
+ f))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; PUBLIC API
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(defn configure
[{:keys [::rpc/climit]} id]
(us/assert! ::rpc/climit climit)
@@ -171,37 +199,14 @@
(defn run!
"Run a function in context of climit.
Intended to be used in virtual threads."
- ([{:keys [::id ::cache ::mtx/metrics]} f]
- (if (and cache id)
- (invoke! cache metrics id nil f)
+ ([{:keys [::id ::cache ::config ::mtx/metrics]} f]
+ (if-let [config (get config id)]
+ (invoke! config cache metrics id nil f)
(f)))
- ([{:keys [::id ::cache ::mtx/metrics]} f executor]
+ ([{:keys [::id ::cache ::config ::mtx/metrics]} f executor]
(let [f #(p/await! (px/submit! executor f))]
- (if (and cache id)
- (invoke! cache metrics id nil f)
+ (if-let [config (get config id)]
+ (invoke! config cache metrics id nil f)
(f)))))
-(def noop-fn (constantly nil))
-
-(defn wrap
- [{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
- (if (and (some? climit) (some? id))
- (if-let [config (get-in climit [::config id])]
- (let [cache (::cache climit)]
- (l/dbg :hint "instrumenting method"
- :limit (id->str id)
- :service-name (::sv/name mdata)
- :timeout (:timeout config)
- :permits (:permits config)
- :queue (:queue config)
- :keyed? (not= key-fn noop-fn))
-
- (fn [cfg params]
- (invoke! cache metrics id (key-fn params) (partial f cfg params))))
-
- (do
- (l/wrn :hint "no config found for specified queue" :id (id->str id))
- f))
-
- f))
diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj
index 949d528acf..c9b55b599e 100644
--- a/backend/src/app/rpc/commands/auth.clj
+++ b/backend/src/app/rpc/commands/auth.clj
@@ -333,7 +333,9 @@
(defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
- params (assoc claims :fullname fullname)
+ params (-> claims
+ (into params)
+ (assoc :fullname fullname))
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)))
diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj
index 81bb6e10a7..ebebbc42f8 100644
--- a/backend/src/app/rpc/commands/binfile.clj
+++ b/backend/src/app/rpc/commands/binfile.clj
@@ -664,9 +664,7 @@
(case feature
"components/v2"
(feat.compv2/migrate-file! options file-id
- :max-procs 2
- :validate? validate?
- :throw-on-validate? true)
+ :validate? validate?)
"fdata/shape-data-type"
nil
diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj
index f6b6c615da..f2e6a19899 100644
--- a/backend/src/app/rpc/commands/files.clj
+++ b/backend/src/app/rpc/commands/files.clj
@@ -226,23 +226,37 @@
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* (pmap/create-tracked)]
- (let [file (fmg/migrate-file file)]
+ (let [;; For avoid unnecesary overhead of creating multiple pointers and
+ ;; handly internally with objects map in their worst case (when
+ ;; probably all shapes and all pointers will be readed in any
+ ;; case), we just realize/resolve them before applying the
+ ;; migration to the file
+ file (-> file
+ (update :data feat.fdata/process-pointers deref)
+ (update :data feat.fdata/process-objects (partial into {}))
+ (fmg/migrate-file))
- ;; NOTE: when file is migrated, we break the rule of no perform
- ;; mutations on get operations and update the file with all
- ;; migrations applied
- ;;
- ;; NOTE: the following code will not work on read-only mode, it
- ;; is a known issue; we keep is not implemented until we really
- ;; need this
- (when (fmg/migrated? file)
- (db/update! conn :file
- {:data (blob/encode (:data file))
- :features (db/create-array conn "text" (:features file))}
- {:id id})
+ ;; When file is migrated, we break the rule of no perform
+ ;; mutations on get operations and update the file with all
+ ;; migrations applied
+ ;;
+ ;; WARN: he following code will not work on read-only mode,
+ ;; it is a known issue; we keep is not implemented until we
+ ;; really need this.
+ file (if (contains? (:features file) "fdata/objects-map")
+ (feat.fdata/enable-objects-map file)
+ file)
+ file (if (contains? (:features file) "fdata/pointer-map")
+ (feat.fdata/enable-pointer-map file)
+ file)]
- (when (contains? (:features file) "fdata/pointer-map")
- (feat.fdata/persist-pointers! cfg id)))
+ (db/update! conn :file
+ {:data (blob/encode (:data file))
+ :features (db/create-array conn "text" (:features file))}
+ {:id id})
+
+ (when (contains? (:features file) "fdata/pointer-map")
+ (feat.fdata/persist-pointers! cfg id))
file)))
@@ -266,7 +280,7 @@
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(decode-row))]
- (if migrate?
+ (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file)
file)))
diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj
index 59273f033e..dbbd1c04d1 100644
--- a/backend/src/app/rpc/commands/files_create.clj
+++ b/backend/src/app/rpc/commands/files_create.clj
@@ -18,14 +18,12 @@
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
- [app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.rpc.quotes :as quotes]
[app.util.blob :as blob]
- [app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -50,47 +48,52 @@
"expected a valid connection"
(db/connection? conn))
- (let [id (or id (uuid/next))
+ (binding [pmap/*tracked* (pmap/create-tracked)
+ cfeat/*current* features]
+ (let [id (or id (uuid/next))
- pointers (pmap/create-tracked)
- pmap? (contains? features "fdata/pointer-map")
- omap? (contains? features "fdata/objects-map")
-
- data (binding [pmap/*tracked* pointers
- cfeat/*current* features
- cfeat/*wrap-with-objects-map-fn* (if omap? omap/wrap identity)
- cfeat/*wrap-with-pointer-map-fn* (if pmap? pmap/wrap identity)]
- (if create-page
+ data (if create-page
(ctf/make-file-data id)
- (ctf/make-file-data id nil)))
+ (ctf/make-file-data id nil))
- features (->> (set/difference features cfeat/frontend-only-features)
- (db/create-array conn "text"))
+ file {:id id
+ :project-id project-id
+ :name name
+ :revn revn
+ :is-shared is-shared
+ :data data
+ :features features
+ :ignore-sync-until ignore-sync-until
+ :modified-at modified-at
+ :deleted-at deleted-at}
- file (db/insert! conn :file
- (d/without-nils
- {:id id
- :project-id project-id
- :name name
- :revn revn
- :is-shared is-shared
- :data (blob/encode data)
- :features features
- :ignore-sync-until ignore-sync-until
- :modified-at modified-at
- :deleted-at deleted-at}))]
+ file (if (contains? features "fdata/objects-map")
+ (feat.fdata/enable-objects-map file)
+ file)
- (binding [pmap/*tracked* pointers]
- (feat.fdata/persist-pointers! cfg id))
+ file (if (contains? features "fdata/pointer-map")
+ (feat.fdata/enable-pointer-map file)
+ file)
- (->> (assoc params :file-id id :role :owner)
- (create-file-role! conn))
+ file (d/without-nils file)]
- (db/update! conn :project
- {:modified-at (dt/now)}
- {:id project-id})
+ (db/insert! conn :file
+ (-> file
+ (update :data blob/encode)
+ (update :features db/encode-pgarray conn "text"))
+ {::db/return-keys false})
- (files/decode-row file)))
+ (when (contains? features "fdata/pointer-map")
+ (feat.fdata/persist-pointers! cfg id))
+
+ (->> (assoc params :file-id id :role :owner)
+ (create-file-role! conn))
+
+ (db/update! conn :project
+ {:modified-at (dt/now)}
+ {:id project-id})
+
+ file)))
(def ^:private schema:create-file
[:map {:title "create-file"}
diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj
index 03e1b04da4..134a127946 100644
--- a/backend/src/app/rpc/commands/files_update.clj
+++ b/backend/src/app/rpc/commands/files_update.clj
@@ -292,9 +292,20 @@
(let [file (update file :data (fn [data]
(-> data
(blob/decode)
- (assoc :id (:id file))
- (fmg/migrate-data)
- (d/without-nils))))
+ (assoc :id (:id file)))))
+
+ ;; For avoid unnecesary overhead of creating multiple pointers
+ ;; and handly internally with objects map in their worst
+ ;; case (when probably all shapes and all pointers will be
+ ;; readed in any case), we just realize/resolve them before
+ ;; applying the migration to the file
+ file (if (fmg/need-migration? file)
+ (-> file
+ (update :data feat.fdata/process-pointers deref)
+ (update :data feat.fdata/process-objects (partial into {}))
+ (fmg/migrate-file))
+ file)
+
;; WARNING: this ruins performance; maybe we need to find
;; some other way to do general validation
@@ -305,14 +316,20 @@
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
+ ;; We do not resolve the objects maps here
+ ;; because there is a lower probability that all
+ ;; shapes needed to be loded into memory, so we
+ ;; leeave it on lazy status
(-> (files/get-file cfg id :migrate? false)
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(fmg/migrate-file))))))
(d/index-by :id)))
+
file (-> (files/check-version! file)
(update :revn inc)
- (update :data cpc/process-changes changes))]
+ (update :data cpc/process-changes changes)
+ (update :data d/without-nils))]
(when (contains? cf/flags :soft-file-validation)
(soft-validate-file! file libs))
@@ -329,12 +346,10 @@
(val/validate-file-schema! file))
(cond-> file
- (and (contains? cfeat/*current* "fdata/objects-map")
- (not (contains? cfeat/*previous* "fdata/objects-map")))
+ (contains? cfeat/*current* "fdata/objects-map")
(feat.fdata/enable-objects-map)
- (and (contains? cfeat/*current* "fdata/pointer-map")
- (not (contains? cfeat/*previous* "fdata/pointer-map")))
+ (contains? cfeat/*current* "fdata/pointer-map")
(feat.fdata/enable-pointer-map)
:always
diff --git a/backend/src/app/srepl/components_v2.clj b/backend/src/app/srepl/components_v2.clj
index fec852f082..5e6a697bb7 100644
--- a/backend/src/app/srepl/components_v2.clj
+++ b/backend/src/app/srepl/components_v2.clj
@@ -6,13 +6,18 @@
(ns app.srepl.components-v2
(:require
+ [app.common.data :as d]
[app.common.logging :as l]
[app.common.pprint :as pp]
+ [app.common.uuid :as uuid]
[app.db :as db]
[app.features.components-v2 :as feat]
+ [app.main :as main]
+ [app.svgo :as svgo]
+ [app.util.cache :as cache]
[app.util.time :as dt]
+ [app.worker :as-alias wrk]
[cuerdas.core :as str]
- [promesa.core :as p]
[promesa.exec :as px]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
@@ -35,14 +40,9 @@
(fn [_ _ oldv newv]
(when (not= (:processed/files oldv)
(:processed/files newv))
- (let [total (:total/files newv)
- completed (:processed/files newv)
- progress (/ (* completed 100.0) total)
- elapsed (tpoint)]
+ (let [elapsed (tpoint)]
(l/dbg :hint "progress"
:completed (:processed/files newv)
- :total (:total/files newv)
- :progress (str (int progress) "%")
:elapsed (dt/format-duration elapsed))))))
(defn- report-progress-teams
@@ -50,88 +50,147 @@
(fn [_ _ oldv newv]
(when (not= (:processed/teams oldv)
(:processed/teams newv))
- (let [total (:total/teams newv)
- completed (:processed/teams newv)
- progress (/ (* completed 100.0) total)
- progress (str (int progress) "%")
+ (let [completed (:processed/teams newv)
elapsed (dt/format-duration (tpoint))]
-
(when (fn? on-progress)
- (on-progress {:total total
- :elapsed elapsed
- :completed completed
- :progress progress}))
-
+ (on-progress {:elapsed elapsed
+ :completed completed}))
(l/dbg :hint "progress"
:completed completed
- :progress progress
:elapsed elapsed)))))
-(defn- get-total-files
- [pool & {:keys [team-id]}]
- (if (some? team-id)
- (let [sql (str/concat
- "SELECT count(f.id) AS count FROM file AS f "
- " JOIN project AS p ON (p.id = f.project_id) "
- " WHERE p.team_id = ? AND f.deleted_at IS NULL "
- " AND p.deleted_at IS NULL")
- res (db/exec-one! pool [sql team-id])]
- (:count res))
+(def ^:private sql:get-teams-by-created-at
+ "WITH teams AS (
+ SELECT id, features
+ FROM team
+ WHERE deleted_at IS NULL
+ ORDER BY created_at DESC
+ ) SELECT * FROM TEAMS %(pred)s")
- (let [sql (str/concat
- "SELECT count(id) AS count FROM file "
- " WHERE deleted_at IS NULL")
- res (db/exec-one! pool [sql])]
- (:count res))))
+(def ^:private sql:get-teams-by-graphics
+ "WITH teams AS (
+ SELECT t.id, t.features,
+ (SELECT count(*)
+ FROM file_media_object AS fmo
+ JOIN file AS f ON (f.id = fmo.file_id)
+ JOIN project AS p ON (p.id = f.project_id)
+ WHERE p.team_id = t.id
+ AND fmo.mtype = 'image/svg+xml'
+ AND fmo.is_local = false) AS graphics
+ FROM team AS t
+ WHERE t.deleted_at IS NULL
+ ORDER BY 3 ASC
+ )
+ SELECT * FROM teams %(pred)s")
-(defn- get-total-teams
- [pool]
- (let [sql (str/concat
- "SELECT count(id) AS count FROM team "
- " WHERE deleted_at IS NULL")
- res (db/exec-one! pool [sql])]
- (:count res)))
+(def ^:private sql:get-teams-by-activity
+ "WITH teams AS (
+ SELECT t.id, t.features,
+ (SELECT coalesce(max(date_trunc('month', f.modified_at)), date_trunc('month', t.modified_at))
+ FROM file AS f
+ JOIN project AS p ON (f.project_id = p.id)
+ WHERE p.team_id = t.id) AS updated_at,
+ (SELECT coalesce(count(*), 0)
+ FROM file AS f
+ JOIN project AS p ON (f.project_id = p.id)
+ WHERE p.team_id = t.id) AS total_files
+ FROM team AS t
+ WHERE t.deleted_at IS NULL
+ ORDER BY 3 DESC, 4 DESC
+ )
+ SELECT * FROM teams %(pred)s")
+(def ^:private sql:get-teams-by-report
+ "WITH teams AS (
+ SELECT t.id t.features, mr.name
+ FROM migration_report AS mr
+ JOIN team AS t ON (t.id = mr.team_id)
+ WHERE t.deleted_at IS NULL
+ AND mr.error IS NOT NULL
+ ORDER BY mr.created_at
+ ) SELECT id, features FROM teams %(pred)s")
-(defn- mark-team-migration!
- [{:keys [::db/pool]} team-id]
- ;; We execute this out of transaction because we want this
- ;; change to be visible to all other sessions before starting
- ;; the migration
- (let [sql (str "UPDATE team SET features = "
- " array_append(features, 'ephimeral/v2-migration') "
- " WHERE id = ?")]
- (db/exec-one! pool [sql team-id])))
+(defn- read-pred
+ [entries]
+ (let [entries (if (and (vector? entries)
+ (keyword? (first entries)))
+ [entries]
+ entries)]
+ (loop [params []
+ queries []
+ entries (seq entries)]
+ (if-let [[op val field] (first entries)]
+ (let [field (name field)
+ cond (case op
+ :lt (str/ffmt "% < ?" field)
+ :lte (str/ffmt "% <= ?" field)
+ :gt (str/ffmt "% > ?" field)
+ :gte (str/ffmt "% >= ?" field)
+ :eq (str/ffmt "% = ?" field))]
+ (recur (conj params val)
+ (conj queries cond)
+ (rest entries)))
-(defn- unmark-team-migration!
- [{:keys [::db/pool]} team-id]
- ;; We execute this out of transaction because we want this
- ;; change to be visible to all other sessions before starting
- ;; the migration
- (let [sql (str "UPDATE team SET features = "
- " array_remove(features, 'ephimeral/v2-migration') "
- " WHERE id = ?")]
- (db/exec-one! pool [sql team-id])))
-
-(def ^:private sql:get-teams
- "SELECT id, features
- FROM team
- WHERE deleted_at IS NULL
- ORDER BY created_at ASC")
+ (let [sql (apply str "WHERE " (str/join " AND " queries))]
+ (apply vector sql params))))))
(defn- get-teams
- [conn]
- (->> (db/cursor conn sql:get-teams)
- (map feat/decode-row)))
+ [conn query pred]
+ (let [query (d/nilv query :created-at)
+ sql (case query
+ :created-at sql:get-teams-by-created-at
+ :activity sql:get-teams-by-activity
+ :graphics sql:get-teams-by-graphics
+ :report sql:get-teams-by-report)
+
+ sql (if pred
+ (let [[pred-sql & pred-params] (read-pred pred)]
+ (apply vector
+ (str/format sql {:pred pred-sql})
+ pred-params))
+ [(str/format sql {:pred ""})])]
+
+ (->> (db/cursor conn sql {:chunk-size 500})
+ (map feat/decode-row)
+ (remove (fn [{:keys [features]}]
+ (contains? features "components/v2")))
+ (map :id))))
+
+(def ^:private sql:report-table
+ "CREATE UNLOGGED TABLE IF NOT EXISTS migration_report (
+ id bigserial NOT NULL,
+ label text NOT NULL,
+ team_id UUID NOT NULL,
+ error text NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ elapsed bigint NOT NULL,
+ PRIMARY KEY (label, created_at, id)
+ )")
+
+(defn- create-report-table!
+ [system]
+ (db/exec-one! system [sql:report-table]))
+
+(defn- clean-reports!
+ [system label]
+ (db/delete! system :migration-report {:label label}))
+
+(defn- report!
+ [system team-id label elapsed error]
+ (db/insert! system :migration-report
+ {:label label
+ :team-id team-id
+ :elapsed (inst-ms elapsed)
+ :error error}
+ {::db/return-keys false}))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file!
- [system file-id & {:keys [rollback? max-procs]
- :or {rollback? true}}]
-
+ [file-id & {:keys [rollback? validate? label] :or {rollback? true validate? false}}]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [tpoint (dt/tpoint)
file-id (if (string? file-id)
@@ -139,8 +198,10 @@
file-id)]
(binding [feat/*stats* (atom {})]
(try
- (-> (assoc system ::db/rollback rollback?)
- (feat/migrate-file! file-id :max-procs max-procs))
+ (-> (assoc main/system ::db/rollback rollback?)
+ (feat/migrate-file! file-id
+ :validate? validate?
+ :label label))
(-> (deref feat/*stats*)
(assoc :elapsed (dt/format-duration (tpoint))))
@@ -153,47 +214,36 @@
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-team!
- [{:keys [::db/pool] :as system} team-id & {:keys [rollback? skip-on-error validate? max-procs]
- :or {rollback? true
- skip-on-error true
- validate? false
- max-procs 1}
- :as opts}]
+ [team-id & {:keys [rollback? skip-on-graphic-error? validate? label]
+ :or {rollback? true
+ validate? true
+ skip-on-graphic-error? false}}]
(l/dbg :hint "migrate:start" :rollback rollback?)
(let [team-id (if (string? team-id)
(parse-uuid team-id)
team-id)
- total (get-total-files pool :team-id team-id)
- stats (atom {:total/files total})
+ stats (atom {})
tpoint (dt/tpoint)]
(add-watch stats :progress-report (report-progress-files tpoint))
- (binding [feat/*stats* stats
- feat/*skip-on-error* skip-on-error]
-
+ (binding [feat/*stats* stats]
(try
- (mark-team-migration! system team-id)
-
- (-> (assoc system ::db/rollback rollback?)
+ (-> (assoc main/system ::db/rollback rollback?)
(feat/migrate-team! team-id
- :max-procs max-procs
+ :label label
:validate? validate?
- :throw-on-validate? (not skip-on-error)))
-
+ :skip-on-graphic-error? skip-on-graphic-error?))
(print-stats!
(-> (deref feat/*stats*)
- (dissoc :total/files)
(assoc :elapsed (dt/format-duration (tpoint)))))
(catch Throwable cause
(l/dbg :hint "migrate:error" :cause cause))
(finally
- (unmark-team-migration! system team-id)
-
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
@@ -204,98 +254,118 @@
until thw maximum number of jobs is reached which by default has the
value of `1`. This is controled with the `:max-jobs` option.
- Each tram migration process also can start multiple procs for
- graphics migration, the total of that procs is controled with the
- `:max-procs` option.
+ If you want to run this on multiple machines you will need to specify
+ the total number of partitions and the current partition.
- Internally, the graphics migration process uses SVGO module which by
- default has a limited number of maximum concurent
- operations (globally), ensure setting up correct number with
- PENPOT_SVGO_MAX_PROCS environment variable."
+ In order to get the report table populated, you will need to provide
+ a correct `:label`. That label is also used for persist a file
+ snaphot before continue with the migration."
+ [& {:keys [max-jobs max-items max-time rollback? validate? query
+ pred max-procs cache on-start on-progress on-error on-end
+ skip-on-graphic-error? label partitions current-partition]
+ :or {validate? false
+ rollback? true
+ max-jobs 1
+ current-partition 1
+ skip-on-graphic-error? true
+ max-items Long/MAX_VALUE}}]
- [{:keys [::db/pool] :as system} & {:keys [max-jobs max-procs max-items
- rollback? validate? preset
- skip-on-error max-time
- on-start on-progress on-error on-end]
- :or {validate? false
- rollback? true
- skip-on-error true
- preset :shutdown-on-failure
- max-jobs 1
- max-procs 10
- max-items Long/MAX_VALUE}
- :as opts}]
+ (when (int? partitions)
+ (when-not (int? current-partition)
+ (throw (IllegalArgumentException. "missing `current-partition` parameter")))
+ (when-not (<= 0 current-partition partitions)
+ (throw (IllegalArgumentException. "invalid value on `current-partition` parameter"))))
- (let [total (get-total-teams pool)
- stats (atom {:total/teams (min total max-items)})
+ (let [stats (atom {})
+ tpoint (dt/tpoint)
+ mtime (some-> max-time dt/duration)
- tpoint (dt/tpoint)
- mtime (some-> max-time dt/duration)
+ factory (px/thread-factory :virtual false :prefix "penpot/migration/")
+ executor (px/cached-executor :factory factory)
- scope (px/structured-task-scope :preset preset :factory :virtual)
- sjobs (ps/create :permits max-jobs)
+ max-procs (or max-procs max-jobs)
+ sjobs (ps/create :permits max-jobs)
+ sprocs (ps/create :permits max-procs)
+ cache (if (int? cache)
+ (cache/create :executor executor
+ :max-items cache)
+ nil)
migrate-team
- (fn [{:keys [id features] :as team}]
+ (fn [team-id]
+ (let [tpoint (dt/tpoint)]
+ (try
+ (db/tx-run! (assoc main/system ::db/rollback rollback?)
+ (fn [system]
+ (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"])
+ (feat/migrate-team! system team-id
+ :label label
+ :validate? validate?
+ :skip-on-graphic-error? skip-on-graphic-error?)))
+
+ (when (string? label)
+ (report! main/system team-id label (tpoint) nil))
+
+ (catch Throwable cause
+ (l/wrn :hint "unexpected error on processing team (skiping)"
+ :team-id (str team-id)
+ :cause cause)
+ (when (string? label)
+ (report! main/system team-id label (tpoint) (ex-message cause))))
+
+ (finally
+ (ps/release! sjobs)))))
+
+ process-team
+ (fn [team-id]
(ps/acquire! sjobs)
(let [ts (tpoint)]
- (cond
- (and mtime (neg? (compare mtime ts)))
+ (if (and mtime (neg? (compare mtime ts)))
(do
(l/inf :hint "max time constraint reached"
- :team-id (str id)
+ :team-id (str team-id)
:elapsed (dt/format-duration ts))
(ps/release! sjobs)
(reduced nil))
- (or (contains? features "ephimeral/v2-migration")
- (contains? features "components/v2"))
- (do
- (l/dbg :hint "skip team" :team-id (str id))
- (ps/release! sjobs))
-
- :else
- (px/submit! scope (fn []
- (try
- (mark-team-migration! system id)
- (-> (assoc system ::db/rollback rollback?)
- (feat/migrate-team! id
- :max-procs max-procs
- :validate? validate?
- :throw-on-validate? (not skip-on-error)))
- (catch Throwable cause
- (l/err :hint "unexpected error on processing team"
- :team-id (str id)
- :cause cause))
- (finally
- (ps/release! sjobs)
- (unmark-team-migration! system id))))))))]
+ (px/run! executor (partial migrate-team team-id)))))]
(l/dbg :hint "migrate:start"
+ :label label
:rollback rollback?
- :total total
:max-jobs max-jobs
- :max-procs max-procs
:max-items max-items)
(add-watch stats :progress-report (report-progress-teams tpoint on-progress))
(binding [feat/*stats* stats
- feat/*skip-on-error* skip-on-error]
+ feat/*cache* cache
+ svgo/*semaphore* sprocs]
(try
(when (fn? on-start)
- (on-start {:total total :rollback rollback?}))
+ (on-start {:rollback rollback?}))
- (db/tx-run! system
- (fn [{:keys [::db/conn]}]
- (run! (partial migrate-team)
- (->> (get-teams conn)
- (take max-items)))))
- (try
- (p/await! scope)
- (finally
- (pu/close! scope)))
+ (when (string? label)
+ (create-report-table! main/system)
+ (clean-reports! main/system label))
+ (db/tx-run! main/system
+ (fn [{:keys [::db/conn] :as system}]
+ (db/exec! conn ["SET statement_timeout = 0"])
+ (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
+
+ (run! process-team
+ (->> (get-teams conn query pred)
+ (filter (fn [team-id]
+ (if (int? partitions)
+ (= current-partition (-> (uuid/hash-int team-id)
+ (mod partitions)
+ (inc)))
+ true)))
+ (take max-items)))
+
+ ;; Close and await tasks
+ (pu/close! executor)))
(if (fn? on-end)
(-> (deref stats)
diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj
index f1a267e48d..5e2a26ee96 100644
--- a/backend/src/app/srepl/helpers.clj
+++ b/backend/src/app/srepl/helpers.clj
@@ -69,7 +69,8 @@
(fn [system]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)]
(-> (files/get-file system id :migrate? migrate?)
- (update :data feat.fdata/process-pointers deref))))))
+ (update :data feat.fdata/process-pointers deref)
+ (update :data feat.fdata/process-objects (partial into {})))))))
(defn validate
"Validate structure, referencial integrity and semantic coherence of
diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj
index e019ad2676..1bbb38b16a 100644
--- a/backend/src/app/storage/s3.clj
+++ b/backend/src/app/storage/s3.clj
@@ -51,6 +51,7 @@
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
software.amazon.awssdk.services.s3.model.GetObjectRequest
+ software.amazon.awssdk.services.s3.model.NoSuchKeyException
software.amazon.awssdk.services.s3.model.ObjectIdentifier
software.amazon.awssdk.services.s3.model.PutObjectRequest
software.amazon.awssdk.services.s3.model.S3Error
@@ -126,17 +127,19 @@
(defmethod impl/get-object-data :s3
[backend object]
(us/assert! ::backend backend)
- (letfn [(no-such-key? [cause]
- (instance? software.amazon.awssdk.services.s3.model.NoSuchKeyException cause))
- (handle-not-found [cause]
- (ex/raise :type :not-found
- :code :object-not-found
- :hint "s3 object not found"
- :cause cause))]
- (-> (get-object-data backend object)
- (p/catch no-such-key? handle-not-found)
- (p/await!))))
+ (let [result (p/await (get-object-data backend object))]
+ (if (ex/exception? result)
+ (cond
+ (ex/instance? NoSuchKeyException result)
+ (ex/raise :type :not-found
+ :code :object-not-found
+ :hint "s3 object not found"
+ :cause result)
+ :else
+ (throw result))
+
+ result)))
(defmethod impl/get-object-bytes :s3
[backend object]
@@ -298,7 +301,7 @@
[path]
(proxy [FilterInputStream] [(io/input-stream path)]
(close []
- (fs/delete path)
+ (ex/ignoring (fs/delete path))
(proxy-super close))))
(defn- get-object-data
diff --git a/backend/src/app/svgo.clj b/backend/src/app/svgo.clj
index 70d7c6b2b3..a846fa7680 100644
--- a/backend/src/app/svgo.clj
+++ b/backend/src/app/svgo.clj
@@ -7,16 +7,10 @@
(ns app.svgo
"A SVG Optimizer service"
(:require
- [app.common.data :as d]
- [app.common.data.macros :as dm]
[app.common.jsrt :as jsrt]
[app.common.logging :as l]
- [app.common.spec :as us]
[app.worker :as-alias wrk]
- [clojure.spec.alpha :as s]
[integrant.core :as ig]
- [promesa.exec :as px]
- [promesa.exec.bulkhead :as bh]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
@@ -26,40 +20,23 @@
nil)
(defn optimize
- [system data]
- (dm/assert! "expect data to be a string" (string? data))
-
- (letfn [(optimize-fn [pool]
- (jsrt/run! pool
- (fn [context]
- (jsrt/set! context "svgData" data)
- (jsrt/eval! context "penpotSvgo.optimize(svgData, {plugins: ['safeAndFastPreset']})"))))]
- (try
- (some-> *semaphore* ps/acquire!)
- (let [{:keys [::jsrt/pool ::wrk/executor]} (::optimizer system)]
- (dm/assert! "expect optimizer instance" (jsrt/pool? pool))
- (px/invoke! executor (partial optimize-fn pool)))
- (finally
- (some-> *semaphore* ps/release!)))))
-
-(s/def ::max-procs (s/nilable ::us/integer))
-
-(defmethod ig/pre-init-spec ::optimizer [_]
- (s/keys :req [::wrk/executor ::max-procs]))
-
-(defmethod ig/prep-key ::optimizer
- [_ cfg]
- (merge {::max-procs 20} (d/without-nils cfg)))
+ [{pool ::optimizer} data]
+ (try
+ (some-> *semaphore* ps/acquire!)
+ (jsrt/run! pool
+ (fn [context]
+ (jsrt/set! context "svgData" data)
+ (jsrt/eval! context "penpotSvgo.optimize(svgData, {plugins: ['safeAndFastPreset']})")))
+ (finally
+ (some-> *semaphore* ps/release!))))
(defmethod ig/init-key ::optimizer
- [_ {:keys [::wrk/executor ::max-procs]}]
- (l/inf :hint "initializing svg optimizer pool" :max-procs max-procs)
- (let [init (jsrt/resource->source "app/common/svg/optimizer.js")
- executor (bh/create :type :executor :executor executor :permits max-procs)]
- {::jsrt/pool (jsrt/pool :init init)
- ::wrk/executor executor}))
+ [_ _]
+ (l/inf :hint "initializing svg optimizer pool")
+ (let [init (jsrt/resource->source "app/common/svg/optimizer.js")]
+ (jsrt/pool :init init)))
(defmethod ig/halt-key! ::optimizer
- [_ {:keys [::jsrt/pool]}]
+ [_ pool]
(l/info :hint "stopping svg optimizer pool")
(pu/close! pool))
diff --git a/backend/src/app/util/cache.clj b/backend/src/app/util/cache.clj
index c5aa733e6f..65861e1797 100644
--- a/backend/src/app/util/cache.clj
+++ b/backend/src/app/util/cache.clj
@@ -9,61 +9,71 @@
(:refer-clojure :exclude [get])
(:require
[app.util.time :as dt]
- [promesa.core :as p]
[promesa.exec :as px])
(:import
com.github.benmanes.caffeine.cache.AsyncCache
- com.github.benmanes.caffeine.cache.AsyncLoadingCache
- com.github.benmanes.caffeine.cache.CacheLoader
+ com.github.benmanes.caffeine.cache.Cache
com.github.benmanes.caffeine.cache.Caffeine
com.github.benmanes.caffeine.cache.RemovalListener
+ com.github.benmanes.caffeine.cache.stats.CacheStats
java.time.Duration
java.util.concurrent.Executor
java.util.function.Function))
(set! *warn-on-reflection* true)
-(defn create-listener
+(defprotocol ICache
+ (get [_ k] [_ k load-fn] "get cache entry")
+ (invalidate! [_] [_ k] "invalidate cache"))
+
+(defprotocol ICacheStats
+ (stats [_] "get stats"))
+
+(defn- create-listener
[f]
(reify RemovalListener
(onRemoval [_ key val cause]
(when val
(f key val cause)))))
-(defn create-loader
- [f]
- (reify CacheLoader
- (load [_ key]
- (f key))))
+(defn- get-stats
+ [^Cache cache]
+ (let [^CacheStats stats (.stats cache)]
+ {:hit-rate (.hitRate stats)
+ :hit-count (.hitCount stats)
+ :req-count (.requestCount stats)
+ :miss-count (.missCount stats)
+ :miss-rate (.missRate stats)}))
(defn create
- [& {:keys [executor on-remove load-fn keepalive]}]
- (as-> (Caffeine/newBuilder) builder
- (if on-remove (.removalListener builder (create-listener on-remove)) builder)
- (if executor (.executor builder ^Executor (px/resolve-executor executor)) builder)
- (if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
- (if load-fn
- (.buildAsync builder ^CacheLoader (create-loader load-fn))
- (.buildAsync builder))))
+ [& {:keys [executor on-remove max-size keepalive]}]
+ (let [cache (as-> (Caffeine/newBuilder) builder
+ (if (fn? on-remove) (.removalListener builder (create-listener on-remove)) builder)
+ (if executor (.executor builder ^Executor (px/resolve-executor executor)) builder)
+ (if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
+ (if (int? max-size) (.maximumSize builder (long max-size)) builder)
+ (.recordStats builder)
+ (.buildAsync builder))
+ cache (.synchronous ^AsyncCache cache)]
+ (reify
+ ICache
+ (get [_ k]
+ (.getIfPresent ^Cache cache ^Object k))
+ (get [_ k load-fn]
+ (.get ^Cache cache
+ ^Object k
+ ^Function (reify Function
+ (apply [_ k]
+ (load-fn k)))))
+ (invalidate! [_]
+ (.invalidateAll ^Cache cache))
+ (invalidate! [_ k]
+ (.invalidateAll ^Cache cache ^Object k))
-(defn invalidate-all!
- [^AsyncCache cache]
- (.invalidateAll (.synchronous cache)))
-
-(defn get
- ([cache key]
- (assert (instance? AsyncLoadingCache cache) "should be AsyncLoadingCache instance")
- (p/await! (.get ^AsyncLoadingCache cache ^Object key)))
- ([cache key not-found-fn]
- (assert (instance? AsyncCache cache) "should be AsyncCache instance")
- (p/await! (.get ^AsyncCache cache
- ^Object key
- ^Function (reify
- Function
- (apply [_ key]
- (not-found-fn key)))))))
+ ICacheStats
+ (stats [_]
+ (get-stats cache)))))
(defn cache?
[o]
- (or (instance? AsyncCache o)
- (instance? AsyncLoadingCache o)))
+ (satisfies? ICache o))
diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj
index 6e8e76b828..bb7b252939 100644
--- a/backend/src/app/util/pointer_map.clj
+++ b/backend/src/app/util/pointer_map.clj
@@ -166,32 +166,36 @@
(assoc [this key val]
(when-not loaded? (load! this))
- (let [odata (assoc odata key val)
- mdata (assoc mdata :created-at (dt/now))
- id (if modified? id (uuid/next))
- pmap (PointerMap. id
- mdata
- odata
- true
- true)]
- (some-> *tracked* (swap! assoc id pmap))
- pmap))
+ (let [odata' (assoc odata key val)]
+ (if (identical? odata odata')
+ this
+ (let [mdata (assoc mdata :created-at (dt/now))
+ id (if modified? id (uuid/next))
+ pmap (PointerMap. id
+ mdata
+ odata'
+ true
+ true)]
+ (some-> *tracked* (swap! assoc id pmap))
+ pmap))))
(assocEx [_ _ _]
(throw (UnsupportedOperationException. "method not implemented")))
(without [this key]
(when-not loaded? (load! this))
- (let [odata (dissoc odata key)
- mdata (assoc mdata :created-at (dt/now))
- id (if modified? id (uuid/next))
- pmap (PointerMap. id
- mdata
- odata
- true
- true)]
- (some-> *tracked* (swap! assoc id pmap))
- pmap))
+ (let [odata' (dissoc odata key)]
+ (if (identical? odata odata')
+ this
+ (let [mdata (assoc mdata :created-at (dt/now))
+ id (if modified? id (uuid/next))
+ pmap (PointerMap. id
+ mdata
+ odata'
+ true
+ true)]
+ (some-> *tracked* (swap! assoc id pmap))
+ pmap))))
Counted
(count [this]
@@ -206,6 +210,8 @@
(defn create
([]
(let [id (uuid/next)
+
+
mdata (assoc *metadata* :created-at (dt/now))
pmap (PointerMap. id mdata {} true true)]
(some-> *tracked* (swap! assoc id pmap))
@@ -225,7 +231,15 @@
(do
(some-> *tracked* (swap! assoc (get-id data) data))
data)
- (into (create) data)))
+ (let [mdata (assoc (meta data) :created-at (dt/now))
+ id (uuid/next)
+ pmap (PointerMap. id
+ mdata
+ data
+ true
+ true)]
+ (some-> *tracked* (swap! assoc id pmap))
+ pmap)))
(fres/add-handlers!
{:name "penpot/pointer-map/v1"
diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj
index d3611d71e6..7785966245 100644
--- a/backend/src/app/util/time.clj
+++ b/backend/src/app/util/time.clj
@@ -123,7 +123,6 @@
FileTime
(inst-ms* [v] (.toMillis ^FileTime v)))
-
(defmethod print-method Duration
[mv ^java.io.Writer writer]
(.write writer (str "#app/duration \"" (str/lower (subs (str mv) 2)) "\"")))
diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj
index 6be373a29c..510aadd892 100644
--- a/backend/test/backend_tests/rpc_file_test.clj
+++ b/backend/test/backend_tests/rpc_file_test.clj
@@ -166,18 +166,21 @@
:name "test"
:id page-id}])
- ;; Check the number of fragments
- (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
- (t/is (= 2 (count rows))))
-
- ;; Check the number of fragments
- (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
- (t/is (= 2 (count rows))))
-
- ;; The file-gc should remove unused fragments
+ ;; The file-gc should mark for remove unused fragments
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
+ ;; Check the number of fragments
+ (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
+ (t/is (= 2 (count rows))))
+
+ ;; The objects-gc should remove unused fragments
+ (let [res (th/run-task! :objects-gc {:min-age 0})]
+ (t/is (= 0 (:processed res))))
+
+ ;; Check the number of fragments
+ (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
+ (t/is (= 2 (count rows))))
;; Add shape to page that should add a new fragment
(update-file!
@@ -202,10 +205,14 @@
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 3 (count rows))))
- ;; The file-gc should remove unused fragments
+ ;; The file-gc should mark for remove unused fragments
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))
+ ;; The objects-gc should remove unused fragments
+ (let [res (th/run-task! :objects-gc {:min-age 0})]
+ (t/is (= 0 (:processed res))))
+
;; Check the number of fragments; should be 3 because changes
;; are also holding pointers to fragments;
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
@@ -235,8 +242,6 @@
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 2 (count rows)))))))
-
-
(t/deftest file-gc-task-with-thumbnails
(letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
(let [mfile {:filename "sample.jpg"
diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj
index 24776a3cb7..95737cad23 100644
--- a/backend/test/backend_tests/rpc_profile_test.clj
+++ b/backend/test/backend_tests/rpc_profile_test.clj
@@ -11,6 +11,7 @@
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
+ [app.rpc.commands.profile :as profile]
[app.tokens :as tokens]
[app.util.time :as dt]
[backend-tests.helpers :as th]
@@ -185,40 +186,12 @@
token (get-in out [:result :token])]
(t/is (string? token))
-
- ;; try register without token
- (let [data {::th/type :register-profile
- :fullname "foobar"
- :accept-terms-and-privacy true}
- out (th/command! data)]
- (let [error (:error out)]
- (t/is (th/ex-info? error))
- (t/is (th/ex-of-type? error :validation))
- (t/is (th/ex-of-code? error :spec-validation))))
-
- ;; try correct register
- (let [data {::th/type :register-profile
- :token token
- :fullname "foobar"
- :accept-terms-and-privacy true
- :accept-newsletter-subscription true}]
- (let [{:keys [result error]} (th/command! data)]
- (t/is (nil? error))))))
-
-(t/deftest prepare-register-and-register-profile-1
- (let [data {::th/type :prepare-register-profile
- :email "user@example.com"
- :password "foobar"}
- out (th/command! data)
- token (get-in out [:result :token])]
- (t/is (string? token))
-
-
;; try register without token
(let [data {::th/type :register-profile
:fullname "foobar"
:accept-terms-and-privacy true}
out (th/command! data)]
+ ;; (th/print-result! out)
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
@@ -228,11 +201,24 @@
(let [data {::th/type :register-profile
:token token
:fullname "foobar"
+ :utm_campaign "utma"
+ :mtm_campaign "mtma"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}]
- (let [{:keys [result error] :as out} (th/command! data)]
- ;; (th/print-result! out)
- (t/is (nil? error))))))
+ (let [{:keys [result error]} (th/command! data)]
+ (t/is (nil? error))))
+
+ (let [profile (some-> (th/db-get :profile {:email "user@example.com"})
+ (profile/decode-row))]
+ (t/is (= "penpot" (:auth-backend profile)))
+ (t/is (= "foobar" (:fullname profile)))
+ (t/is (false? (:is-active profile)))
+ (t/is (uuid? (:default-team-id profile)))
+ (t/is (uuid? (:default-project-id profile)))
+
+ (let [props (:props profile)]
+ (t/is (= "utma" (:penpot/utm-campaign props)))
+ (t/is (= "mtma" (:penpot/mtm-campaign props)))))))
(t/deftest prepare-register-and-register-profile-2
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
diff --git a/common/deps.edn b/common/deps.edn
index f8c649fab6..61c52e9099 100644
--- a/common/deps.edn
+++ b/common/deps.edn
@@ -32,7 +32,7 @@
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2023.11.09-407"}
- funcool/promesa {:git/sha "484b7f5c0d08d817746caa685ed9ac5583eb37fa"
+ funcool/promesa {:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0"
:git/url "https://github.com/funcool/promesa"}
funcool/datoteka {:mvn/version "3.0.66"
diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc
index d814b14392..12e3f7762b 100644
--- a/common/src/app/common/data.cljc
+++ b/common/src/app/common/data.cljc
@@ -7,7 +7,7 @@
(ns app.common.data
"A collection if helpers for working with data structures and other
data resources."
- (:refer-clojure :exclude [read-string hash-map merge name update-vals
+ (:refer-clojure :exclude [read-string hash-map merge name
parse-double group-by iteration concat mapcat
parse-uuid max min])
#?(:cljs
@@ -216,12 +216,19 @@
[coll]
(into [] (remove nil?) coll))
+
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
- ([] (remove (comp nil? val)))
+ ([]
+ (remove (comp nil? val)))
([data]
- (into {} (without-nils) data)))
+ (reduce-kv (fn [data k v]
+ (if (nil? v)
+ (dissoc data k)
+ data))
+ data
+ data)))
(defn without-qualified
([]
diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc
index b01914ca56..2070986fe3 100644
--- a/common/src/app/common/exceptions.cljc
+++ b/common/src/app/common/exceptions.cljc
@@ -7,10 +7,12 @@
(ns app.common.exceptions
"A helpers for work with exceptions."
#?(:cljs (:require-macros [app.common.exceptions]))
+ (:refer-clojure :exclude [instance?])
(:require
#?(:clj [clojure.stacktrace :as strace])
[app.common.pprint :as pp]
[app.common.schema :as sm]
+ [clojure.core :as c]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[expound.alpha :as expound])
@@ -20,6 +22,9 @@
#?(:clj (set! *warn-on-reflection* true))
+(def ^:dynamic *data-length* 8)
+(def ^:dynamic *data-level* 8)
+
(defmacro error
[& {:keys [type hint] :as params}]
`(ex-info ~(or hint (name type))
@@ -49,20 +54,38 @@
(defn ex-info?
[v]
- (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v))
+ (c/instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v))
(defn error?
[v]
- (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v))
+ (c/instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v))
(defn exception?
[v]
- (instance? #?(:clj java.lang.Throwable :cljs js/Error) v))
+ (c/instance? #?(:clj java.lang.Throwable :cljs js/Error) v))
#?(:clj
(defn runtime-exception?
[v]
- (instance? RuntimeException v)))
+ (c/instance? RuntimeException v)))
+
+#?(:clj
+ (defn instance?
+ [class cause]
+ (loop [cause cause]
+ (if (c/instance? class cause)
+ true
+ (if-let [cause (ex-cause cause)]
+ (recur cause)
+ false)))))
+
+;; NOTE: idea for a macro for error handling
+;; (pu/try-let [cause (p/await (get-object-data backend object))]
+;; (ex/instance? NoSuchKeyException cause)
+;; (ex/raise :type :not-found
+;; :code :object-not-found
+;; :hint "s3 object not found"
+;; :cause cause))
(defn explain
[data & {:as opts}]
@@ -91,8 +114,8 @@
data? true
explain? true
chain? true
- data-length 8
- data-level 5}}]
+ data-length *data-length*
+ data-level *data-level*}}]
(letfn [(print-trace-element [^StackTraceElement e]
(let [class (.getClassName e)
diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc
index 12e18921eb..e35914d73f 100644
--- a/common/src/app/common/files/defaults.cljc
+++ b/common/src/app/common/files/defaults.cljc
@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
-(def version 38)
+(def version 44)
diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc
index dce09d5dd5..46b9ac66e8 100644
--- a/common/src/app/common/files/helpers.cljc
+++ b/common/src/app/common/files/helpers.cljc
@@ -92,9 +92,11 @@
(= :image (dm/get-prop shape :type))))
(defn svg-raw-shape?
- [shape]
- (and (some? shape)
- (= :svg-raw (dm/get-prop shape :type))))
+ ([objects id]
+ (svg-raw-shape? (get objects id)))
+ ([shape]
+ (and (some? shape)
+ (= :svg-raw (dm/get-prop shape :type)))))
(defn path-shape?
([objects id]
@@ -753,3 +755,15 @@
[frame-id (get-parent-ids objects frame-id)]))]
(recur frame-id frame-parents (rest selected))))))
+
+(defn fixed?
+ [objects shape-id]
+ (let [ids-to-check
+ (concat
+ [shape-id]
+ (get-children-ids objects shape-id)
+ (->> (get-parent-ids objects shape-id)
+ (take-while #(and (not= % uuid/zero) (not (root-frame? objects %))))))]
+ (boolean
+ (->> ids-to-check
+ (d/seek (fn [id] (dm/get-in objects [id :fixed-scroll])))))))
diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc
index c5952d5718..025f1d8417 100644
--- a/common/src/app/common/files/migrations.cljc
+++ b/common/src/app/common/files/migrations.cljc
@@ -19,9 +19,11 @@
[app.common.geom.shapes.text :as gsht]
[app.common.logging :as l]
[app.common.math :as mth]
+ [app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.text :as txt]
[app.common.types.shape :as cts]
+ [app.common.types.shape.shadow :as ctss]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
@@ -31,6 +33,10 @@
(defmulti migrate :version)
+(defn need-migration?
+ [{:keys [data]}]
+ (> cfd/version (:version data 0)))
+
(defn migrate-data
([data] (migrate-data data version))
([data to-version]
@@ -318,19 +324,21 @@
(= "#7B7D85" fill-color)))
(dissoc :fill-color :fill-opacity))))
- (update-container [{:keys [objects] :as container}]
- (loop [objects objects
- shapes (->> (vals objects)
- (filter cfh/image-shape?))]
- (if-let [shape (first shapes)]
- (let [{:keys [id frame-id] :as shape'} (process-shape shape)]
- (if (identical? shape shape')
- (recur objects (rest shapes))
- (recur (-> objects
- (assoc id shape')
- (d/update-when frame-id dissoc :thumbnail))
- (rest shapes))))
- (assoc container :objects objects))))]
+ (update-container [container]
+ (if (contains? container :objects)
+ (loop [objects (:objects container)
+ shapes (->> (vals objects)
+ (filter cfh/image-shape?))]
+ (if-let [shape (first shapes)]
+ (let [{:keys [id frame-id] :as shape'} (process-shape shape)]
+ (if (identical? shape shape')
+ (recur objects (rest shapes))
+ (recur (-> objects
+ (assoc id shape')
+ (d/update-when frame-id dissoc :thumbnail))
+ (rest shapes))))
+ (assoc container :objects objects)))
+ container))]
(-> data
(update :pages-index update-vals update-container)
@@ -380,7 +388,7 @@
(assign-fills)))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
@@ -409,7 +417,7 @@
(assoc :fills [])))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
@@ -424,7 +432,7 @@
(dissoc :position-data)))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
@@ -440,7 +448,7 @@
(dissoc :position-data)))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
@@ -527,7 +535,7 @@
(assoc object :frame-id calculated-frame-id)))
(update-container [container]
- (update container :objects #(update-vals % (partial update-object %))))]
+ (d/update-when container :objects #(update-vals % (partial update-object %))))]
(-> data
(update :pages-index update-vals update-container)
@@ -565,22 +573,7 @@
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
(update-container [container]
- (update container :objects update-vals update-object))]
-
- (-> data
- (update :pages-index update-vals update-container)
- (update :components update-vals update-container))))
-
-(defmethod migrate 30
- [data]
- (letfn [(update-object [object]
- (if (and (cfh/frame-shape? object)
- (not (:shapes object)))
- (assoc object :shapes [])
- object))
-
- (update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
@@ -613,7 +606,8 @@
object)))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
+
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
@@ -624,13 +618,13 @@
;; Ensure all root objects are well formed shapes.
(if (= (:id object) uuid/zero)
(-> object
- (assoc :parent-id uuid/zero
- :frame-id uuid/zero)
+ (assoc :parent-id uuid/zero)
+ (assoc :frame-id uuid/zero)
(cts/setup-shape))
object))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container))))
@@ -642,7 +636,7 @@
(dissoc object :x :y :width :height)
object))
(update-container [container]
- (update container :objects update-vals update-object))]
+ (d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
@@ -694,8 +688,158 @@
shape)))
(update-container [container]
- (update container :objects update-vals update-shape))]
+ (d/update-when container :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
+
+(defmethod migrate 39
+ [data]
+ (letfn [(update-shape [shape]
+ (cond
+ (and (cfh/bool-shape? shape)
+ (not (contains? shape :bool-content)))
+ (assoc shape :bool-content [])
+
+ (and (cfh/path-shape? shape)
+ (not (contains? shape :content)))
+ (assoc shape :content [])
+
+ :else
+ shape))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-shape))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+(defmethod migrate 40
+ [data]
+ (letfn [(update-shape [{:keys [content shapes] :as shape}]
+ ;; Fix frame shape that in reallity is a path shape
+ (if (and (cfh/frame-shape? shape)
+ (contains? shape :selrect)
+ (seq content)
+ (not (seq shapes))
+ (contains? (first content) :command))
+ (-> shape
+ (assoc :type :path)
+ (assoc :x nil)
+ (assoc :y nil)
+ (assoc :width nil)
+ (assoc :height nil))
+ shape))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-shape))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+(defmethod migrate 41
+ [data]
+ (letfn [(update-shape [shape]
+ (cond
+ (or (cfh/bool-shape? shape)
+ (cfh/path-shape? shape))
+ shape
+
+ ;; Fix all shapes that has geometry broken but still
+ ;; preservers the selrect, so we recalculate the
+ ;; geometry from selrect.
+ (and (contains? shape :selrect)
+ (or (nil? (:x shape))
+ (nil? (:y shape))
+ (nil? (:width shape))
+ (nil? (:height shape))))
+ (let [selrect (:selrect shape)]
+ (-> shape
+ (assoc :x (:x selrect))
+ (assoc :y (:y selrect))
+ (assoc :width (:width selrect))
+ (assoc :height (:height selrect))))
+
+ :else
+ shape))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-shape))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+(defmethod migrate 42
+ [data]
+ (letfn [(update-object [object]
+ (if (and (or (cfh/frame-shape? object)
+ (cfh/group-shape? object)
+ (cfh/bool-shape? object))
+ (not (:shapes object)))
+ (assoc object :shapes [])
+ object))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-object))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+(def ^:private valid-fill?
+ (sm/lazy-validator ::cts/fill))
+
+(defmethod migrate 43
+ [data]
+ (letfn [(number->string [v]
+ (if (number? v)
+ (str v)
+ v))
+
+ (update-text-node [node]
+ (-> node
+ (d/update-when :fills #(filterv valid-fill? %))
+ (d/update-when :font-size number->string)
+ (d/update-when :font-weight number->string)
+ (d/without-nils)))
+
+ (update-object [object]
+ (if (cfh/text-shape? object)
+ (update object :content #(txt/transform-nodes identity update-text-node %))
+ object))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-object))]
+
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
+
+(def ^:private valid-shadow?
+ (sm/lazy-validator ::ctss/shadow))
+
+(defmethod migrate 44
+ [data]
+ (letfn [(fix-shadow [shadow]
+ (if (string? (:color shadow))
+ (let [color {:color (:color shadow)
+ :opacity 1}]
+ (assoc shadow :color color))
+ shadow))
+
+ (update-object [object]
+ (d/update-when object :shadow
+ #(into []
+ (comp (map fix-shadow)
+ (filter valid-shadow?))
+ %)))
+
+ (update-container [container]
+ (d/update-when container :objects update-vals update-object))]
+ (-> data
+ (update :pages-index update-vals update-container)
+ (update :components update-vals update-container))))
diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc
index 4034a15aba..4a824682e9 100644
--- a/common/src/app/common/files/repair.cljc
+++ b/common/src/app/common/files/repair.cljc
@@ -66,6 +66,19 @@
(pcb/with-file-data file-data)
(pcb/update-shapes [(:parent-id shape)] repair-shape))))
+(defmethod repair-error :duplicated-children
+ [_ {:keys [shape page-id] :as error} file-data _]
+ (let [repair-shape
+ (fn [shape]
+ ; Remove duplicated
+ (log/debug :hint " -> remove duplicated children")
+ (update shape :shapes distinct))]
+
+ (log/dbg :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id)
+ (-> (pcb/empty-changes nil page-id)
+ (pcb/with-file-data file-data)
+ (pcb/update-shapes [(:id shape)] repair-shape))))
+
(defmethod repair-error :child-not-found
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
@@ -326,7 +339,8 @@
(log/dbg :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
- (pcb/update-shapes [(:id shape)] repair-shape))))
+ (pcb/update-shapes [(:id shape)] repair-shape)
+ (pcb/change-parent uuid/zero [shape] nil {:component-swap true}))))
(defmethod repair-error :root-copy-not-allowed
[_ {:keys [shape page-id] :as error} file-data _]
diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc
index 1c25b82dcd..01373f93c8 100644
--- a/common/src/app/common/files/validate.cljc
+++ b/common/src/app/common/files/validate.cljc
@@ -26,6 +26,7 @@
#{:invalid-geometry
:parent-not-found
:child-not-in-parent
+ :duplicated-children
:child-not-found
:frame-not-found
:invalid-frame
@@ -105,7 +106,7 @@
(nil? (:selrect shape))
(nil? (:points shape))))
(report-error :invalid-geometry
- "Shape greometry is invalid"
+ "Shape geometry is invalid"
shape file page)))
(defn- check-parent-children
@@ -123,6 +124,11 @@
(str/ffmt "Shape % not in parent's children list" (:id shape))
shape file page)))
+ (when-not (= (count (:shapes shape)) (count (distinct (:shapes shape))))
+ (report-error :duplicated-children
+ (str/ffmt "Shape % has duplicated children" (:id shape))
+ shape file page))
+
(doseq [child-id (:shapes shape)]
(let [child (ctst/get-shape page child-id)]
(if (nil? child)
@@ -367,63 +373,63 @@
[shape-id file page libraries & {:keys [context] :or {context :not-component}}]
(let [shape (ctst/get-shape page shape-id)]
(when (some? shape)
- (do
- (check-geometry shape file page)
- (check-parent-children shape file page)
- (check-frame shape file page)
+ (check-geometry shape file page)
+ (check-parent-children shape file page)
+ (check-frame shape file page)
- (if (ctk/instance-head? shape)
- (if (not= :frame (:type shape))
- (report-error :instance-head-not-frame
- "Instance head should be a frame"
+ (if (ctk/instance-head? shape)
+ (if (not= :frame (:type shape))
+ (report-error :instance-head-not-frame
+ "Instance head should be a frame"
+ shape file page)
+
+ (if (ctk/instance-root? shape)
+ (if (ctk/main-instance? shape)
+ (if (not= context :not-component)
+ (report-error :root-main-not-allowed
+ "Root main component not allowed inside other component"
+ shape file page)
+ (check-shape-main-root-top shape file page libraries))
+
+ (if (not= context :not-component)
+ (report-error :root-copy-not-allowed
+ "Root copy component not allowed inside other component"
+ shape file page)
+ (check-shape-copy-root-top shape file page libraries)))
+
+ (if (ctk/main-instance? shape)
+ ;; mains can't be nested into mains
+ (if (or (= context :not-component) (= context :main-top))
+ (report-error :nested-main-not-allowed
+ "Nested main component only allowed inside other component"
+ shape file page)
+ (check-shape-main-root-nested shape file page libraries))
+
+ (if (= context :not-component)
+ (report-error :nested-copy-not-allowed
+ "Nested copy component only allowed inside other component"
+ shape file page)
+ (check-shape-copy-root-nested shape file page libraries)))))
+
+ (if (ctk/in-component-copy? shape)
+ (if-not (#{:copy-top :copy-nested :copy-any} context)
+ (report-error :not-head-copy-not-allowed
+ "Non-root copy only allowed inside a copy"
shape file page)
+ (check-shape-copy-not-root shape file page libraries))
- (if (ctk/instance-root? shape)
- (if (ctk/main-instance? shape)
- (if (not= context :not-component)
- (report-error :root-main-not-allowed
- "Root main component not allowed inside other component"
- shape file page)
- (check-shape-main-root-top shape file page libraries))
-
- (if (not= context :not-component)
- (report-error :root-copy-not-allowed
- "Root copy component not allowed inside other component"
- shape file page)
- (check-shape-copy-root-top shape file page libraries)))
-
- (if (ctk/main-instance? shape)
- (if (= context :not-component)
- (report-error :nested-main-not-allowed
- "Nested main component only allowed inside other component"
- shape file page)
- (check-shape-main-root-nested shape file page libraries))
-
- (if (= context :not-component)
- (report-error :nested-copy-not-allowed
- "Nested copy component only allowed inside other component"
- shape file page)
- (check-shape-copy-root-nested shape file page libraries)))))
-
- (if (ctk/in-component-copy? shape)
- (if-not (#{:copy-top :copy-nested :copy-any} context)
- (report-error :not-head-copy-not-allowed
- "Non-root copy only allowed inside a copy"
+ (if (ctn/inside-component-main? (:objects page) shape)
+ (if-not (#{:main-top :main-nested :main-any} context)
+ (report-error :not-head-main-not-allowed
+ "Non-root main only allowed inside a main component"
shape file page)
- (check-shape-copy-not-root shape file page libraries))
+ (check-shape-main-not-root shape file page libraries))
- (if (ctn/inside-component-main? (:objects page) shape)
- (if-not (#{:main-top :main-nested :main-any} context)
- (report-error :not-head-main-not-allowed
- "Non-root main only allowed inside a main component"
- shape file page)
- (check-shape-main-not-root shape file page libraries))
-
- (if (#{:main-top :main-nested :main-any} context)
- (report-error :not-component-not-allowed
- "Not compoments are not allowed inside a main"
- shape file page)
- (check-shape-not-component shape file page libraries)))))))))
+ (if (#{:main-top :main-nested :main-any} context)
+ (report-error :not-component-not-allowed
+ "Not compoments are not allowed inside a main"
+ shape file page)
+ (check-shape-not-component shape file page libraries))))))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
@@ -483,6 +489,9 @@
(sm/lazy-explainer ::ctf/data))
(defn validate-file-schema!
+ "Validates the file itself, without external dependencies, it
+ performs the schema checking and some semantical validation of the
+ content."
[{:keys [id data] :as file}]
(when-not (valid-fdata? data)
(ex/raise :type :validation
diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc
index 018beaeb39..84f0b52418 100644
--- a/common/src/app/common/geom/shapes/path.cljc
+++ b/common/src/app/common/geom/shapes/path.cljc
@@ -981,6 +981,7 @@
selrect (-> points
(gco/transform-points points-center transform-inverse)
(grc/points->rect))]
+
[points selrect]))
(defn open-path?
diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc
index 7fed63b77b..b1e743f643 100644
--- a/common/src/app/common/schema.cljc
+++ b/common/src/app/common/schema.cljc
@@ -14,6 +14,7 @@
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi]
[app.common.schema.registry :as sr]
+ [app.common.time :as tm]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[clojure.core :as c]
@@ -625,7 +626,8 @@
{:title "inst"
:description "Satisfies Inst protocol"
:error/message "expected to be number in safe range"
- :gen/gen (sg/small-int)
+ :gen/gen (->> (sg/small-int)
+ (sg/fmap (fn [v] (tm/instant v))))
::oapi/type "number"
::oapi/format "int64"}})
@@ -658,6 +660,9 @@
;; ---- PREDICATES
+(def valid-safe-number?
+ (lazy-validator ::safe-number))
+
(def check-safe-int!
(check-fn ::safe-int))
diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc
index f1aa8c90fd..83e00bfd87 100644
--- a/common/src/app/common/schema/generators.cljc
+++ b/common/src/app/common/schema/generators.cljc
@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
- (:refer-clojure :exclude [set subseq uuid for filter map])
+ (:refer-clojure :exclude [set subseq uuid for filter map let])
#?(:cljs (:require-macros [app.common.schema.generators]))
(:require
[app.common.schema.registry :as sr]
@@ -37,6 +37,10 @@
[& params]
`(tp/for-all ~@params))
+(defmacro let
+ [& params]
+ `(tg/let ~@params))
+
(defn check!
[p & {:keys [num] :or {num 20} :as options}]
(tc/quick-check num p (assoc options :reporter-fn default-reporter-fn :max-size 50)))
@@ -124,6 +128,10 @@
[f g]
(tg/fmap f g))
+(defn mcat
+ [f g]
+ (tg/bind g f))
+
(defn tuple
[& opts]
(apply tg/tuple opts))
diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc
index 7974eaa28d..4ea6278cc9 100644
--- a/common/src/app/common/svg.cljc
+++ b/common/src/app/common/svg.cljc
@@ -31,7 +31,7 @@
(def xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)")
(def matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)")
-(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
+(def number-regex #"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?")
(def tags-to-remove #{:linearGradient :radialGradient :metadata :mask :clipPath :filter :title})
@@ -759,40 +759,39 @@
;; Transforms spec:
;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute
-(defn format-translate-params
+(defn- format-translate-params
[params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0) 0)]
[(gpt/point (nth params 0) (nth params 1))]))
-(defn format-scale-params
+(defn- format-scale-params
[params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0))]
[(gpt/point (nth params 0) (nth params 1))]))
-(defn format-rotate-params
+(defn- format-rotate-params
[params]
(assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params)))
(if (= (count params) 1)
[(nth params 0) (gpt/point 0 0)]
[(nth params 0) (gpt/point (nth params 1) (nth params 2))]))
-(defn format-skew-x-params
+(defn- format-skew-x-params
[params]
(assert (= (count params) 1))
[(nth params 0) 0])
-(defn format-skew-y-params
+(defn- format-skew-y-params
[params]
(assert (= (count params) 1))
[0 (nth params 0)])
-(defn to-matrix
- [{:keys [type params]}]
- (assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type))
+(defn- to-matrix
+ [type params]
(case type
"matrix" (apply gmt/matrix params)
"translate" (apply gmt/translate-matrix (format-translate-params params))
@@ -801,20 +800,27 @@
"skewX" (apply gmt/skew-matrix (format-skew-x-params params))
"skewY" (apply gmt/skew-matrix (format-skew-y-params params))))
-(defn parse-transform
- [transform-attr]
- (if transform-attr
- (let [process-matrix
- (fn [[_ type params]]
- (let [params (->> (re-seq number-regex params)
- (filter #(-> % first seq))
- (map (comp d/parse-double first)))]
- {:type type :params params}))
+(def ^:private
+ xf-parse-numbers
+ (comp
+ (map first)
+ (keep not-empty)
+ (map d/parse-double)))
+
+(defn parse-numbers
+ [data]
+ (->> (re-seq number-regex data)
+ (into [] xf-parse-numbers)))
+
+(defn parse-transform
+ [transform]
+ (if (string? transform)
+ (->> (re-seq matrices-regex transform)
+ (map (fn [[_ type params]]
+ (let [params (parse-numbers params)]
+ (to-matrix type params))))
+ (reduce gmt/multiply (gmt/matrix)))
- matrices (->> (re-seq matrices-regex transform-attr)
- (map process-matrix)
- (map to-matrix))]
- (reduce gmt/multiply (gmt/matrix) matrices))
(gmt/matrix)))
(defn format-move [[x y]] (str "M" x " " y))
@@ -872,11 +878,21 @@
transform
(update :transform append-transform))))
-(defn inherit-attributes [group-attrs {:keys [attrs] :as node}]
+(defn inherit-attributes
+ [group-attrs {:keys [attrs] :as node}]
(if (map? node)
- (let [attrs (-> (format-styles attrs)
- (add-transform (:transform group-attrs)))
- attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)]
+ (let [attrs (-> (format-styles attrs)
+ (add-transform (:transform group-attrs)))
+ group-attrs (format-styles group-attrs)
+
+ ;; Don't inherit a property that is already in the style attribute
+ inherit-style (-> (:style group-attrs) (d/without-keys (keys attrs)))
+ inheritable-props (->> inheritable-props (remove #(contains? (:styles attrs) %)))
+ group-attrs (-> group-attrs (assoc :style inherit-style))
+
+ attrs (-> (select-keys group-attrs inheritable-props)
+ (d/deep-merge attrs)
+ (d/without-nils))]
(assoc node :attrs attrs))
node))
@@ -958,8 +974,7 @@
is-other? #{:r :stroke-width}]
(if is-percent?
- ;; JS parseFloat removes the % symbol
- (let [attr-num (d/parse-double attr-val)]
+ (let [attr-num (d/parse-double (str/rtrim attr-val "%"))]
(str (cond
(is-x? attr-key) (fix-coord :x :width attr-num)
(is-y? attr-key) (fix-coord :y :height attr-num)
@@ -975,7 +990,7 @@
(fix-percent-attr-numeric [_ attr-val]
(let [is-percent? (str/ends-with? attr-val "%")]
(if is-percent?
- (str (let [attr-num (d/parse-double attr-val)]
+ (str (let [attr-num (d/parse-double (str/rtrim attr-val "%"))]
(/ attr-num 100)))
attr-val)))
@@ -988,7 +1003,7 @@
(get-in node [:attrs :patternUnits])
(get-in node [:attrs :clipUnits]))]
(cond-> node
- (= "objectBoundingBox" units)
+ (or (= "objectBoundingBox" units) (nil? units))
(update :attrs fix-percent-attrs-numeric)
(not= "objectBoundingBox" units)
diff --git a/common/src/app/common/svg/shapes_builder.cljc b/common/src/app/common/svg/shapes_builder.cljc
index 6cb5429aa6..a831d3c0ea 100644
--- a/common/src/app/common/svg/shapes_builder.cljc
+++ b/common/src/app/common/svg/shapes_builder.cljc
@@ -57,13 +57,14 @@
clean-value))
(defn- svg-dimensions
- [data]
- (let [width (dm/get-in data [:attrs :width] 100)
- height (dm/get-in data [:attrs :height] 100)
- viewbox (or (dm/get-in data [:attrs :viewBox])
+ [{:keys [attrs] :as data}]
+ (let [width (:width attrs 100)
+ height (:height attrs 100)
+ viewbox (or (:viewBox attrs)
(dm/str "0 0 " width " " height))
- [x y width height] (->> (str/split viewbox #"\s+")
- (map d/parse-double))
+
+ [x y width height] (csvg/parse-numbers viewbox)
+
width (if (= width 0) 1 width)
height (if (= height 0) 1 height)]
@@ -265,19 +266,19 @@
(gmt/transform-in (gpt/point svg-data)))
origin (gpt/negate (gpt/point svg-data))
- rect (-> (parse-rect-attrs attrs)
+ vbox (parse-rect-attrs attrs)
+ rect (-> vbox
(update :x - (:x origin))
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
(csvg/attrs->props))]
-
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :rect)
(assoc :name name)
(assoc :frame-id frame-id)
- (assoc :svg-viewbox rect)
+ (assoc :svg-viewbox vbox)
(assoc :svg-attrs props)
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
@@ -303,6 +304,11 @@
rx (d/nilv r rx)
ry (d/nilv r ry)
+
+ ;; There are some svg circles in the internet that does not
+ ;; have cx and cy attrs, so we default them to 0
+ cx (d/nilv cx 0)
+ cy (d/nilv cy 0)
origin (gpt/negate (gpt/point svg-data))
rect (grc/make-rect
@@ -395,9 +401,9 @@
(str/trim (:stroke style)))
color (cond
- (= stroke "currentColor") clr/black
- (= stroke "none") nil
- :else (clr/parse stroke))
+ (= stroke "currentColor") clr/black
+ (= stroke "none") nil
+ (clr/color-string? stroke) (clr/parse stroke))
opacity (when (some? color)
(d/parse-double
@@ -415,17 +421,21 @@
(get style :strokeLinecap))
linecap (some-> linecap str/trim keyword)
- attrs (-> attrs
- (dissoc :stroke)
- (dissoc :strokeWidth)
- (dissoc :strokeOpacity)
- (update :style (fn [style]
- (-> style
- (dissoc :stroke)
- (dissoc :strokeLinecap)
- (dissoc :strokeWidth)
- (dissoc :strokeOpacity))))
- (d/without-nils))]
+ attrs
+ (-> attrs
+ (cond-> linecap
+ (dissoc :strokeLinecap))
+ (cond-> (some? color)
+ (dissoc :stroke :strokeWidth :strokeOpacity))
+ (update
+ :style
+ (fn [style]
+ (-> style
+ (cond-> linecap
+ (dissoc :strokeLinecap))
+ (cond-> (some? color)
+ (dissoc :stroke :strokeWidth :strokeOpacity)))))
+ (d/without-nils))]
(cond-> (assoc shape :svg-attrs attrs)
(some? color)
@@ -467,6 +477,16 @@
(-> (update-in [:svg-attrs :style] dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode)))))
+(defn setup-other [shape]
+ (cond-> shape
+ (= (dm/get-in shape [:svg-attrs :display]) "none")
+ (-> (update-in [:svg-attrs :style] dissoc :display)
+ (assoc :hidden true))
+
+ (= (dm/get-in shape [:svg-attrs :style :display]) "none")
+ (-> (update :svg-attrs dissoc :display)
+ (assoc :hidden true))))
+
(defn tag->name
"Given a tag returns its layer name"
[tag]
@@ -488,8 +508,16 @@
att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs)
- href-id (-> (or (:href attrs) (:xlink:href attrs) " ") (subs 1))
- use-tag? (and (= :use tag) (contains? defs href-id))]
+
+ href-id (or (:href attrs) (:xlink:href attrs) " ")
+ href-id (if (and (string? href-id)
+ (pos? (count href-id)))
+ (subs href-id 1)
+ href-id)
+
+ use-tag? (and (= :use tag)
+ (some? href-id)
+ (contains? defs href-id))]
(if use-tag?
(let [;; Merge the data of the use definition with the properties passed as attributes
@@ -518,20 +546,20 @@
:image (create-image-shape name frame-id svg-data element)
#_other (create-raw-svg name frame-id svg-data element))]
-
(when (some? shape)
- (let [shape (-> shape
- (assoc :svg-defs (select-keys defs references))
- (setup-fill)
- (setup-stroke)
- (setup-opacity)
- (update :svg-attrs (fn [attrs]
- (if (empty? (:style attrs))
- (dissoc attrs :style)
- attrs))))]
- [(cond-> shape
- hidden (assoc :hidden true))
+ [(-> shape
+ (assoc :svg-defs (select-keys defs references))
+ (setup-fill)
+ (setup-stroke)
+ (setup-opacity)
+ (setup-other)
+ (update :svg-attrs (fn [attrs]
+ (if (empty? (:style attrs))
+ (dissoc attrs :style)
+ attrs)))
+ (cond-> ^boolean hidden
+ (assoc :hidden true)))
- (cond->> (:content element)
- (contains? csvg/parent-tags tag)
- (mapv #(csvg/inherit-attributes attrs %)))]))))))
+ (cond->> (:content element)
+ (contains? csvg/parent-tags tag)
+ (mapv (partial csvg/inherit-attributes attrs)))])))))
diff --git a/common/src/app/common/time.cljc b/common/src/app/common/time.cljc
index c32c82411e..6cd8601d66 100644
--- a/common/src/app/common/time.cljc
+++ b/common/src/app/common/time.cljc
@@ -4,16 +4,16 @@
;;
;; Copyright (c) KALEIDOS INC
-;; Here we put the time functions that are common between frontend and backend.
-;; In the future we may create an unified API for both.
-
(ns app.common.time
+ "A new cross-platform date and time API. It should be prefered over
+ a platform specific implementation found on `app.util.time`."
#?(:cljs
(:require
["luxon" :as lxn])
:clj
(:import
- java.time.Instant)))
+ java.time.Instant
+ java.time.Duration)))
#?(:cljs
(def DateTime lxn/DateTime))
@@ -24,4 +24,47 @@
(defn now
[]
#?(:clj (Instant/now)
- :cljs (.local ^js DateTime)))
\ No newline at end of file
+ :cljs (.local ^js DateTime)))
+
+(defn instant
+ [s]
+ #?(:clj (Instant/ofEpochMilli s)
+ :cljs (.fromMillis ^js DateTime s #js {:zone "local" :setZone false})))
+
+#?(:cljs
+ (extend-protocol IComparable
+ DateTime
+ (-compare [it other]
+ (if ^boolean (.equals it other)
+ 0
+ (if (< (inst-ms it) (inst-ms other)) -1 1)))
+
+ Duration
+ (-compare [it other]
+ (if ^boolean (.equals it other)
+ 0
+ (if (< (inst-ms it) (inst-ms other)) -1 1)))))
+
+
+#?(:cljs
+ (extend-type DateTime
+ cljs.core/IEquiv
+ (-equiv [o other]
+ (and (instance? DateTime other)
+ (== (.valueOf o) (.valueOf other))))))
+
+#?(:cljs
+ (extend-protocol cljs.core/Inst
+ DateTime
+ (inst-ms* [inst] (.toMillis ^js inst))
+
+ Duration
+ (inst-ms* [inst] (.toMillis ^js inst)))
+
+ :clj
+ (extend-protocol clojure.core/Inst
+ Duration
+ (inst-ms* [v] (.toMillis ^Duration v))
+
+ Instant
+ (inst-ms* [v] (.toEpochMilli ^Instant v))))
diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc
index 8049628941..3a726d77a5 100644
--- a/common/src/app/common/types/color.cljc
+++ b/common/src/app/common/types/color.cljc
@@ -70,18 +70,20 @@
[:offset ::sm/safe-number]]]]])
(sm/define! ::color
- [:map {:title "Color"}
- [:id {:optional true} ::sm/uuid]
- [:name {:optional true} :string]
- [:path {:optional true} [:maybe :string]]
- [:value {:optional true} [:maybe :string]]
- [:color {:optional true} [:maybe ::rgb-color]]
- [:opacity {:optional true} [:maybe ::sm/safe-number]]
- [:modified-at {:optional true} ::sm/inst]
- [:ref-id {:optional true} ::sm/uuid]
- [:ref-file {:optional true} ::sm/uuid]
- [:gradient {:optional true} [:maybe ::gradient]]
- [:image {:optional true} [:maybe ::image-color]]])
+ [:and
+ [:map {:title "Color"}
+ [:id {:optional true} ::sm/uuid]
+ [:name {:optional true} :string]
+ [:path {:optional true} [:maybe :string]]
+ [:value {:optional true} [:maybe :string]]
+ [:color {:optional true} [:maybe ::rgb-color]]
+ [:opacity {:optional true} [:maybe ::sm/safe-number]]
+ [:modified-at {:optional true} ::sm/inst]
+ [:ref-id {:optional true} ::sm/uuid]
+ [:ref-file {:optional true} ::sm/uuid]
+ [:gradient {:optional true} [:maybe ::gradient]]
+ [:image {:optional true} [:maybe ::image-color]]]
+ [::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/define! ::recent-color
[:and
diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc
index b00a41b1a4..cdb8f7e3d9 100644
--- a/common/src/app/common/types/container.cljc
+++ b/common/src/app/common/types/container.cljc
@@ -387,3 +387,49 @@
(if (ctk/in-component-copy? parent)
true
(has-any-copy-parent? objects (:parent-id shape))))))
+
+(defn has-any-main?
+ "Check if the shape has any children or parent that is a main component."
+ [objects shape]
+ (let [children (cfh/get-children-with-self objects (:id shape))
+ parents (cfh/get-parents objects (:id shape))]
+ (or
+ (some ctk/main-instance? children)
+ (some ctk/main-instance? parents))))
+
+(defn valid-shape-for-component?
+ "Check if a main component can be generated from this shape in terms of nested components:
+ - A main can't be the ancestor of another main
+ - A main can't be nested in copies"
+ [objects shape]
+ (and
+ (not (has-any-main? objects shape))
+ (not (has-any-copy-parent? objects shape))))
+
+(defn- invalid-structure-for-component?
+ "Check if the structure generated nesting children in parent is invalid in terms of nested components"
+ [objects parent children]
+ (let [selected-main-instance? (some true? (map #(has-any-main? objects %) children))
+ parent-in-component? (in-any-component? objects parent)
+ comps-nesting-loop? (not (->> children
+ (map #(cfh/components-nesting-loop? objects (:id %) (:id parent)))
+ (every? nil?)))]
+ (or
+ ;;We don't want to change the structure of component copies
+ (ctk/in-component-copy? parent)
+ ;; If we are moving something containing a main instance the container can't be part of a component (neither main nor copy)
+ (and selected-main-instance? parent-in-component?)
+ ;; Avoid placing a shape as a direct or indirect child of itself,
+ ;; or inside its main component if it's in a copy.
+ comps-nesting-loop?)))
+
+(defn find-valid-parent-and-frame-ids
+ "Navigate trough the ancestors until find one that is valid"
+ [parent-id objects children]
+ (let [parent (get objects parent-id)]
+ (if (invalid-structure-for-component? objects parent children)
+ (find-valid-parent-and-frame-ids (:parent-id parent) objects children)
+ [parent-id
+ (if (= :frame (:type parent))
+ parent-id
+ (:frame-id parent))])))
diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc
index ec24c52a27..6c1d427dff 100644
--- a/common/src/app/common/types/page.cljc
+++ b/common/src/app/common/types/page.cljc
@@ -7,7 +7,6 @@
(ns app.common.types.page
(:require
[app.common.data :as d]
- [app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.color :as-alias ctc]
[app.common.types.grid :as ctg]
@@ -71,13 +70,9 @@
(defn make-empty-page
[id name]
- (let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*
- wrap-pointer-fn cfeat/*wrap-with-pointer-map-fn*]
- (-> empty-page-data
- (assoc :id id)
- (assoc :name name)
- (update :objects wrap-objects-fn)
- (wrap-pointer-fn))))
+ (-> empty-page-data
+ (assoc :id id)
+ (assoc :name name)))
;; --- Helpers for flow
diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc
index 28289219d4..cdc7ccb163 100644
--- a/common/src/app/common/types/shape.cljc
+++ b/common/src/app/common/types/shape.cljc
@@ -25,6 +25,7 @@
[app.common.types.shape.export :as ctse]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctsl]
+ [app.common.types.shape.path :as ctsp]
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctsx]
[app.common.uuid :as uuid]
@@ -46,6 +47,7 @@
:bool
:rect
:path
+ :text
:circle
:svg-raw
:image})
@@ -126,21 +128,24 @@
[:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]])
-(sm/define! ::minimal-shape-attrs
+(sm/define! ::shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
- [:id {:optional false} ::sm/uuid]
- [:name {:optional false} :string]
- [:type {:optional false} [::sm/one-of shape-types]]
- [:x {:optional false} [:maybe ::sm/safe-number]]
- [:y {:optional false} [:maybe ::sm/safe-number]]
- [:width {:optional false} [:maybe ::sm/safe-number]]
- [:height {:optional false} [:maybe ::sm/safe-number]]
- [:selrect {:optional false} ::selrect]
- [:points {:optional false} ::points]
- [:transform {:optional false} ::gmt/matrix]
- [:transform-inverse {:optional false} ::gmt/matrix]
- [:parent-id {:optional false} ::sm/uuid]
- [:frame-id {:optional false} ::sm/uuid]])
+ [:id ::sm/uuid]
+ [:name :string]
+ [:type [::sm/one-of shape-types]]
+ [:selrect ::selrect]
+ [:points ::points]
+ [:transform ::gmt/matrix]
+ [:transform-inverse ::gmt/matrix]
+ [:parent-id ::sm/uuid]
+ [:frame-id ::sm/uuid]])
+
+(sm/define! ::shape-geom-attrs
+ [:map {:title "ShapeGeometryAttrs"}
+ [:x ::sm/safe-number]
+ [:y ::sm/safe-number]
+ [:width ::sm/safe-number]
+ [:height ::sm/safe-number]])
(sm/define! ::shape-attrs
[:map {:title "ShapeAttrs"}
@@ -199,7 +204,7 @@
(sm/define! ::group-attrs
[:map {:title "GroupAttrs"}
[:type [:= :group]]
- [:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]])
+ [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]])
(sm/define! ::frame-attrs
[:map {:title "FrameAttrs"}
@@ -212,7 +217,7 @@
(sm/define! ::bool-attrs
[:map {:title "BoolAttrs"}
[:type [:= :bool]]
- [:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]
+ [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]
;; FIXME: improve this schema
[:bool-type :keyword]
@@ -252,16 +257,7 @@
(sm/define! ::path-attrs
[:map {:title "PathAttrs"}
[:type [:= :path]]
- [:x {:optional true} [:maybe ::sm/safe-number]]
- [:y {:optional true} [:maybe ::sm/safe-number]]
- [:width {:optional true} [:maybe ::sm/safe-number]]
- [:height {:optional true} [:maybe ::sm/safe-number]]
- [:content
- {:optional true}
- [:vector
- [:map
- [:command :keyword]
- [:params {:optional true} [:maybe :map]]]]]])
+ [:content ::ctsp/content]])
(sm/define! ::text-attrs
[:map {:title "TextAttrs"}
@@ -271,72 +267,96 @@
(sm/define! ::shape-map
[:multi {:dispatch :type :title "Shape"}
[:group
- [:merge {:title "GroupShape"}
+ [:and {:title "GroupShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::group-attrs
::ctsl/layout-child-attrs]]
[:frame
- [:merge {:title "FrameShape"}
- ::minimal-shape-attrs
+ [:and {:title "FrameShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::frame-attrs
::ctsl/layout-attrs
::ctsl/layout-child-attrs]]
[:bool
- [:merge {:title "BoolShape"}
+ [:and {:title "BoolShape"}
+ ::shape-base-attrs
::shape-attrs
- ::minimal-shape-attrs
::bool-attrs
::ctsl/layout-child-attrs]]
[:rect
- [:merge {:title "RectShape"}
+ [:and {:title "RectShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::rect-attrs
::ctsl/layout-child-attrs]]
[:circle
- [:merge {:title "CircleShape"}
+ [:and {:title "CircleShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::circle-attrs
::ctsl/layout-child-attrs]]
[:image
- [:merge {:title "ImageShape"}
+ [:and {:title "ImageShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::image-attrs
::ctsl/layout-child-attrs]]
[:svg-raw
- [:merge {:title "SvgRawShape"}
+ [:and {:title "SvgRawShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::svg-raw-attrs
::ctsl/layout-child-attrs]]
[:path
- [:merge {:title "PathShape"}
+ [:and {:title "PathShape"}
+ ::shape-base-attrs
::shape-attrs
- ::minimal-shape-attrs
::path-attrs
::ctsl/layout-child-attrs]]
[:text
- [:merge {:title "TextShape"}
+ [:and {:title "TextShape"}
+ ::shape-base-attrs
+ ::shape-geom-attrs
::shape-attrs
- ::minimal-shape-attrs
::text-attrs
::ctsl/layout-child-attrs]]])
(sm/define! ::shape
[:and
{:title "Shape"
- :gen/gen (->> (sg/generator ::shape-map)
+ :gen/gen (->> (sg/generator ::shape-base-attrs)
+ (sg/mcat (fn [{:keys [type] :as shape}]
+ (sg/let [attrs1 (sg/generator ::shape-attrs)
+ attrs2 (sg/generator ::shape-geom-attrs)
+ attrs3 (case type
+ :text (sg/generator ::text-attrs)
+ :path (sg/generator ::path-attrs)
+ :svg-raw (sg/generator ::svg-raw-attrs)
+ :image (sg/generator ::image-attrs)
+ :circle (sg/generator ::circle-attrs)
+ :rect (sg/generator ::rect-attrs)
+ :bool (sg/generator ::bool-attrs)
+ :group (sg/generator ::group-attrs)
+ :frame (sg/generator ::frame-attrs))]
+ (if (or (= type :path)
+ (= type :bool))
+ (merge attrs1 shape attrs3)
+ (merge attrs1 shape attrs2 attrs3)))))
(sg/fmap map->Shape))}
::shape-map
[:fn shape?]])
@@ -491,7 +511,12 @@
the shape. The props must have :x :y :width :height."
[{:keys [type] :as props}]
(let [shape (make-minimal-shape type)
- shape (merge shape (d/without-nils props))
+
+ ;; The props can be custom records that does not
+ ;; work properly with without-nils, so we first make
+ ;; it plain map for proceed
+ props (d/without-nils (into {} props))
+ shape (merge shape (d/without-nils (into {} props)))
shape (case (:type shape)
(:bool :path) (setup-path shape)
:image (-> shape setup-rect setup-image)
diff --git a/common/src/app/common/types/shape/path.cljc b/common/src/app/common/types/shape/path.cljc
new file mode 100644
index 0000000000..d633bb85c6
--- /dev/null
+++ b/common/src/app/common/types/shape/path.cljc
@@ -0,0 +1,47 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.common.types.shape.path
+ (:require
+ [app.common.schema :as sm]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; SCHEMA
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(sm/define! ::segment
+ [:multi {:title "PathSegment" :dispatch :command}
+ [:line-to
+ [:map
+ [:command [:= :line-to]]
+ [:params
+ [:map
+ [:x ::sm/safe-number]
+ [:y ::sm/safe-number]]]]]
+ [:close-path
+ [:map
+ [:command [:= :close-path]]]]
+ [:move-to
+ [:map
+ [:command [:= :move-to]]
+ [:params
+ [:map
+ [:x ::sm/safe-number]
+ [:y ::sm/safe-number]]]]]
+ [:curve-to
+ [:map
+ [:command [:= :curve-to]]
+ [:params
+ [:map
+ [:x ::sm/safe-number]
+ [:y ::sm/safe-number]
+ [:c1x ::sm/safe-number]
+ [:c1y ::sm/safe-number]
+ [:c2x ::sm/safe-number]
+ [:c2y ::sm/safe-number]]]]]])
+
+(sm/define! ::content
+ [:vector ::segment])
diff --git a/common/src/app/common/types/shape/shadow.cljc b/common/src/app/common/types/shape/shadow.cljc
index d04886fa3a..cc2fd81c3c 100644
--- a/common/src/app/common/types/shape/shadow.cljc
+++ b/common/src/app/common/types/shape/shadow.cljc
@@ -7,8 +7,7 @@
(ns app.common.types.shape.shadow
(:require
[app.common.schema :as sm]
- [app.common.types.color :as ctc]
- [app.common.types.shape.shadow.color :as-alias shadow-color]))
+ [app.common.types.color :as ctc]))
(def styles #{:drop-shadow :inner-shadow})
@@ -21,11 +20,4 @@
[:blur ::sm/safe-number]
[:spread ::sm/safe-number]
[:hidden :boolean]
- ;;FIXME: reuse color?
- [:color
- [:map
- [:color {:optional true} :string]
- [:opacity {:optional true} ::sm/safe-number]
- [:gradient {:optional true} [:maybe ::ctc/gradient]]
- [:file-id {:optional true} [:maybe ::sm/uuid]]
- [:id {:optional true} [:maybe ::sm/uuid]]]]])
+ [:color ::ctc/color]])
diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc
index f8e50a6cbc..064a3eae0f 100644
--- a/common/src/app/common/types/shape_tree.cljc
+++ b/common/src/app/common/types/shape_tree.cljc
@@ -176,11 +176,11 @@
(->> (get-root-shapes objects)
(mapv :id)))
-(defn get-base
- [objects id-a id-b]
+(defn- get-base
+ [id-a id-b id-parents]
- (let [[parents-a parents-a-index] (cfh/get-parent-ids-with-index objects id-a)
- [parents-b parents-b-index] (cfh/get-parent-ids-with-index objects id-b)
+ (let [[parents-a parents-a-index] (get id-parents id-a)
+ [parents-b parents-b-index] (get id-parents id-b)
parents-a (cons id-a parents-a)
parents-b (into #{id-b} parents-b)
@@ -194,9 +194,9 @@
[base-id idx-a idx-b]))
(defn- is-shape-over-shape?
- [objects base-shape-id over-shape-id bottom-frames?]
+ [objects base-shape-id over-shape-id bottom-frames? id-parents]
- (let [[base-id index-a index-b] (get-base objects base-shape-id over-shape-id)]
+ (let [[base-id index-a index-b] (get-base base-shape-id over-shape-id id-parents)]
(cond
;; The base the base shape, so the other item is below (if not bottom-frames)
(= base-id base-shape-id)
@@ -234,33 +234,37 @@
([objects ids {:keys [bottom-frames?] :as options
:or {bottom-frames? false}}]
- (letfn [(comp [id-a id-b]
- (cond
- (= id-a id-b)
- 0
+ ;; Create an index of the parents of the shapes. This will speed the sorting because we use
+ ;; this information down the line.
+ (let [id-parents (into {} (map #(vector % (cfh/get-parent-ids-with-index objects %))) ids)]
+ (letfn [(comp [id-a id-b]
+ (cond
+ (= id-a id-b)
+ 0
- (is-shape-over-shape? objects id-a id-b bottom-frames?)
- 1
+ (is-shape-over-shape? objects id-a id-b bottom-frames? id-parents)
+ 1
- :else
- -1))]
- (sort comp ids))))
+ :else
+ -1))]
+ (sort comp ids)))))
(defn sort-z-index-objects
([objects items]
(sort-z-index-objects objects items nil))
([objects items {:keys [bottom-frames?]
:or {bottom-frames? false}}]
- (d/unstable-sort
- (fn [obj-a obj-b]
- (let [id-a (dm/get-prop obj-a :id)
- id-b (dm/get-prop obj-b :id)]
- (if (= id-a id-b)
- 0
- (if ^boolean (is-shape-over-shape? objects id-a id-b bottom-frames?)
- 1
- -1))))
- items)))
+ (let [id-parents (into {} (map #(vector (dm/get-prop % :id) (cfh/get-parent-ids-with-index objects (dm/get-prop % :id)))) items)]
+ (d/unstable-sort
+ (fn [obj-a obj-b]
+ (let [id-a (dm/get-prop obj-a :id)
+ id-b (dm/get-prop obj-b :id)]
+ (if (= id-a id-b)
+ 0
+ (if ^boolean (is-shape-over-shape? objects id-a id-b bottom-frames? id-parents)
+ 1
+ -1))))
+ items))))
(defn get-frame-by-position
([objects position]
diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc
index b205c64534..2086a0a5bd 100644
--- a/common/src/app/common/uuid.cljc
+++ b/common/src/app/common/uuid.cljc
@@ -75,3 +75,12 @@
with base62. It is only safe to use with uuid v4 and penpot custom v8"
[id]
(impl/short-v8 (dm/str id))))
+
+#?(:clj
+ (defn hash-int
+ [id]
+ (let [a (.getMostSignificantBits ^UUID id)
+ b (.getLeastSignificantBits ^UUID id)]
+ (+ (clojure.lang.Murmur3/hashLong a)
+ (clojure.lang.Murmur3/hashLong b)))))
+
diff --git a/frontend/resources/images/icons/download-refactor.svg b/frontend/resources/images/icons/download-refactor.svg
new file mode 100644
index 0000000000..ee4af8deda
--- /dev/null
+++ b/frontend/resources/images/icons/download-refactor.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/feedback-refactor.svg b/frontend/resources/images/icons/feedback-refactor.svg
new file mode 100644
index 0000000000..56a509263d
--- /dev/null
+++ b/frontend/resources/images/icons/feedback-refactor.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/pin-refactor.svg b/frontend/resources/images/icons/pin-refactor.svg
new file mode 100644
index 0000000000..2d71bc0e2f
--- /dev/null
+++ b/frontend/resources/images/icons/pin-refactor.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/styles/common/refactor/common-dashboard.scss b/frontend/resources/styles/common/refactor/common-dashboard.scss
index f2de6c4eca..7e1830ab70 100644
--- a/frontend/resources/styles/common/refactor/common-dashboard.scss
+++ b/frontend/resources/styles/common/refactor/common-dashboard.scss
@@ -33,13 +33,6 @@
}
}
- svg {
- fill: $db-secondary;
- height: $s-12;
- margin-right: $s-4;
- width: $s-12;
- }
-
nav {
display: flex;
align-items: flex-end;
@@ -98,7 +91,6 @@
display: flex;
align-items: center;
cursor: pointer;
- margin-left: $s-8;
svg {
fill: $df-secondary;
@@ -119,22 +111,9 @@
.dashboard-header-actions {
display: flex;
+ column-gap: $s-16;
}
- .pin-icon {
- margin: 0 $s-8 0 $s-24;
- background-color: transparent;
- border: none;
- svg {
- fill: $df-secondary;
- }
-
- &.active {
- svg {
- fill: $db-cuaternary;
- }
- }
- }
.dashboard-header-options {
li {
a {
diff --git a/frontend/src/app/main/data/dashboard/shortcuts.cljs b/frontend/src/app/main/data/dashboard/shortcuts.cljs
index 2181e96def..3d27669b6d 100644
--- a/frontend/src/app/main/data/dashboard/shortcuts.cljs
+++ b/frontend/src/app/main/data/dashboard/shortcuts.cljs
@@ -32,9 +32,10 @@
:subsections [:general-dashboard]
:fn #(st/emit! (dd/create-element))}
- :toggle-light-dark {:tooltip (ds/meta (ds/alt "Q"))
- :command (ds/c-mod "alt+q")
- :fn #(st/emit! (du/toggle-theme))}})
+ :toggle-theme {:tooltip (ds/meta (ds/alt "M"))
+ :command (ds/c-mod "alt+m")
+ :subsections [:general-dashboard]
+ :fn #(st/emit! (du/toggle-theme))}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs
index ae361f3e15..af8401d8a5 100644
--- a/frontend/src/app/main/data/viewer.cljs
+++ b/frontend/src/app/main/data/viewer.cljs
@@ -558,7 +558,7 @@
;; --- Overlays
(defn- open-overlay*
- [state frame position snap-to close-click-outside background-overlay animation]
+ [state frame position snap-to close-click-outside background-overlay animation fixed-source?]
(cond-> state
:always
(update :viewer-overlays conj
@@ -568,7 +568,8 @@
:snap-to snap-to
:close-click-outside close-click-outside
:background-overlay background-overlay
- :animation animation})
+ :animation animation
+ :fixed-source? fixed-source?})
(some? animation)
(assoc-in [:viewer-animations (:id frame)]
@@ -588,7 +589,7 @@
:animation animation})))
(defn open-overlay
- [frame-id position snap-to close-click-outside background-overlay animation]
+ [frame-id position snap-to close-click-outside background-overlay animation fixed-source?]
(dm/assert! (uuid? frame-id))
(dm/assert! (gpt/point? position))
(dm/assert! (or (nil? close-click-outside)
@@ -613,12 +614,13 @@
snap-to
close-click-outside
background-overlay
- animation)
+ animation
+ fixed-source?)
state)))))
(defn toggle-overlay
- [frame-id position snap-to close-click-outside background-overlay animation]
+ [frame-id position snap-to close-click-outside background-overlay animation fixed-source?]
(dm/assert! (uuid? frame-id))
(dm/assert! (gpt/point? position))
(dm/assert! (or (nil? close-click-outside)
@@ -644,7 +646,8 @@
snap-to
close-click-outside
background-overlay
- animation)
+ animation
+ fixed-source?)
(close-overlay* state
(:id frame)
(ctsi/invert-direction animation)))))))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index ee5cf792d9..243772c992 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -1845,18 +1845,9 @@
tree-root (get-tree-root-shapes pobjects)
only-one-root-shape? (and
(< 1 (count pobjects))
- (= 1 (count tree-root)))
- all-objects (merge page-objects pobjects)
- comps-nesting-loop? (not (->> (keys pobjects)
- (map #(cfh/components-nesting-loop? all-objects % (:id base)))
- (every? nil?)))]
+ (= 1 (count tree-root)))]
(cond
- comps-nesting-loop?
- ;; Avoid placing a shape as a direct or indirect child of itself,
- ;; or inside its main component if it's in a copy.
- [uuid/zero uuid/zero (gpt/subtract position orig-pos)]
-
(selected-frame? state)
(if (or (any-same-frame-from-selected? state (keys pobjects))
@@ -1869,7 +1860,7 @@
paste-y (:y selected-frame-obj)
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
- [(:frame-id base) parent-id delta index])
+ [parent-id delta index])
;; Paste inside selected frame otherwise
(let [selected-frame-obj (get page-objects (first page-selected))
@@ -1902,20 +1893,19 @@
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right and bottom in the original frame
(gpt/point paste-x paste-y))]
- [frame-id frame-id delta (dec (count (:shapes selected-frame-obj)))]))
+ [frame-id delta (dec (count (:shapes selected-frame-obj)))]))
(empty? page-selected)
(let [frame-id (ctst/top-nested-frame page-objects position)
delta (gpt/subtract position orig-pos)]
- [frame-id frame-id delta])
+ [frame-id delta])
:else
- (let [frame-id (:frame-id base)
- parent-id (:parent-id base)
+ (let [parent-id (:parent-id base)
delta (if in-viewport?
(gpt/subtract position orig-pos)
(gpt/subtract (gpt/point (:selrect base)) orig-pos))]
- [frame-id parent-id delta index]))))
+ [parent-id delta index]))))
;; Change the indexes of the pasted shapes
(change-add-obj-index [objects selected index change]
@@ -1953,64 +1943,65 @@
(ptk/reify ::paste-shapes
ptk/WatchEvent
(watch [it state _]
- (let [file-id (:current-file-id state)
- page (wsh/lookup-page state)
+ (let [file-id (:current-file-id state)
+ page (wsh/lookup-page state)
- media-idx (->> (:media pdata)
- (d/index-by :prev-id))
+ media-idx (->> (:media pdata)
+ (d/index-by :prev-id))
- selected (:selected pdata)
- objects (:objects pdata)
+ selected (:selected pdata)
+ objects (:objects pdata)
- position (deref ms/mouse-position)
+ position (deref ms/mouse-position)
;; Calculate position for the pasted elements
- [frame-id
- parent-id
+ [candidate-parent-id
delta
- index] (calculate-paste-position state objects selected position)
+ index] (calculate-paste-position state objects selected position)
- ;; We don't want to change the structure of component
- ;; copies If the parent-id or the frame-id are
- ;; component-copies, we need to get the first not copy
- ;; parent
- parent-id (:id (ctn/get-first-not-copy-parent (:objects page) parent-id))
- frame-id (:id (ctn/get-first-not-copy-parent (:objects page) frame-id))
+ page-objects (:objects page)
- objects (update-vals objects (partial process-shape file-id frame-id parent-id))
- all-objects (merge (:objects page) objects)
+ [parent-id
+ frame-id] (ctn/find-valid-parent-and-frame-ids candidate-parent-id page-objects (vals objects))
- libraries (wsh/get-libraries state)
- ldata (wsh/get-file state file-id)
+ index (if (= candidate-parent-id parent-id)
+ index
+ 0)
- drop-cell (when (ctl/grid-layout? all-objects parent-id)
- (gslg/get-drop-cell frame-id all-objects position))
+ objects (update-vals objects (partial process-shape file-id frame-id parent-id))
- changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id)
- (pcb/amend-changes (partial process-rchange media-idx))
- (pcb/amend-changes (partial change-add-obj-index objects selected index)))
+ all-objects (merge page-objects objects)
+
+ libraries (wsh/get-libraries state)
+ ldata (wsh/get-file state file-id)
+
+ drop-cell (when (ctl/grid-layout? all-objects parent-id)
+ (gslg/get-drop-cell frame-id all-objects position))
+
+ changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id)
+ (pcb/amend-changes (partial process-rchange media-idx))
+ (pcb/amend-changes (partial change-add-obj-index objects selected index)))
;; Adds a resize-parents operation so the groups are
;; updated. We add all the new objects
- changes (->> (:redo-changes changes)
- (filter add-obj?)
- (map :id)
- (pcb/resize-parents changes))
+ changes (->> (:redo-changes changes)
+ (filter add-obj?)
+ (map :id)
+ (pcb/resize-parents changes))
- selected (into (d/ordered-set)
- (comp
- (filter add-obj?)
- (filter #(contains? selected (:old-id %)))
- (map :obj)
- (map :id))
- (:redo-changes changes))
+ selected (into (d/ordered-set)
+ (comp
+ (filter add-obj?)
+ (filter #(contains? selected (:old-id %)))
+ (map :obj)
+ (map :id))
+ (:redo-changes changes))
- changes (cond-> changes
- (some? drop-cell)
- (pcb/update-shapes [parent-id]
- #(ctl/add-children-to-cell % selected all-objects drop-cell)))
-
- undo-id (js/Symbol)]
+ changes (cond-> changes
+ (some? drop-cell)
+ (pcb/update-shapes [parent-id]
+ #(ctl/add-children-to-cell % selected all-objects drop-cell)))
+ undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 54047f0e3c..d24b8c95f7 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -340,12 +340,16 @@
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state _]
- (let [objects (wsh/lookup-page-objects state)
- selected (->> (wsh/lookup-selected state)
- (cfh/clean-loops objects)
- (remove #(ctn/has-any-copy-parent? objects (get objects %)))) ;; We don't want to change the structure of component copies
- components-v2 (features/active-feature? state "components/v2")]
- (rx/of (add-component2 selected components-v2))))))
+ (let [objects (wsh/lookup-page-objects state)
+ selected (->> (wsh/lookup-selected state)
+ (cfh/clean-loops objects))
+ selected-objects (map #(get objects %) selected)
+ components-v2 (features/active-feature? state "components/v2")
+ ;; We don't want to change the structure of component copies
+ can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))]
+
+ (when can-make-component
+ (rx/of (add-component2 selected components-v2)))))))
(defn add-multiple-components
"Add several new components to current file library, from the currently selected shapes."
@@ -353,19 +357,22 @@
(ptk/reify ::add-multiple-components
ptk/WatchEvent
(watch [_ state _]
- (let [components-v2 (features/active-feature? state "components/v2")
- objects (wsh/lookup-page-objects state)
- selected (->> (wsh/lookup-selected state)
- (cfh/clean-loops objects)
- (remove #(ctn/has-any-copy-parent? objects (get objects %)))) ;; We don't want to change the structure of component copies
- added-components (map
- #(add-component2 [%] components-v2)
- selected)
+ (let [components-v2 (features/active-feature? state "components/v2")
+ objects (wsh/lookup-page-objects state)
+ selected (->> (wsh/lookup-selected state)
+ (cfh/clean-loops objects))
+ selected-objects (map #(get objects %) selected)
+ ;; We don't want to change the structure of component copies
+ can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))
+ added-components (map
+ #(add-component2 [%] components-v2)
+ selected)
undo-id (js/Symbol)]
- (rx/concat
- (rx/of (dwu/start-undo-transaction undo-id))
- (rx/from added-components)
- (rx/of (dwu/commit-undo-transaction undo-id)))))))
+ (when can-make-component
+ (rx/concat
+ (rx/of (dwu/start-undo-transaction undo-id))
+ (rx/from added-components)
+ (rx/of (dwu/commit-undo-transaction undo-id))))))))
(defn rename-component
"Rename the component with the given id, in the current file library."
diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs
index 2e926e3154..c5760e99f7 100644
--- a/frontend/src/app/main/data/workspace/path/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/path/drawing.cljs
@@ -276,7 +276,8 @@
(cond-> (some? drop-index)
(with-meta {:index drop-index})))))))))
-(defn handle-new-shape-result [shape-id]
+(defn handle-new-shape-result
+ [shape-id]
(ptk/reify ::handle-new-shape-result
ptk/UpdateEvent
(update [_ state]
@@ -293,7 +294,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [content (get-in state [:workspace-drawing :object :content] [])]
- (if (seq content)
+ (if (and (seq content) (> (count content) 1))
(rx/of (setup-frame)
(dwdc/handle-finish-drawing)
(dwe/start-edition-mode shape-id)
diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs
index 8f83f67eef..7053aab626 100644
--- a/frontend/src/app/main/data/workspace/path/edition.cljs
+++ b/frontend/src/app/main/data/workspace/path/edition.cljs
@@ -319,9 +319,9 @@
(= (ptk/type %) ::start-path-edit))))
interrupt (->> stream (rx/filter #(= % :interrupt)) (rx/take 1))]
(rx/concat
- (rx/of (dwc/hide-toolbar))
- (rx/of (undo/start-path-undo))
- (rx/of (drawing/change-edit-mode mode))
+ (rx/of (dwc/hide-toolbar)
+ (undo/start-path-undo)
+ (drawing/change-edit-mode mode))
(->> interrupt
(rx/map #(stop-path-edit id))
(rx/take-until stopper)))))))
diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs
index ffbf365784..74e0954689 100644
--- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs
@@ -8,6 +8,7 @@
(:require
[app.main.data.shortcuts :as ds]
[app.main.data.workspace :as dw]
+ [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.path :as drp]
[app.main.store :as st]
[beicon.v2.core :as rx]
@@ -26,10 +27,13 @@
;; Not interrupt when we're editing a path
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
(get-in state [:workspace-local :edition]))
+ content (get-in state [:workspace-drawing :object :content])
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
(if-not (= :draw path-edit-mode)
(rx/of :interrupt)
- (rx/empty))))))
+ (if (<= (count content) 1)
+ (rx/of (dwc/show-toolbar))
+ (rx/empty)))))))
(def shortcuts
{:move-nodes {:tooltip "M"
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index 05742a14bd..7c6aecea5b 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -59,7 +59,7 @@
(assoc-in state [:workspace-local :selrect] selrect))))
(defn handle-area-selection
- [preserve? ignore-groups?]
+ [preserve?]
(ptk/reify ::handle-area-selection
ptk/WatchEvent
(watch [_ state stream]
@@ -114,7 +114,20 @@
(rx/buffer-time 100)
(rx/map last)
(rx/pipe (rxo/distinct-contiguous))
- (rx/map #(select-shapes-by-current-selrect preserve? ignore-groups?))))
+ (rx/with-latest-from ms/keyboard-mod ms/keyboard-shift)
+ (rx/map
+ (fn [[_ mod? shift?]]
+ (select-shapes-by-current-selrect shift? mod?))))
+
+ ;; The last "tick" from the mouse cannot be buffered so we are sure
+ ;; a selection is returned. Without this we can have empty selections on
+ ;; very fast movement
+ (->> selrect-stream
+ (rx/last)
+ (rx/with-latest-from ms/keyboard-mod ms/keyboard-shift)
+ (rx/map
+ (fn [[_ mod? shift?]]
+ (select-shapes-by-current-selrect shift? mod? false)))))
(->> (rx/of (update-selrect nil))
;; We need the async so the current event finishes before updating the selrect
@@ -307,34 +320,39 @@
;; --- Select Shapes (By selrect)
(defn select-shapes-by-current-selrect
- [preserve? ignore-groups?]
- (ptk/reify ::select-shapes-by-current-selrect
- ptk/WatchEvent
- (watch [_ state _]
- (let [page-id (:current-page-id state)
- objects (wsh/lookup-page-objects state)
- selected (wsh/lookup-selected state)
- initial-set (if preserve?
- selected
- lks/empty-linked-set)
- selrect (dm/get-in state [:workspace-local :selrect])
- blocked? (fn [id] (dm/get-in objects [id :blocked] false))]
+ ([preserve? ignore-groups?]
+ (select-shapes-by-current-selrect preserve? ignore-groups? true))
+ ([preserve? ignore-groups? buffered?]
+ (ptk/reify ::select-shapes-by-current-selrect
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [page-id (:current-page-id state)
+ objects (wsh/lookup-page-objects state)
+ selected (wsh/lookup-selected state)
+ initial-set (if preserve?
+ selected
+ lks/empty-linked-set)
+ selrect (dm/get-in state [:workspace-local :selrect])
+ blocked? (fn [id] (dm/get-in objects [id :blocked] false))
- (when selrect
- (rx/empty)
- (->> (uw/ask-buffered!
- {:cmd :selection/query
- :page-id page-id
- :rect selrect
- :include-frames? true
- :ignore-groups? ignore-groups?
- :full-frame? true
- :using-selrect? true})
- (rx/map #(cfh/clean-loops objects %))
- (rx/map #(into initial-set (comp
- (filter (complement blocked?))
- (remove (partial cfh/hidden-parent? objects))) %))
- (rx/map select-shapes)))))))
+ ask-worker (if buffered? uw/ask-buffered! uw/ask!)]
+
+ (if (some? selrect)
+ (->> (ask-worker
+ {:cmd :selection/query
+ :page-id page-id
+ :rect selrect
+ :include-frames? true
+ :ignore-groups? ignore-groups?
+ :full-frame? true
+ :using-selrect? true})
+ (rx/filter some?)
+ (rx/map #(cfh/clean-loops objects %))
+ (rx/map #(into initial-set (comp
+ (filter (complement blocked?))
+ (remove (partial cfh/hidden-parent? objects))) %))
+ (rx/map select-shapes))
+ (rx/empty)))))))
(defn select-inside-group
[group-id position]
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 81689f6b29..b5d91faa3a 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -551,9 +551,10 @@
;; THEME
- :toggle-light-dark {:tooltip (ds/meta (ds/alt "Q"))
- :command (ds/c-mod "alt+q")
- :fn #(st/emit! (du/toggle-theme))}})
+ :toggle-theme {:tooltip (ds/meta (ds/alt "M"))
+ :command (ds/c-mod "alt+m")
+ :subsections [:basics]
+ :fn #(st/emit! (du/toggle-theme))}})
(def opacity-shortcuts
(into {} (->>
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index de8195d90a..dda92054b9 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -560,13 +560,14 @@
(rx/map
(fn [[move-vector mod?]]
- (let [position (gpt/add from-position move-vector)
- exclude-frames (if mod? exclude-frames exclude-frames-siblings)
- target-frame (ctst/top-nested-frame objects position exclude-frames)
- flex-layout? (ctl/flex-layout? objects target-frame)
- grid-layout? (ctl/grid-layout? objects target-frame)
- drop-index (when flex-layout? (gslf/get-drop-index target-frame objects position))
- cell-data (when (and grid-layout? (not mod?)) (gslg/get-drop-cell target-frame objects position))]
+ (let [position (gpt/add from-position move-vector)
+ exclude-frames (if mod? exclude-frames exclude-frames-siblings)
+ target-frame (ctst/top-nested-frame objects position exclude-frames)
+ [target-frame _] (ctn/find-valid-parent-and-frame-ids target-frame objects shapes)
+ flex-layout? (ctl/flex-layout? objects target-frame)
+ grid-layout? (ctl/grid-layout? objects target-frame)
+ drop-index (when flex-layout? (gslf/get-drop-index target-frame objects position))
+ cell-data (when (and grid-layout? (not mod?)) (gslg/get-drop-cell target-frame objects position))]
(array move-vector target-frame drop-index cell-data))))
(rx/take-until stopper))]
@@ -587,16 +588,12 @@
[(assoc move-vector :x 0) :x]
:else
- [move-vector nil])
+ [move-vector nil])]
- nesting-loop? (some #(cfh/components-nesting-loop? objects (:id %) target-frame) shapes)
- is-component-copy? (ctk/in-component-copy? (get objects target-frame))]
- (cond-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector))
- (and (not nesting-loop?) (not is-component-copy?))
- (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)
- :always
- (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis}))))))
+ (-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector))
+ (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)
+ (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis}))))))
(->> move-stream
(rx/with-latest-from ms/mouse-position-alt)
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 58fce57916..1a63a5032b 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -86,7 +86,7 @@
[files selected]
(let [get-file #(get files %)
sim-file #(select-keys % [:id :name :project-id :is-shared])
- xform (comp (map get-file)
+ xform (comp (keep get-file)
(map sim-file))]
(->> (into #{} xform selected)
(d/index-by :id))))
@@ -96,14 +96,15 @@
;; we need to this because :dashboard-search-result is a list
;; of maps and we need a map of maps (using :id as key).
(let [files (d/index-by :id (:dashboard-search-result state))]
- (dashboard-extract-selected files (dm/get-in state [:dashboard-local :selected-files]))))
+ (->> (dm/get-in state [:dashboard-local :selected-files])
+ (dashboard-extract-selected files))))
st/state))
(def dashboard-selected-files
(l/derived (fn [state]
- (dashboard-extract-selected (:dashboard-files state)
- (dm/get-in state [:dashboard-local :selected-files])))
- st/state =))
+ (->> (dm/get-in state [:dashboard-local :selected-files])
+ (dashboard-extract-selected (:dashboard-files state))))
+ st/state))
;; ---- Workspace refs
diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs
index 536e98b661..4fc3d970d8 100644
--- a/frontend/src/app/main/streams.cljs
+++ b/frontend/src/app/main/streams.cljs
@@ -112,6 +112,20 @@
(rx/sub! ob sub)
sub))
+(defonce keyboard-shift
+ (let [sub (rx/behavior-subject nil)
+ ob (->> keyboard
+ (rx/filter kbd/shift-key?)
+ (rx/map kbd/key-down-event?)
+ ;; Fix a situation caused by using `ctrl+alt` kind of
+ ;; shortcuts, that makes keyboard-alt stream
+ ;; registering the key pressed but on blurring the
+ ;; window (unfocus) the key down is never arrived.
+ (rx/merge window-blur)
+ (rx/pipe (rxo/distinct-contiguous)))]
+ (rx/sub! ob sub)
+ sub))
+
(defonce keyboard-meta
(let [sub (rx/behavior-subject nil)
ob (->> keyboard
diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs
index 0694361ba4..bfe0fa6ee6 100644
--- a/frontend/src/app/main/ui/context.cljs
+++ b/frontend/src/app/main/ui/context.cljs
@@ -29,3 +29,4 @@
(def workspace-read-only? (mf/create-context nil))
(def is-component? (mf/create-context false))
+(def sidebar (mf/create-context nil))
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index 283460530b..dd7c452d0e 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -54,12 +54,14 @@
projects))
(mf/defc file-menu
- [{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id] :as props}]
+ {::mf/wrap-props false}
+ [{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id]}]
(assert (seq files) "missing `files` prop")
(assert (boolean? show?) "missing `show?` prop")
(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 [is-lib-page? (= :libraries origin)
is-search-page? (= :search origin)
top (or top 0)
@@ -88,15 +90,15 @@
(apply st/emit! (map dd/duplicate-file files))
(st/emit! (dm/success (tr "dashboard.success-duplicate-file" (i18n/c (count files))))))
- delete-fn
+ on-delete-accept
(fn [_]
(apply st/emit! (map dd/delete-file files))
- (st/emit! (dm/success (tr "dashboard.success-delete-file" (i18n/c (count files))))))
+ (st/emit! (dm/success (tr "dashboard.success-delete-file" (i18n/c (count files))))
+ (dd/clear-selected-files)))
on-delete
(fn [event]
(dom/stop-propagation event)
-
(let [num-shared (filter #(:is-shared %) files)]
(if (< 0 (count num-shared))
@@ -104,7 +106,7 @@
{:type :delete-shared-libraries
:origin :delete
:ids (into #{} (map :id) files)
- :on-accept delete-fn
+ :on-accept on-delete-accept
:count-libraries (count num-shared)}))
(if multi?
@@ -113,13 +115,13 @@
:title (tr "modals.delete-file-multi-confirm.title" file-count)
:message (tr "modals.delete-file-multi-confirm.message" file-count)
:accept-label (tr "modals.delete-file-multi-confirm.accept" file-count)
- :on-accept delete-fn}))
+ :on-accept on-delete-accept}))
(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-accept on-delete-accept}))))))
on-move-success
(fn [team-id project-id]
diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs
index afbeb5afc4..049688c2bd 100644
--- a/frontend/src/app/main/ui/dashboard/files.cljs
+++ b/frontend/src/app/main/ui/dashboard/files.cljs
@@ -13,6 +13,7 @@
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
+ [app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
@@ -92,7 +93,7 @@
[:div {:class (stl/css :dashboard-header-actions)}
[:a
- {:class (stl/css :btn-secondary :btn-small)
+ {:class (stl/css :btn-secondary :btn-small :new-file)
:tab-index "0"
:on-click on-create-click
:data-test "new-file"
@@ -102,21 +103,11 @@
(tr "dashboard.new-file")]
(when-not (:is-default project)
- [:button
- {:class (stl/css-case :icon true
- :pin-icon true
- :tooltip true
- :tooltip-bottom true
- :active (:is-pinned project))
- :tab-index "0"
+ [:> pin-button*
+ {:tab-index 0
+ :is-pinned (:is-pinned project)
:on-click toggle-pin
- :alt (tr "dashboard.pin-unpin")
- :on-key-down (fn [event]
- (when (kbd/enter? event)
- (toggle-pin event)))}
- (if (:is-pinned project)
- i/pin-fill
- i/pin)])
+ :on-key-down (fn [event] (when (kbd/enter? event) (toggle-pin event)))}])
[:div
{:class (stl/css :icon :tooltip :tooltip-bottom-left)
diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss
index 72b0485a54..9173bac111 100644
--- a/frontend/src/app/main/ui/dashboard/files.scss
+++ b/frontend/src/app/main/ui/dashboard/files.scss
@@ -26,3 +26,7 @@
margin-top: $s-12;
}
}
+
+.new-file {
+ margin-inline-end: $s-8;
+}
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 172f4f4ec8..107943180e 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.dashboard.grid
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
@@ -217,6 +218,9 @@
{:wrap [mf/memo]}
[{:keys [file origin library-view?] :as props}]
(let [file-id (:id file)
+
+ ;; FIXME: this breaks react hooks rule, hooks should never to
+ ;; be in a conditional code
selected-files (if (= origin :search)
(mf/deref refs/dashboard-selected-search)
(mf/deref refs/dashboard-selected-files))
@@ -446,8 +450,9 @@
[:& loading-placeholder]
(seq files)
- (for [slice (partition-all limit files)]
- [:ul {:class (stl/css :grid-row)}
+ (for [[index slice] (d/enumerate (partition-all limit files))]
+
+ [:ul {:class (stl/css :grid-row) :key (dm/str index)}
(when @dragging?
[:li {:class (stl/css :grid-item)}])
(for [item slice]
diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs
index d207630b94..e2f63f5c35 100644
--- a/frontend/src/app/main/ui/dashboard/import.cljs
+++ b/frontend/src/app/main/ui/dashboard/import.cljs
@@ -216,7 +216,9 @@
[:div {:class (stl/css :file-name-label)}
(:name file)
- (when is-shared? i/library-refactor)])
+ (when is-shared?
+ [:span {:class (stl/css :icon)}
+ i/library-refactor])])
[:div {:class (stl/css :edit-entry-buttons)}
(when (= "application/zip" (:type file))
@@ -242,9 +244,10 @@
(let [library-data (->> @state :files (d/seek #(= library-id (:file-id %))))
error? (or (:deleted? library-data) (:import-error library-data))]
(when (some? library-data)
- [:div {:class (stl/css-case :linked-library-tag true
- :error error?)}
- i/detach-refactor (:name library-data)])))]]))
+ [:div {:class (stl/css :linked-library)}
+ (:name library-data)
+ [:span {:class (stl/css-case :linked-library-tag true
+ :error error?)} i/detach-refactor]])))]]))
(mf/defc import-dialog
{::mf/register modal/components
diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss
index 4221530e0d..fc726714e4 100644
--- a/frontend/src/app/main/ui/dashboard/import.scss
+++ b/frontend/src/app/main/ui/dashboard/import.scss
@@ -107,7 +107,19 @@
}
.file-name-label {
@include titleTipography;
+ display: flex;
+ align-items: center;
+ gap: $s-12;
flex-grow: 1;
+ .icon {
+ @include flexCenter;
+ height: $s-16;
+ width: $s-16;
+ svg {
+ @extend .button-icon-small;
+ stroke: var(--icon-foreground);
+ }
+ }
}
.edit-entry-buttons {
@include flexRow;
@@ -128,18 +140,22 @@
color: var(--modal-text-foreground-color);
}
- .linked-libraries {
+ .linked-library {
+ display: flex;
+ align-items: center;
+ gap: $s-12;
+ color: var(--modal-text-foreground-color);
.linked-library-tag {
@include flexCenter;
height: $s-24;
- width: $s-16;
+ width: $s-24;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
&.error {
svg {
- stroke: var(--error-color);
+ stroke: var(--status-error-color);
}
}
}
@@ -147,46 +163,46 @@
&.loading {
.file-name {
- color: var(--pending-color);
+ color: var(--status-pending-color);
.file-icon {
:global(#loader-pencil) {
- color: var(--pending-color);
- stroke: var(--pending-color);
- fill: var(--pending-color);
+ color: var(--status-pending-color);
+ stroke: var(--status-pending-color);
+ fill: var(--status-pending-color);
}
}
}
}
&.warning {
.file-name {
- color: var(--warning-color);
+ color: var(--status-warning-color);
.file-icon svg {
- stroke: var(--warning-color);
+ stroke: var(--status-warning-color);
}
.file-icon.icon-fill svg {
- fill: var(--warning-color);
+ fill: var(--status-warning-color);
}
}
}
&.success {
.file-name {
- color: var(--ok-color);
+ color: var(--status-success-color);
.file-icon svg {
- stroke: var(--ok-color);
+ stroke: var(--status-success-color);
}
.file-icon.icon-fill svg {
- fill: var(--ok-color);
+ fill: var(--status-success-color);
}
}
}
&.error {
.file-name {
- color: var(--error-color);
+ color: var(--status-error-color);
.file-icon svg {
- stroke: var(--error-color);
+ stroke: var(--status-error-color);
}
.file-icon.icon-fill svg {
- fill: var(--error-color);
+ fill: var(--status-error-color);
}
}
}
diff --git a/frontend/src/app/main/ui/dashboard/pin_button.cljs b/frontend/src/app/main/ui/dashboard/pin_button.cljs
new file mode 100644
index 0000000000..9319be947c
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/pin_button.cljs
@@ -0,0 +1,26 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.dashboard.pin-button
+ (:require-macros
+ [app.common.data.macros :as dm]
+ [app.main.style :as stl]
+ [app.main.ui.icons :refer [icon-xref]])
+ (:require
+ [app.main.ui.icons :as i]
+ [app.util.i18n :as i18n :refer [tr]]
+ [rumext.v2 :as mf]))
+
+(def pin-icon (icon-xref :pin-refactor (stl/css :icon)))
+
+(mf/defc pin-button*
+ {::mf/props :obj}
+ [{:keys [aria-label is-pinned class] :as props}]
+ (let [aria-label (or aria-label (tr "dashboard.pin-unpin"))
+ class (dm/str (or class "") " " (stl/css-case :button true :button-active is-pinned))
+ props (mf/spread-props props {:class class
+ :aria-label aria-label})]
+ [:> "button" props pin-icon]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/dashboard/pin_button.scss b/frontend/src/app/main/ui/dashboard/pin_button.scss
new file mode 100644
index 0000000000..50997fe243
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/pin_button.scss
@@ -0,0 +1,33 @@
+// 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) KALEIDOS INC
+
+@use "common/refactor/common-refactor.scss" as *;
+
+.button {
+ --pin-button-icon-color: #{$df-secondary};
+ --pin-button-bg-color: none;
+
+ width: $s-32;
+ height: $s-32;
+ background: var(--pin-button-bg-color);
+ border: none;
+ border-radius: $br-8;
+ display: grid;
+ place-content: center;
+ cursor: pointer;
+}
+
+.button-active {
+ --pin-button-icon-color: #{$da-primary};
+ --pin-button-bg-color: #{$db-cuaternary};
+}
+
+.icon {
+ width: $s-16;
+ height: $s-16;
+ fill: none;
+ stroke: var(--pin-button-icon-color);
+}
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index b9f059289e..e7636b2255 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -20,6 +20,7 @@
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [line-grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
+ [app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
@@ -304,18 +305,7 @@
[:div {:class (stl/css :project-actions)}
(when-not (:is-default project)
- [:button
- {:class (stl/css-case :pin-icon true
- :tooltip true
- :tooltip-bottom true
- :active (:is-pinned project))
- :on-click toggle-pin
- :alt (tr "dashboard.pin-unpin")
- :aria-label (tr "dashboard.pin-unpin")
- :tab-index "0"}
- (if (:is-pinned project)
- i/pin-fill
- i/pin)])
+ [:> pin-button* {:is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}])
[:button
{:class (stl/css :btn-secondary :btn-small :tooltip :tooltip-bottom)
diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss
index 15941852b5..6082e29376 100644
--- a/frontend/src/app/main/ui/dashboard/projects.scss
+++ b/frontend/src/app/main/ui/dashboard/projects.scss
@@ -125,26 +125,6 @@
}
}
}
-
- .pin-icon {
- cursor: pointer;
- display: flex;
- align-items: center;
- margin-right: $s-16;
- background-color: transparent;
- border: none;
- svg {
- width: $s-16;
- height: $s-16;
- fill: $df-secondary;
- }
-
- &.active {
- svg {
- fill: $da-primary;
- }
- }
- }
}
.grid-container {
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index 219883e931..9b7a67b383 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -698,7 +698,7 @@
:team-id (:id team)
:selected? (= (:id item) (:id project))}])]
[:div {:class (stl/css :sidebar-empty-placeholder)}
- [:span {:class (stl/css :icon)} i/pin]
+ [:span {:class (stl/css :icon)} i/pin-refactor]
[:span {:class (stl/css :text)} (tr "dashboard.no-projects-placeholder")]])]]))
(mf/defc profile-section
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss
index d46cdcd3ac..8c0520ea4c 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.scss
+++ b/frontend/src/app/main/ui/dashboard/sidebar.scss
@@ -258,12 +258,13 @@
padding: $s-12;
color: $df-secondary;
display: flex;
- align-items: flex-start;
+ align-items: center;
.icon {
padding: 0 $s-12;
svg {
- fill: $df-secondary;
+ fill: none;
+ stroke: currentColor;
width: $s-12;
height: $s-12;
}
diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs
index 8b61fc95b8..953f9b4b3e 100644
--- a/frontend/src/app/main/ui/dashboard/templates.cljs
+++ b/frontend/src/app/main/ui/dashboard/templates.cljs
@@ -113,7 +113,7 @@
:alt (:name item)}]]
[:div {:class (stl/css :card-name)}
[:span (:name item)]
- [:span {:class (stl/css :icon)} i/download]]]]))
+ [:span {:class (stl/css :icon)} i/download-refactor]]]]))
(mf/defc card-item-link
{::mf/wrap-props false}
diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss
index 9a62effef8..bec721aa6b 100644
--- a/frontend/src/app/main/ui/dashboard/templates.scss
+++ b/frontend/src/app/main/ui/dashboard/templates.scss
@@ -157,7 +157,8 @@
svg {
width: $s-16;
height: $s-16;
- fill: $df-secondary;
+ fill: none;
+ stroke: currentColor;
}
span {
font-weight: $fw500;
diff --git a/frontend/src/app/main/ui/debug/components_preview.cljs b/frontend/src/app/main/ui/debug/components_preview.cljs
index 5b71879134..5a841ac8e8 100644
--- a/frontend/src/app/main/ui/debug/components_preview.cljs
+++ b/frontend/src/app/main/ui/debug/components_preview.cljs
@@ -248,7 +248,6 @@
{:title "Button tertiary with icon"}
[:button {:class (stl/css :button-tertiary)}
i/add-refactor]]]
-
[:div {:class (stl/css :components-group)}
[:h3 "Inputs"]
[:& component-wrapper
diff --git a/frontend/src/app/main/ui/delete_shared.cljs b/frontend/src/app/main/ui/delete_shared.cljs
index c31b77bdfe..f1254fc864 100644
--- a/frontend/src/app/main/ui/delete_shared.cljs
+++ b/frontend/src/app/main/ui/delete_shared.cljs
@@ -26,7 +26,7 @@
::mf/register-as :delete-shared-libraries
::mf/wrap-props false}
[{:keys [ids on-accept on-cancel accept-style origin count-libraries]}]
- (let [references* (mf/use-state {})
+ (let [references* (mf/use-state nil)
references (deref references*)
on-accept (or on-accept noop)
@@ -78,8 +78,8 @@
(mf/with-effect [ids]
(->> (rx/from ids)
- (rx/map #(array-map :file-id %))
- (rx/mapcat #(rp/cmd! :get-library-file-references %))
+ (rx/filter some?)
+ (rx/mapcat #(rp/cmd! :get-library-file-references {:file-id %}))
(rx/mapcat identity)
(rx/map (juxt :id :name))
(rx/reduce conj [])
diff --git a/frontend/src/app/main/ui/export.cljs b/frontend/src/app/main/ui/export.cljs
index 7fa938cbc4..a9085b3174 100644
--- a/frontend/src/app/main/ui/export.cljs
+++ b/frontend/src/app/main/ui/export.cljs
@@ -328,8 +328,7 @@
::mf/register-as :export
::mf/wrap-props false}
[{:keys [team-id files has-libraries? binary? features]}]
- (let [_ (println "-a-a-a-a")
- state* (mf/use-state
+ (let [state* (mf/use-state
#(let [files (mapv (fn [file] (assoc file :loading? true)) files)]
{:status :prepare
:selected :all
diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs
index b5a46415e0..d97fdad92f 100644
--- a/frontend/src/app/main/ui/hooks.cljs
+++ b/frontend/src/app/main/ui/hooks.cljs
@@ -215,11 +215,15 @@
([stream on-subscribe]
(use-stream stream (mf/deps) on-subscribe))
([stream deps on-subscribe]
+ (use-stream stream deps on-subscribe nil))
+ ([stream deps on-subscribe on-dispose]
(mf/use-effect
deps
(fn []
(let [sub (->> stream (rx/subs! on-subscribe))]
- #(rx/dispose! sub))))))
+ #(do
+ (rx/dispose! sub)
+ (when on-dispose (on-dispose))))))))
;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
(defn use-previous
diff --git a/frontend/src/app/main/ui/icons.clj b/frontend/src/app/main/ui/icons.clj
index 4f6e054e04..68edb7660c 100644
--- a/frontend/src/app/main/ui/icons.clj
+++ b/frontend/src/app/main/ui/icons.clj
@@ -11,9 +11,9 @@
[rumext.v2]))
(defmacro icon-xref
- [id]
+ [id & [class]]
(let [href (str "#icon-" (name id))
- class (str "icon-" (name id))]
+ class (or class (str "icon-" (name id)))]
`(rumext.v2/html
[:svg {:width 500 :height 500 :class ~class}
[:use {:href ~href}]])))
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index fd0c0657ff..b6b5dd4af1 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -329,6 +329,7 @@
(def ^:icon desc-sort-refactor (icon-xref :desc-sort-refactor))
(def ^:icon detach-refactor (icon-xref :detach-refactor))
(def ^:icon document-refactor (icon-xref :document-refactor))
+(def ^:icon download-refactor (icon-xref :download-refactor))
(def ^:icon drop-refactor (icon-xref :drop-refactor))
(def ^:icon easing-linear-refactor (icon-xref :easing-linear-refactor))
(def ^:icon easing-ease-refactor (icon-xref :easing-ease-refactor))
@@ -338,6 +339,7 @@
(def ^:icon effects-refactor (icon-xref :effects-refactor))
(def ^:icon elipse-refactor (icon-xref :elipse-refactor))
(def ^:icon expand-refactor (icon-xref :expand-refactor))
+(def ^:icon feedback-refactor (icon-xref :feedback-refactor))
(def ^:icon fill-content-refactor (icon-xref :fill-content-refactor))
(def ^:icon filter-refactor (icon-xref :filter-refactor))
(def ^:icon fixed-width-refactor (icon-xref :fixed-width-refactor))
@@ -410,6 +412,7 @@
(def ^:icon path-refactor (icon-xref :path-refactor))
(def ^:icon pentool-refactor (icon-xref :pentool-refactor))
(def ^:icon picker-refactor (icon-xref :picker-refactor))
+(def ^:icon pin-refactor (icon-xref :pin-refactor))
(def ^:icon play-refactor (icon-xref :play-refactor))
(def ^:icon rectangle-refactor (icon-xref :rectangle-refactor))
(def ^:icon reload-refactor (icon-xref :reload-refactor))
diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs
index c9dbcb6789..71cd5bf461 100644
--- a/frontend/src/app/main/ui/settings/sidebar.cljs
+++ b/frontend/src/app/main/ui/settings/sidebar.cljs
@@ -104,7 +104,7 @@
(when (contains? cf/flags :user-feedback)
[:li {:class (when feedback? (stl/css :current))
:on-click go-settings-feedback}
- i/msg-info
+ i/feedback-refactor
[:span {:class (stl/css :element-title)} (tr "labels.give-feedback")]])]]]))
(mf/defc sidebar
diff --git a/frontend/src/app/main/ui/settings/sidebar.scss b/frontend/src/app/main/ui/settings/sidebar.scss
index c7593f9b26..f0eb99029a 100644
--- a/frontend/src/app/main/ui/settings/sidebar.scss
+++ b/frontend/src/app/main/ui/settings/sidebar.scss
@@ -60,7 +60,8 @@
}
svg {
- fill: currentColor;
+ stroke: currentColor;
+ fill: none;
margin-right: $s-8;
height: $s-12;
width: $s-12;
diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs
index d7daebf3be..56127db661 100644
--- a/frontend/src/app/main/ui/shapes/attrs.cljs
+++ b/frontend/src/app/main/ui/shapes/attrs.cljs
@@ -157,7 +157,7 @@
[shape]
(add-layer-styles! #js {} shape))
-(defn- get-svg-props
+(defn get-svg-props
[shape render-id]
(let [attrs (get shape :svg-attrs {})
defs (get shape :svg-defs {})]
diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
index 91d4586ae7..32dfaf9b9e 100644
--- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs
+++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
@@ -31,7 +31,7 @@
suffix (if (some? index) (dm/str "-" index) "")
clip-id (dm/str "inner-stroke-" render-id "-" shape-id suffix)
href (dm/str "#stroke-shape-" render-id "-" shape-id suffix)]
- [:> "clipPath" #js {:id clip-id}
+ [:> "clipPath" {:id clip-id}
[:use {:href href}]]))
(mf/defc outer-stroke-mask
@@ -473,28 +473,30 @@
shape-blur (get shape :blur)
shape-fills (get shape :fills)
shape-shadow (get shape :shadow)
- shape-strokes (get shape :strokes)
+ shape-strokes (not-empty (get shape :strokes))
+
+ svg-attrs (attrs/get-svg-props shape render-id)
style (-> (obj/get props "style")
(obj/clone)
(attrs/add-layer-styles! shape))
- props #js {:id stroke-id
- :className "strokes"
- :style style}
+ props (mf/spread-props svg-attrs
+ {:id stroke-id
+ :className "strokes"
+ :style style})]
- props (if ^boolean (cfh/frame-shape? shape)
- props
- (cond-> props
- (and (some? shape-blur)
- (not ^boolean (:hidden shape-blur)))
- (obj/set! "filter" (dm/fmt "url(#filter-blur-%)" render-id))
- (and (empty? shape-fills)
- (some? (->> shape-shadow (remove :hidden) seq)))
- (obj/set! "filter" (dm/fmt "url(#filter-%)" render-id))))]
+ (when-not ^boolean (cfh/frame-shape? shape)
+ (when (and (some? shape-blur)
+ (not ^boolean (:hidden shape-blur)))
+ (obj/set! props "filter" (dm/fmt "url(#filter-blur-%)" render-id)))
- (when (d/not-empty? shape-strokes)
+ (when (and (empty? shape-fills)
+ (some? (->> shape-shadow (remove :hidden) not-empty)))
+ (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id))))
+
+ (when (some? shape-strokes)
[:> :g props
(for [[index value] (reverse (d/enumerate shape-strokes))]
[:& shape-custom-stroke {:shape shape
diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs
index 899dee3863..22137758ab 100644
--- a/frontend/src/app/main/ui/viewer.cljs
+++ b/frontend/src/app/main/ui/viewer.cljs
@@ -139,6 +139,7 @@
background-overlay? (:background-overlay overlay)
overlay-frame (:frame overlay)
overlay-position (:position overlay)
+ fixed-base? (:fixed-source? overlay)
size
(mf/with-memo [page overlay zoom]
@@ -168,21 +169,42 @@
:top 0}
:on-click on-click}])
- [:div {:class (stl/css :viewer-overlay :viewport-container)
- :id (dm/str "overlay-" (:id overlay-frame))
- :style {:width (:width size)
- :height (:height size)
- :left (* (:x overlay-position) zoom)
- :top (* (:y overlay-position) zoom)}}
+ (if fixed-base?
+ [:div {:class (stl/css :viewport-container-wrapper)
+ :style {:position "absolute"
+ :left (* (:x overlay-position) zoom)
+ :top (* (:y overlay-position) zoom)
+ :width (:width size)
+ :height (:height size)
+ :z-index 2}}
+ [:div {:class (stl/css :viewer-overlay :viewport-container)
+ :id (dm/str "overlay-" (:id overlay-frame))
+ :style {:width (:width size)
+ :height (:height size)
+ :position "fixed"}}
+ [:& interactions/viewport
+ {:frame overlay-frame
+ :base-frame frame
+ :frame-offset overlay-position
+ :size size
+ :delta delta
+ :page page
+ :interactions-mode interactions-mode}]]]
- [:& interactions/viewport
- {:frame overlay-frame
- :base-frame frame
- :frame-offset overlay-position
- :size size
- :delta delta
- :page page
- :interactions-mode interactions-mode}]]]))
+ [:div {:class (stl/css :viewer-overlay :viewport-container)
+ :id (dm/str "overlay-" (:id overlay-frame))
+ :style {:width (:width size)
+ :height (:height size)
+ :left (* (:x overlay-position) zoom)
+ :top (* (:y overlay-position) zoom)}}
+ [:& interactions/viewport
+ {:frame overlay-frame
+ :base-frame frame
+ :frame-offset overlay-position
+ :size size
+ :delta delta
+ :page page
+ :interactions-mode interactions-mode}]])]))
(mf/defc viewer-wrapper
{::mf/wrap-props false}
@@ -354,7 +376,6 @@
wrapper (dom/get-element "inspect-svg-wrapper")
section (dom/get-element "inspect-svg-container")
target (.-target event)]
- ;; TODO: Reemplazar el dom/class? por un data-attribute
(when (or (dom/child? target wrapper) (dom/id? target "inspect-svg-container"))
(let [norm-event ^js (nw/normalize-wheel event)
mod? (kbd/mod? event)
@@ -436,7 +457,9 @@
fullscreen-dom? (dom/fullscreen?)]
(when (not= fullscreen? fullscreen-dom?)
(if fullscreen?
- (wapi/request-fullscreen wrapper)
+ (let [layout (dom/get-element "viewer-layout")]
+ (dom/set-data! layout "force-visible" false)
+ (wapi/request-fullscreen wrapper))
(wapi/exit-fullscreen))))))
(mf/use-effect
@@ -521,16 +544,9 @@
:data-fullscreen fullscreen?
:data-force-visible (:show-thumbnails local)}
+
[:div {:class (stl/css :viewer-content)}
- [:& header/header {:project project
- :index index
- :file file
- :page page
- :frame frame
- :permissions permissions
- :zoom zoom
- :section section
- :interactions-mode interactions-mode}]
+
[:button {:on-click on-thumbnails-close
:class (stl/css-case :thumbnails-close true
@@ -587,7 +603,17 @@
:overlays overlays
:zoom zoom
:section section
- :index index}]]))]]]))
+ :index index}]]))]]
+
+ [:& header/header {:project project
+ :index index
+ :file file
+ :page page
+ :frame frame
+ :permissions permissions
+ :zoom zoom
+ :section section
+ :interactions-mode interactions-mode}]]))
;; --- Component: Viewer
diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss
index 7b98ecfa39..3a4e36d3f7 100644
--- a/frontend/src/app/main/ui/viewer.scss
+++ b/frontend/src/app/main/ui/viewer.scss
@@ -23,6 +23,15 @@
background-color: var(--viewer-background-color);
}
+.empty-state {
+ @include titleTipography;
+ color: var(--empty-message-foreground-color);
+ display: grid;
+ place-items: center;
+ height: 100%;
+ width: 100%;
+}
+
.viewer-header {
grid-row: 1 / span 1;
}
@@ -58,6 +67,7 @@
flex-flow: wrap;
overflow: auto;
}
+
.inspect-layout .viewer-section {
flex-wrap: nowrap;
margin-top: 0;
@@ -76,6 +86,7 @@
top: calc(50vh - $s-32);
z-index: $z-index-2;
background-color: var(--viewer-controls-background-color);
+ transition: transform 400ms ease 300ms;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
@@ -189,3 +200,19 @@
[data-force-visible="true"] .viewer-bottom {
transform: translateY(0);
}
+
+[data-fullscreen="true"] .viewer-go-next {
+ transform: translateX($s-40);
+}
+
+[data-fullscreen="true"] .viewer-go-prev {
+ transform: translateX(-$s-40);
+}
+
+[data-force-visible="true"] .viewer-go-next {
+ transform: translateX(0);
+}
+
+[data-force-visible="true"] .viewer-go-prev {
+ transform: translateX(0);
+}
diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss
index 6eebe53ba5..da758dffe7 100644
--- a/frontend/src/app/main/ui/viewer/header.scss
+++ b/frontend/src/app/main/ui/viewer/header.scss
@@ -303,10 +303,20 @@
}
/** FULLSCREEN */
+[data-fullscreen="true"] .viewer-header::after {
+ content: " ";
+ position: absolute;
+ width: 100%;
+ height: $s-48;
+ left: 0;
+ top: $s-48;
+}
+
[data-fullscreen="true"] .viewer-header {
transform: translateY(-$s-48);
}
-[data-force-visible="true"] .viewer-header {
+[data-force-visible="true"] .viewer-header,
+[data-fullscreen="true"] .viewer-header:hover {
transform: translateY(0);
}
diff --git a/frontend/src/app/main/ui/viewer/inspect/exports.cljs b/frontend/src/app/main/ui/viewer/inspect/exports.cljs
index 141eea215e..8e41f4a2f2 100644
--- a/frontend/src/app/main/ui/viewer/inspect/exports.cljs
+++ b/frontend/src/app/main/ui/viewer/inspect/exports.cljs
@@ -86,27 +86,26 @@
(mf/use-callback
(mf/deps shapes)
(fn [index event]
- (let [target (dom/get-target event)
- value (dom/get-value target)
- value (d/parse-double value)]
- (swap! exports assoc-in [index :scale] value))))
+ (let [scale (d/parse-double event)]
+ (swap! exports assoc-in [index :scale] scale))))
on-suffix-change
(mf/use-callback
(mf/deps shapes)
- (fn [index event]
- (let [target (dom/get-target event)
- value (dom/get-value target)]
+ (fn [event]
+ (let [value (dom/get-target-val event)
+ index (-> (dom/get-current-target event)
+ (dom/get-data "value")
+ (d/parse-integer))]
(swap! exports assoc-in [index :suffix] value))))
on-type-change
(mf/use-callback
(mf/deps shapes)
(fn [index event]
- (let [target (dom/get-target event)
- value (dom/get-value target)
- value (keyword value)]
- (swap! exports assoc-in [index :type] value))))
+ (let [type (keyword event)]
+ (swap! exports assoc-in [index :type] type))))
+
manage-key-down
(mf/use-callback
(fn [event]
@@ -177,7 +176,7 @@
:type "text"
:value (:suffix export)
:placeholder (tr "workspace.options.export.suffix")
- :data-value index
+ :data-value (str index)
:on-change on-suffix-change
:on-key-down manage-key-down}]]]
diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs
index 60f1928c42..dc4f30ef16 100644
--- a/frontend/src/app/main/ui/viewer/interactions.cljs
+++ b/frontend/src/app/main/ui/viewer/interactions.cljs
@@ -39,6 +39,23 @@
(into [frame-id])
(reduce update-fn objects))))
+(defn get-fixed-ids
+ [objects]
+ (let [fixed-ids (filter :fixed-scroll (vals objects))
+
+ ;; we have to consider the children if the fixed element is a group
+ fixed-children-ids
+ (into #{} (mapcat #(cfh/get-children-ids objects (:id %)) fixed-ids))
+
+ parent-children-ids
+ (->> fixed-ids
+ (mapcat #(cons (:id %) (cfh/get-parent-ids objects (:id %))))
+ (remove #(= % uuid/zero)))
+
+ fixed-ids
+ (concat fixed-children-ids parent-children-ids)]
+ fixed-ids))
+
(mf/defc viewport-svg
{::mf/wrap [mf/memo]
::mf/wrap-props false}
@@ -48,31 +65,25 @@
base (unchecked-get props "base")
offset (unchecked-get props "offset")
size (unchecked-get props "size")
+ fixed? (unchecked-get props "fixed?")
delta (or (unchecked-get props "delta") (gpt/point 0 0))
vbox (:vbox size)
- fixed-ids (filter :fixed-scroll (vals (:objects page)))
+ frame (cond-> frame fixed? (assoc :fixed-scroll true))
- ;; we have con consider the children if the fixed element is a group
- fixed-children-ids
- (into #{} (mapcat #(cfh/get-children-ids (:objects page) (:id %)) fixed-ids))
+ objects (:objects page)
+ objects (cond-> objects fixed? (assoc-in [(:id frame) :fixed-scroll] true))
- parent-children-ids
- (->> fixed-ids
- (mapcat #(cons (:id %) (cfh/get-parent-ids (:objects page) (:id %))))
- (remove #(= % uuid/zero)))
-
- fixed-ids
- (concat fixed-children-ids parent-children-ids)
+ fixed-ids (get-fixed-ids objects)
not-fixed-ids
- (->> (remove (set fixed-ids) (keys (:objects page)))
+ (->> (remove (set fixed-ids) (keys objects))
(remove #(= % uuid/zero)))
calculate-objects
(fn [ids]
(->> ids
- (map (d/getf (:objects page)))
+ (map (d/getf objects))
(concat [frame])
(d/index-by :id)
(prepare-objects frame size delta)))
@@ -112,28 +123,41 @@
[:& (mf/provider shapes/base-frame-ctx) {:value base}
[:& (mf/provider shapes/frame-offset-ctx) {:value offset}
- ;; We have two different svgs for fixed and not fixed elements so we can emulate the sticky css attribute in svg
- [:svg {:class (stl/css :fixed)
- :view-box vbox
- :width (:width size)
- :height (:height size)
- :version "1.1"
- :xmlnsXlink "http://www.w3.org/1999/xlink"
- :xmlns "http://www.w3.org/2000/svg"
- :fill "none"
- :style {:width (:width size)
- :height (:height size)}}
- [:& wrapper-fixed {:shape fixed-frame :view-box vbox}]]
+ (if fixed?
+ [:svg {:class (stl/css :fixed)
+ :view-box vbox
+ :width (:width size)
+ :height (:height size)
+ :version "1.1"
+ :xmlnsXlink "http://www.w3.org/1999/xlink"
+ :xmlns "http://www.w3.org/2000/svg"
+ :fill "none"}
+ [:& wrapper-not-fixed {:shape frame :view-box vbox}]]
- [:svg {:class (stl/css :not-fixed)
- :view-box vbox
- :width (:width size)
- :height (:height size)
- :version "1.1"
- :xmlnsXlink "http://www.w3.org/1999/xlink"
- :xmlns "http://www.w3.org/2000/svg"
- :fill "none"}
- [:& wrapper-not-fixed {:shape frame :view-box vbox}]]]]))
+ [:*
+ ;; We have two different svgs for fixed and not fixed elements so we can emulate the sticky css attribute in svg
+ [:svg {:class (stl/css :fixed)
+ :view-box vbox
+ :width (:width size)
+ :height (:height size)
+ :version "1.1"
+ :xmlnsXlink "http://www.w3.org/1999/xlink"
+ :xmlns "http://www.w3.org/2000/svg"
+ :fill "none"
+ :style {:width (:width size)
+ :height (:height size)
+ :z-index 1}}
+ [:& wrapper-fixed {:shape fixed-frame :view-box vbox}]]
+
+ [:svg {:class (stl/css :not-fixed)
+ :view-box vbox
+ :width (:width size)
+ :height (:height size)
+ :version "1.1"
+ :xmlnsXlink "http://www.w3.org/1999/xlink"
+ :xmlns "http://www.w3.org/2000/svg"
+ :fill "none"}
+ [:& wrapper-not-fixed {:shape frame :view-box vbox}]]])]]))
(mf/defc viewport
{::mf/wrap [mf/memo]
@@ -150,7 +174,8 @@
page (unchecked-get props "page")
frame (unchecked-get props "frame")
- base (unchecked-get props "base-frame")]
+ base (unchecked-get props "base-frame")
+ fixed? (unchecked-get props "fixed?")]
(mf/with-effect [mode]
(let [on-click
@@ -190,7 +215,8 @@
:base base
:offset offset
:size size
- :delta delta}]))
+ :delta delta
+ :fixed? fixed?}]))
(mf/defc flows-menu
{::mf/wrap [mf/memo]}
diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs
index d976fbc744..6a739d3208 100644
--- a/frontend/src/app/main/ui/viewer/shapes.cljs
+++ b/frontend/src/app/main/ui/viewer/shapes.cljs
@@ -70,6 +70,7 @@
background-overlay (:background-overlay interaction)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
+ fixed-base? (cfh/fixed? objects relative-to-id)
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
@@ -83,7 +84,8 @@
snap-to
close-click-outside
background-overlay
- (:animation interaction)))))
+ (:animation interaction)
+ fixed-base?))))
:toggle-overlay
(let [dest-frame-id (:destination interaction)
@@ -96,6 +98,7 @@
relative-to-shape (or (get objects relative-to-id) base-frame)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
+ fixed-base? (cfh/fixed? objects (:id base-frame))
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
@@ -112,7 +115,8 @@
snap-to
close-click-outside
background-overlay
- (:animation interaction)))))
+ (:animation interaction)
+ fixed-base?))))
:close-overlay
(let [dest-frame-id (or (:destination interaction)
@@ -152,6 +156,7 @@
relative-to-shape (or (get objects relative-to-id) base-frame)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
+ fixed-base? (cfh/fixed? objects (:id base-frame))
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
@@ -168,7 +173,8 @@
snap-to
close-click-outside
background-overlay
- (:animation interaction)))))
+ (:animation interaction)
+ fixed-base?))))
:close-overlay
@@ -184,6 +190,7 @@
background-overlay (:background-overlay interaction)
overlays-ids (set (map :id overlays))
relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame)
+ fixed-base? (cfh/fixed? objects (:id base-frame))
[position snap-to] (ctsi/calc-overlay-position interaction
shape
objects
@@ -197,7 +204,8 @@
snap-to
close-click-outside
background-overlay
- (:animation interaction)))))
+ (:animation interaction)
+ fixed-base?))))
nil))
(defn- on-pointer-down
diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs
index c12f88a186..8363e438f3 100644
--- a/frontend/src/app/main/ui/workspace.cljs
+++ b/frontend/src/app/main/ui/workspace.cljs
@@ -29,7 +29,6 @@
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
- [app.main.ui.workspace.top-toolbar :refer [top-toolbar]]
[app.main.ui.workspace.viewport :refer [viewport]]
[app.util.debug :as dbg]
[app.util.dom :as dom]
@@ -110,7 +109,6 @@
(when-not hide-ui?
[:*
- [:& top-toolbar {:layout layout}]
(if (:collapse-left-sidebar layout)
[:& collapsed-button]
[:& left-sidebar {:layout layout
diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs
index da56b63388..12fde79e3d 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/context_menu.cljs
@@ -422,13 +422,13 @@
(let [components-v2 (features/use-feature "components/v2")
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects)
- any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes))
+ can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes))
heads (filter ctk/instance-head? shapes)
components-menu-entries (cmm/generate-components-menu-entries heads components-v2)
do-add-component #(st/emit! (dwl/add-component))
do-add-multiple-components #(st/emit! (dwl/add-multiple-components))]
[:*
- (when-not any-in-copy? ;; We don't want to change the structure of component copies
+ (when can-make-component ;; We don't want to change the structure of component copies
[:*
[:& menu-separator]
diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs
index db497e7bd0..0cf25ceeee 100644
--- a/frontend/src/app/main/ui/workspace/sidebar.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar.cljs
@@ -12,6 +12,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
+ [app.main.ui.context :as muc]
[app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.workspace.comments :refer [comments-sidebar]]
[app.main.ui.workspace.left-header :refer [left-header]]
@@ -58,23 +59,23 @@
on-tab-change
(mf/use-fn #(st/emit! (dw/go-to-layout %)))]
- [:aside {:ref parent-ref
- :id "left-sidebar-aside"
- :data-size (str size)
- :class (stl/css-case :left-settings-bar true
- :global/two-row (<= size 300)
- :global/three-row (and (> size 300) (<= size 400))
- :global/four-row (> size 400))
- :style #js {"--width" (dm/str size "px")}}
+ [:& (mf/provider muc/sidebar) {:value :left}
+ [:aside {:ref parent-ref
+ :id "left-sidebar-aside"
+ :data-size (str size)
+ :class (stl/css-case :left-settings-bar true
+ :global/two-row (<= size 300)
+ :global/three-row (and (> size 300) (<= size 400))
+ :global/four-row (> size 400))
+ :style #js {"--width" (dm/str size "px")}}
- [:& left-header {:file file :layout layout :project project :page-id page-id
- :class (stl/css :left-header)}]
+ [:& left-header {:file file :layout layout :project project :page-id page-id
+ :class (stl/css :left-header)}]
- [:div {:on-pointer-down on-pointer-down
- :on-lost-pointer-capture on-lost-pointer-capture
- :on-pointer-move on-pointer-move
- :class (stl/css :resize-area)}]
- [:*
+ [:div {:on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-pointer-move on-pointer-move
+ :class (stl/css :resize-area)}]
(cond
(true? shortcuts?)
[:& shortcuts-container {:class (stl/css :settings-bar-content)}]
@@ -110,7 +111,6 @@
[:& layers-toolbox {:size-parent size
:size size-pages}]]]
-
(when-not ^boolean mode-inspect?
[:& tab-element {:id :assets
:title (tr "workspace.toolbar.assets")}
@@ -159,27 +159,28 @@
(obj/set! "on-change-section" handle-change-section)
(obj/set! "on-expand" handle-expand))]
- [:aside {:class (stl/css-case :right-settings-bar true
- :not-expand (not can-be-expanded?)
- :expanded (> size 276))
+ [:& (mf/provider muc/sidebar) {:value :right}
+ [:aside {:class (stl/css-case :right-settings-bar true
+ :not-expand (not can-be-expanded?)
+ :expanded (> size 276))
- :id "right-sidebar-aside"
- :data-size (str size)
- :style #js {"--width" (when can-be-expanded? (dm/str size "px"))}}
- (when can-be-expanded?
- [:div {:class (stl/css :resize-area)
- :on-pointer-down on-pointer-down
- :on-lost-pointer-capture on-lost-pointer-capture
- :on-pointer-move on-pointer-move}])
- [:& right-header {:file file :layout layout :page-id page-id}]
+ :id "right-sidebar-aside"
+ :data-size (str size)
+ :style #js {"--width" (when can-be-expanded? (dm/str size "px"))}}
+ (when can-be-expanded?
+ [:div {:class (stl/css :resize-area)
+ :on-pointer-down on-pointer-down
+ :on-lost-pointer-capture on-lost-pointer-capture
+ :on-pointer-move on-pointer-move}])
+ [:& right-header {:file file :layout layout :page-id page-id}]
- [:div {:class (stl/css :settings-bar-inside)}
- (cond
- (true? is-comments?)
- [:& comments-sidebar]
+ [:div {:class (stl/css :settings-bar-inside)}
+ (cond
+ (true? is-comments?)
+ [:& comments-sidebar]
- (true? is-history?)
- [:& history-toolbox]
+ (true? is-history?)
+ [:& history-toolbox]
- :else
- [:> options-toolbox props])]]))
+ :else
+ [:> options-toolbox props])]]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
index a131dd26b3..313f225b2e 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
@@ -14,6 +14,7 @@
&:last-child {
margin-block-end: $s-24;
}
+ height: 100%;
}
.file-name {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
index cb7cd5f04b..fe1db42005 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
@@ -10,7 +10,6 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
- [app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
@@ -261,7 +260,7 @@
on-drop
(mf/use-fn
- (mf/deps id index objects expanded?)
+ (mf/deps id index objects expanded? selected)
(fn [side _data]
(let [shape (get objects id)
@@ -276,6 +275,8 @@
:else
(cfh/get-parent-id objects id))
+ [parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected))
+
parent (get objects parent-id)
to-index (cond
@@ -283,9 +284,7 @@
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(= side :top) (inc index)
:else index)]
-
- (when-not (ctk/in-component-copy? parent) ;; We don't want to change the structure of component copies
- (st/emit! (dw/relocate-selected-shapes parent-id to-index))))))
+ (st/emit! (dw/relocate-selected-shapes parent-id to-index)))))
on-hold
(mf/use-fn
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
index 0fdc7d4747..88f0ef0b40 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
@@ -152,8 +152,9 @@
:library-colors library-colors}))
(mf/defc color-selection-menu
- {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
- [{:keys [shapes file-id shared-libs] :as props}]
+ {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]
+ ::mf/wrap-props false}
+ [{:keys [shapes file-id shared-libs]}]
(let [{:keys [grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
(prepare-colors shapes file-id shared-libs))
@@ -175,7 +176,9 @@
(fn [new-color old-color from-picker?]
(let [old-color (-> old-color (dissoc :name :path) d/without-nils)
- ;; When dragging on the color picker sometimes all the shapes hasn't updated the color to the prev value so we need this extra calculation
+ ;; When dragging on the color picker sometimes all
+ ;; the shapes hasn't updated the color to the prev
+ ;; value so we need this extra calculation
shapes-by-old-color (get @grouped-colors* old-color)
prev-color (d/seek #(get @grouped-colors* %) @prev-colors*)
shapes-by-prev-color (get @grouped-colors* prev-color)
@@ -225,7 +228,7 @@
[:div {:class (stl/css :element-content)}
[:div {:class (stl/css :selected-color-group)}
(for [[index color] (d/enumerate (take 3 library-colors))]
- [:& color-row {:key (dm/str "library-color-" (:color color))
+ [:& color-row {:key (dm/str "library-color-" index)
:color color
:index index
:on-detach on-detach
@@ -239,7 +242,7 @@
(tr "workspace.options.more-lib-colors")])
(when @expand-lib-color
(for [[index color] (d/enumerate (drop 3 library-colors))]
- [:& color-row {:key (dm/str "library-color-" (:color color))
+ [:& color-row {:key (dm/str "library-color-" index)
:color color
:index index
:on-detach on-detach
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 c54d3a13d5..1f9d39d1b8 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
@@ -93,7 +93,7 @@
(into [] xform fonts)))
(mf/defc font-selector
- [{:keys [on-select on-close current-font show-recent] :as props}]
+ [{:keys [on-select on-close current-font show-recent full-size] :as props}]
(let [selected (mf/use-state current-font)
state (mf/use-state {:term "" :backends #{}})
@@ -103,6 +103,8 @@
fonts (mf/use-memo (mf/deps @state) #(filter-fonts @state @fonts/fonts))
recent-fonts (mf/deref refs/workspace-recent-fonts)
+ full-size? (boolean (and full-size recent-fonts show-recent))
+
select-next
(mf/use-fn
(mf/deps fonts)
@@ -170,13 +172,13 @@
(.scrollToPosition ^js inst offset)))))
[:div {:class (stl/css :font-selector)}
- [:div {:class (stl/css :font-selector-dropdown)}
+ [:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)}
[:div {:class (stl/css :header)}
[:& search-bar {:on-change on-filter-change
:value (:term @state)
:placeholder (tr "workspace.options.search-font")}]
(when (and recent-fonts show-recent)
- [*
+ [:section {:class (stl/css :show-recent)}
[:p {:class (stl/css :title)} (tr "workspace.options.recent-fonts")]
(for [[idx font] (d/enumerate recent-fonts)]
[:& font-item {:key (dm/str "font-" idx)
@@ -185,7 +187,8 @@
:on-click on-select-and-close
:current? (= (:id font) (:id @selected))}])])]
- [:div {:class (stl/css :fonts-list)}
+ [:div {:class (stl/css-case :fonts-list true
+ :fonts-list-full-size full-size?)}
[:> rvt/AutoSizer {}
(fn [props]
(let [width (unchecked-get props "width")
@@ -214,7 +217,7 @@
(mf/defc font-options
{::mf/wrap-props false}
- [{:keys [values on-change on-blur show-recent]}]
+ [{:keys [values on-change on-blur show-recent full-size-selector]}]
(let [{:keys [font-id font-size font-variant-id]} values
font-id (or font-id (:font-id txt/default-text-attrs))
@@ -285,6 +288,7 @@
{:current-font font
:on-close on-font-selector-close
:on-select on-font-select
+ :full-size full-size-selector
:show-recent show-recent}])
[:div {:class (stl/css :font-option)
@@ -416,14 +420,16 @@
(mf/defc text-options
{::mf/wrap-props false}
[{:keys [ids editor values on-change on-blur show-recent]}]
- (let [opts #js {:editor editor
+ (let [full-size-selector? (and show-recent (= (mf/use-ctx ctx/sidebar) :right))
+ opts #js {:editor editor
:ids ids
:values values
:on-change on-change
:on-blur on-blur
- :show-recent show-recent}]
-
- [:div {:class (stl/css :text-options)}
+ :show-recent show-recent
+ :full-size-selector full-size-selector?}]
+ [:div {:class (stl/css-case :text-options true
+ :text-options-full-size full-size-selector?)}
[:> font-options opts]
[:div {:class (stl/css :typography-variations)}
[:> spacing-options opts]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
index 1040e56752..85a8222c41 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
@@ -247,8 +247,10 @@
.text-options {
@include flexColumn;
- position: relative;
margin-bottom: $s-8;
+ &:not(.text-options-full-size) {
+ position: relative;
+ }
.font-option {
@include titleTipography;
@extend .asset-element;
@@ -342,71 +344,99 @@
position: absolute;
top: 0;
left: 0;
+ right: 0;
height: 100%;
width: 100%;
z-index: $z-index-3;
+}
- .font-selector-dropdown {
+.show-recent {
+ border-radius: $br-8 $br-8 0 0;
+ background: var(--dropdown-background-color);
+ border: $s-1 solid var(--color-background-quaternary);
+ border-block-end: none;
+}
+
+.font-selector-dropdown {
+ width: 100%;
+ &:not(.font-selector-dropdown-full-size) {
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
- .header {
- display: flex;
- flex-direction: column;
- position: relative;
- margin-bottom: $s-2;
- background-color: var(--dropdown-background-color);
- .title {
- @include tabTitleTipography;
- margin: 9px 17px;
- color: var(--title-foreground-color);
- }
+ }
+ .header {
+ display: grid;
+ row-gap: $s-2;
+ .title {
+ @include tabTitleTipography;
+ color: var(--title-foreground-color);
+ margin: 0;
+ padding: $s-12;
}
- .fonts-list {
- @include menuShadow;
- position: relative;
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- min-height: 100%;
- width: 100%;
- height: 100%;
- padding: $s-2;
- border-radius: $br-8;
- background-color: var(--dropdown-background-color);
- }
- .font-wrapper {
- padding-bottom: $s-4;
- cursor: pointer;
- .font-item {
- @extend .asset-element;
- margin-bottom: $s-4;
- border-radius: $br-8;
- display: flex;
- .icon {
- @include flexCenter;
- height: $s-28;
- width: $s-28;
- svg {
- @extend .button-icon-small;
- stroke: var(--icon-foreground);
- }
- }
- &.selected {
- color: var(--assets-item-name-foreground-color-hover);
- .icon {
- svg {
- stroke: var(--assets-item-name-foreground-color-hover);
- }
- }
- }
+ }
- .label {
- @include titleTipography;
- flex-grow: 1;
+ .font-wrapper {
+ padding-bottom: $s-4;
+ cursor: pointer;
+ .font-item {
+ @extend .asset-element;
+ margin-bottom: $s-4;
+ border-radius: $br-8;
+ display: flex;
+ .icon {
+ @include flexCenter;
+ height: $s-28;
+ width: $s-28;
+ svg {
+ @extend .button-icon-small;
+ stroke: var(--icon-foreground);
}
}
+ &.selected {
+ color: var(--assets-item-name-foreground-color-hover);
+ .icon {
+ svg {
+ stroke: var(--assets-item-name-foreground-color-hover);
+ }
+ }
+ }
+
+ .label {
+ @include titleTipography;
+ flex-grow: 1;
+ }
}
}
}
+
+.font-selector-dropdown-full-size {
+ height: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this.
+ display: grid;
+ grid-template-rows: auto 1fr;
+ padding: $s-2 $s-12 $s-12 $s-12;
+}
+
+.fonts-list {
+ @include menuShadow;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-height: 100%;
+ width: 100%;
+ height: 100%;
+ padding: $s-2;
+ border-radius: $br-8;
+ background-color: var(--dropdown-background-color);
+ overflow: hidden;
+ &:not(.fonts-list-full-size) {
+ margin-block-start: $s-2;
+ }
+}
+
+.fonts-list-full-size {
+ border-start-start-radius: 0;
+ border-start-end-radius: 0;
+ border: $s-1 solid var(--color-background-quaternary);
+}
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 7fdc82f6c8..23c5714e81 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -274,6 +274,7 @@
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
[:div.viewport {:style #js {"--zoom" zoom}}
+ [:& top-bar/top-bar {:layout layout}]
[:div.viewport-overlays
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
;; inside a foreign object "dummy" so this awkward behaviour is take into account
@@ -302,9 +303,7 @@
:vbox vbox
:options options
:layout layout
- :viewport-ref viewport-ref}])
-
- [:& top-bar/top-bar]]
+ :viewport-ref viewport-ref}])]
[:svg.render-shapes
{:id "render"
diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
index 3a9174a9df..30fa24840f 100644
--- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs
@@ -106,7 +106,7 @@
(st/emit! (dd/start-drawing drawing-tool)))
(or (not id) mod?)
- (st/emit! (dw/handle-area-selection shift? mod?))
+ (st/emit! (dw/handle-area-selection shift?))
(not drawing-tool)
(when-not workspace-read-only?
diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
index 01d95a9be8..a9ab0ffb36 100644
--- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
@@ -159,7 +159,6 @@
[group-id objects hover-ids]
(and (contains? #{:group :bool} (get-in objects [group-id :type]))
-
;; If there are no children in the hover-ids we're in the empty side
(->> hover-ids
(remove #(contains? #{:group :bool} (get-in objects [% :type])))
@@ -253,76 +252,98 @@
(fn [_]
(reset! hover-top-frame-id (ctt/top-nested-frame objects (deref last-point-ref)))))
- (hooks/use-stream
- over-shapes-stream
- (mf/deps page-id objects show-measures?)
- (fn [ids]
- (let [selected (mf/ref-val selected-ref)
- focus (mf/ref-val focus-ref)
- mod? (mf/ref-val mod-ref)
+ ;; This ref is a cache of sorted ids. Sorting is expensive so we save the list
+ (let [sorted-ids-cache (mf/use-ref {})]
+ (hooks/use-stream
+ over-shapes-stream
+ (mf/deps page-id objects show-measures?)
+ (fn [ids]
+ (let [selected (mf/ref-val selected-ref)
+ focus (mf/ref-val focus-ref)
+ mod? (mf/ref-val mod-ref)
+ cached-ids (mf/ref-val sorted-ids-cache)
- ids (into (d/ordered-set)
- (remove #(dm/get-in objects [% :blocked]))
- (ctt/sort-z-index objects ids {:bottom-frames? mod?}))
+ make-sorted-ids
+ (fn [mod? ids]
+ (let [sorted-ids
+ (into (d/ordered-set)
+ (comp (remove #(dm/get-in objects [% :blocked]))
+ (remove (partial cfh/svg-raw-shape? objects)))
+ (ctt/sort-z-index objects ids {:bottom-frames? mod?}))]
+ (mf/set-ref-val! sorted-ids-cache (assoc cached-ids [mod? ids] sorted-ids))
+ sorted-ids))
- grouped? (fn [id]
- (and (cfh/group-shape? objects id)
- (not (cfh/mask-shape? objects id))))
+ ids (or (get cached-ids [mod? ids]) (make-sorted-ids mod? ids))
- selected-with-parents
- (into #{} (mapcat #(cfh/get-parent-ids objects %)) selected)
+ grouped?
+ (fn [id]
+ (and (cfh/group-shape? objects id)
+ (not (cfh/mask-shape? objects id))))
- root-frame-with-data?
- #(as-> (get objects %) obj
- (and (cfh/root-frame? obj)
- (d/not-empty? (:shapes obj))
- (not (ctk/instance-head? obj))
- (not (ctk/main-instance? obj))))
+ selected-with-parents
+ (into #{} (mapcat #(cfh/get-parent-ids objects %)) selected)
- ;; Set with the elements to remove from the hover list
- remove-id-xf
- (cond
- mod?
- (filter grouped?)
+ root-frame-with-data?
+ #(as-> (get objects %) obj
+ (and (cfh/root-frame? obj)
+ (d/not-empty? (:shapes obj))
+ (not (ctk/instance-head? obj))
+ (not (ctk/main-instance? obj))))
- (not mod?)
- (filter #(or (root-frame-with-data? %)
- (group-empty-space? % objects ids))))
+ ;; Set with the elements to remove from the hover list
+ remove-id-xf
+ (cond
+ mod?
+ (filter grouped?)
- remove-id?
- (into selected-with-parents remove-id-xf ids)
+ (not mod?)
+ (let [child-parent?
+ (into #{}
+ (comp (remove #(cfh/group-like-shape? objects %))
+ (mapcat #(cfh/get-parent-ids objects %)))
+ ids)]
+ (filter #(or (root-frame-with-data? %)
+ (and (contains? #{:group :bool} (dm/get-in objects [% :type]))
+ (not (contains? child-parent? %)))))))
- no-fill-nested-frames?
- (fn [id]
- (let [shape (get objects id)]
- (and (cfh/frame-shape? shape)
- (not (cfh/is-direct-child-of-root? shape))
- (empty? (get shape :fills)))))
+ remove-id?
+ (into selected-with-parents remove-id-xf ids)
+ no-fill-nested-frames?
+ (fn [id]
+ (let [shape (get objects id)]
+ (and (cfh/frame-shape? shape)
+ (not (cfh/is-direct-child-of-root? shape))
+ (empty? (get shape :fills)))))
- hover-shape
- (->> ids
- (remove remove-id?)
- (remove (partial cfh/hidden-parent? objects))
- (remove #(and mod? (no-fill-nested-frames? %)))
- (filter #(or (empty? focus) (cpf/is-in-focus? objects focus %)))
- (first)
- (get objects))
-
- ;; We keep track of a diferent shape for measures
- measure-hover-shape
- (when show-measures?
+ hover-shape
(->> ids
- (remove #(group-empty-space? % objects ids))
+ (remove remove-id?)
(remove (partial cfh/hidden-parent? objects))
(remove #(and mod? (no-fill-nested-frames? %)))
(filter #(or (empty? focus) (cpf/is-in-focus? objects focus %)))
(first)
- (get objects)))]
+ (get objects))
- (reset! hover hover-shape)
- (reset! measure-hover measure-hover-shape)
- (reset! hover-ids ids))))))
+ ;; We keep track of a diferent shape for measures
+ measure-hover-shape
+ (when show-measures?
+ (->> ids
+ (remove #(group-empty-space? % objects ids))
+ (remove (partial cfh/hidden-parent? objects))
+ (remove #(and mod? (no-fill-nested-frames? %)))
+ (filter #(or (empty? focus) (cpf/is-in-focus? objects focus %)))
+ (first)
+ (get objects)))]
+
+
+ (reset! hover hover-shape)
+ (reset! measure-hover measure-hover-shape)
+ (reset! hover-ids ids)))
+
+ (fn []
+ ;; Clean the cache
+ (mf/set-ref-val! sorted-ids-cache {}))))))
(defn setup-viewport-modifiers
[modifiers objects]
diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
index 04eeb79d1f..d7b7776f71 100644
--- a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
@@ -13,6 +13,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
+ [app.main.ui.workspace.top-toolbar :refer [top-toolbar]]
[app.main.ui.workspace.viewport.grid-layout-editor :refer [grid-edition-actions]]
[app.main.ui.workspace.viewport.path-actions :refer [path-actions]]
[app.util.i18n :as i18n :refer [tr]]
@@ -36,7 +37,7 @@
(mf/defc top-bar
{::mf/wrap [mf/memo]}
- []
+ [{:keys [layout]}]
(let [edition (mf/deref refs/selected-edition)
selected (mf/deref refs/selected-objects)
drawing (mf/deref refs/workspace-drawing)
@@ -50,6 +51,7 @@
(not= :curve (:tool drawing)))
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
+ hide-ui? (:hide-ui layout)
path-edition? (or (and single? editing?
(and (not (cfh/text-shape? shape))
@@ -58,13 +60,17 @@
grid-edition? (and single? editing? (ctl/grid-layout? shape))]
- (cond
- workspace-read-only?
- [:& view-only-actions]
+ [:*
+ (when-not hide-ui?
+ [:& top-toolbar {:layout layout}])
- path-edition?
- [:div.viewport-actions
- [:& path-actions {:shape shape}]]
+ (cond
+ workspace-read-only?
+ [:& view-only-actions]
- grid-edition?
- [:& grid-edition-actions {:shape shape}])))
+ path-edition?
+ [:div.viewport-actions
+ [:& path-actions {:shape shape}]]
+
+ grid-edition?
+ [:& grid-edition-actions {:shape shape}])]))
diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs
index 6de386953c..fa73fe356b 100644
--- a/frontend/src/app/worker/selection.cljs
+++ b/frontend/src/app/worker/selection.cljs
@@ -127,7 +127,7 @@
match-criteria?
(fn [shape]
(and (not (:hidden shape))
- (or (= :frame (:type shape)) ;; We return frames even if blocked
+ (or (cfh/frame-shape? shape) ;; We return frames even if blocked
(not (:blocked shape)))
(or (not frame-id) (= frame-id (:frame-id shape)))
(case (:type shape)
@@ -135,8 +135,11 @@
(:bool :group) (not ignore-groups?)
true)
+ ;; This condition controls when to check for overlapping. Otherwise the
+ ;; shape needs to be fully contained.
(or (not full-frame?)
- (not= :frame (:type shape))
+ (and (not ignore-groups?) (contains? shape :component-id))
+ (and (not ignore-groups?) (not (cfh/root-frame? shape)))
(and (d/not-empty? (:shapes shape))
(gsh/rect-contains-shape? rect shape))
(and (empty? (:shapes shape))
@@ -192,7 +195,10 @@
overlaps?
(fn [shape]
- (if (and (false? using-selrect?) (empty? (:fills shape)))
+ (if (and (false? using-selrect?)
+ (empty? (:fills shape))
+ (not (contains? (-> shape :svg-attrs) :fill))
+ (not (contains? (-> shape :svg-attrs :style) :fill)))
(case (:type shape)
;; If the shape has no fills the overlap depends on the stroke
:rect (and (overlaps-outer-shape? shape) (not (overlaps-inner-shape? shape)))
diff --git a/frontend/test/frontend_tests/state_components_test.cljs b/frontend/test/frontend_tests/state_components_test.cljs
index fb341f12e9..7cb62b2123 100644
--- a/frontend/test/frontend_tests/state_components_test.cljs
+++ b/frontend/test/frontend_tests/state_components_test.cljs
@@ -170,7 +170,7 @@
(dwl/add-component)
:the/end))))
-(t/deftest test-add-component-from-component
+(t/deftest test-add-component-from-component-instance
(t/async
done
(let [state (-> thp/initial-state
@@ -178,30 +178,27 @@
(thp/sample-shape :shape1 :rect
{:name "Rect 1"})
(thp/make-component :main1 :component1
- [(thp/id :shape1)]))
+ [(thp/id :shape1)])
+ (thp/instantiate-component :instance1 (thp/id :component1)))
store (the/prepare-store state done
(fn [new-state]
- ;; Expected shape tree:
- ;;
- ;; [Page]
- ;; Root Frame
- ;; Rect 1
- ;; Rect 1
- ;; Rect 1
- ;;
- ;; [Rect 1]
- ;; page1 / Rect 1
- ;;
- ;; [Rect 1]
- ;; page1 / Rect 1
- ;;
+ ;; Expected shape tree:
+ ;;
+ ;; [Page: Page]
+ ;; Root Frame
+ ;; Rect 1 #
+ ;; Rect 1
+ ;; Rect 1 #
+ ;; Rect 1* @--> Rect 1
+ ;; Rect 1 ---> Rect 1
+ ;;
(let [[[instance1 shape1]
[c-instance1 c-shape1]
component1]
(thl/resolve-instance-and-main
new-state
- (thp/id :main1)
+ (thp/id :instance1)
true)
[[instance2 instance1' shape1']
@@ -225,6 +222,56 @@
(t/is (= (:name c-instance1') "Rect 1"))
(t/is (= (:name c-instance2) "Rect 1")))))]
+ (ptk/emit!
+ store
+ (dw/select-shape (thp/id :instance1))
+ (dwl/add-component)
+ :the/end))))
+
+
+(t/deftest test-add-component-from-component-main
+ (t/async
+ done
+ (let [state (-> thp/initial-state
+ (thp/sample-page)
+ (thp/sample-shape :shape1 :rect
+ {:name "Rect 1"})
+ (thp/make-component :main1 :component1
+ [(thp/id :shape1)]))
+
+ store (the/prepare-store state done
+ (fn [new-state]
+ ;; Expected shape tree:
+ ;;
+ ;; [Page]
+ ;; Root Frame
+ ;; Rect 1
+ ;; Rect 1
+ ;;
+ ;; [Rect 1]
+ ;; page1 / Rect 1
+ ;;
+ (let [file (wsh/get-local-file new-state)
+ components (ctkl/components file)
+ page (thp/current-page new-state)
+ shape1 (thp/get-shape new-state :shape1)
+ parent1 (ctn/get-shape page (:parent-id shape1))
+ main1 (thp/get-shape state :main1)
+ [[instance1 shape1]
+ [c-instance1 c-shape1]
+ component1]
+ (thl/resolve-instance-and-main
+ new-state
+ (:id main1))]
+ ;; Creating a component from a main doesn't generate a new component
+ (t/is (= (count components) 1))
+
+ (t/is (= (:name shape1) "Rect 1"))
+ (t/is (= (:name instance1) "Rect 1"))
+ (t/is (= (:name component1) "Rect 1"))
+ (t/is (= (:name c-shape1) "Rect 1"))
+ (t/is (= (:name c-instance1) "Rect 1")))))]
+
(ptk/emit!
store
(dw/select-shape (thp/id :main1))
@@ -580,7 +627,62 @@
(dwl/detach-component (:id instance1))
:the/end))))
-(t/deftest test-add-nested-component
+
+
+(t/deftest test-add-nested-component-instance
+ (t/async
+ done
+ (let [state (-> thp/initial-state
+ (thp/sample-page)
+ (thp/sample-shape :shape1 :rect
+ {:name "Rect 1"})
+ (thp/make-component :main1 :component1
+ [(thp/id :shape1)])
+ (thp/instantiate-component :instance1 (thp/id :component1)))
+
+ store (the/prepare-store state done
+ (fn [new-state]
+ ;; Expected shape tree:
+ ;;
+ ;; [Page]
+ ;; Root Frame
+ ;; Rect 1
+ ;; Rect 1
+ ;; Board
+ ;; Rect 1
+ ;; Rect 1
+ ;;
+ ;; [Rect 1]
+ ;; page1 / Rect 1
+ ;;
+ ;; [Board]
+ ;; page1 / Board
+ ;;
+ (let [instance1 (thp/get-shape new-state :instance1)
+
+ [[group shape1 shape2]
+ [c-group c-shape1 c-shape2]
+ component]
+ (thl/resolve-instance-and-main
+ new-state
+ (:parent-id instance1))]
+
+ (t/is (= (:name group) "Board"))
+ (t/is (= (:name shape1) "Rect 1"))
+ (t/is (= (:name shape2) "Rect 1"))
+ (t/is (= (:name component) "Board"))
+ (t/is (= (:name c-group) "Board"))
+ (t/is (= (:name c-shape1) "Rect 1"))
+ (t/is (= (:name c-shape2) "Rect 1")))))]
+
+ (ptk/emit!
+ store
+ (dw/select-shape (thp/id :instance1))
+ (dwsh/create-artboard-from-selection)
+ (dwl/add-component)
+ :the/end))))
+
+(t/deftest test-add-nested-component-main
(t/async
done
(let [state (-> thp/initial-state
@@ -594,34 +696,36 @@
;;
;; [Page]
;; Root Frame
- ;; Group
+ ;; Board
;; Rect 1
;; Rect 1
;;
;; [Rect 1]
;; page1 / Rect 1
;;
- ;; [Group]
- ;; page1 / Group
;;
- (let [page (thp/current-page new-state)
+ (let [file (wsh/get-local-file new-state)
+ components (ctkl/components file)
+ page (thp/current-page new-state)
+
shape1 (thp/get-shape new-state :shape1)
parent1 (ctn/get-shape page (:parent-id shape1))
- [[group shape1 shape2]
- [c-group c-shape1 c-shape2]
+ [[group shape1]
+ [c-group c-shape1]
component]
(thl/resolve-instance-and-main
new-state
- (:parent-id parent1))]
+ (:parent-id shape1))]
- (t/is (= (:name group) "Board"))
+ ;; Creating a component from something containing a main doesn't generate a new component
+ (t/is (= (count components) 1))
+
+ (t/is (= (:name group) "Rect 1"))
(t/is (= (:name shape1) "Rect 1"))
- (t/is (= (:name shape2) "Rect 1"))
- (t/is (= (:name component) "Board"))
- (t/is (= (:name c-group) "Board"))
- (t/is (= (:name c-shape1) "Rect 1"))
- (t/is (= (:name c-shape2) "Rect 1")))))]
+ (t/is (= (:name component) "Rect 1"))
+ (t/is (= (:name c-group) "Rect 1"))
+ (t/is (= (:name c-shape1) "Rect 1")))))]
(ptk/emit!
store
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 428cf98f57..0affa485fa 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -2976,6 +2976,9 @@ msgstr "Snap to guides"
msgid "shortcuts.toggle-textpalette"
msgstr "Toggle text palette"
+msgid "shortcuts.toggle-theme"
+msgstr "Change theme"
+
msgid "shortcuts.toggle-visibility"
msgstr "Show / Hide"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index be493d3c2a..fbd44e23a4 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -3022,6 +3022,9 @@ msgstr "Alinear a las guias"
msgid "shortcuts.toggle-textpalette"
msgstr "Mostrar/ocultar paleta de textos"
+msgid "shortcuts.toggle-theme"
+msgstr "Cambiar tema"
+
msgid "shortcuts.toggle-visibility"
msgstr "Mostrar/ocultar elemento"